diff --git a/docs/api.md b/docs/api.md index 42bbafc98..92edf033a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -38,6 +38,8 @@ - [ListPeersResponse](#xudrpc.ListPeersResponse) - [LndChannels](#xudrpc.LndChannels) - [LndInfo](#xudrpc.LndInfo) + - [OpenChannelRequest](#xudrpc.OpenChannelRequest) + - [OpenChannelResponse](#xudrpc.OpenChannelResponse) - [Order](#xudrpc.Order) - [OrderRemoval](#xudrpc.OrderRemoval) - [OrderUpdate](#xudrpc.OrderUpdate) @@ -586,6 +588,33 @@ + + +### OpenChannelRequest + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| node_pub_key | [string](#string) | | The node pub key of the peer with which to open channel with. | +| currency | [string](#string) | | The ticker symbol of the currency to open the channel for. | +| amount | [int64](#int64) | | The amount of the channel denominated in satoshis. | + + + + + + + + +### OpenChannelResponse + + + + + + + ### Order @@ -1069,6 +1098,7 @@ | AddPair | [AddPairRequest](#xudrpc.AddPairRequest) | [AddPairResponse](#xudrpc.AddPairResponse) | Adds a trading pair to the list of supported trading pairs. The newly supported pair is advertised to peers so they may begin sending orders for it. | | RemoveOrder | [RemoveOrderRequest](#xudrpc.RemoveOrderRequest) | [RemoveOrderResponse](#xudrpc.RemoveOrderResponse) | Removes an order from the order book by its local id. This should be called when an order is canceled or filled outside of xud. Removed orders become immediately unavailable for swaps, and peers are notified that the order is no longer valid. Any portion of the order that is on hold due to ongoing swaps will not be removed until after the swap attempts complete. | | ChannelBalance | [ChannelBalanceRequest](#xudrpc.ChannelBalanceRequest) | [ChannelBalanceResponse](#xudrpc.ChannelBalanceResponse) | Gets the total balance available across all payment channels for one or all currencies. | +| OpenChannel | [OpenChannelRequest](#xudrpc.OpenChannelRequest) | [OpenChannelResponse](#xudrpc.OpenChannelResponse) | Opens a payment channel to a peer with the given node pub key for the specified amount and currency. | | Connect | [ConnectRequest](#xudrpc.ConnectRequest) | [ConnectResponse](#xudrpc.ConnectResponse) | Attempts to connect to a node. Once connected, the node is added to the list of peers and becomes available for swaps and trading. A handshake exchanges information about the peer's supported trading and swap clients. Orders will be shared with the peer upon connection and upon new order placements. | | Ban | [BanRequest](#xudrpc.BanRequest) | [BanResponse](#xudrpc.BanResponse) | Bans a node and immediately disconnects from it. This can be used to prevent any connections to a specific node. | | Unban | [UnbanRequest](#xudrpc.UnbanRequest) | [UnbanResponse](#xudrpc.UnbanResponse) | Removes a ban from a node manually and, optionally, attempts to connect to it. | diff --git a/lib/Xud.ts b/lib/Xud.ts index 944c686dd..c89f8e069 100644 --- a/lib/Xud.ts +++ b/lib/Xud.ts @@ -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; @@ -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. @@ -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); diff --git a/lib/cli/commands/openchannel.ts b/lib/cli/commands/openchannel.ts new file mode 100644 index 000000000..3a8a238d3 --- /dev/null +++ b/lib/cli/commands/openchannel.ts @@ -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 '; + +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)); +}; diff --git a/lib/grpc/GrpcService.ts b/lib/grpc/GrpcService.ts index 5e6e63a80..5a4aac43d 100644 --- a/lib/grpc/GrpcService.ts +++ b/lib/grpc/GrpcService.ts @@ -258,6 +258,19 @@ class GrpcService { } } + /** + * See [[Service.openChannel]] + */ + public openChannel: grpc.handleUnaryCall = 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]] */ diff --git a/lib/lndclient/LndClient.ts b/lib/lndclient/LndClient.ts index aa716be46..3be5a68d2 100644 --- a/lib/lndclient/LndClient.ts +++ b/lib/lndclient/LndClient.ts @@ -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'; @@ -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; } @@ -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; @@ -99,6 +101,10 @@ class LndClient extends SwapClient { return this.identityPubKey; } + public get uris() { + return this.urisList; + } + public get chain() { return this.chainIdentifier; } @@ -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()}`; @@ -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); @@ -382,19 +395,72 @@ class LndClient extends SwapClient { /** * Connects to another lnd node. */ - public connectPeer = (pubkey: string, host: string, port: number): Promise => { + public connectPeer = (pubkey: string, address: string): Promise => { 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('connectPeer', request); } + /** + * Opens a channel given peerPubKey and amount. + */ + public openChannel = async ( + { peerIdentifier: peerPubKey, units, lndUris }: + { peerIdentifier: string, units: number, lndUris: string[] }, + ): Promise => { + 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 => { + 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 => { + private openChannelSync = (node_pubkey_string: string, local_funding_amount: number): Promise => { const request = new lndrpc.OpenChannelRequest; request.setNodePubkeyString(node_pubkey_string); request.setLocalFundingAmount(local_funding_amount); diff --git a/lib/p2p/Peer.ts b/lib/p2p/Peer.ts index b9eab2172..8e121900a 100644 --- a/lib/p2p/Peer.ts +++ b/lib/p2p/Peer.ts @@ -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 { diff --git a/lib/p2p/Pool.ts b/lib/p2p/Pool.ts index ba50cc978..0a7d37bfd 100644 --- a/lib/p2p/Pool.ts +++ b/lib/p2p/Pool.ts @@ -117,6 +117,7 @@ class Pool extends EventEmitter { pairs: [], raidenAddress: '', lndPubKeys: {}, + lndUris: {}, tokenIdentifiers: {}, }; @@ -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(); } diff --git a/lib/p2p/packets/utils.ts b/lib/p2p/packets/utils.ts index 84d2c8ae5..afe1eafbe 100644 --- a/lib/p2p/packets/utils.ts +++ b/lib/p2p/packets/utils.ts @@ -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) ); }; @@ -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); @@ -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()); } diff --git a/lib/p2p/types.ts b/lib/p2p/types.ts index 8858a0eae..6c00bb110 100644 --- a/lib/p2p/types.ts +++ b/lib/p2p/types.ts @@ -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 }; }; diff --git a/lib/proto/xudp2p_pb.d.ts b/lib/proto/xudp2p_pb.d.ts index 489994ef4..6e9f8dea9 100644 --- a/lib/proto/xudp2p_pb.d.ts +++ b/lib/proto/xudp2p_pb.d.ts @@ -94,6 +94,29 @@ export namespace Node { } } +export class LndUris extends jspb.Message { + clearLndUriList(): void; + getLndUriList(): Array; + setLndUriList(value: Array): void; + addLndUri(value: string, index?: number): string; + + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): LndUris.AsObject; + static toObject(includeInstance: boolean, msg: LndUris): LndUris.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: LndUris, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): LndUris; + static deserializeBinaryFromReader(message: LndUris, reader: jspb.BinaryReader): LndUris; +} + +export namespace LndUris { + export type AsObject = { + lndUriList: Array, + } +} + export class NodeState extends jspb.Message { clearAddressesList(): void; getAddressesList(): Array
; @@ -117,6 +140,10 @@ export class NodeState extends jspb.Message { clearTokenIdentifiersMap(): void; + getLndUrisMap(): jspb.Map; + clearLndUrisMap(): void; + + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): NodeState.AsObject; static toObject(includeInstance: boolean, msg: NodeState): NodeState.AsObject; @@ -136,6 +163,8 @@ export namespace NodeState { lndPubKeysMap: Array<[string, string]>, tokenIdentifiersMap: Array<[string, string]>, + + lndUrisMap: Array<[string, LndUris.AsObject]>, } } diff --git a/lib/proto/xudp2p_pb.js b/lib/proto/xudp2p_pb.js index 294c1d8fa..0bf2ba4c9 100644 --- a/lib/proto/xudp2p_pb.js +++ b/lib/proto/xudp2p_pb.js @@ -15,6 +15,7 @@ goog.exportSymbol('proto.xudp2p.Address', null, global); goog.exportSymbol('proto.xudp2p.DisconnectingPacket', null, global); goog.exportSymbol('proto.xudp2p.GetNodesPacket', null, global); goog.exportSymbol('proto.xudp2p.GetOrdersPacket', null, global); +goog.exportSymbol('proto.xudp2p.LndUris', null, global); goog.exportSymbol('proto.xudp2p.Node', null, global); goog.exportSymbol('proto.xudp2p.NodeState', null, global); goog.exportSymbol('proto.xudp2p.NodeStateUpdatePacket', null, global); @@ -650,6 +651,169 @@ proto.xudp2p.Node.prototype.clearAddressesList = function() { +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.xudp2p.LndUris = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.xudp2p.LndUris.repeatedFields_, null); +}; +goog.inherits(proto.xudp2p.LndUris, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.xudp2p.LndUris.displayName = 'proto.xudp2p.LndUris'; +} +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.xudp2p.LndUris.repeatedFields_ = [1]; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto suitable for use in Soy templates. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. + * @param {boolean=} opt_includeInstance Whether to include the JSPB instance + * for transitional soy proto support: http://goto/soy-param-migration + * @return {!Object} + */ +proto.xudp2p.LndUris.prototype.toObject = function(opt_includeInstance) { + return proto.xudp2p.LndUris.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Whether to include the JSPB + * instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.xudp2p.LndUris} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.xudp2p.LndUris.toObject = function(includeInstance, msg) { + var f, obj = { + lndUriList: jspb.Message.getRepeatedField(msg, 1) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.xudp2p.LndUris} + */ +proto.xudp2p.LndUris.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.xudp2p.LndUris; + return proto.xudp2p.LndUris.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.xudp2p.LndUris} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.xudp2p.LndUris} + */ +proto.xudp2p.LndUris.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.addLndUri(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.xudp2p.LndUris.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.xudp2p.LndUris.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.xudp2p.LndUris} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.xudp2p.LndUris.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getLndUriList(); + if (f.length > 0) { + writer.writeRepeatedString( + 1, + f + ); + } +}; + + +/** + * repeated string lnd_uri = 1; + * @return {!Array.} + */ +proto.xudp2p.LndUris.prototype.getLndUriList = function() { + return /** @type {!Array.} */ (jspb.Message.getRepeatedField(this, 1)); +}; + + +/** @param {!Array.} value */ +proto.xudp2p.LndUris.prototype.setLndUriList = function(value) { + jspb.Message.setField(this, 1, value || []); +}; + + +/** + * @param {!string} value + * @param {number=} opt_index + */ +proto.xudp2p.LndUris.prototype.addLndUri = function(value, opt_index) { + jspb.Message.addToRepeatedField(this, 1, value, opt_index); +}; + + +proto.xudp2p.LndUris.prototype.clearLndUriList = function() { + this.setLndUriList([]); +}; + + + /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -708,7 +872,8 @@ proto.xudp2p.NodeState.toObject = function(includeInstance, msg) { pairsList: jspb.Message.getRepeatedField(msg, 4), raidenAddress: jspb.Message.getFieldWithDefault(msg, 5, ""), lndPubKeysMap: (f = msg.getLndPubKeysMap()) ? f.toObject(includeInstance, undefined) : [], - tokenIdentifiersMap: (f = msg.getTokenIdentifiersMap()) ? f.toObject(includeInstance, undefined) : [] + tokenIdentifiersMap: (f = msg.getTokenIdentifiersMap()) ? f.toObject(includeInstance, undefined) : [], + lndUrisMap: (f = msg.getLndUrisMap()) ? f.toObject(includeInstance, proto.xudp2p.LndUris.toObject) : [] }; if (includeInstance) { @@ -770,6 +935,12 @@ proto.xudp2p.NodeState.deserializeBinaryFromReader = function(msg, reader) { jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readString); }); break; + case 8: + var value = msg.getLndUrisMap(); + reader.readMessage(value, function(message, reader) { + jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readMessage, proto.xudp2p.LndUris.deserializeBinaryFromReader); + }); + break; default: reader.skipField(); break; @@ -829,6 +1000,10 @@ proto.xudp2p.NodeState.serializeBinaryToWriter = function(message, writer) { if (f && f.getLength() > 0) { f.serializeBinary(7, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeString); } + f = message.getLndUrisMap(true); + if (f && f.getLength() > 0) { + f.serializeBinary(8, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeMessage, proto.xudp2p.LndUris.serializeBinaryToWriter); + } }; @@ -943,6 +1118,24 @@ proto.xudp2p.NodeState.prototype.clearTokenIdentifiersMap = function() { }; +/** + * map lnd_uris = 8; + * @param {boolean=} opt_noLazyCreate Do not create the map if + * empty, instead returning `undefined` + * @return {!jspb.Map} + */ +proto.xudp2p.NodeState.prototype.getLndUrisMap = function(opt_noLazyCreate) { + return /** @type {!jspb.Map} */ ( + jspb.Message.getMapField(this, 8, opt_noLazyCreate, + proto.xudp2p.LndUris)); +}; + + +proto.xudp2p.NodeState.prototype.clearLndUrisMap = function() { + this.getLndUrisMap().clear(); +}; + + /** * Generated by JsPbCodeGenerator. diff --git a/lib/proto/xudrpc.swagger.json b/lib/proto/xudrpc.swagger.json index 8cf5f6f67..a527d8b01 100644 --- a/lib/proto/xudrpc.swagger.json +++ b/lib/proto/xudrpc.swagger.json @@ -263,6 +263,23 @@ ] } }, + "/v1/openchannel": { + "post": { + "summary": "Opens a payment channel to a peer with the given node pub key for the specified amount and currency.", + "operationId": "OpenChannel", + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/xudrpcOpenChannelResponse" + } + } + }, + "tags": [ + "Xud" + ] + } + }, "/v1/orders": { "get": { "summary": "Gets orders from the order book. This call returns the state of the order book at a given point\nin time, although it is not guaranteed to still be vaild by the time a response is received\nand processed by a client. It accepts an optional trading pair id parameter. If specified, only\norders for that particular trading pair are returned. Otherwise, all orders are returned. Orders\nare separated into buys and sells for each trading pair, but unsorted.", @@ -942,6 +959,9 @@ } } }, + "xudrpcOpenChannelResponse": { + "type": "object" + }, "xudrpcOrder": { "type": "object", "properties": { diff --git a/lib/proto/xudrpc_grpc_pb.d.ts b/lib/proto/xudrpc_grpc_pb.d.ts index a849c0769..25eabfb05 100644 --- a/lib/proto/xudrpc_grpc_pb.d.ts +++ b/lib/proto/xudrpc_grpc_pb.d.ts @@ -62,6 +62,7 @@ interface IXudService extends grpc.ServiceDefinition; responseDeserialize: grpc.deserialize; } +interface IXudService_IOpenChannel extends grpc.MethodDefinition { + path: string; // "/xudrpc.Xud/OpenChannel" + requestStream: boolean; // false + responseStream: boolean; // false + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} interface IXudService_IConnect extends grpc.MethodDefinition { path: string; // "/xudrpc.Xud/Connect" requestStream: boolean; // false @@ -298,6 +308,7 @@ export interface IXudServer { addPair: grpc.handleUnaryCall; removeOrder: grpc.handleUnaryCall; channelBalance: grpc.handleUnaryCall; + openChannel: grpc.handleUnaryCall; connect: grpc.handleUnaryCall; ban: grpc.handleUnaryCall; unban: grpc.handleUnaryCall; @@ -332,6 +343,9 @@ export interface IXudClient { channelBalance(request: xudrpc_pb.ChannelBalanceRequest, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.ChannelBalanceResponse) => void): grpc.ClientUnaryCall; channelBalance(request: xudrpc_pb.ChannelBalanceRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.ChannelBalanceResponse) => void): grpc.ClientUnaryCall; channelBalance(request: xudrpc_pb.ChannelBalanceRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.ChannelBalanceResponse) => void): grpc.ClientUnaryCall; + openChannel(request: xudrpc_pb.OpenChannelRequest, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.OpenChannelResponse) => void): grpc.ClientUnaryCall; + openChannel(request: xudrpc_pb.OpenChannelRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.OpenChannelResponse) => void): grpc.ClientUnaryCall; + openChannel(request: xudrpc_pb.OpenChannelRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.OpenChannelResponse) => void): grpc.ClientUnaryCall; connect(request: xudrpc_pb.ConnectRequest, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.ConnectResponse) => void): grpc.ClientUnaryCall; connect(request: xudrpc_pb.ConnectRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.ConnectResponse) => void): grpc.ClientUnaryCall; connect(request: xudrpc_pb.ConnectRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.ConnectResponse) => void): grpc.ClientUnaryCall; @@ -401,6 +415,9 @@ export class XudClient extends grpc.Client implements IXudClient { public channelBalance(request: xudrpc_pb.ChannelBalanceRequest, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.ChannelBalanceResponse) => void): grpc.ClientUnaryCall; public channelBalance(request: xudrpc_pb.ChannelBalanceRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.ChannelBalanceResponse) => void): grpc.ClientUnaryCall; public channelBalance(request: xudrpc_pb.ChannelBalanceRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.ChannelBalanceResponse) => void): grpc.ClientUnaryCall; + public openChannel(request: xudrpc_pb.OpenChannelRequest, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.OpenChannelResponse) => void): grpc.ClientUnaryCall; + public openChannel(request: xudrpc_pb.OpenChannelRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.OpenChannelResponse) => void): grpc.ClientUnaryCall; + public openChannel(request: xudrpc_pb.OpenChannelRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.OpenChannelResponse) => void): grpc.ClientUnaryCall; public connect(request: xudrpc_pb.ConnectRequest, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.ConnectResponse) => void): grpc.ClientUnaryCall; public connect(request: xudrpc_pb.ConnectRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.ConnectResponse) => void): grpc.ClientUnaryCall; public connect(request: xudrpc_pb.ConnectRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.ConnectResponse) => void): grpc.ClientUnaryCall; diff --git a/lib/proto/xudrpc_grpc_pb.js b/lib/proto/xudrpc_grpc_pb.js index 910c13361..73547a466 100644 --- a/lib/proto/xudrpc_grpc_pb.js +++ b/lib/proto/xudrpc_grpc_pb.js @@ -323,6 +323,28 @@ function deserialize_xudrpc_ListPeersResponse(buffer_arg) { return xudrpc_pb.ListPeersResponse.deserializeBinary(new Uint8Array(buffer_arg)); } +function serialize_xudrpc_OpenChannelRequest(arg) { + if (!(arg instanceof xudrpc_pb.OpenChannelRequest)) { + throw new Error('Expected argument of type xudrpc.OpenChannelRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_xudrpc_OpenChannelRequest(buffer_arg) { + return xudrpc_pb.OpenChannelRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_xudrpc_OpenChannelResponse(arg) { + if (!(arg instanceof xudrpc_pb.OpenChannelResponse)) { + throw new Error('Expected argument of type xudrpc.OpenChannelResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_xudrpc_OpenChannelResponse(buffer_arg) { + return xudrpc_pb.OpenChannelResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + function serialize_xudrpc_OrderUpdate(arg) { if (!(arg instanceof xudrpc_pb.OrderUpdate)) { throw new Error('Expected argument of type xudrpc.OrderUpdate'); @@ -624,6 +646,18 @@ var XudService = exports.XudService = { responseSerialize: serialize_xudrpc_ChannelBalanceResponse, responseDeserialize: deserialize_xudrpc_ChannelBalanceResponse, }, + // Opens a payment channel to a peer with the given node pub key for the specified amount and currency. + openChannel: { + path: '/xudrpc.Xud/OpenChannel', + requestStream: false, + responseStream: false, + requestType: xudrpc_pb.OpenChannelRequest, + responseType: xudrpc_pb.OpenChannelResponse, + requestSerialize: serialize_xudrpc_OpenChannelRequest, + requestDeserialize: deserialize_xudrpc_OpenChannelRequest, + responseSerialize: serialize_xudrpc_OpenChannelResponse, + responseDeserialize: deserialize_xudrpc_OpenChannelResponse, + }, // Attempts to connect to a node. Once connected, the node is added to the list of peers and // becomes available for swaps and trading. A handshake exchanges information about the peer's // supported trading and swap clients. Orders will be shared with the peer upon connection and diff --git a/lib/proto/xudrpc_pb.d.ts b/lib/proto/xudrpc_pb.d.ts index 6ee6022b3..7d58e1a3f 100644 --- a/lib/proto/xudrpc_pb.d.ts +++ b/lib/proto/xudrpc_pb.d.ts @@ -318,6 +318,52 @@ export namespace ChannelBalanceResponse { } } +export class OpenChannelRequest extends jspb.Message { + getNodePubKey(): string; + setNodePubKey(value: string): void; + + getCurrency(): string; + setCurrency(value: string): void; + + getAmount(): number; + setAmount(value: number): void; + + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): OpenChannelRequest.AsObject; + static toObject(includeInstance: boolean, msg: OpenChannelRequest): OpenChannelRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: OpenChannelRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): OpenChannelRequest; + static deserializeBinaryFromReader(message: OpenChannelRequest, reader: jspb.BinaryReader): OpenChannelRequest; +} + +export namespace OpenChannelRequest { + export type AsObject = { + nodePubKey: string, + currency: string, + amount: number, + } +} + +export class OpenChannelResponse extends jspb.Message { + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): OpenChannelResponse.AsObject; + static toObject(includeInstance: boolean, msg: OpenChannelResponse): OpenChannelResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: OpenChannelResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): OpenChannelResponse; + static deserializeBinaryFromReader(message: OpenChannelResponse, reader: jspb.BinaryReader): OpenChannelResponse; +} + +export namespace OpenChannelResponse { + export type AsObject = { + } +} + export class ConnectRequest extends jspb.Message { getNodeUri(): string; setNodeUri(value: string): void; diff --git a/lib/proto/xudrpc_pb.js b/lib/proto/xudrpc_pb.js index 7d9f1d5e3..ce5ca90be 100644 --- a/lib/proto/xudrpc_pb.js +++ b/lib/proto/xudrpc_pb.js @@ -44,6 +44,8 @@ goog.exportSymbol('proto.xudrpc.ListPeersRequest', null, global); goog.exportSymbol('proto.xudrpc.ListPeersResponse', null, global); goog.exportSymbol('proto.xudrpc.LndChannels', null, global); goog.exportSymbol('proto.xudrpc.LndInfo', null, global); +goog.exportSymbol('proto.xudrpc.OpenChannelRequest', null, global); +goog.exportSymbol('proto.xudrpc.OpenChannelResponse', null, global); goog.exportSymbol('proto.xudrpc.Order', null, global); goog.exportSymbol('proto.xudrpc.OrderRemoval', null, global); goog.exportSymbol('proto.xudrpc.OrderSide', null, global); @@ -2150,6 +2152,318 @@ proto.xudrpc.ChannelBalanceResponse.prototype.clearBalancesMap = function() { +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.xudrpc.OpenChannelRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.xudrpc.OpenChannelRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.xudrpc.OpenChannelRequest.displayName = 'proto.xudrpc.OpenChannelRequest'; +} + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto suitable for use in Soy templates. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. + * @param {boolean=} opt_includeInstance Whether to include the JSPB instance + * for transitional soy proto support: http://goto/soy-param-migration + * @return {!Object} + */ +proto.xudrpc.OpenChannelRequest.prototype.toObject = function(opt_includeInstance) { + return proto.xudrpc.OpenChannelRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Whether to include the JSPB + * instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.xudrpc.OpenChannelRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.xudrpc.OpenChannelRequest.toObject = function(includeInstance, msg) { + var f, obj = { + nodePubKey: jspb.Message.getFieldWithDefault(msg, 1, ""), + currency: jspb.Message.getFieldWithDefault(msg, 2, ""), + amount: jspb.Message.getFieldWithDefault(msg, 3, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.xudrpc.OpenChannelRequest} + */ +proto.xudrpc.OpenChannelRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.xudrpc.OpenChannelRequest; + return proto.xudrpc.OpenChannelRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.xudrpc.OpenChannelRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.xudrpc.OpenChannelRequest} + */ +proto.xudrpc.OpenChannelRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setNodePubKey(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.setCurrency(value); + break; + case 3: + var value = /** @type {number} */ (reader.readInt64()); + msg.setAmount(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.xudrpc.OpenChannelRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.xudrpc.OpenChannelRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.xudrpc.OpenChannelRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.xudrpc.OpenChannelRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getNodePubKey(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getCurrency(); + if (f.length > 0) { + writer.writeString( + 2, + f + ); + } + f = message.getAmount(); + if (f !== 0) { + writer.writeInt64( + 3, + f + ); + } +}; + + +/** + * optional string node_pub_key = 1; + * @return {string} + */ +proto.xudrpc.OpenChannelRequest.prototype.getNodePubKey = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** @param {string} value */ +proto.xudrpc.OpenChannelRequest.prototype.setNodePubKey = function(value) { + jspb.Message.setField(this, 1, value); +}; + + +/** + * optional string currency = 2; + * @return {string} + */ +proto.xudrpc.OpenChannelRequest.prototype.getCurrency = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** @param {string} value */ +proto.xudrpc.OpenChannelRequest.prototype.setCurrency = function(value) { + jspb.Message.setField(this, 2, value); +}; + + +/** + * optional int64 amount = 3; + * @return {number} + */ +proto.xudrpc.OpenChannelRequest.prototype.getAmount = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0)); +}; + + +/** @param {number} value */ +proto.xudrpc.OpenChannelRequest.prototype.setAmount = function(value) { + jspb.Message.setField(this, 3, value); +}; + + + +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.xudrpc.OpenChannelResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.xudrpc.OpenChannelResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.xudrpc.OpenChannelResponse.displayName = 'proto.xudrpc.OpenChannelResponse'; +} + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto suitable for use in Soy templates. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. + * @param {boolean=} opt_includeInstance Whether to include the JSPB instance + * for transitional soy proto support: http://goto/soy-param-migration + * @return {!Object} + */ +proto.xudrpc.OpenChannelResponse.prototype.toObject = function(opt_includeInstance) { + return proto.xudrpc.OpenChannelResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Whether to include the JSPB + * instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.xudrpc.OpenChannelResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.xudrpc.OpenChannelResponse.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.xudrpc.OpenChannelResponse} + */ +proto.xudrpc.OpenChannelResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.xudrpc.OpenChannelResponse; + return proto.xudrpc.OpenChannelResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.xudrpc.OpenChannelResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.xudrpc.OpenChannelResponse} + */ +proto.xudrpc.OpenChannelResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.xudrpc.OpenChannelResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.xudrpc.OpenChannelResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.xudrpc.OpenChannelResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.xudrpc.OpenChannelResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + + /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a diff --git a/lib/raidenclient/RaidenClient.ts b/lib/raidenclient/RaidenClient.ts index f42e6bc98..9812874ff 100644 --- a/lib/raidenclient/RaidenClient.ts +++ b/lib/raidenclient/RaidenClient.ts @@ -5,7 +5,15 @@ import errors from './errors'; import { SwapDeal } from '../swaps/types'; import { SwapClientType, SwapState, SwapRole } from '../constants/enums'; import assert from 'assert'; -import { RaidenClientConfig, RaidenInfo, OpenChannelPayload, Channel, TokenPaymentRequest, TokenPaymentResponse } from './types'; +import { + RaidenClientConfig, + RaidenInfo, + OpenChannelPayload, + Channel, + TokenPaymentRequest, + TokenPaymentResponse, +} from './types'; +import { UnitConverter } from '../utils/UnitConverter'; type RaidenErrorResponse = { errors: string }; @@ -40,23 +48,22 @@ class RaidenClient extends SwapClient { private port: number; private host: string; private disable: boolean; - - // TODO: Populate the mapping from the database (Currency.decimalPlaces). - private static readonly UNITS_PER_CURRENCY: { [key: string]: number } = { - WETH: 10 ** 10, - DAI: 10 ** 10, - }; + private unitConverter: UnitConverter; /** * Creates a raiden client. */ - constructor(config: RaidenClientConfig, logger: Logger) { + constructor( + { config, logger, unitConverter }: + { config: RaidenClientConfig, logger: Logger, unitConverter: UnitConverter }, + ) { super(logger); const { disable, host, port } = config; this.port = port; this.host = host; this.disable = disable; + this.unitConverter = unitConverter; } /** @@ -83,7 +90,7 @@ class RaidenClient extends SwapClient { this.address = newAddress; } - this.emit('connectionVerified', newAddress); + this.emit('connectionVerified', { newIdentifier: newAddress }); await this.setStatus(ClientStatus.ConnectionVerified); } catch (err) { this.logger.error( @@ -287,18 +294,41 @@ class RaidenClient extends SwapClient { } const channels = await this.getChannels(this.tokenAddresses.get(currency)); - const balance = channels.filter(channel => channel.state === 'opened') + const units = channels.filter(channel => channel.state === 'opened') .map(channel => channel.balance) - .reduce((sum, acc) => sum + acc, 0) - / (RaidenClient.UNITS_PER_CURRENCY[currency] || 1); + .reduce((sum, acc) => sum + acc, 0); + const balance = this.unitConverter.unitsToAmount({ + currency, + units, + }); return { balance, pendingOpenBalance: 0 }; } /** * Creates a payment channel. + */ + public openChannel = async ( + { peerIdentifier: peerAddress, units, currency }: + { peerIdentifier: string, units: number, currency: string }, + ): Promise => { + const tokenAddress = this.tokenAddresses.get(currency); + if (!tokenAddress) { + throw(errors.TOKEN_ADDRESS_NOT_FOUND); + } + await this.openChannelRequest({ + partner_address: peerAddress, + token_address: tokenAddress, + total_deposit: units, + // TODO: The amount of blocks that the settle timeout should have + settle_timeout: 500, + }); + } + + /** + * Creates a payment channel request. * @returns The channel_address for the newly created channel. */ - public openChannel = async (payload: OpenChannelPayload): Promise => { + private openChannelRequest = async (payload: OpenChannelPayload): Promise => { const endpoint = 'channels'; const res = await this.sendRequest(endpoint, 'PUT', payload); diff --git a/lib/raidenclient/types.ts b/lib/raidenclient/types.ts index 0e50b9e23..05c867af3 100644 --- a/lib/raidenclient/types.ts +++ b/lib/raidenclient/types.ts @@ -22,7 +22,7 @@ export type OpenChannelPayload = { partner_address: string; token_address: string; total_deposit: number; - settle_timeout: 100; + settle_timeout: number; }; /** diff --git a/lib/service/Service.ts b/lib/service/Service.ts index 4476a12b7..fef4dca0b 100644 --- a/lib/service/Service.ts +++ b/lib/service/Service.ts @@ -165,6 +165,29 @@ class Service { await this.pool.addOutbound({ host, port }, nodePubKey, retryConnecting, true); } + /* + * Opens a payment channel to a specified node, currency and amount. + */ + public openChannel = async ( + args: { nodePubKey: string, amount: number, currency: string }, + ) => { + const { nodePubKey, amount, currency } = args; + argChecks.HAS_NODE_PUB_KEY({ nodePubKey }); + argChecks.POSITIVE_AMOUNT({ amount }); + argChecks.VALID_CURRENCY({ currency }); + try { + const peer = this.pool.getPeer(nodePubKey); + await this.swapClientManager.openChannel({ + peer, + amount, + currency, + }); + } catch (e) { + const errorMessage = e.message || 'unknown'; + throw errors.OPEN_CHANNEL_FAILURE(currency, nodePubKey, amount, errorMessage); + } + } + /* * Ban a XU node manually and disconnect from it. */ diff --git a/lib/service/errors.ts b/lib/service/errors.ts index c4dbba307..153fa16eb 100644 --- a/lib/service/errors.ts +++ b/lib/service/errors.ts @@ -6,6 +6,7 @@ const errorCodes = { NOMATCHING_MODE_IS_REQUIRED: codesPrefix.concat('.2'), UNIMPLEMENTED: codesPrefix.concat('.3'), PENDING_CALL_CONFLICT: codesPrefix.concat('.4'), + OPEN_CHANNEL_FAILURE: codesPrefix.concat('.5'), }; const errors = { @@ -25,6 +26,10 @@ const errors = { message: 'a pending call is ongoing that conflicts with this call', code: errorCodes.PENDING_CALL_CONFLICT, }, + OPEN_CHANNEL_FAILURE: (currency: string, nodePubKey: string, amount: number, message: string) => ({ + message: `failed to open channel with nodePubKey: ${nodePubKey}, currency: ${currency}, amount: ${amount}, message: ${message}`, + code: errorCodes.OPEN_CHANNEL_FAILURE, + }), }; export { errorCodes }; diff --git a/lib/swaps/SwapClient.ts b/lib/swaps/SwapClient.ts index 2031dfbcd..e6eab0ca1 100644 --- a/lib/swaps/SwapClient.ts +++ b/lib/swaps/SwapClient.ts @@ -17,9 +17,14 @@ type ChannelBalance = { pendingOpenBalance: number, }; +export type SwapClientInfo = { + newIdentifier?: string; + newUris?: string[]; +}; + interface SwapClient { - on(event: 'connectionVerified', listener: (newIdentifier?: string) => void): this; - emit(event: 'connectionVerified', newIdentifier?: string): boolean; + on(event: 'connectionVerified', listener: (swapClientInfo: SwapClientInfo) => void): this; + emit(event: 'connectionVerified', swapClientInfo: SwapClientInfo): boolean; } /** @@ -128,6 +133,20 @@ abstract class SwapClient extends EventEmitter { */ public abstract async getHeight(): Promise; + /** + * Opens a payment channel given peerIdentifier, amount + * optional currency and optional lndUris. + */ + public abstract async openChannel( + { peerIdentifier, units, currency, lndUris }: + { + peerIdentifier: string, + units: number, + currency?: string, + lndUris?: string[], + }, + ): Promise; + public isConnected(): boolean { return this.status === ClientStatus.ConnectionVerified; } diff --git a/lib/swaps/SwapClientManager.ts b/lib/swaps/SwapClientManager.ts index e555f97c5..f94ca5e0b 100644 --- a/lib/swaps/SwapClientManager.ts +++ b/lib/swaps/SwapClientManager.ts @@ -9,6 +9,8 @@ import { Currency } from '../orderbook/types'; import { Models } from '../db/DB'; import { SwapClientType } from '../constants/enums'; import { EventEmitter } from 'events'; +import Peer from '../p2p/Peer'; +import { UnitConverter } from '../utils/UnitConverter'; export function isRaidenClient(swapClient: SwapClient): swapClient is RaidenClient { return (swapClient.type === SwapClientType.Raiden); @@ -18,11 +20,18 @@ export function isLndClient(swapClient: SwapClient): swapClient is LndClient { return (swapClient.type === SwapClientType.Lnd); } +type LndUpdate = { + currency: string, + pubKey: string, + chain?: string, + uris?: string[], +}; + interface SwapClientManager { - on(event: 'lndUpdate', listener: (currency: string, pubKey: string, chain?: string) => void): this; + on(event: 'lndUpdate', listener: (lndUpdate: LndUpdate) => void): this; on(event: 'raidenUpdate', listener: (tokenAddresses: Map, address?: string) => void): this; on(event: 'htlcAccepted', listener: (swapClient: SwapClient, rHash: string, amount: number, currency: string) => void): this; - emit(event: 'lndUpdate', currency: string, pubKey: string, chain?: string): boolean; + emit(event: 'lndUpdate', lndUpdate: LndUpdate): boolean; emit(event: 'raidenUpdate', tokenAddresses: Map, address?: string): boolean; emit(event: 'htlcAccepted', swapClient: SwapClient, rHash: string, amount: number, currency: string): boolean; } @@ -35,10 +44,15 @@ class SwapClientManager extends EventEmitter { constructor( private config: Config, private loggers: Loggers, + private unitConverter: UnitConverter, ) { super(); - this.raidenClient = new RaidenClient(config.raiden, loggers.raiden); + this.raidenClient = new RaidenClient({ + unitConverter, + config: config.raiden, + logger: loggers.raiden, + }); } /** @@ -252,12 +266,52 @@ class SwapClientManager extends EventEmitter { await Promise.all(closePromises); } + /** + * Opens a payment channel. + * @param peer a peer to open the payment channel with. + * @param currency a currency for the payment channel. + * @param amount the size of the payment channel local balance + * @returns Nothing upon success, throws otherwise. + */ + public openChannel = async ( + { peer, amount, currency }: + { peer: Peer, amount: number, currency: string }, + ): Promise => { + const swapClient = this.get(currency); + if (!swapClient) { + throw errors.SWAP_CLIENT_NOT_FOUND(currency); + } + const peerIdentifier = peer.getIdentifier(swapClient.type, currency); + if (!peerIdentifier) { + throw new Error('unable to get swap client pubKey for peer'); + } + const units = this.unitConverter.amountToUnits({ + amount, + currency, + }); + if (isLndClient(swapClient)) { + const lndUris = peer.getLndUris(currency); + if (!lndUris) { + throw new Error('unable to get lnd listening uris'); + } + await swapClient.openChannel({ peerIdentifier, units, lndUris }); + return; + } + // fallback to raiden for all non-lnd currencies + await swapClient.openChannel({ peerIdentifier, units, currency }); + } + private bind = () => { for (const [currency, swapClient] of this.swapClients.entries()) { if (isLndClient(swapClient)) { - swapClient.on('connectionVerified', (newPubKey) => { - if (newPubKey) { - this.emit('lndUpdate', currency, newPubKey, swapClient.chain); + swapClient.on('connectionVerified', ({ newIdentifier, newUris }) => { + if (newIdentifier) { + this.emit('lndUpdate', { + currency, + uris: newUris, + pubKey: newIdentifier, + chain: swapClient.chain, + }); } }); // lnd clients emit htlcAccepted evented we must handle @@ -270,9 +324,10 @@ class SwapClientManager extends EventEmitter { // duplicate listeners in case raiden client is associated with // multiple currencies if (!this.raidenClient.isDisabled()) { - this.raidenClient.on('connectionVerified', (newAddress) => { - if (newAddress) { - this.emit('raidenUpdate', this.raidenClient.tokenAddresses, newAddress); + this.raidenClient.on('connectionVerified', (swapClientInfo) => { + const { newIdentifier } = swapClientInfo; + if (newIdentifier) { + this.emit('raidenUpdate', this.raidenClient.tokenAddresses, newIdentifier); } }); } diff --git a/lib/swaps/Swaps.ts b/lib/swaps/Swaps.ts index 8565c71ca..36e266a95 100644 --- a/lib/swaps/Swaps.ts +++ b/lib/swaps/Swaps.ts @@ -36,7 +36,7 @@ class Swaps extends EventEmitter { private usedHashes = new Set(); private repository: SwapRepository; /** Number of smallest units per currency. */ - // TODO: Populate the mapping from the database (Currency.decimalPlaces). + // TODO: Use UnitConverter class instead private static readonly UNITS_PER_CURRENCY: { [key: string]: number } = { BTC: 1, LTC: 1, @@ -122,9 +122,14 @@ class Swaps extends EventEmitter { public init = async () => { // update pool with lnd pubkeys and raiden address - this.swapClientManager.getLndClientsMap().forEach((lndClient) => { - if (lndClient.pubKey && lndClient.chain) { - this.pool.updateLndState(lndClient.currency, lndClient.pubKey, lndClient.chain); + this.swapClientManager.getLndClientsMap().forEach(({ pubKey, chain, currency, uris }) => { + if (pubKey && chain) { + this.pool.updateLndState({ + currency, + pubKey, + chain, + uris, + }); } }); if (this.swapClientManager.raidenClient.address) { diff --git a/lib/utils/UnitConverter.ts b/lib/utils/UnitConverter.ts new file mode 100644 index 000000000..a6c111e2c --- /dev/null +++ b/lib/utils/UnitConverter.ts @@ -0,0 +1,37 @@ +class UnitConverter { + /** Number of smallest units per currency. */ + private UNITS_PER_CURRENCY: { [key: string]: number } = { + BTC: 1, + LTC: 1, + WETH: 10 ** 10, + DAI: 10 ** 10, + }; + public init = () => { + // TODO: Populate the mapping from the database (Currency.decimalPlaces). + // this.UNITS_PER_CURRENCY = await fetchUnitsPerCurrencyFromDatabase(); + } + + public amountToUnits = ( + { currency, amount }: + { currency: string, amount: number }, + ): number => { + const unitsPerCurrency = this.UNITS_PER_CURRENCY[currency]; + if (!unitsPerCurrency) { + throw new Error(`cannot convert ${currency} amount of ${amount} to units because units per currency was not found in the database`); + } + return Math.floor(amount * unitsPerCurrency); + } + + public unitsToAmount = ( + { currency, units }: + { currency: string, units: number }, + ): number => { + const unitsPerCurrency = this.UNITS_PER_CURRENCY[currency]; + if (!unitsPerCurrency) { + throw new Error(`cannot convert ${currency} units of ${units} to amount because units per currency was not found in the database`); + } + return units / unitsPerCurrency; + } +} + +export { UnitConverter }; diff --git a/proto/xudp2p.proto b/proto/xudp2p.proto index 6ce5dce41..9b92d92a5 100644 --- a/proto/xudp2p.proto +++ b/proto/xudp2p.proto @@ -20,12 +20,17 @@ message Node { repeated Address addresses = 2; } +message LndUris { + repeated string lnd_uri = 1; +} + message NodeState { repeated Address addresses = 3; repeated string pairs = 4; string raiden_address = 5; map lnd_pub_keys = 6; map token_identifiers = 7; + map lnd_uris = 8; } message PingPacket { diff --git a/proto/xudrpc.proto b/proto/xudrpc.proto index 82dcbab5c..84a6d78a3 100644 --- a/proto/xudrpc.proto +++ b/proto/xudrpc.proto @@ -83,6 +83,13 @@ service Xud { }; } + /* Opens a payment channel to a peer with the given node pub key for the specified amount and currency. */ + rpc OpenChannel(OpenChannelRequest) returns (OpenChannelResponse) { + option (google.api.http) = { + post: "/v1/openchannel" + }; + } + /* Attempts to connect to a node. Once connected, the node is added to the list of peers and * becomes available for swaps and trading. A handshake exchanges information about the peer's * supported trading and swap clients. Orders will be shared with the peer upon connection and @@ -313,6 +320,17 @@ message ChannelBalanceResponse { map balances = 1 [json_name = "orders"]; } +message OpenChannelRequest { + // The node pub key of the peer with which to open channel with. + string node_pub_key = 1 [json_name = "peer_pub_key"]; + // The ticker symbol of the currency to open the channel for. + string currency = 2 [json_name = "currency"]; + // The amount of the channel denominated in satoshis. + int64 amount = 3 [json_name = "amount"]; +} + +message OpenChannelResponse {} + message ConnectRequest { // The uri of the node to connect to in "[nodePubKey]@[host]:[port]" format. string node_uri = 1 [json_name = "node_uri"]; diff --git a/test/jest/LndClient.spec.ts b/test/jest/LndClient.spec.ts new file mode 100644 index 000000000..410845b33 --- /dev/null +++ b/test/jest/LndClient.spec.ts @@ -0,0 +1,177 @@ +import LndClient from '../../lib/lndclient/LndClient'; +import { LndClientConfig } from '../../lib/lndclient/types'; +import Logger from '../../lib/Logger'; + +jest.mock('../../lib/Logger'); +const mockedLogger = >Logger; +describe('LndClient', () => { + let lnd: LndClient; + let config: LndClientConfig; + let currency: string; + let logger: Logger; + + beforeEach(() => { + config = { + disable: false, + certpath: '/cert/path', + macaroonpath: '/macaroon/path', + host: '127.0.0.1', + port: 4321, + cltvdelta: 144, + nomacaroons: true, + }; + currency = 'BTC'; + logger = new mockedLogger(); + logger.error = jest.fn(); + logger.info = jest.fn(); + logger.trace = jest.fn(); + }); + + afterEach(async () => { + jest.clearAllMocks(); + await lnd.close(); + }); + + describe('openChannel', () => { + const peerPubKey = '02f8895eb03c37b2665415be4d83b20228acc0abc55ebf6728565141c66cfc164a'; + const units = 16000000; + const externalIp1 = '123.456.789.321:9735'; + const externalIp2 = '192.168.63.155:9777'; + const lndListeningUris = [ + `${peerPubKey}@${externalIp1}`, + `${peerPubKey}@${externalIp2}`, + ]; + + test('it throws when connectPeer fails', async () => { + expect.assertions(3); + lnd = new LndClient(config, currency, logger); + lnd['connectPeer'] = jest.fn().mockImplementation(() => { + throw new Error('connectPeer failed'); + }); + try { + await lnd.openChannel({ + units, + peerIdentifier: peerPubKey, + lndUris: [lndListeningUris[0]], + }); + } catch (e) { + expect(e).toMatchSnapshot(); + } + expect(lnd['connectPeer']).toHaveBeenCalledTimes(1); + expect(lnd['connectPeer']) + .toHaveBeenCalledWith(peerPubKey, externalIp1); + }); + + test('it tries all 2 lnd uris when connectPeer to first one fails', async () => { + expect.assertions(3); + lnd = new LndClient(config, currency, logger); + lnd['openChannelSync'] = jest.fn().mockReturnValue(Promise.resolve()); + const connectPeerFail = () => { + throw new Error('connectPeer failed'); + }; + lnd['connectPeer'] = jest.fn() + .mockImplementationOnce(connectPeerFail) + .mockImplementationOnce(() => { + return Promise.resolve(); + }); + await lnd.openChannel({ + units, + peerIdentifier: peerPubKey, + lndUris: lndListeningUris, + }); + expect(lnd['connectPeer']).toHaveBeenCalledTimes(2); + expect(lnd['connectPeer']) + .toHaveBeenCalledWith(peerPubKey, externalIp1); + expect(lnd['connectPeer']) + .toHaveBeenCalledWith(peerPubKey, externalIp2); + }); + + test('it does succeed when connecting to already connected peer', async () => { + expect.assertions(4); + lnd = new LndClient(config, currency, logger); + lnd['openChannelSync'] = jest.fn().mockReturnValue(Promise.resolve()); + const alreadyConnected = () => { + throw new Error('already connected'); + }; + lnd['connectPeer'] = jest.fn() + .mockImplementation(alreadyConnected); + await lnd.openChannel({ + units, + peerIdentifier: peerPubKey, + lndUris: lndListeningUris, + }); + expect(lnd['connectPeer']).toHaveBeenCalledTimes(1); + expect(lnd['connectPeer']) + .toHaveBeenCalledWith(peerPubKey, externalIp1); + expect(lnd['openChannelSync']).toHaveBeenCalledTimes(1); + expect(lnd['openChannelSync']) + .toHaveBeenCalledWith(peerPubKey, units); + }); + + test('it throws when timeout reached', async () => { + expect.assertions(3); + jest.useFakeTimers(); + lnd = new LndClient(config, currency, logger); + lnd['openChannelSync'] = jest.fn().mockReturnValue(Promise.resolve()); + const timeOut = () => { + jest.runAllTimers(); + return new Promise(() => {}); + }; + lnd['connectPeer'] = jest.fn() + .mockImplementation(timeOut); + try { + await lnd.openChannel({ + units, + peerIdentifier: peerPubKey, + lndUris: lndListeningUris, + }); + } catch (e) { + expect(e).toMatchSnapshot(); + } + expect(lnd['connectPeer']).toHaveBeenCalledTimes(2); + expect(lnd['openChannelSync']).not.toHaveBeenCalled(); + jest.clearAllTimers(); + }); + + test('it stops trying to connect to lnd uris when first once succeeds', async () => { + expect.assertions(3); + lnd = new LndClient(config, currency, logger); + lnd['openChannelSync'] = jest.fn().mockReturnValue(Promise.resolve()); + lnd['connectPeer'] = jest.fn() + .mockImplementationOnce(() => { + return Promise.resolve(); + }); + await lnd.openChannel({ + units, + peerIdentifier: peerPubKey, + lndUris: lndListeningUris, + }); + expect(lnd['connectPeer']).toHaveBeenCalledTimes(1); + expect(lnd['connectPeer']) + .toHaveBeenCalledWith(peerPubKey, externalIp1); + expect(lnd['connectPeer']) + .not.toHaveBeenCalledWith(peerPubKey, externalIp2); + }); + + test('it throws when openchannel fails', async () => { + expect.assertions(2); + lnd = new LndClient(config, currency, logger); + lnd['connectPeer'] = jest.fn().mockReturnValue(Promise.resolve()); + lnd['openChannelSync'] = jest.fn().mockImplementation(() => { + throw new Error('openChannelSync error'); + }); + try { + await lnd.openChannel({ + units, + peerIdentifier: peerPubKey, + lndUris: lndListeningUris, + }); + } catch (e) { + expect(e).toMatchSnapshot(); + } + expect(lnd['openChannelSync']).toHaveBeenCalledTimes(1); + }); + + }); + +}); diff --git a/test/jest/Orderbook.spec.ts b/test/jest/Orderbook.spec.ts index b77c39e01..3b1bd4346 100644 --- a/test/jest/Orderbook.spec.ts +++ b/test/jest/Orderbook.spec.ts @@ -10,6 +10,7 @@ import SwapClientManager from '../../lib/swaps/SwapClientManager'; import Network from '../../lib/p2p/Network'; import { XuNetwork, SwapClientType } from '../../lib/constants/enums'; import NodeKey from '../../lib/nodekey/NodeKey'; +import { UnitConverter } from '../../lib/utils/UnitConverter'; jest.mock('../../lib/db/DB', () => { return jest.fn().mockImplementation(() => { @@ -98,6 +99,7 @@ describe('OrderBook', () => { let peer: Peer; let swapClientManager: SwapClientManager; let network: Network; + let unitConverter: UnitConverter; beforeEach(async () => { config = new Config(); @@ -116,7 +118,9 @@ describe('OrderBook', () => { version: '1.0.0', nodeKey: new mockedNodeKey(), }); - swapClientManager = new SwapClientManager(config, loggers); + unitConverter = new UnitConverter(); + unitConverter.init(); + swapClientManager = new SwapClientManager(config, loggers, unitConverter); swaps = new Swaps(loggers.swaps, db.models, pool, swapClientManager); swaps.swapClientManager = swapClientManager; orderbook = new Orderbook({ diff --git a/test/jest/Pool.spec.ts b/test/jest/Pool.spec.ts index 66e4cd800..2fbd6b246 100644 --- a/test/jest/Pool.spec.ts +++ b/test/jest/Pool.spec.ts @@ -49,7 +49,7 @@ describe('P2P Pool', () => { test('updateLndState sets lnd pub key and token identifier', async () => { const lndPubKey = 'lndPubKey'; const chain = 'bitcoin-regtest'; - pool.updateLndState('BTC', lndPubKey, chain); + pool.updateLndState({ chain, pubKey: lndPubKey, currency: 'BTC' }); expect(pool.getTokenIdentifier('BTC')).toEqual(chain); expect(pool['nodeState'].lndPubKeys['BTC']).toEqual(lndPubKey); }); diff --git a/test/jest/RaidenClient.spec.ts b/test/jest/RaidenClient.spec.ts index c245e8b86..e1990156c 100644 --- a/test/jest/RaidenClient.spec.ts +++ b/test/jest/RaidenClient.spec.ts @@ -2,6 +2,7 @@ import RaidenClient from '../../lib/raidenclient/RaidenClient'; import { RaidenClientConfig, TokenPaymentResponse } from '../../lib/raidenclient/types'; import Logger from '../../lib/Logger'; import { SwapDeal } from '../../lib/swaps/types'; +import { UnitConverter } from '../../lib/utils/UnitConverter'; const getValidTokenPaymentResponse = () => { return { @@ -16,8 +17,8 @@ const getValidTokenPaymentResponse = () => { }; }; -const channelBalance1 = 25000000; -const channelBalance2 = 5000000; +const channelBalance1 = 250000000000000000; +const channelBalance2 = 50000000000000000; const channelBalanceTokenAddress = '0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8'; const getChannelsResponse = [ { @@ -26,7 +27,7 @@ const getChannelsResponse = [ partner_address: '0x61C808D82A3Ac53231750daDc13c777b59310bD9', token_address: channelBalanceTokenAddress, balance: channelBalance1, - total_deposit: 35000000, + total_deposit: 350000000000000000, state: 'opened', settle_timeout: 100, reveal_timeout: 30, @@ -37,7 +38,7 @@ const getChannelsResponse = [ partner_address: '0x2A4722462bb06511b036F00C7EbF938B2377F446', token_address: channelBalanceTokenAddress, balance: channelBalance2, - total_deposit: 35000000, + total_deposit: 350000000000000000, state: 'opened', settle_timeout: 100, reveal_timeout: 30, @@ -88,6 +89,7 @@ describe('RaidenClient', () => { let raiden: RaidenClient; let config: RaidenClientConfig; let raidenLogger: Logger; + let unitConverter: UnitConverter; beforeEach(() => { config = { @@ -98,6 +100,8 @@ describe('RaidenClient', () => { raidenLogger = new Logger({}); raidenLogger.info = jest.fn(); raidenLogger.error = jest.fn(); + unitConverter = new UnitConverter(); + unitConverter.init(); }); afterEach(async () => { @@ -105,39 +109,117 @@ describe('RaidenClient', () => { await raiden.close(); }); - test('sendPayment removes 0x from secret', async () => { - raiden = new RaidenClient(config, raidenLogger); - await raiden.init(); - const validTokenPaymentResponse: TokenPaymentResponse = getValidTokenPaymentResponse(); - raiden['tokenPayment'] = jest.fn() - .mockReturnValue(Promise.resolve(validTokenPaymentResponse)); - raiden.tokenAddresses.get = jest.fn().mockReturnValue(validTokenPaymentResponse.token_address); - const deal: SwapDeal = getValidDeal(); - await expect(raiden.sendPayment(deal)) - .resolves.toMatchSnapshot(); + describe('sendPayment', () => { + test('it removes 0x from secret', async () => { + raiden = new RaidenClient({ unitConverter, config, logger: raidenLogger }); + await raiden.init(); + const validTokenPaymentResponse: TokenPaymentResponse = getValidTokenPaymentResponse(); + raiden['tokenPayment'] = jest.fn() + .mockReturnValue(Promise.resolve(validTokenPaymentResponse)); + raiden.tokenAddresses.get = jest.fn().mockReturnValue(validTokenPaymentResponse.token_address); + const deal: SwapDeal = getValidDeal(); + await expect(raiden.sendPayment(deal)) + .resolves.toMatchSnapshot(); + }); + + test('it rejects in case of empty secret response', async () => { + raiden = new RaidenClient({ unitConverter, config, logger: raidenLogger }); + await raiden.init(); + const invalidTokenPaymentResponse: TokenPaymentResponse = { + ...getValidTokenPaymentResponse(), + secret: '', + }; + raiden['tokenPayment'] = jest.fn() + .mockReturnValue(Promise.resolve(invalidTokenPaymentResponse)); + raiden.tokenAddresses.get = jest.fn().mockReturnValue(invalidTokenPaymentResponse.token_address); + const deal: SwapDeal = getValidDeal(); + await expect(raiden.sendPayment(deal)) + .rejects.toMatchSnapshot(); + }); }); - test('sendPayment rejects in case of empty secret response', async () => { - raiden = new RaidenClient(config, raidenLogger); - await raiden.init(); - const invalidTokenPaymentResponse: TokenPaymentResponse = { - ...getValidTokenPaymentResponse(), - secret: '', - }; - raiden['tokenPayment'] = jest.fn() - .mockReturnValue(Promise.resolve(invalidTokenPaymentResponse)); - raiden.tokenAddresses.get = jest.fn().mockReturnValue(invalidTokenPaymentResponse.token_address); - const deal: SwapDeal = getValidDeal(); - await expect(raiden.sendPayment(deal)) - .rejects.toMatchSnapshot(); + describe('openChannel', () => { + let peerRaidenAddress: string; + let units: number; + let currency: string; + + beforeEach(() => { + peerRaidenAddress = '0x10D8CCAD85C7dc123090B43aA1f98C00a303BFC5'; + units = 5000000; + currency = 'WETH'; + }); + + test('it fails when tokenAddress for currency not found', async () => { + expect.assertions(1); + raiden = new RaidenClient({ unitConverter, config, logger: raidenLogger }); + await raiden.init(); + try { + await raiden.openChannel({ + units, + currency, + peerIdentifier: peerRaidenAddress, + }); + } catch (e) { + expect(e).toMatchSnapshot(); + } + }); + + test('it throws when openChannel fails', async () => { + expect.assertions(1); + raiden = new RaidenClient({ unitConverter, config, logger: raidenLogger }); + const peerRaidenAddress = '0x10D8CCAD85C7dc123090B43aA1f98C00a303BFC5'; + const currency = 'WETH'; + const mockTokenAddresses = new Map(); + const wethTokenAddress = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; + mockTokenAddresses.set('WETH', wethTokenAddress); + raiden.tokenAddresses = mockTokenAddresses; + raiden['openChannelRequest'] = jest.fn().mockImplementation(() => { + throw new Error('openChannelRequest error'); + }); + await raiden.init(); + try { + await raiden.openChannel({ + units, + currency, + peerIdentifier: peerRaidenAddress, + }); + } catch (e) { + expect(e).toMatchSnapshot(); + } + }); + + test('it opens a channel', async () => { + expect.assertions(2); + raiden = new RaidenClient({ unitConverter, config, logger: raidenLogger }); + const peerRaidenAddress = '0x10D8CCAD85C7dc123090B43aA1f98C00a303BFC5'; + const currency = 'WETH'; + const mockTokenAddresses = new Map(); + const wethTokenAddress = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; + mockTokenAddresses.set('WETH', wethTokenAddress); + raiden.tokenAddresses = mockTokenAddresses; + raiden['openChannelRequest'] = jest.fn().mockReturnValue(Promise.resolve()); + await raiden.init(); + await raiden.openChannel({ + units, + currency, + peerIdentifier: peerRaidenAddress, + }); + expect(raiden['openChannelRequest']).toHaveBeenCalledTimes(1); + expect(raiden['openChannelRequest']).toHaveBeenCalledWith({ + partner_address: peerRaidenAddress, + token_address: wethTokenAddress, + total_deposit: units, + settle_timeout: 500, + }); + }); }); test('channelBalance calculates the total balance of open channels for a currency', async () => { - raiden = new RaidenClient(config, raidenLogger); + raiden = new RaidenClient({ unitConverter, config, logger: raidenLogger }); await raiden.init(); raiden.tokenAddresses.get = jest.fn().mockReturnValue(channelBalanceTokenAddress); raiden['getChannels'] = jest.fn() .mockReturnValue(Promise.resolve(getChannelsResponse)); - await expect(raiden.channelBalance('ABC')).resolves.toHaveProperty('balance', channelBalance1 + channelBalance2); + await expect(raiden.channelBalance('WETH')).resolves.toHaveProperty('balance', 30000000); }); }); diff --git a/test/jest/Service.spec.ts b/test/jest/Service.spec.ts new file mode 100644 index 000000000..6722c7e16 --- /dev/null +++ b/test/jest/Service.spec.ts @@ -0,0 +1,93 @@ +import Service, { ServiceComponents } from '../../lib/service/Service'; +import Orderbook from '../../lib/orderbook/OrderBook'; +import Swaps from '../../lib/swaps/Swaps'; +import SwapClientManager from '../../lib/swaps/SwapClientManager'; +import Pool from '../../lib/p2p/Pool'; +import Peer from '../../lib/p2p/Peer'; + +jest.mock('../../lib/orderbook/OrderBook'); +const mockedOrderbook = >Orderbook; +jest.mock('../../lib/swaps/Swaps'); +const mockedSwaps = >Swaps; +jest.mock('../../lib/swaps/SwapClientManager'); +const mockedSwapClientManager = >SwapClientManager; +jest.mock('../../lib/p2p/Pool'); +const mockedPool = >Pool; +jest.mock('../../lib/p2p/Peer'); +const mockedPeer = >Peer; + +const getArgs = () => { + return { + nodePubKey: '02f8895eb03c37b2665415be4d83b20228acc0abc55ebf6728565141c66cfc164a', + amount: 16000000, + currency: 'BTC', + }; +}; +describe('Service', () => { + let service: Service; + let components: ServiceComponents; + let peer: Peer; + + beforeEach(() => { + components = { + orderBook: new mockedOrderbook(), + swapClientManager: new mockedSwapClientManager(), + pool: new mockedPool(), + swaps: new mockedSwaps(), + version: '1.0.0', + shutdown: jest.fn(), + }; + peer = new mockedPeer(); + components.pool.getPeer = jest.fn().mockReturnValue(peer); + components.swapClientManager.openChannel = jest.fn(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('openChannel', () => { + test('gets peer from pool for swapClientManager', async () => { + service = new Service(components); + const args = getArgs(); + await service.openChannel(args); + expect(components.pool.getPeer) + .toHaveBeenCalledWith(args.nodePubKey); + expect(components.swapClientManager.openChannel) + .toHaveBeenCalledWith({ + peer, + amount: args.amount, + currency: args.currency, + }); + }); + + test('throws when peer not found', async () => { + expect.assertions(1); + service = new Service(components); + const args = getArgs(); + components.pool.getPeer = jest.fn().mockImplementation(() => { + throw new Error('peer not found'); + }); + try { + await service.openChannel(args); + } catch (e) { + expect(e).toMatchSnapshot(); + } + }); + + test('throws when failure from swapClientManager', async () => { + expect.assertions(1); + service = new Service(components); + const args = getArgs(); + components.swapClientManager.openChannel = jest.fn().mockImplementation(() => { + throw new Error('swapClientManager openChannel failure'); + }); + try { + await service.openChannel(args); + } catch (e) { + expect(e).toMatchSnapshot(); + } + }); + }); + +}); diff --git a/test/jest/SwapClientManager.spec.ts b/test/jest/SwapClientManager.spec.ts index 467661dd6..31b506849 100644 --- a/test/jest/SwapClientManager.spec.ts +++ b/test/jest/SwapClientManager.spec.ts @@ -3,6 +3,8 @@ import DB from '../../lib/db/DB'; import SwapClientManager from '../../lib/swaps/SwapClientManager'; import Config from '../../lib/Config'; import { SwapClientType } from '../../lib/constants/enums'; +import Peer from '../../lib/p2p/Peer'; +import { UnitConverter } from '../../lib/utils/UnitConverter'; jest.mock('../../lib/db/DB', () => { return jest.fn().mockImplementation(() => { @@ -28,6 +30,7 @@ const mockLndPubKey = 1; const lndInfoMock = jest.fn(() => Promise.resolve()); const onListenerMock = jest.fn(); const closeMock = jest.fn(); +const mockLndOpenChannel = jest.fn(); jest.mock('../../lib/lndclient/LndClient', () => { return jest.fn().mockImplementation(() => { return { @@ -38,11 +41,13 @@ jest.mock('../../lib/lndclient/LndClient', () => { isDisabled: () => false, getLndInfo: lndInfoMock, close: closeMock, + openChannel: mockLndOpenChannel, }; }); }); const mockRaidenAddress = 1234567890; let mockRaidenClientIsDisabled = false; +const mockRaidenOpenChannel = jest.fn(); jest.mock('../../lib/raidenclient/RaidenClient', () => { return jest.fn().mockImplementation(() => { const tokenAddresses = new Map(); @@ -54,6 +59,7 @@ jest.mock('../../lib/raidenclient/RaidenClient', () => { address: mockRaidenAddress, isDisabled: () => mockRaidenClientIsDisabled, close: closeMock, + openChannel: mockRaidenOpenChannel, }; }); }); @@ -70,11 +76,14 @@ const loggers = { swaps: logger, http: logger, }; +jest.mock('../../lib/p2p/Peer'); +const mockedPeer = >Peer; describe('Swaps.SwapClientManager', () => { let config: Config; let db: DB; let swapClientManager: SwapClientManager; + let unitConverter: UnitConverter; beforeEach(async () => { config = new Config(); @@ -104,6 +113,8 @@ describe('Swaps.SwapClientManager', () => { port: 1234, }; db = new DB(loggers.db, config.dbpath); + unitConverter = new UnitConverter(); + unitConverter.init(); }); afterEach(() => { @@ -111,7 +122,7 @@ describe('Swaps.SwapClientManager', () => { }); test('it initializes lnd-ltc, lnd-btc and raiden', async () => { - swapClientManager = new SwapClientManager(config, loggers); + swapClientManager = new SwapClientManager(config, loggers, unitConverter); await swapClientManager.init(db.models); expect(swapClientManager['swapClients'].size).toEqual(3); expect(onListenerMock).toHaveBeenCalledTimes(5); @@ -133,7 +144,7 @@ describe('Swaps.SwapClientManager', () => { test('it initializes lnd-ltc and lnd-btc', async () => { config.raiden.disable = true; mockRaidenClientIsDisabled = true; - swapClientManager = new SwapClientManager(config, loggers); + swapClientManager = new SwapClientManager(config, loggers, unitConverter); await swapClientManager.init(db.models); expect(swapClientManager['swapClients'].size).toEqual(2); expect(onListenerMock).toHaveBeenCalledTimes(4); @@ -147,7 +158,7 @@ describe('Swaps.SwapClientManager', () => { config.lnd.LTC!.disable = true; config.raiden.disable = true; mockRaidenClientIsDisabled = true; - swapClientManager = new SwapClientManager(config, loggers); + swapClientManager = new SwapClientManager(config, loggers, unitConverter); await swapClientManager.init(db.models); expect(swapClientManager['swapClients'].size).toEqual(1); expect(onListenerMock).toHaveBeenCalledTimes(2); @@ -160,7 +171,7 @@ describe('Swaps.SwapClientManager', () => { config.lnd.BTC!.disable = true; config.lnd.LTC!.disable = true; config.raiden.disable = true; - swapClientManager = new SwapClientManager(config, loggers); + swapClientManager = new SwapClientManager(config, loggers, unitConverter); await swapClientManager.init(db.models); expect(swapClientManager['swapClients'].size).toEqual(0); expect(onListenerMock).toHaveBeenCalledTimes(0); @@ -170,14 +181,116 @@ describe('Swaps.SwapClientManager', () => { expect(closeMock).toHaveBeenCalledTimes(0); }); - it('closes lnd-btc, lnd-ltc and raiden', async () => { + test('closes lnd-btc, lnd-ltc and raiden', async () => { config.raiden.disable = false; mockRaidenClientIsDisabled = false; - swapClientManager = new SwapClientManager(config, loggers); + swapClientManager = new SwapClientManager(config, loggers, unitConverter); await swapClientManager.init(db.models); expect(swapClientManager['swapClients'].size).toEqual(3); await swapClientManager.close(); expect(closeMock).toHaveBeenCalledTimes(3); }); + describe('openChannel', () => { + let peer: Peer; + let peerLndPubKey: string; + + beforeEach(() => { + peer = new mockedPeer(); + peerLndPubKey = '02afaef2634e5c7ca8d682b828a62bd040929b1e4b5030b21e2a0a891cf545b2e1'; + peer.getIdentifier = jest.fn().mockReturnValue(peerLndPubKey); + }); + + test('it fails without swap client', async () => { + expect.assertions(1); + const currency = 'BTC'; + const amount = 16000000; + swapClientManager = new SwapClientManager(config, loggers, unitConverter); + swapClientManager.get = jest.fn().mockReturnValue(undefined); + await swapClientManager.init(db.models); + try { + await swapClientManager.openChannel({ peer, currency, amount }); + } catch (e) { + expect(e).toMatchSnapshot(); + } + }); + + test('it fails without peerSwapClientPubKey', async () => { + expect.assertions(1); + const currency = 'BTC'; + const amount = 16000000; + swapClientManager = new SwapClientManager(config, loggers, unitConverter); + peer.getIdentifier = jest.fn().mockReturnValue(undefined); + await swapClientManager.init(db.models); + try { + await swapClientManager.openChannel({ peer, currency, amount }); + } catch (e) { + expect(e).toMatchSnapshot(); + } + }); + + test('it fails lnd without lndUris', async () => { + expect.assertions(1); + const currency = 'BTC'; + const amount = 16000000; + swapClientManager = new SwapClientManager(config, loggers, unitConverter); + peer.getLndUris = jest.fn().mockReturnValue(undefined); + await swapClientManager.init(db.models); + try { + await swapClientManager.openChannel({ peer, currency, amount }); + } catch (e) { + expect(e).toMatchSnapshot(); + } + }); + + test('it opens a channel using lnd', async () => { + const currency = 'BTC'; + const amount = 16000000; + swapClientManager = new SwapClientManager(config, loggers, unitConverter); + const getClientSpy = jest.spyOn(swapClientManager, 'get'); + const lndListeningUris = [ + '123.456.789.321:9735', + '192.168.63.155:9777', + ]; + peer.getLndUris = jest.fn().mockReturnValue(lndListeningUris); + await swapClientManager.init(db.models); + await swapClientManager.openChannel({ peer, currency, amount }); + expect(getClientSpy).toHaveBeenCalledWith(currency); + expect(peer.getIdentifier).toHaveBeenCalledWith(SwapClientType.Lnd, currency); + expect(mockLndOpenChannel).toHaveBeenCalledTimes(1); + expect(mockLndOpenChannel).toHaveBeenCalledWith( + expect.objectContaining({ + units: amount, + lndUris: lndListeningUris, + peerIdentifier: peerLndPubKey, + }), + ); + }); + + test('it opens a channel using raiden', async () => { + const currency = 'WETH'; + const amount = 5000000; + const expectedUnits = 50000000000000000; + const peerRaidenAddress = '0x10D8CCAD85C7dc123090B43aA1f98C00a303BFC5'; + peer.getIdentifier = jest.fn().mockReturnValue(peerRaidenAddress); + swapClientManager = new SwapClientManager(config, loggers, unitConverter); + const getClientSpy = jest.spyOn(swapClientManager, 'get'); + peer.getLndUris = jest.fn(); + await swapClientManager.init(db.models); + await swapClientManager.openChannel({ peer, currency, amount }); + expect(getClientSpy).toHaveBeenCalledWith(currency); + expect(peer.getIdentifier).toHaveBeenCalledWith(SwapClientType.Raiden, currency); + expect(peer.getLndUris).toHaveBeenCalledTimes(0); + expect(mockRaidenOpenChannel).toHaveBeenCalledTimes(1); + expect(mockRaidenOpenChannel).toHaveBeenCalledWith( + expect.objectContaining({ + currency, + units: expectedUnits, + peerIdentifier: peerRaidenAddress, + }), + ); + }); + + }); + }); diff --git a/test/jest/UnitConverter.spec.ts b/test/jest/UnitConverter.spec.ts new file mode 100644 index 000000000..27eb15734 --- /dev/null +++ b/test/jest/UnitConverter.spec.ts @@ -0,0 +1,86 @@ +import { UnitConverter } from '../../lib/utils/UnitConverter'; + +describe('UnitConverter', () => { + + describe('amountToUnits', () => { + + test('converts BTC amount to units', () => { + const unitConverter = new UnitConverter(); + unitConverter.init(); + const amount = 99999999; + expect(unitConverter. + amountToUnits({ + amount, + currency: 'BTC', + }, + )).toEqual(amount); + }); + + test('converts WETH amount to units', () => { + const unitConverter = new UnitConverter(); + unitConverter.init(); + expect(unitConverter. + amountToUnits({ + amount: 7500000, + currency: 'WETH', + }, + )).toEqual(75000000000000000); + }); + + test('throws error upon unknown currency', () => { + expect.assertions(1); + const unitConverter = new UnitConverter(); + unitConverter.init(); + try { + unitConverter.amountToUnits({ + amount: 123, + currency: 'ABC', + }); + } catch (e) { + expect(e).toMatchSnapshot(); + } + }); + + }); + + describe('unitsToAmount', () => { + + test('converts BTC units to amount', () => { + const unitConverter = new UnitConverter(); + unitConverter.init(); + const units = 99999999; + expect(unitConverter. + unitsToAmount({ + units, + currency: 'BTC', + }, + )).toEqual(units); + }); + + test('converts WETH units to amount', () => { + const unitConverter = new UnitConverter(); + unitConverter.init(); + expect(unitConverter. + unitsToAmount({ + units: 75000000000000000, + currency: 'WETH', + }, + )).toEqual(7500000); + }); + + test('throws error upon unknown currency', () => { + expect.assertions(1); + const unitConverter = new UnitConverter(); + unitConverter.init(); + try { + unitConverter.unitsToAmount({ + units: 123, + currency: 'ABC', + }); + } catch (e) { + expect(e).toMatchSnapshot(); + } + }); + + }); +}); diff --git a/test/jest/__snapshots__/LndClient.spec.ts.snap b/test/jest/__snapshots__/LndClient.spec.ts.snap new file mode 100644 index 000000000..01590e90c --- /dev/null +++ b/test/jest/__snapshots__/LndClient.spec.ts.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LndClient openChannel it throws when connectPeer fails 1`] = `[Error: connectPeerAddreses failed]`; + +exports[`LndClient openChannel it throws when openchannel fails 1`] = `[Error: openChannelSync error]`; + +exports[`LndClient openChannel it throws when timeout reached 1`] = `[Error: connectPeerAddreses failed]`; diff --git a/test/jest/__snapshots__/RaidenClient.spec.ts.snap b/test/jest/__snapshots__/RaidenClient.spec.ts.snap index 6c7858603..8e8a881c1 100644 --- a/test/jest/__snapshots__/RaidenClient.spec.ts.snap +++ b/test/jest/__snapshots__/RaidenClient.spec.ts.snap @@ -1,10 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RaidenClient sendPayment rejects in case of empty secret response 1`] = ` +exports[`RaidenClient openChannel it fails when tokenAddress for currency not found 1`] = ` +Object { + "code": "5.7", + "message": "raiden token address not found", +} +`; + +exports[`RaidenClient openChannel it throws when openChannel fails 1`] = `[Error: openChannelRequest error]`; + +exports[`RaidenClient sendPayment it rejects in case of empty secret response 1`] = ` Object { "code": "5.8", "message": "raiden TokenPaymentResponse is invalid", } `; -exports[`RaidenClient sendPayment removes 0x from secret 1`] = `"9f345e3751d8b7f38d34b7a3dd636a9d7a0c2f36d991615e6653501f30c6ec56"`; +exports[`RaidenClient sendPayment it removes 0x from secret 1`] = `"9f345e3751d8b7f38d34b7a3dd636a9d7a0c2f36d991615e6653501f30c6ec56"`; diff --git a/test/jest/__snapshots__/Service.spec.ts.snap b/test/jest/__snapshots__/Service.spec.ts.snap new file mode 100644 index 000000000..b0028c982 --- /dev/null +++ b/test/jest/__snapshots__/Service.spec.ts.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Service openChannel throws when failure from swapClientManager 1`] = ` +Object { + "code": "6.5", + "message": "failed to open channel with nodePubKey: 02f8895eb03c37b2665415be4d83b20228acc0abc55ebf6728565141c66cfc164a, currency: BTC, amount: 16000000, message: swapClientManager openChannel failure", +} +`; + +exports[`Service openChannel throws when peer not found 1`] = ` +Object { + "code": "6.5", + "message": "failed to open channel with nodePubKey: 02f8895eb03c37b2665415be4d83b20228acc0abc55ebf6728565141c66cfc164a, currency: BTC, amount: 16000000, message: peer not found", +} +`; diff --git a/test/jest/__snapshots__/SwapClientManager.spec.ts.snap b/test/jest/__snapshots__/SwapClientManager.spec.ts.snap new file mode 100644 index 000000000..1789ae6e9 --- /dev/null +++ b/test/jest/__snapshots__/SwapClientManager.spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Swaps.SwapClientManager openChannel it fails lnd without lndUris 1`] = `[Error: unable to get lnd listening uris]`; + +exports[`Swaps.SwapClientManager openChannel it fails without peerSwapClientPubKey 1`] = `[Error: unable to get swap client pubKey for peer]`; + +exports[`Swaps.SwapClientManager openChannel it fails without swap client 1`] = ` +Object { + "code": "7.1", + "message": "swapClient for currency BTC not found", +} +`; diff --git a/test/jest/__snapshots__/UnitConverter.spec.ts.snap b/test/jest/__snapshots__/UnitConverter.spec.ts.snap new file mode 100644 index 000000000..216973aa9 --- /dev/null +++ b/test/jest/__snapshots__/UnitConverter.spec.ts.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UnitConverter amountToUnits throws error upon unknown currency 1`] = `[Error: cannot convert ABC amount of 123 to units because units per currency was not found in the database]`; + +exports[`UnitConverter unitsToAmount throws error upon unknown currency 1`] = `[Error: cannot convert ABC units of 123 to amount because units per currency was not found in the database]`; diff --git a/test/unit/Parser.spec.ts b/test/unit/Parser.spec.ts index 5aeb6698b..21d6df33f 100644 --- a/test/unit/Parser.spec.ts +++ b/test/unit/Parser.spec.ts @@ -194,6 +194,7 @@ describe('Parser', () => { pairs: [uuid()], raidenAddress: uuid(), lndPubKeys: { BTC: uuid(), LTC: uuid() }, + lndUris: { BTC: [''], LTC: [''] }, tokenIdentifiers: { BTC: 'bitcoin-testnet', LTC: 'litecoin-testnet' }, };