diff --git a/server/schema/channel/types.ts b/server/schema/channel/types.ts index d5062e19..e4c573d7 100644 --- a/server/schema/channel/types.ts +++ b/server/schema/channel/types.ts @@ -96,6 +96,7 @@ export const channelTypes = gql` channel_age: Int! pending_payments: [pendingPaymentType]! pending_resume: pendingResumeType! + bosScore: BosScore } type closeChannelType { diff --git a/server/schema/tbase/resolvers.ts b/server/schema/tbase/resolvers.ts index 51612a4b..62c4546c 100644 --- a/server/schema/tbase/resolvers.ts +++ b/server/schema/tbase/resolvers.ts @@ -105,6 +105,18 @@ const getBosNodeScoresQuery = ` } `; +const getLastNodeScoreQuery = ` + query GetLastNodeScore($publicKey: String!, $token: String!) { + getLastNodeScore(publicKey: $publicKey, token: $token) { + alias + public_key + score + updated + position + } + } +`; + export const tbaseResolvers = { Query: { getBaseInfo: async (_: undefined, __: undefined, context: ContextType) => { @@ -318,4 +330,42 @@ export const tbaseResolvers = { return true; }, }, + channelType: { + bosScore: async ( + { + partner_public_key, + }: { + partner_public_key: string; + }, + _: undefined, + { tokenAuth }: ContextType + ) => { + if (!tokenAuth) { + return null; + } + + const { data, error } = await graphqlFetchWithProxy( + appUrls.tbase, + getLastNodeScoreQuery, + { + publicKey: partner_public_key, + token: tokenAuth, + } + ); + + if (error) { + return null; + } + + return ( + data?.getLastNodeScore || { + alias: '', + public_key: partner_public_key, + score: 0, + updated: '', + position: 0, + } + ); + }, + }, }; diff --git a/src/components/price/Price.tsx b/src/components/price/Price.tsx index a5abff1c..1d440f87 100644 --- a/src/components/price/Price.tsx +++ b/src/components/price/Price.tsx @@ -54,6 +54,8 @@ interface GetPriceProps { noUnit?: boolean; } +export type FormatFnType = (options: GetPriceProps) => string; + export const getPrice = ( currency: string, displayValues: boolean, diff --git a/src/graphql/queries/__generated__/getChannels.generated.tsx b/src/graphql/queries/__generated__/getChannels.generated.tsx index d9c0956a..103c9ced 100644 --- a/src/graphql/queries/__generated__/getChannels.generated.tsx +++ b/src/graphql/queries/__generated__/getChannels.generated.tsx @@ -31,6 +31,9 @@ export type GetChannelsQuery = ( { __typename?: 'nodePolicyType' } & Pick )> } + )>, bosScore?: Types.Maybe<( + { __typename?: 'BosScore' } + & Pick )> } )>> } ); @@ -93,6 +96,13 @@ export const GetChannelsDocument = gql` cltv_delta } } + bosScore { + alias + public_key + score + updated + position + } } } `; diff --git a/src/graphql/queries/__tests__/__snapshots__/queryTests.ts.snap b/src/graphql/queries/__tests__/__snapshots__/queryTests.ts.snap index 378cf9af..4bbef0ea 100644 --- a/src/graphql/queries/__tests__/__snapshots__/queryTests.ts.snap +++ b/src/graphql/queries/__tests__/__snapshots__/queryTests.ts.snap @@ -260,6 +260,7 @@ Object { "data": Object { "getChannels": Array [ Object { + "bosScore": null, "capacity": 1000, "channel_age": 123356, "commit_transaction_fee": 1000, @@ -310,6 +311,7 @@ Object { "unsettled_balance": 1000, }, Object { + "bosScore": null, "capacity": 1000, "channel_age": 123356, "commit_transaction_fee": 1000, @@ -360,6 +362,7 @@ Object { "unsettled_balance": 1000, }, Object { + "bosScore": null, "capacity": 1000, "channel_age": 123356, "commit_transaction_fee": 1000, diff --git a/src/graphql/queries/getChannels.ts b/src/graphql/queries/getChannels.ts index 8c0f2eb9..6f6e67d6 100644 --- a/src/graphql/queries/getChannels.ts +++ b/src/graphql/queries/getChannels.ts @@ -57,6 +57,13 @@ export const GET_CHANNELS = gql` cltv_delta } } + bosScore { + alias + public_key + score + updated + position + } } } `; diff --git a/src/graphql/types.ts b/src/graphql/types.ts index c10e9d8b..d69ffba3 100644 --- a/src/graphql/types.ts +++ b/src/graphql/types.ts @@ -680,6 +680,7 @@ export type ChannelType = { channel_age: Scalars['Int']; pending_payments: Array>; pending_resume: PendingResumeType; + bosScore?: Maybe; }; export type CloseChannelType = { diff --git a/src/views/channels/channels/ChannelBars.tsx b/src/views/channels/channels/ChannelBars.tsx new file mode 100644 index 00000000..6b6c8e15 --- /dev/null +++ b/src/views/channels/channels/ChannelBars.tsx @@ -0,0 +1,166 @@ +import { FC } from 'react'; +import { BalanceBars, SingleBar, SumBar } from 'src/components/balance'; +import { ProgressBar } from 'src/components/generic/CardGeneric'; +import { FormatFnType } from 'src/components/price/Price'; +import { useConfigState } from 'src/context/ConfigContext'; +import { ChannelType } from 'src/graphql/types'; +import { getPercent } from 'src/utils/helpers'; +import { ChannelStatsColumn, ChannelStatsLine } from './Channel.style'; +import { WUMBO_MIN_SIZE } from './Channels'; + +const MAX_HTLCS = 483; + +const getBar = (top: number, bottom: number) => { + const percent = (top / bottom) * 100; + return Math.min(percent, 100); +}; + +type ChannelBarsProps = { + info: ChannelType; + format: FormatFnType; + details: { + biggestRateFee: number; + biggestBaseFee: number; + biggestPartner: number; + mostChannels: number; + biggest: number; + }; +}; + +export const ChannelBars: FC = ({ + info, + format, + details: { + biggestRateFee, + biggestBaseFee, + biggestPartner, + mostChannels, + biggest, + }, +}) => { + const { + partner_fee_info, + partner_node_info, + local_balance, + remote_balance, + pending_resume, + } = info; + + const { incoming_amount = 200, outgoing_amount = 150 } = pending_resume; + + const { capacity: partnerNodeCapacity = 0, channel_count } = + partner_node_info?.node || {}; + + const { base_fee_mtokens, fee_rate } = + partner_fee_info?.partner_node_policies || {}; + + const { base_fee_mtokens: node_base, fee_rate: node_rate } = + partner_fee_info?.node_policies || {}; + + const { channelBarType, subBar } = useConfigState(); + + const maxRate = Math.min(fee_rate || 0, 10000); + const maxNodeRate = Math.min(node_rate || 0, 10000); + + const localBalance = format({ + amount: local_balance, + breakNumber: true, + noUnit: true, + }); + const remoteBalance = format({ + amount: remote_balance, + breakNumber: true, + noUnit: true, + }); + + switch (channelBarType) { + case 'fees': + return ( + + + + + ); + case 'size': + return ( + + + + + + {channel_count && ( + + + + + )} + + ); + case 'proportional': + return ( + + {subBar === 'fees' && ( + + )} + = WUMBO_MIN_SIZE} + /> + + ); + case 'htlcs': + return ( + + {subBar === 'fees' && ( + + )} + + + ); + default: + return ( + + {subBar === 'fees' && ( + + )} + + + ); + } +}; diff --git a/src/views/channels/channels/ChannelBarsInfo.tsx b/src/views/channels/channels/ChannelBarsInfo.tsx new file mode 100644 index 00000000..3c157b0c --- /dev/null +++ b/src/views/channels/channels/ChannelBarsInfo.tsx @@ -0,0 +1,106 @@ +import { FC } from 'react'; +import { renderLine } from 'src/components/generic/helpers'; +import { Separation, SubTitle } from 'src/components/generic/Styled'; +import { FormatFnType } from 'src/components/price/Price'; +import { useConfigState } from 'src/context/ConfigContext'; +import { ChannelType } from 'src/graphql/types'; + +const MAX_HTLCS = 483; + +type ChannelBarsInfoProps = { + info: ChannelType; + format: FormatFnType; +}; + +export const ChannelBarsInfo: FC = ({ + info: { + local_balance, + remote_balance, + partner_fee_info, + partner_node_info, + pending_resume, + }, + format, +}) => { + const { channelBarType } = useConfigState(); + + const { + total_amount, + incoming_amount = 200, + outgoing_amount = 150, + } = pending_resume; + + const { capacity: partnerNodeCapacity = 0, channel_count } = + partner_node_info?.node || {}; + + const { base_fee_mtokens, fee_rate } = + partner_fee_info?.partner_node_policies || {}; + + const { base_fee_mtokens: node_base, fee_rate: node_rate } = + partner_fee_info?.node_policies || {}; + + const feeRate = format({ amount: fee_rate, override: 'ppm' }); + const nodeFeeRate = format({ amount: node_rate, override: 'ppm' }); + + const formatLocal = format({ amount: local_balance }); + const formatRemote = format({ amount: remote_balance }); + const nodeCapacity = format({ amount: partnerNodeCapacity }); + const baseFee = format({ + amount: Number(base_fee_mtokens) / 1000, + override: 'sat', + }); + + const nodeBaseFee = format({ + amount: Number(node_base) / 1000, + override: 'sat', + }); + + switch (channelBarType) { + case 'fees': + return ( + <> + {renderLine('Fee Rate', nodeFeeRate)} + {renderLine('Partner Fee Rate', feeRate)} + + {renderLine('Base Fee', nodeBaseFee)} + {renderLine('Partner Base Fee', baseFee)} + + ); + case 'size': + return ( + <> + {renderLine('Partner Capacity', nodeCapacity)} + {renderLine('Partner Channels', channel_count)} + + ); + case 'proportional': + return ( + <> + {renderLine('Local Balance', formatLocal)} + {renderLine('Remote Balance', formatRemote)} + + ); + case 'htlcs': + return ( + <> + Pending HTLCS + {renderLine('Total', `${total_amount}/${MAX_HTLCS}`)} + {renderLine( + 'Incoming', + `${incoming_amount}/${MAX_HTLCS - outgoing_amount}` + )} + {renderLine( + 'Outgoing', + `${outgoing_amount}/${MAX_HTLCS - incoming_amount}` + )} + + ); + default: + return ( + <> + {renderLine('Local Balance', formatLocal)} + {renderLine('Remote Balance', formatRemote)} + + ); + } +}; diff --git a/src/views/channels/channels/ChannelBosScore.tsx b/src/views/channels/channels/ChannelBosScore.tsx new file mode 100644 index 00000000..180a5048 --- /dev/null +++ b/src/views/channels/channels/ChannelBosScore.tsx @@ -0,0 +1,62 @@ +import { FC } from 'react'; +import { + getDateDif, + getFormatDate, + renderLine, +} from 'src/components/generic/helpers'; +import { DarkSubTitle } from 'src/components/generic/Styled'; +import { Link } from 'src/components/link/Link'; +import { BosScore } from 'src/graphql/types'; +import { chartColors } from 'src/styles/Themes'; +import styled from 'styled-components'; + +const S = { + missingToken: styled.div` + width: 100%; + border: 1px solid ${chartColors.darkyellow}; + padding: 8px; + border-radius: 4px; + margin: 8px 0 0; + font-size: 14px; + text-align: center; + + :hover { + background-color: ${chartColors.darkyellow}; + } + `, +}; + +export const ChannelBosScore: FC<{ score?: BosScore | null }> = ({ score }) => { + if (!score) { + return ( + <> + BOS Score + + + Get a token to view this nodes latest BOS score and historical info. + + + + ); + } + + if (!score.alias) { + return ( + This node has not appeared in the BOS list + ); + } + + return ( + <> + BOS Score + {renderLine('Score', score.score)} + {renderLine('Position', score.position)} + {renderLine('Last Time on List', `${getDateDif(score.updated)} ago`)} + {renderLine('Last Time Date', getFormatDate(score.updated))} + {renderLine( + 'Historical', + View History + )} + + ); +}; diff --git a/src/views/channels/channels/ChannelCard.tsx b/src/views/channels/channels/ChannelCard.tsx index 00ef1260..08d6c5cf 100644 --- a/src/views/channels/channels/ChannelCard.tsx +++ b/src/views/channels/channels/ChannelCard.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import ReactTooltip from 'react-tooltip'; import { ArrowDown, @@ -9,56 +9,34 @@ import { X, } from 'react-feather'; import { ChannelType } from 'src/graphql/types'; -import { BalanceBars, SingleBar, SumBar } from 'src/components/balance'; import { useRebalanceState, useRebalanceDispatch, } from 'src/context/RebalanceContext'; -import { ChangeDetails } from 'src/components/modal/changeDetails/ChangeDetails'; -import { - getPercent, - formatSeconds, - blockToTime, - formatSats, -} from '../../../utils/helpers'; -import { ProgressBar, MainInfo } from '../../../components/generic/CardGeneric'; +import differenceInDays from 'date-fns/differenceInDays'; +import { MainInfo } from '../../../components/generic/CardGeneric'; import { SubCard, - Separation, - Sub4Title, ResponsiveLine, DarkSubTitle, - SubTitle, } from '../../../components/generic/Styled'; import { useConfigState } from '../../../context/ConfigContext'; -import { - getFormatDate, - getDateDif, - renderLine, - getTransactionLink, - getNodeLink, -} from '../../../components/generic/helpers'; -import Modal from '../../../components/modal/ReactModal'; -import { CloseChannel } from '../../../components/modal/closeChannel/CloseChannel'; -import { ColorButton } from '../../../components/buttons/colorButton/ColorButton'; +import { renderLine } from '../../../components/generic/helpers'; import { getPrice } from '../../../components/price/Price'; import { usePriceState } from '../../../context/PriceContext'; import { ChannelNodeTitle, ChannelBarSide, ChannelIconPadding, - ChannelStatsColumn, ChannelSingleLine, - ChannelStatsLine, ChannelBalanceRow, ChannelBalanceButton, - WumboTag, ChannelAlias, } from './Channel.style'; -import { WUMBO_MIN_SIZE } from './Channels'; import { getTitleColor } from './helpers'; - -const MAX_HTLCS = 483; +import { ChannelDetails } from './ChannelDetails'; +import { ChannelBars } from './ChannelBars'; +import { ChannelBarsInfo } from './ChannelBarsInfo'; const getSymbol = (status: boolean) => { return status ? : ; @@ -68,11 +46,6 @@ const getPrivate = (status: boolean) => { return status && ; }; -const getBar = (top: number, bottom: number) => { - const percent = (top / bottom) * 100; - return Math.min(percent, 100); -}; - interface ChannelCardProps { channelInfo: ChannelType; index: number; @@ -99,8 +72,7 @@ export const ChannelCard: React.FC = ({ const dispatch = useRebalanceDispatch(); const { inChannel, outChannel } = useRebalanceState(); - const { channelBarType, channelBarStyle, subBar } = useConfigState(); - const [modalOpen, setModalOpen] = useState('none'); + const { channelBarStyle } = useConfigState(); const { currency, displayValues } = useConfigState(); const priceContext = usePriceState(); @@ -108,103 +80,39 @@ export const ChannelCard: React.FC = ({ const { capacity, - commit_transaction_fee, - commit_transaction_weight, id, is_active, is_closing, is_opening, is_partner_initiated, is_private, - is_static_remote_key, - local_balance, - local_reserve, partner_public_key, - received, - remote_balance, - remote_reserve, - sent, - time_offline, - time_online, - transaction_id, - transaction_vout, - unsettled_balance, partner_node_info, - partner_fee_info, - channel_age, - pending_resume, + bosScore, } = channelInfo; - const { - total_amount, - total_tokens, - incoming_tokens, - incoming_amount = 200, - outgoing_tokens, - outgoing_amount = 150, - } = pending_resume; - - const isIn = inChannel?.id === id; - const isOut = outChannel?.id === id; - - const { - alias, - capacity: partnerNodeCapacity = 0, - channel_count, - updated_at, - } = partner_node_info?.node || {}; - - const { base_fee_mtokens, fee_rate, cltv_delta } = - partner_fee_info?.partner_node_policies || {}; - - const { - base_fee_mtokens: node_base, - fee_rate: node_rate, - cltv_delta: node_cltv, - max_htlc_mtokens, - min_htlc_mtokens, - } = partner_fee_info?.node_policies || {}; - - const formatBalance = format({ amount: capacity }); - const formatLocal = format({ amount: local_balance }); - const formatRemote = format({ amount: remote_balance }); - const formatReceived = format({ amount: received }); - const formatSent = format({ amount: sent }); - const commitFee = format({ amount: commit_transaction_fee }); - const commitWeight = format({ amount: commit_transaction_weight }); - const localReserve = format({ amount: local_reserve }); - const remoteReserve = format({ amount: remote_reserve }); - const nodeCapacity = format({ amount: partnerNodeCapacity }); + const barDetails = { + biggestRateFee, + biggestBaseFee, + biggestPartner, + mostChannels, + biggest, + }; - const localBalance = format({ - amount: local_balance, - breakNumber: true, - noUnit: true, - }); - const remoteBalance = format({ - amount: remote_balance, - breakNumber: true, - noUnit: true, - }); + let withinAWeek = false; - const baseFee = format({ - amount: Number(base_fee_mtokens) / 1000, - override: 'sat', - }); + if (bosScore?.updated) { + withinAWeek = differenceInDays(new Date(), new Date(bosScore.updated)) < 7; + } - const nodeBaseFee = format({ - amount: Number(node_base) / 1000, - override: 'sat', - }); + const isBosNode = !!bosScore?.alias && withinAWeek; - const maxRate = Math.min(fee_rate || 0, 10000); - const feeRate = format({ amount: fee_rate, override: 'ppm' }); + const isIn = inChannel?.id === id; + const isOut = outChannel?.id === id; - const maxNodeRate = Math.min(node_rate || 0, 10000); - const nodeFeeRate = format({ amount: node_rate, override: 'ppm' }); + const { alias } = partner_node_info?.node || {}; - const max_htlc = Number(max_htlc_mtokens) / 1000; - const min_htlc = Number(min_htlc_mtokens) / 1000; + const formatBalance = format({ amount: capacity }); const handleClick = () => { if (indexOpen === index) { @@ -214,256 +122,6 @@ export const ChannelCard: React.FC = ({ } }; - const renderPartner = () => - alias ? ( - <> - {renderLine('Node Capacity:', nodeCapacity)} - {renderLine('Channel Count:', channel_count)} - {renderLine( - 'Last Update:', - `${getDateDif(updated_at)} ago (${getFormatDate(updated_at)})` - )} - {renderLine('Base Fee:', baseFee)} - {renderLine('Fee Rate:', `${feeRate}`)} - {renderLine('CTLV Delta:', cltv_delta)} - - ) : ( - Partner node not found - ); - - const renderWumboInfo = () => { - if (local_balance + remote_balance >= WUMBO_MIN_SIZE) { - return ( - <> - - This channel is Wumbo! - - ); - } - - return null; - }; - - const renderDetails = () => { - return ( - <> - {renderWumboInfo()} - - {renderLine('Status:', is_active ? 'Active' : 'Not Active')} - {renderLine('Is Opening:', is_opening ? 'True' : 'False')} - {renderLine('Is Closing:', is_closing ? 'True' : 'False')} - {renderLine( - 'Balancedness:', - getPercent(local_balance, remote_balance) / 100 - )} - - {renderLine('Base Fee:', nodeBaseFee)} - {renderLine('Fee Rate:', `${nodeFeeRate}`)} - {renderLine('CTLV Delta:', node_cltv)} - {renderLine('Max HTLC (sats)', formatSats(max_htlc))} - {renderLine('Min HTLC (sats)', formatSats(min_htlc))} - setModalOpen('details')} - > - Update Details - - - {renderLine('Local Balance:', formatLocal)} - {renderLine('Remote Balance:', formatRemote)} - {renderLine('Received:', formatReceived)} - {renderLine('Sent:', formatSent)} - {renderLine('Local Reserve:', localReserve)} - {renderLine('Remote Reserve:', remoteReserve)} - - {renderLine('Node Public Key:', getNodeLink(partner_public_key))} - {renderLine('Transaction Id:', getTransactionLink(transaction_id))} - - Pending HTLCS - {!total_amount && renderLine('Total Amount', 'None')} - {renderLine('Total Amount', total_amount)} - {renderLine('Total Tokens', total_tokens)} - {renderLine('Incoming Tokens', incoming_tokens)} - {renderLine('Outgoing Tokens', outgoing_tokens)} - {renderLine('Incoming Amount', incoming_amount)} - {renderLine('Outgoing Amount', outgoing_amount)} - - {renderLine('Channel Age:', blockToTime(channel_age))} - {renderLine('Channel Block Age:', channel_age)} - {renderLine('Channel Id:', id)} - {renderLine('Commit Fee:', commitFee)} - {renderLine('Commit Weight:', commitWeight)} - - {renderLine( - 'Is Static Remote Key:', - is_static_remote_key ? 'True' : 'False' - )} - {renderLine('Time Offline:', formatSeconds(time_offline))} - {renderLine('Time Online:', formatSeconds(time_online))} - {renderLine('Transaction Vout:', transaction_vout)} - {renderLine('Unsettled Balance:', unsettled_balance)} - - Partner Node Info - {renderPartner()} - - setModalOpen('close')} - > - Close Channel - - - ); - }; - - const renderBars = () => { - switch (channelBarType) { - case 'fees': - return ( - - - - - ); - case 'size': - return ( - - - - - - {channel_count && ( - - - - - )} - - ); - case 'proportional': - return ( - - {subBar === 'fees' && ( - - )} - = WUMBO_MIN_SIZE} - /> - - ); - case 'htlcs': - return ( - - {subBar === 'fees' && ( - - )} - - - ); - default: - return ( - - {subBar === 'fees' && ( - - )} - - - ); - } - }; - - const renderBarsInfo = () => { - switch (channelBarType) { - case 'fees': - return ( - <> - {renderLine('Fee Rate', nodeFeeRate)} - {renderLine('Partner Fee Rate', feeRate)} - - {renderLine('Base Fee', nodeBaseFee)} - {renderLine('Partner Base Fee', baseFee)} - - ); - case 'size': - return ( - <> - {renderLine('Partner Capacity', nodeCapacity)} - {renderLine('Partner Channels', channel_count)} - - ); - case 'proportional': - return ( - <> - {renderLine('Local Balance', formatLocal)} - {renderLine('Remote Balance', formatRemote)} - - ); - case 'htlcs': - return ( - <> - Pending HTLCS - {renderLine('Total', `${total_amount}/${MAX_HTLCS}`)} - {renderLine( - 'Incoming', - `${incoming_amount}/${MAX_HTLCS - outgoing_amount}` - )} - {renderLine( - 'Outgoing', - `${outgoing_amount}/${MAX_HTLCS - incoming_amount}` - )} - - ); - default: - return ( - <> - {renderLine('Local Balance', formatLocal)} - {renderLine('Remote Balance', formatRemote)} - - ); - } - }; - const getSubCardProps = () => { switch (channelBarStyle) { case 'ultracompact': @@ -496,7 +154,12 @@ export const ChannelCard: React.FC = ({ data-for={`node_status_tip_${index}`} > {alias || partner_public_key?.substring(0, 6)} @@ -514,7 +177,11 @@ export const ChannelCard: React.FC = ({ )} - {renderBars()} + {channelBarStyle === 'balancing' && ( = ({ - {index === indexOpen && renderDetails()} + {index === indexOpen && ( + + )} + {isBosNode && renderLine('BOS Node', 'True')} {renderLine('Status:', is_active ? 'Active' : 'Not Active')} {is_opening && renderLine('Is Opening:', 'True')} {is_closing && renderLine('Is Closing:', 'True')} @@ -559,31 +229,8 @@ export const ChannelCard: React.FC = ({ effect={'solid'} place={'bottom'} > - {renderBarsInfo()} + - setModalOpen('none')} - > - {modalOpen === 'close' ? ( - setModalOpen('none')} - channelId={id} - channelName={alias} - /> - ) : ( - setModalOpen('none')} - transaction_id={transaction_id} - transaction_vout={transaction_vout} - base_fee_mtokens={node_base} - max_htlc_mtokens={max_htlc_mtokens} - min_htlc_mtokens={min_htlc_mtokens} - fee_rate={node_rate} - cltv_delta={node_cltv} - /> - )} - ); }; diff --git a/src/views/channels/channels/ChannelDetails.tsx b/src/views/channels/channels/ChannelDetails.tsx new file mode 100644 index 00000000..67d41a25 --- /dev/null +++ b/src/views/channels/channels/ChannelDetails.tsx @@ -0,0 +1,247 @@ +import { FC, useState } from 'react'; +import { ColorButton } from 'src/components/buttons/colorButton/ColorButton'; +import { + getDateDif, + getFormatDate, + getNodeLink, + getTransactionLink, + renderLine, +} from 'src/components/generic/helpers'; +import { + DarkSubTitle, + Separation, + Sub4Title, +} from 'src/components/generic/Styled'; +import { ChangeDetails } from 'src/components/modal/changeDetails/ChangeDetails'; +import { CloseChannel } from 'src/components/modal/closeChannel/CloseChannel'; +import Modal from 'src/components/modal/ReactModal'; +import { ChannelType } from 'src/graphql/types'; +import { + blockToTime, + formatSats, + formatSeconds, + getPercent, +} from 'src/utils/helpers'; +import { FormatFnType } from 'src/components/price/Price'; +import { ChannelBosScore } from './ChannelBosScore'; +import { WUMBO_MIN_SIZE } from './Channels'; +import { WumboTag } from './Channel.style'; + +type ChannelDetailsProps = { + info: ChannelType; + format: FormatFnType; +}; + +export const ChannelDetails: FC = ({ + info: { + commit_transaction_fee, + commit_transaction_weight, + id, + is_active, + is_closing, + is_opening, + is_static_remote_key, + local_balance, + local_reserve, + partner_public_key, + received, + remote_balance, + remote_reserve, + sent, + time_offline, + time_online, + transaction_id, + transaction_vout, + unsettled_balance, + partner_node_info, + partner_fee_info, + channel_age, + pending_resume, + bosScore, + }, + format, +}) => { + const [modalOpen, setModalOpen] = useState('none'); + + const { + total_amount, + total_tokens, + incoming_tokens, + incoming_amount = 200, + outgoing_tokens, + outgoing_amount = 150, + } = pending_resume; + + const { + alias, + capacity: partnerNodeCapacity = 0, + channel_count, + updated_at, + } = partner_node_info?.node || {}; + + const { base_fee_mtokens, fee_rate, cltv_delta } = + partner_fee_info?.partner_node_policies || {}; + + const { + base_fee_mtokens: node_base, + fee_rate: node_rate, + cltv_delta: node_cltv, + max_htlc_mtokens, + min_htlc_mtokens, + } = partner_fee_info?.node_policies || {}; + + const formatLocal = format({ amount: local_balance }); + const formatRemote = format({ amount: remote_balance }); + const formatReceived = format({ amount: received }); + const formatSent = format({ amount: sent }); + const commitFee = format({ amount: commit_transaction_fee }); + const commitWeight = format({ amount: commit_transaction_weight }); + const localReserve = format({ amount: local_reserve }); + const remoteReserve = format({ amount: remote_reserve }); + const nodeCapacity = format({ amount: partnerNodeCapacity }); + + const baseFee = format({ + amount: Number(base_fee_mtokens) / 1000, + override: 'sat', + }); + + const nodeBaseFee = format({ + amount: Number(node_base) / 1000, + override: 'sat', + }); + + const feeRate = format({ amount: fee_rate, override: 'ppm' }); + const nodeFeeRate = format({ amount: node_rate, override: 'ppm' }); + + const max_htlc = Number(max_htlc_mtokens) / 1000; + const min_htlc = Number(min_htlc_mtokens) / 1000; + + const renderPartner = () => + alias ? ( + <> + {renderLine('Node Capacity:', nodeCapacity)} + {renderLine('Channel Count:', channel_count)} + {renderLine( + 'Last Update:', + `${getDateDif(updated_at)} ago (${getFormatDate(updated_at)})` + )} + {renderLine('Base Fee:', baseFee)} + {renderLine('Fee Rate:', `${feeRate}`)} + {renderLine('CTLV Delta:', cltv_delta)} + + ) : ( + Partner node not found + ); + + const renderWumboInfo = () => { + if (local_balance + remote_balance >= WUMBO_MIN_SIZE) { + return ( + <> + + This channel is Wumbo! + + ); + } + + return null; + }; + + return ( + <> + {renderWumboInfo()} + + {renderLine('Status:', is_active ? 'Active' : 'Not Active')} + {renderLine('Is Opening:', is_opening ? 'True' : 'False')} + {renderLine('Is Closing:', is_closing ? 'True' : 'False')} + {renderLine( + 'Balancedness:', + getPercent(local_balance, remote_balance) / 100 + )} + + + + {renderLine('Base Fee:', nodeBaseFee)} + {renderLine('Fee Rate:', `${nodeFeeRate}`)} + {renderLine('CTLV Delta:', node_cltv)} + {renderLine('Max HTLC (sats)', formatSats(max_htlc))} + {renderLine('Min HTLC (sats)', formatSats(min_htlc))} + setModalOpen('details')} + > + Update Details + + + {renderLine('Local Balance:', formatLocal)} + {renderLine('Remote Balance:', formatRemote)} + {renderLine('Received:', formatReceived)} + {renderLine('Sent:', formatSent)} + {renderLine('Local Reserve:', localReserve)} + {renderLine('Remote Reserve:', remoteReserve)} + + {renderLine('Node Public Key:', getNodeLink(partner_public_key))} + {renderLine('Transaction Id:', getTransactionLink(transaction_id))} + + Pending HTLCS + {!total_amount && renderLine('Total Amount', 'None')} + {renderLine('Total Amount', total_amount)} + {renderLine('Total Tokens', total_tokens)} + {renderLine('Incoming Tokens', incoming_tokens)} + {renderLine('Outgoing Tokens', outgoing_tokens)} + {renderLine('Incoming Amount', incoming_amount)} + {renderLine('Outgoing Amount', outgoing_amount)} + + {renderLine('Channel Age:', blockToTime(channel_age))} + {renderLine('Channel Block Age:', channel_age)} + {renderLine('Channel Id:', id)} + {renderLine('Commit Fee:', commitFee)} + {renderLine('Commit Weight:', commitWeight)} + + {renderLine( + 'Is Static Remote Key:', + is_static_remote_key ? 'True' : 'False' + )} + {renderLine('Time Offline:', formatSeconds(time_offline))} + {renderLine('Time Online:', formatSeconds(time_online))} + {renderLine('Transaction Vout:', transaction_vout)} + {renderLine('Unsettled Balance:', unsettled_balance)} + + Partner Node Info + {renderPartner()} + + setModalOpen('close')} + > + Close Channel + + setModalOpen('none')} + > + {modalOpen === 'close' ? ( + setModalOpen('none')} + channelId={id} + channelName={alias} + /> + ) : ( + setModalOpen('none')} + transaction_id={transaction_id} + transaction_vout={transaction_vout} + base_fee_mtokens={node_base} + max_htlc_mtokens={max_htlc_mtokens} + min_htlc_mtokens={min_htlc_mtokens} + fee_rate={node_rate} + cltv_delta={node_cltv} + /> + )} + + + ); +}; diff --git a/src/views/channels/channels/helpers.ts b/src/views/channels/channels/helpers.ts index e1928f9b..a07de302 100644 --- a/src/views/channels/channels/helpers.ts +++ b/src/views/channels/channels/helpers.ts @@ -1,17 +1,20 @@ -import { themeColors } from 'src/styles/Themes'; +import { chartColors, themeColors } from 'src/styles/Themes'; export const getTitleColor = ( active: boolean, opening: boolean, - closing: boolean + closing: boolean, + isBosNode: boolean ): string | undefined => { switch (true) { - case active: - return undefined; + case !active: + return 'red'; case opening: case closing: return themeColors.blue2; + case isBosNode && active: + return chartColors.darkyellow; default: - return 'red'; + return undefined; } }; diff --git a/src/views/scores/AreaGraph.tsx b/src/views/scores/AreaGraph.tsx index 5ec8df25..0af81d40 100644 --- a/src/views/scores/AreaGraph.tsx +++ b/src/views/scores/AreaGraph.tsx @@ -22,7 +22,7 @@ const tooltipStyles = { }; const getDate = (d: DataType) => new Date(d.date); -const getValue = (d: DataType) => d.value; +const getValue = (d: DataType) => d?.value || 0; const bisectDate = bisector(d => new Date(d.date)).left; type DataType = { diff --git a/src/views/scores/NodeGraph.tsx b/src/views/scores/NodeGraph.tsx index 0b9b9491..1dee7b33 100644 --- a/src/views/scores/NodeGraph.tsx +++ b/src/views/scores/NodeGraph.tsx @@ -50,6 +50,10 @@ export const Graph = () => { })) .reverse(); + if (!final.length) { + return null; + } + return ( diff --git a/src/views/scores/NodeScores.tsx b/src/views/scores/NodeScores.tsx index 4d31a2da..41ca6a31 100644 --- a/src/views/scores/NodeScores.tsx +++ b/src/views/scores/NodeScores.tsx @@ -1,7 +1,12 @@ import React, { FC, useEffect } from 'react'; import { useRouter } from 'next/router'; import { isArray } from 'underscore'; -import { Card, CardWithTitle, SubTitle } from 'src/components/generic/Styled'; +import { + Card, + CardWithTitle, + DarkSubTitle, + SubTitle, +} from 'src/components/generic/Styled'; import { LoadingCard } from 'src/components/loading/LoadingCard'; import { useBaseState } from 'src/context/BaseContext'; import { useGetBosNodeScoresQuery } from 'src/graphql/queries/__generated__/getBosNodeScores.generated'; @@ -59,7 +64,15 @@ export const NodeScores: FC = ({ Historical Scores - + {!tableData.length ? ( + This node has no BOS score history + ) : ( +
+ )} ); diff --git a/src/views/token/PaidCard.tsx b/src/views/token/PaidCard.tsx index 4a57b302..e4c243d9 100644 --- a/src/views/token/PaidCard.tsx +++ b/src/views/token/PaidCard.tsx @@ -27,6 +27,7 @@ export const PaidCard: FC<{ id: string }> = ({ id }) => { const [getToken, { data, loading }] = useCreateBaseTokenMutation({ onError: err => toast.error(getErrorContent(err)), variables: { id }, + refetchQueries: ['GetChannels'], }); useEffect(() => { diff --git a/src/views/token/RecoverToken.tsx b/src/views/token/RecoverToken.tsx index 39ec2aba..1da650de 100644 --- a/src/views/token/RecoverToken.tsx +++ b/src/views/token/RecoverToken.tsx @@ -15,6 +15,7 @@ export const RecoverToken = () => { const dispatch = useBaseDispatch(); const [getToken, { data, loading }] = useCreateBaseTokenMutation({ onError: err => toast.error(getErrorContent(err)), + refetchQueries: ['GetChannels'], }); useEffect(() => {