Skip to content

Commit

Permalink
[LN] Add support for watchtowers (#2638)
Browse files Browse the repository at this point in the history
This adds a watchtowers tab to delivery wtclient functions:
- list towers
- add tower
- remove tower

Co-authored-by: Matheus Degiovani <opensource@matheusd.com>
  • Loading branch information
fguisso and matheusd committed Oct 19, 2020
1 parent 797dc89 commit 42a454b
Show file tree
Hide file tree
Showing 20 changed files with 3,315 additions and 18 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Expand Up @@ -14,3 +14,5 @@ app/middleware/ln/rpc_grpc_pb.js
app/middleware/ln/rpc_pb.js
app/middleware/ln/walletunlocker_grpc_pb.js
app/middleware/ln/walletunlocker_pb.js
app/middleware/ln/wtclient_grpc_pb.js
app/middleware/ln/wtclient_pb.js
78 changes: 73 additions & 5 deletions app/actions/LNActions.js
Expand Up @@ -290,6 +290,12 @@ const connectToLNWallet = (
certPath,
macaroonPath
);
const wtClient = await ln.getWatchtowerClient(
address,
port,
certPath,
macaroonPath
);

// Ensure the dcrlnd instance and decrediton are connected to the same(ish)
// wallet. For this test to fail the user would have had to manually change a
Expand Down Expand Up @@ -318,9 +324,9 @@ const connectToLNWallet = (
);
}

dispatch({ lnClient, type: LNWALLET_CONNECT_SUCCESS });
dispatch({ lnClient, wtClient, type: LNWALLET_CONNECT_SUCCESS });

return { client: lnClient };
return { client: lnClient, wtClient };
};

const waitForDcrlndSynced = (lnClient) => async () => {
Expand Down Expand Up @@ -926,8 +932,10 @@ export const verifyBackup = (destPath) => async (dispatch, getState) => {
}
};

export const LNWALLET_GETNETWORKINFO_ATTEMPT = "LNWALLET_GETNETWORKINFO_ATTEMPT";
export const LNWALLET_GETNETWORKINFO_SUCCESS = "LNWALLET_GETNETWORKINFO_SUCCESS";
export const LNWALLET_GETNETWORKINFO_ATTEMPT =
"LNWALLET_GETNETWORKINFO_ATTEMPT";
export const LNWALLET_GETNETWORKINFO_SUCCESS =
"LNWALLET_GETNETWORKINFO_SUCCESS";
export const LNWALLET_GETNETWORKINFO_FAILED = "LNWALLET_GETNETWORKINFO_FAILED";

export const getNetworkInfo = () => async (dispatch, getState) => {
Expand All @@ -947,7 +955,7 @@ export const LNWALLET_GETNODEINFO_ATTEMPT = "LNWALLET_GETNODEINFO_ATTEMPT";
export const LNWALLET_GETNODEINFO_SUCCESS = "LNWALLET_GETNODEINFO_SUCCESS";
export const LNWALLET_GETNODEINFO_FAILED = "LNWALLET_GETNODEINFO_FAILED";

export const getNodeInfo = nodeID => async (dispatch, getState) => {
export const getNodeInfo = (nodeID) => async (dispatch, getState) => {
const { client } = getState().ln;
if (!client) throw new Error("unconnected to ln wallet");

Expand Down Expand Up @@ -976,3 +984,63 @@ export const getRoutesInfo = (nodeID, amt) => async (dispatch, getState) => {
dispatch({ error, type: LNWALLET_GETROUTESINFO_FAILED });
}
};

export const LNWALLET_ADDWATCHTOWER_ATTEMPT = "LNWALLET_ADDWATCHTOWER_ATTEMPT";
export const LNWALLET_ADDWATCHTOWER_SUCCESS = "LNWALLET_ADDWATCHTOWER_SUCCESS";
export const LNWALLET_ADDWATCHTOWER_FAILED = "LNWALLET_ADDWATCHTOWER_FAILED";

export const addWatchtower = (wtPubKey, addr) => async (
dispatch,
getState
) => {
const { wtClient } = getState().ln;
if (!wtClient) throw new Error("unconnected to ln wallet");

dispatch({ type: LNWALLET_ADDWATCHTOWER_ATTEMPT });
try {
await ln.addTower(wtClient, wtPubKey, addr);
dispatch({ type: LNWALLET_ADDWATCHTOWER_SUCCESS });
} catch (error) {
dispatch({ error, type: LNWALLET_ADDWATCHTOWER_FAILED });
}
};

export const LNWALLET_LISTWATCHTOWERS_ATTEMPT = "LNWALLET_LISTWATCHTOWERS_ATTEMPT";
export const LNWALLET_LISTWATCHTOWERS_SUCCESS = "LNWALLET_LISTWATCHTOWERS_SUCCESS";
export const LNWALLET_LISTWATCHTOWERS_FAILED = "LNWALLET_LISTWATCHTOWERS_FAILED";

export const listWatchtowers = () => async (
dispatch,
getState
) => {
const { wtClient } = getState().ln;
if (!wtClient) throw new Error("unconnected to ln wallet");

dispatch({ type: LNWALLET_LISTWATCHTOWERS_ATTEMPT });
try {
const towersList = await ln.listWatchtowers(wtClient);
dispatch({ towersList, type: LNWALLET_LISTWATCHTOWERS_SUCCESS });
} catch (error) {
dispatch({ error, type: LNWALLET_LISTWATCHTOWERS_FAILED });
}
};

export const LNWALLET_REMOVEWATCHTOWER_ATTEMPT = "LNWALLET_REMOVEWATCHTOWER_ATTEMPT";
export const LNWALLET_REMOVEWATCHTOWER_SUCCESS = "LNWALLET_REMOVEWATCHTOWER_SUCCESS";
export const LNWALLET_REMOVEWATCHTOWER_FAILED = "LNWALLET_REMOVEWATCHTOWER_FAILED";

export const removeWatchtower = wtPubKey => async (
dispatch,
getState
) => {
const { wtClient } = getState().ln;
if (!wtClient) throw new Error("unconnected to ln wallet");

dispatch({ type: LNWALLET_REMOVEWATCHTOWER_ATTEMPT });
try {
await ln.removeTower(wtClient, wtPubKey);
dispatch({ type: LNWALLET_REMOVEWATCHTOWER_SUCCESS });
} catch (error) {
dispatch({ error, type: LNWALLET_REMOVEWATCHTOWER_FAILED });
}
};
104 changes: 104 additions & 0 deletions app/components/views/LNPage/WatchtowersTab/WatchtowersTab.jsx
@@ -0,0 +1,104 @@
import { useWatchtowersTab } from "./hooks";
import { FormattedMessage as T } from "react-intl";
import { useState } from "react";
import styles from "./WatchtowersTab.module.css";
import { TextInput } from "inputs";
import { Subtitle, Tooltip } from "shared";
import { KeyBlueButton } from "buttons";
import { CopyableText } from "pi-ui";

const AddWatchtower = ({
addWatchtower,
listWatchtowers
}) => {
const [pubkey, setPubkey] = useState("");
const [addr, setAddr] = useState("");

return (
<div className={styles.addWatchtower}>
<div className={styles.addWatchtowerContent}>
<div className={styles.addWatchtowerNest}>
<div className={styles.addWatchtowerNestPrefix}>
<T id="ln.watchtowersTab.Pubkey" m="Tower ID:" />
</div>
<TextInput
value={pubkey}
onChange={(e) => setPubkey(e.target.value.trim())}
/>
</div>
<div className={styles.addWatchtowerNest}>
<div className={styles.addWatchtowerNestPrefix}>
<T id="ln.watchtowersTab.address" m="Address:" />
</div>
<TextInput
value={addr}
onChange={(e) => setAddr(e.target.value.trim())}
/>
</div>
</div>
<KeyBlueButton
className={styles.addWatchtowerButton}
onClick={() => addWatchtower(pubkey, addr).then(() =>
listWatchtowers())
}>
<T id="ln.watchtowersTab.addBtn" m="Add" />
</KeyBlueButton>

</div>
);
};

const WatchtowersTab = () => {
const {
addWatchtower,
removeWatchtower,
listWatchtowers,
towersList
} = useWatchtowersTab();

return (
<>
<Subtitle title={<T id="ln.watchtowersTab.addWatchtowerHeader" m="Add Watchtower" />} />
<AddWatchtower
addWatchtower={addWatchtower}
listWatchtowers={listWatchtowers}
towersList={towersList}
/>
{towersList.length > 0 ? (
<Subtitle title={<T id="ln.watchtowersTab.listWatchtowers" m="Watchtowers connected" />} />
) : null }

{towersList.map((tower) =>
<div
className={`
${styles.tower} ${tower.activeSessionCandidate ? styles.statusTrue : styles.statusFalse}
`}
key={tower.pubkey}
>
<Tooltip
className={styles.removeTowerBtn}
text={
<T id="ln.watchtowersTab.removeTowerBtn" m="Remove tower" />
}><a
className={styles.removeIcon}
onClick={() => {
removeWatchtower(tower.pubkeyHex);
listWatchtowers();
}}
href="#">
&times;
</a></Tooltip>
<p>Sessions: {tower.numSessions}</p>
<CopyableText id="copyable" className={styles.copyableText}>{tower.pubkeyHex}</CopyableText>
<div className={styles.addrsWrapper}>
{tower.addressesList.map((addrs) => (
<p key={`ips-${addrs}`}>{addrs}</p>
))}</div>
</div>
)}

</>
);
};

export default WatchtowersTab;
@@ -0,0 +1,81 @@
.addWatchtowerContent {
background-color: var(--background-back-color);
padding-top: 20px;
padding-left: 30px;
width: 784px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}

.addWatchtowerNest {
display: flex;
margin-bottom: 20px;
}

.addWatchtowerNest input {
width: 550px;
}

.addWatchtowerNestPrefix {
margin-right: 20px;
min-width: 125px;
margin-left: 0;
font-size: 19px;
line-height: 24px;
text-transform: capitalize;
text-align: right;
}

.addWatchtowerButton {
position: relative;
margin: 20px 0 0 730px;
}

.tower {
margin-top: 1em;
background-color: var(--background-back-color);
padding: 2em;
display: grid;
grid-template-columns: 1fr 5fr 0fr;
width: 750px;
min-height: 40px;
}

.tower.statusFalse {
border-left: 10px solid var(--vote-no-color);
}

.tower.statusTrue {
border-left: 10px solid var(--accent-color);
}

.addrsWrapper {
margin-right: 2em;
}

.removeTowerBtn { position: absolute; }

.removeIcon,
.removeIcon:link,
.removeIcon:visited {
position: absolute;
font-size: var(--font-size-xxlarge);
bottom: 0.1rem;
left: 75.5rem;
color: var(--modal-close-color);
transition: all 0.3s;
}

.removeIcon:hover,
.removeIcon:link:hover,
.removeIcon:visited:hover {
color: var(--color-primary);
}

.tower div { align-items: unset; }
.copyableText span {
font-size: 14px;
background: none;
padding: 0;
}
@@ -0,0 +1,15 @@
import { DescriptionHeader } from "layout";
import { FormattedMessage as T } from "react-intl";

const WatchtowersTabHeader = () => (
<DescriptionHeader
description={
<T
id="ln.description.watchtowers"
m="Manage connection to watchtowers."
/>
}
/>
);

export default WatchtowersTabHeader;
26 changes: 26 additions & 0 deletions app/components/views/LNPage/WatchtowersTab/hooks.js
@@ -0,0 +1,26 @@
import * as sel from "selectors";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import * as lna from "actions/LNActions";

export function useWatchtowersTab() {
const dispatch = useDispatch();
const addWatchtower = (wtPubKey, addr) =>
dispatch(lna.addWatchtower(wtPubKey, addr));
const removeWatchtower = wtPubKey =>
dispatch(lna.removeWatchtower(wtPubKey));
const listWatchtowers = () =>
dispatch(lna.listWatchtowers());
const towersList = useSelector(sel.lnTowersList);

useEffect(() => {
dispatch(lna.listWatchtowers());
}, [dispatch]);

return {
addWatchtower,
removeWatchtower,
listWatchtowers,
towersList
};
}
8 changes: 8 additions & 0 deletions app/components/views/LNPage/index.js
Expand Up @@ -9,6 +9,8 @@ import { default as InvoicesTab, InvoicesTabHeader } from "./InvoicesTab";
import { default as PaymentsTab, PaymentsTabHeader } from "./PaymentsTab";
import NetworkTabHeader from "./NetworkTab/NetworkTabHeader";
import NetworkTab from "./NetworkTab/NetworkTab";
import WatchtowersTab from "./WatchtowersTab/WatchtowersTab";
import WatchtowersTabHeader from "./WatchtowersTab/WatchtowersTabHeader";
import "style/LN.less";

const LNPageHeader = () => (
Expand Down Expand Up @@ -53,6 +55,12 @@ const LNActivePage = () => (
header={NetworkTabHeader}
link={<T id="ln.tab.network" m="Network" />}
/>
<Tab
path="/ln/watchtowers"
component={WatchtowersTab}
header={WatchtowersTabHeader}
link={<T id="ln.tab.watchtowers" m="Watchtowers" />}
/>
</TabbedPage>
);

Expand Down
3 changes: 2 additions & 1 deletion app/index.js
Expand Up @@ -417,7 +417,8 @@ const initialState = {
scbPath: "",
scbUpdatedTime: 0,
nodeInfo: null,
getNodeInfoAttempt: false
getNodeInfoAttempt: false,
towersList: []
},
locales: locales
};
Expand Down
4 changes: 3 additions & 1 deletion app/main_dev/launch.js
Expand Up @@ -708,7 +708,9 @@ export const launchDCRLnd = (
"--node=dcrw",
"--dcrwallet.grpchost=localhost:" + walletPort,
"--dcrwallet.certpath=" + path.join(walletPath, "rpc.cert"),
"--dcrwallet.accountnumber=" + walletAccount
"--dcrwallet.accountnumber=" + walletAccount,
"--wtclient.active",
"--wtclient.sweep-fee-rate=10000000" // In atoms/byte, so 1e4 (default fee rate) * 1e3
];

if (testnet) {
Expand Down

0 comments on commit 42a454b

Please sign in to comment.