Skip to content

Commit

Permalink
feat(service): add open channel support
Browse files Browse the repository at this point in the history
  • Loading branch information
Karl Ranna committed Jul 10, 2019
1 parent beb238d commit dab84af
Show file tree
Hide file tree
Showing 39 changed files with 1,726 additions and 81 deletions.
30 changes: 30 additions & 0 deletions docs/api.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion lib/Xud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import HttpServer from './http/HttpServer';
import SwapClientManager from './swaps/SwapClientManager';
import InitService from './service/InitService';
import { promises as fs } from 'fs';
import { UnitConverter } from './utils/UnitConverter';

const version: string = require('../package.json').version;

Expand All @@ -39,6 +40,7 @@ class Xud extends EventEmitter {
private swaps!: Swaps;
private shuttingDown = false;
private swapClientManager?: SwapClientManager;
private unitConverter?: UnitConverter;

/**
* Create an Exchange Union daemon.
Expand Down Expand Up @@ -67,7 +69,10 @@ class Xud extends EventEmitter {
this.db = new DB(loggers.db, this.config.dbpath);
await this.db.init(this.config.network, this.config.initdb);

this.swapClientManager = new SwapClientManager(this.config, loggers);
this.unitConverter = new UnitConverter();
this.unitConverter.init();

this.swapClientManager = new SwapClientManager(this.config, loggers, this.unitConverter);
await this.swapClientManager.init(this.db.models);

const nodeKeyPath = NodeKey.getPath(this.config.xudir, this.config.instanceid);
Expand Down
31 changes: 31 additions & 0 deletions lib/cli/commands/openchannel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Arguments } from 'yargs';
import { callback, loadXudClient } from '../command';
import { OpenChannelRequest } from '../../proto/xudrpc_pb';
import { coinsToSats } from '../utils';

export const command = 'openchannel <nodePubKey> <currency> <amount>';

export const describe = 'open a payment channel';

export const builder = {
nodePubKey: {
description: 'The node pub key of the peer to open the channel with',
type: 'string',
},
currency: {
description: 'The ticker symbol for the currency',
type: 'string',
},
amount: {
type: 'number',
description: 'The amount of the channel',
},
};

export const handler = (argv: Arguments) => {
const request = new OpenChannelRequest();
request.setNodePubKey(argv.nodePubKey);
request.setCurrency(argv.currency.toUpperCase());
request.setAmount(coinsToSats(argv.amount));
loadXudClient(argv).openChannel(request, callback(argv));
};
13 changes: 13 additions & 0 deletions lib/grpc/GrpcService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,19 @@ class GrpcService {
}
}

/**
* See [[Service.openChannel]]
*/
public openChannel: grpc.handleUnaryCall<xudrpc.OpenChannelRequest, xudrpc.OpenChannelResponse> = async (call, callback) => {
try {
await this.service.openChannel(call.request.toObject());
const response = new xudrpc.OpenChannelResponse();
callback(null, response);
} catch (err) {
callback(this.getGrpcError(err), null);
}
}

/**
* See [[Service.connect]]
*/
Expand Down
86 changes: 76 additions & 10 deletions lib/lndclient/LndClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import grpc, { ChannelCredentials, ClientReadableStream } from 'grpc';
import Logger from '../Logger';
import SwapClient, { ClientStatus } from '../swaps/SwapClient';
import SwapClient, { ClientStatus, SwapClientInfo } from '../swaps/SwapClient';
import errors from './errors';
import { LightningClient, WalletUnlockerClient } from '../proto/lndrpc_grpc_pb';
import { InvoicesClient } from '../proto/lndinvoices_grpc_pb';
Expand All @@ -26,9 +26,9 @@ interface InvoicesMethodIndex extends InvoicesClient {
}

interface LndClient {
on(event: 'connectionVerified', listener: (newIdentifier?: string) => void): this;
on(event: 'connectionVerified', listener: (swapClientInfo: SwapClientInfo) => void): this;
on(event: 'htlcAccepted', listener: (rHash: string, amount: number) => void): this;
emit(event: 'connectionVerified', newIdentifier?: string): boolean;
emit(event: 'connectionVerified', swapClientInfo: SwapClientInfo): boolean;
emit(event: 'htlcAccepted', rHash: string, amount: number): boolean;
}

Expand All @@ -44,6 +44,8 @@ class LndClient extends SwapClient {
private credentials!: ChannelCredentials;
/** The identity pub key for this lnd instance. */
private identityPubKey?: string;
/** List of client's public listening uris that are advertised to the network */
private urisList?: string[];
/** The identifier for the chain this lnd instance is using in the format [chain]-[network] like "bitcoin-testnet" */
private chainIdentifier?: string;
private channelSubscription?: ClientReadableStream<lndrpc.ChannelEventUpdate>;
Expand Down Expand Up @@ -99,6 +101,10 @@ class LndClient extends SwapClient {
return this.identityPubKey;
}

public get uris() {
return this.urisList;
}

public get chain() {
return this.chainIdentifier;
}
Expand Down Expand Up @@ -209,10 +215,14 @@ class LndClient extends SwapClient {

/** The new lnd pub key value if different from the one we had previously. */
let newPubKey: string | undefined;
let newUris: string[] = [];
if (this.identityPubKey !== getInfoResponse.getIdentityPubkey()) {
newPubKey = getInfoResponse.getIdentityPubkey();
this.logger.debug(`pubkey is ${newPubKey}`);
this.identityPubKey = newPubKey;
newUris = getInfoResponse.getUrisList();
this.logger.debug(`uris are ${newUris}`);
this.urisList = newUris;
}
const chain = getInfoResponse.getChainsList()[0];
const chainIdentifier = `${chain.getChain()}-${chain.getNetwork()}`;
Expand All @@ -224,7 +234,10 @@ class LndClient extends SwapClient {
this.logger.error(`chain switched from ${this.chainIdentifier} to ${chainIdentifier}`);
await this.setStatus(ClientStatus.Disabled);
}
this.emit('connectionVerified', newPubKey);
this.emit('connectionVerified', {
newUris,
newIdentifier: newPubKey,
});

this.invoices = new InvoicesClient(this.uri, this.credentials);

Expand Down Expand Up @@ -382,19 +395,72 @@ class LndClient extends SwapClient {
/**
* Connects to another lnd node.
*/
public connectPeer = (pubkey: string, host: string, port: number): Promise<lndrpc.ConnectPeerResponse> => {
public connectPeer = (pubkey: string, address: string): Promise<lndrpc.ConnectPeerResponse> => {
const request = new lndrpc.ConnectPeerRequest();
const address = new lndrpc.LightningAddress();
address.setHost(`${host}:${port}`);
address.setPubkey(pubkey);
request.setAddr(address);
const lightningAddress = new lndrpc.LightningAddress();
lightningAddress.setHost(address);
lightningAddress.setPubkey(pubkey);
request.setAddr(lightningAddress);
return this.unaryCall<lndrpc.ConnectPeerRequest, lndrpc.ConnectPeerResponse>('connectPeer', request);
}

/**
* Opens a channel given peerPubKey and amount.
*/
public openChannel = async (
{ peerIdentifier: peerPubKey, units, lndUris }:
{ peerIdentifier: string, units: number, lndUris: string[] },
): Promise<void> => {
const connectionEstablished = await this.connectPeerAddreses(lndUris);
if (connectionEstablished) {
await this.openChannelSync(peerPubKey, units);
} else {
throw new Error('connectPeerAddreses failed');
}
}

/**
* Tries to connect to a given list of peer's node addresses
* in a sequential order.
* Returns true when successful, otherwise false.
*/
private connectPeerAddreses = async (
peerListeningUris: string[],
): Promise<boolean> => {
const splitListeningUris = peerListeningUris
.map((uri) => {
const splitUri = uri.split('@');
return {
peerPubKey: splitUri[0],
address: splitUri[1],
};
});
const CONNECT_TIMEOUT = 4000;
for (const uri of splitListeningUris) {
const { peerPubKey, address } = uri;
let timeout;
try {
timeout = setTimeout(() => {
throw new Error('connectPeer has timed out');
}, CONNECT_TIMEOUT);
await this.connectPeer(peerPubKey, address);
return true;
} catch (e) {
if (e.message && e.message.includes('already connected')) {
return true;
}
this.logger.trace(`connectPeer failed: ${e}`);
} finally {
timeout && clearTimeout(timeout);
}
}
return false;
}

/**
* Opens a channel with a connected lnd node.
*/
public openChannel = (node_pubkey_string: string, local_funding_amount: number): Promise<lndrpc.ChannelPoint> => {
private openChannelSync = (node_pubkey_string: string, local_funding_amount: number): Promise<lndrpc.ChannelPoint> => {
const request = new lndrpc.OpenChannelRequest;
request.setNodePubkeyString(node_pubkey_string);
request.setLocalFundingAmount(local_funding_amount);
Expand Down
11 changes: 11 additions & 0 deletions lib/p2p/Peer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,17 @@ class Peer extends EventEmitter {

public isPairActive = (pairId: string) => this.activePairs.has(pairId);

/**
* Gets lnd client's listening uris for the provided currency.
* @param currency
*/
public getLndUris(currency: string): string[] | undefined {
if (this.nodeState && this.nodeState.lndUris) {
return this.nodeState.lndUris[currency];
}
return;
}

private sendRaw = (data: Buffer) => {
if (this.socket && !this.socket.destroyed) {
try {
Expand Down
9 changes: 8 additions & 1 deletion lib/p2p/Pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ class Pool extends EventEmitter {
pairs: [],
raidenAddress: '',
lndPubKeys: {},
lndUris: {},
tokenIdentifiers: {},
};

Expand Down Expand Up @@ -222,8 +223,14 @@ class Pool extends EventEmitter {
* Updates our lnd pub key and chain identifier for a given currency and sends a node state
* update packet to currently connected peers to notify them of the change.
*/
public updateLndState = (currency: string, pubKey: string, chain?: string) => {
public updateLndState = (
{ currency, pubKey, chain, uris }:
{ currency: string, pubKey: string, chain?: string, uris?: string[] },
) => {
this.nodeState.lndPubKeys[currency] = pubKey;
if (uris) {
this.nodeState.lndUris[currency] = uris;
}
this.nodeState.tokenIdentifiers[currency] = chain;
this.sendNodeStateUpdate();
}
Expand Down
26 changes: 26 additions & 0 deletions lib/p2p/packets/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const validateNodeState = (nodeState?: pb.NodeState.AsObject) => {
&& nodeState.pairsList
&& nodeState.lndPubKeysMap
&& nodeState.tokenIdentifiersMap
&& nodeState.lndUrisMap
&& nodeState.addressesList.every(addr => !!addr.host)
);
};
Expand All @@ -18,10 +19,32 @@ export const convertNodeState = (nodeState: pb.NodeState.AsObject) => {
addresses: nodeState.addressesList,
raidenAddress: nodeState.raidenAddress,
lndPubKeys: convertKvpArrayToKvps(nodeState.lndPubKeysMap),
lndUris: convertLndUris(nodeState.lndUrisMap),
tokenIdentifiers: convertKvpArrayToKvps(nodeState.tokenIdentifiersMap),
});
};

const convertLndUris =
(kvpArray: [string, pb.LndUris.AsObject][]):
{ [key: string]: string[] } => {
const kvps: { [key: string]: string[] } = {};
kvpArray.forEach((kvp) => {
kvps[kvp[0]] = kvp[1].lndUriList;
});

return kvps;
};

const setLndUrisMap = (obj: any, map: { set: (key: string, value: any) => any }) => {
for (const key in obj) {
if (obj[key] !== undefined) {
const lndUris = new pb.LndUris();
lndUris.setLndUriList(obj[key]);
map.set(key, lndUris);
}
}
};

export const serializeNodeState = (nodeState: NodeState): pb.NodeState => {
const pbNodeState = new pb.NodeState();
pbNodeState.setPairsList(nodeState.pairs);
Expand All @@ -35,6 +58,9 @@ export const serializeNodeState = (nodeState: NodeState): pb.NodeState => {
if (nodeState.lndPubKeys) {
setObjectToMap(nodeState.lndPubKeys, pbNodeState.getLndPubKeysMap());
}
if (nodeState.lndUris) {
setLndUrisMap(nodeState.lndUris, pbNodeState.getLndUrisMap());
}
if (nodeState.tokenIdentifiers) {
setObjectToMap(nodeState.tokenIdentifiers, pbNodeState.getTokenIdentifiersMap());
}
Expand Down
2 changes: 2 additions & 0 deletions lib/p2p/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export type NodeState = {
raidenAddress: string;
/** An object mapping currency symbols to lnd pub keys. */
lndPubKeys: { [currency: string]: string | undefined };
/** An object mapping currency symbols to lnd listening uris */
lndUris: { [currency: string]: string[] | undefined };
/** An object mapping currency symbols to token identifiers such as lnd chains or raiden token contract addresses. */
tokenIdentifiers: { [currency: string]: string | undefined };
};
Expand Down
Loading

0 comments on commit dab84af

Please sign in to comment.