feat: balance from channel view

This commit is contained in:
AP 2020-06-27 19:15:49 +02:00
parent 76aacd3c52
commit 830586b6bd
15 changed files with 361 additions and 55 deletions

View file

@ -2,20 +2,18 @@ import * as React from 'react';
import { ThemeProvider } from 'styled-components';
import { ModalProvider, BaseModalBackground } from 'styled-react-modal';
import { useRouter } from 'next/router';
import { toast } from 'react-toastify';
import Head from 'next/head';
import { StyledToastContainer } from 'src/components/toastContainer/ToastContainer';
import { ContextProvider } from '../src/context/ContextProvider';
import { useConfigState, ConfigProvider } from '../src/context/ConfigContext';
import { GlobalStyles } from '../src/styles/GlobalStyle';
import { Header } from '../src/layouts/header/Header';
import { Footer } from '../src/layouts/footer/Footer';
import 'react-toastify/dist/ReactToastify.css';
import 'react-toastify/dist/ReactToastify.min.css';
import { PageWrapper, HeaderBodyWrapper } from '../src/layouts/Layout.styled';
import { parseCookies } from '../src/utils/cookies';
import 'react-circular-progressbar/dist/styles.css';
toast.configure({ draggable: false, pauseOnFocusLoss: false });
const Wrapper: React.FC = ({ children }) => {
const { theme } = useConfigState();
const { pathname } = useRouter();
@ -50,6 +48,7 @@ const App = ({ Component, pageProps, initialConfig }) => (
</Wrapper>
</ContextProvider>
</ConfigProvider>
<StyledToastContainer />
</>
);

View file

@ -76,14 +76,14 @@ export const bosResolvers = {
const lnd = getLnd(auth, context);
const filteredParams = {
...(avoid.length > 0 && { avoid }),
avoid,
out_channels,
...(in_through && { in_through }),
...(is_avoiding_high_inbound && { is_avoiding_high_inbound }),
...(max_fee > 0 && { max_fee }),
...(max_fee_rate > 0 && { max_fee_rate }),
...(max_rebalance > 0 && { max_rebalance }),
...(node && { node }),
...(out_channels.length > 0 && { out_channels }),
...(out_through && { out_through }),
...(target && { target }),
};

View file

@ -1,4 +1,4 @@
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import {
progressBackground,
mediaWidths,
@ -65,8 +65,16 @@ export const StatusLine = styled.div`
margin: 0 0 -8px 0;
`;
export const MainInfo = styled.div`
cursor: pointer;
type MainProps = {
disabled?: boolean;
};
export const MainInfo = styled.div<MainProps>`
${({ disabled }) =>
!disabled &&
css`
cursor: pointer;
`}
`;
export const StatusDot = styled.div`

View file

@ -1,5 +1,5 @@
import styled from 'styled-components';
import { chartColors } from 'src/styles/Themes';
import { chartColors, mediaWidths } from 'src/styles/Themes';
export const BetaNotification = styled.div`
width: 100%;
@ -9,4 +9,9 @@ export const BetaNotification = styled.div`
color: black;
margin-bottom: 16px;
padding: 4px 0;
@media (${mediaWidths.mobile}) {
margin-top: 8px;
margin-bottom: 8px;
}
`;

View file

@ -0,0 +1,22 @@
import styled from 'styled-components';
import { ToastContainer } from 'react-toastify';
import { themeColors, chartColors } from 'src/styles/Themes';
export const StyledToastContainer = styled(ToastContainer)`
.Toastify__toast {
border-radius: 4px;
background-color: ${themeColors.blue2};
}
.Toastify__toast--error {
border-radius: 4px;
background-color: ${chartColors.red};
}
.Toastify__toast--warning {
border-radius: 4px;
background-color: ${chartColors.orange};
}
.Toastify__toast--success {
border-radius: 4px;
background-color: ${chartColors.green};
}
`;

View file

@ -6,7 +6,11 @@ import omit from 'lodash.omit';
const themeTypes = ['dark', 'light'];
const currencyTypes = ['sat', 'btc', 'fiat'];
export type channelBarStyleTypes = 'normal' | 'compact' | 'ultracompact';
export type channelBarStyleTypes =
| 'normal'
| 'compact'
| 'ultracompact'
| 'balancing';
export type channelBarTypeTypes = 'balance' | 'fees' | 'size' | 'proportional';
export type channelSortTypes =
| 'none'

View file

@ -4,13 +4,16 @@ import { BitcoinInfoProvider } from './BitcoinContext';
import { StatusProvider } from './StatusContext';
import { PriceProvider } from './PriceContext';
import { ChatProvider } from './ChatContext';
import { RebalanceProvider } from './RebalanceContext';
export const ContextProvider: React.FC = ({ children }) => (
<AccountProvider>
<BitcoinInfoProvider>
<PriceProvider>
<ChatProvider>
<StatusProvider>{children}</StatusProvider>
<StatusProvider>
<RebalanceProvider>{children}</RebalanceProvider>
</StatusProvider>
</ChatProvider>
</PriceProvider>
</BitcoinInfoProvider>

View file

@ -0,0 +1,75 @@
import React, { createContext, useContext, useReducer } from 'react';
import { ChannelType } from 'src/graphql/types';
type State = {
inChannel: ChannelType | null;
outChannel: ChannelType | null;
};
type ActionType =
| {
type: 'setIn';
channel: ChannelType | null;
}
| {
type: 'setOut';
channel: ChannelType | null;
}
| {
type: 'clear';
};
type Dispatch = (action: ActionType) => void;
export const StateContext = createContext<State | undefined>(undefined);
export const DispatchContext = createContext<Dispatch | undefined>(undefined);
const initialState = {
inChannel: null,
outChannel: null,
};
const stateReducer = (state: State, action: ActionType): State => {
switch (action.type) {
case 'setIn':
return { ...state, inChannel: action.channel };
case 'setOut':
return { ...state, outChannel: action.channel };
case 'clear':
return initialState;
default:
return state;
}
};
const RebalanceProvider = ({ children }) => {
const [state, dispatch] = useReducer(stateReducer, initialState);
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>{children}</StateContext.Provider>
</DispatchContext.Provider>
);
};
const useRebalanceState = () => {
const context = useContext(StateContext);
if (context === undefined) {
throw new Error(
'useRebalanceState must be used within a RebalanceProvider'
);
}
return context;
};
const useRebalanceDispatch = () => {
const context = useContext(DispatchContext);
if (context === undefined) {
throw new Error(
'useRebalanceDispatch must be used within a RebalanceProvider'
);
}
return context;
};
export { RebalanceProvider, useRebalanceState, useRebalanceDispatch };

View file

@ -16,6 +16,10 @@ import { chartColors } from 'src/styles/Themes';
import { ViewSwitch } from 'src/components/viewSwitch/ViewSwitch';
import { useMutationResultWithReset } from 'src/hooks/UseMutationWithReset';
import { BetaNotification } from 'src/components/notification/Beta';
import {
useRebalanceState,
useRebalanceDispatch,
} from 'src/context/RebalanceContext';
import { AdvancedResult } from './AdvancedResult';
import { ModalNodes } from './Modals/ModalNodes';
import { ModalChannels } from './Modals/ModalChannels';
@ -165,7 +169,29 @@ const SettingLine: React.FC<{ title: string }> = ({ children, title }) => (
export const AdvancedBalance = () => {
const [openType, openTypeSet] = React.useState<string>('none');
const [isDetailed, isDetailedSet] = React.useState<boolean>(false);
const [state, dispatch] = React.useReducer(reducer, initialState);
const rebalanceDispatch = useRebalanceDispatch();
const { inChannel, outChannel } = useRebalanceState();
const in_through = inChannel
? {
alias: inChannel.partner_node_info?.node?.alias,
id: inChannel.partner_public_key,
}
: defaultRebalanceId;
const out_through = outChannel
? {
alias: outChannel.partner_node_info?.node?.alias,
id: outChannel.partner_public_key,
}
: defaultRebalanceId;
const [state, dispatch] = React.useReducer(reducer, {
...initialState,
in_through,
out_through,
});
const [rebalance, { data: _data, loading }] = useBosRebalanceMutation({
onError: error => toast.error(getErrorContent(error)),
@ -231,21 +257,6 @@ export const AdvancedBalance = () => {
{hasAvoid ? <Minus size={18} /> : <Plus size={18} />}
</ColorButton>
</SettingLine>
<SettingLine title={'Decrease Inbound Of'}>
{hasInChannel ? (
<RebalanceTag>{state.in_through.alias}</RebalanceTag>
) : null}
<ColorButton
color={hasInChannel ? chartColors.red : undefined}
onClick={() =>
hasInChannel
? dispatch({ type: 'inChannel', channel: defaultRebalanceId })
: openTypeSet('inChannel')
}
>
{hasInChannel ? <Minus size={18} /> : <Plus size={18} />}
</ColorButton>
</SettingLine>
{!hasOutChannels && (
<SettingLine title={'Increase Inbound Of'}>
{hasOutChannel ? (
@ -253,19 +264,40 @@ export const AdvancedBalance = () => {
) : null}
<ColorButton
color={hasOutChannel ? chartColors.red : undefined}
onClick={() =>
hasOutChannel
? dispatch({
type: 'outChannel',
channel: defaultRebalanceId,
})
: openTypeSet('outChannel')
}
onClick={() => {
if (hasOutChannel) {
rebalanceDispatch({ type: 'setOut', channel: null });
dispatch({
type: 'outChannel',
channel: defaultRebalanceId,
});
} else {
openTypeSet('outChannel');
}
}}
>
{hasOutChannel ? <Minus size={18} /> : <Plus size={18} />}
</ColorButton>
</SettingLine>
)}
<SettingLine title={'Decrease Inbound Of'}>
{hasInChannel ? (
<RebalanceTag>{state.in_through.alias}</RebalanceTag>
) : null}
<ColorButton
color={hasInChannel ? chartColors.red : undefined}
onClick={() => {
if (hasInChannel) {
rebalanceDispatch({ type: 'setIn', channel: null });
dispatch({ type: 'inChannel', channel: defaultRebalanceId });
} else {
openTypeSet('inChannel');
}
}}
>
{hasInChannel ? <Minus size={18} /> : <Plus size={18} />}
</ColorButton>
</SettingLine>
{!hasOutChannel && (
<SettingLine title={'Out Through Channels'}>
{hasOutChannels && (

View file

@ -74,6 +74,13 @@ export const RebalanceTag = styled.div`
border-radius: 4px;
margin-right: 8px;
font-size: 14px;
@media (${mediaWidths.mobile}) {
max-width: 80px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
`;
export const RebalanceLine = styled(SingleLine)`

View file

@ -18,16 +18,19 @@ import { Input } from 'src/components/input/Input';
import { BalanceCard } from 'src/views/balance/BalanceCard';
import { BalanceRoute } from 'src/views/balance/BalanceRoute';
import { Price } from 'src/components/price/Price';
import {
useRebalanceState,
useRebalanceDispatch,
} from 'src/context/RebalanceContext';
export const SimpleBalance = () => {
const { auth } = useAccountState();
const [outgoing, setOutgoing] = useState<ChannelType | null>();
const [incoming, setIncoming] = useState<ChannelType | null>();
const dispatch = useRebalanceDispatch();
const { inChannel: incoming, outChannel: outgoing } = useRebalanceState();
const [amount, setAmount] = useState<number>();
const [maxFee, setMaxFee] = useState<number>();
const [blocked, setBlocked] = useState(false);
const { loading, data } = useGetChannelsQuery({
@ -43,17 +46,15 @@ export const SimpleBalance = () => {
const handleReset = (type: string) => {
switch (type) {
case 'outgoing':
setOutgoing(undefined);
setIncoming(undefined);
dispatch({ type: 'clear' });
break;
case 'incoming':
setIncoming(undefined);
dispatch({ type: 'setIn', channel: null });
break;
case 'all':
dispatch({ type: 'clear' });
setMaxFee(undefined);
setAmount(undefined);
setOutgoing(undefined);
setIncoming(undefined);
setBlocked(false);
break;
default:
@ -91,8 +92,9 @@ export const SimpleBalance = () => {
}
const callback = isOutgoing
? !outgoing && { callback: () => setOutgoing(channel) }
: outgoing && !incoming && { callback: () => setIncoming(channel) };
? !outgoing && { callback: () => dispatch({ type: 'setOut', channel }) }
: outgoing &&
!incoming && { callback: () => dispatch({ type: 'setIn', channel }) };
return (
<BalanceCard

View file

@ -1,5 +1,12 @@
import styled from 'styled-components';
import { mediaWidths } from 'src/styles/Themes';
import styled, { css } from 'styled-components';
import {
mediaWidths,
colorButtonBackground,
textColor,
colorButtonBorder,
chartColors,
disabledTextColor,
} from 'src/styles/Themes';
export const ChannelIconPadding = styled.div`
display: flex;
@ -21,11 +28,11 @@ export const ChannelStatsLine = styled.div`
export const ChannelBarSide = styled.div`
width: 50%;
display: flex;
flex-direction: column;
cursor: pointer;
align-items: center;
@media (${mediaWidths.mobile}) {
width: 100%;
flex-direction: column;
}
`;
@ -53,3 +60,58 @@ export const IconCursor = styled.div`
cursor: pointer;
margin-left: 8px;
`;
export const ChannelBalanceRow = styled.div`
display: flex;
@media (${mediaWidths.mobile}) {
width: 100%;
}
`;
type BalanceButtonProps = {
selected?: boolean;
disabled?: boolean;
};
export const ChannelBalanceButton = styled.button<BalanceButtonProps>`
display: flex;
justify-content: center;
align-items: center;
outline: none;
padding: 6px 8px;
border: none;
text-decoration: none;
border-radius: 4px;
white-space: nowrap;
font-size: 14px;
box-sizing: border-box;
margin-left: 8px;
color: ${({ disabled }) => (disabled ? disabledTextColor : textColor)};
${({ disabled }) =>
!disabled &&
css`
cursor: pointer;
`}
background-color: ${({ selected }) =>
selected ? chartColors.orange : colorButtonBackground};
@media(${mediaWidths.mobile}) {
margin: 8px 8px 16px;
width: 100%;
}
:hover {
background-color: ${({ selected, disabled }) =>
disabled
? colorButtonBackground
: selected
? chartColors.orange2
: colorButtonBorder};
}
`;
export const ChannelGoToToast = styled.div`
width: 100%;
text-align: center;
`;

View file

@ -1,8 +1,19 @@
import React, { useState } from 'react';
import ReactTooltip from 'react-tooltip';
import { ArrowDown, ArrowUp, EyeOff } from 'react-feather';
import {
ArrowDown,
ArrowUp,
EyeOff,
ChevronsUp,
ChevronsDown,
X,
} from 'react-feather';
import { ChannelType } from 'src/graphql/types';
import { BalanceBars } from 'src/components/balance';
import {
useRebalanceState,
useRebalanceDispatch,
} from 'src/context/RebalanceContext';
import { getPercent, formatSeconds } from '../../../utils/helpers';
import {
ProgressBar,
@ -40,6 +51,8 @@ import {
ChannelStatsColumn,
ChannelSingleLine,
ChannelStatsLine,
ChannelBalanceRow,
ChannelBalanceButton,
} from './Channel.style';
const getSymbol = (status: boolean) => {
@ -78,6 +91,9 @@ export const ChannelCard: React.FC<ChannelCardProps> = ({
biggestBaseFee,
biggestRateFee,
}) => {
const dispatch = useRebalanceDispatch();
const { inChannel, outChannel } = useRebalanceState();
const { channelBarType, channelBarStyle } = useConfigState();
const [modalOpen, setModalOpen] = useState(false);
@ -114,6 +130,9 @@ export const ChannelCard: React.FC<ChannelCardProps> = ({
partner_fee_info,
} = channelInfo;
const isIn = inChannel?.id === id;
const isOut = outChannel?.id === id;
const {
alias,
capacity: partnerNodeCapacity = 0,
@ -340,6 +359,7 @@ export const ChannelCard: React.FC<ChannelCardProps> = ({
const getSubCardProps = () => {
switch (channelBarStyle) {
case 'ultracompact':
case 'balancing':
return {
withMargin: '0 0 4px 0',
padding: index === indexOpen ? '0 0 16px' : '2px 0',
@ -357,7 +377,10 @@ export const ChannelCard: React.FC<ChannelCardProps> = ({
return (
<SubCard key={`${index}-${id}`} noCard={true} {...getSubCardProps()}>
<MainInfo onClick={() => handleClick()}>
<MainInfo
disabled={channelBarStyle === 'balancing'}
onClick={() => channelBarStyle !== 'balancing' && handleClick()}
>
{channelBarStyle === 'normal' && (
<StatusLine>
{getStatusDot(is_active, 'active')}
@ -368,7 +391,10 @@ export const ChannelCard: React.FC<ChannelCardProps> = ({
<ResponsiveLine>
<ChannelNodeTitle style={{ flexGrow: 2 }}>
{alias || partner_public_key?.substring(0, 6)}
{channelBarStyle !== 'ultracompact' && (
{!(
channelBarStyle === 'ultracompact' ||
channelBarStyle === 'balancing'
) && (
<ChannelSingleLine>
<DarkSubTitle>{formatBalance}</DarkSubTitle>
<ChannelIconPadding>
@ -380,6 +406,32 @@ export const ChannelCard: React.FC<ChannelCardProps> = ({
</ChannelNodeTitle>
<ChannelBarSide data-tip data-for={`node_balance_tip_${index}`}>
{renderBars()}
{channelBarStyle === 'balancing' && (
<ChannelBalanceRow>
<ChannelBalanceButton
selected={isOut}
disabled={isIn}
onClick={() =>
isOut
? dispatch({ type: 'setOut', channel: null })
: dispatch({ type: 'setOut', channel: channelInfo })
}
>
{isOut ? <X size={16} /> : <ChevronsUp size={16} />}
</ChannelBalanceButton>
<ChannelBalanceButton
selected={isIn}
disabled={isOut}
onClick={() =>
isIn
? dispatch({ type: 'setIn', channel: null })
: dispatch({ type: 'setIn', channel: channelInfo })
}
>
{isIn ? <X size={16} /> : <ChevronsDown size={16} />}
</ChannelBalanceButton>
</ChannelBalanceRow>
)}
</ChannelBarSide>
</ResponsiveLine>
</MainInfo>

View file

@ -59,6 +59,12 @@ export const ChannelManage = () => {
>
Ultra-Compact
</SingleButton>
<SingleButton
selected={channelBarStyle === 'balancing'}
onClick={() => changeStyle('balancing')}
>
Balancing
</SingleButton>
</MultiButton>
</MarginLine>
<MarginLine>

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { toast } from 'react-toastify';
import { useAccountState } from 'src/context/AccountContext';
import { useGetChannelsQuery } from 'src/graphql/queries/__generated__/getChannels.generated';
@ -6,17 +6,46 @@ import { useConfigState } from 'src/context/ConfigContext';
import { sortBy } from 'underscore';
import { getPercent } from 'src/utils/helpers';
import { ChannelType } from 'src/graphql/types';
import { useRebalanceState } from 'src/context/RebalanceContext';
import { useRouter } from 'next/router';
import { appendBasePath } from 'src/utils/basePath';
import { Card } from '../../../components/generic/Styled';
import { getErrorContent } from '../../../utils/error';
import { LoadingCard } from '../../../components/loading/LoadingCard';
import { ChannelCard } from './ChannelCard';
import { ChannelGoToToast } from './Channel.style';
export const Channels: React.FC = () => {
const toastId = useRef(null);
const { push } = useRouter();
const { sortDirection, channelSort } = useConfigState();
const [indexOpen, setIndexOpen] = useState(0);
const { auth } = useAccountState();
const { inChannel, outChannel } = useRebalanceState();
const hasIn = !!inChannel;
const hasOut = !!outChannel;
useEffect(() => {
if (hasIn && hasOut) {
toastId.current = toast.info(
<ChannelGoToToast>Click to go to rebalancing</ChannelGoToToast>,
{
position: 'bottom-right',
autoClose: false,
closeButton: false,
onClick: () => push(appendBasePath('/rebalance')),
}
);
}
if (!hasIn || !hasOut) {
toast.dismiss(toastId.current);
}
return () => toast.dismiss();
}, [hasIn, hasOut, push]);
const { loading, data } = useGetChannelsQuery({
skip: !auth,
variables: { auth },