diff --git a/bin/xud b/bin/xud index 4be687dcc..04c6e4f0f 100755 --- a/bin/xud +++ b/bin/xud @@ -72,6 +72,11 @@ const { argv } = require('yargs') type: 'string', alias: 'x', }, + 'debug.raidenDirectChannelChecks': { + describe: 'Whether to require direct channels for raiden payments', + type: 'boolean', + default: undefined, + }, 'http.port': { describe: 'Port to listen for http requests', type: 'number', diff --git a/lib/Config.ts b/lib/Config.ts index 0c0a8f859..836737532 100644 --- a/lib/Config.ts +++ b/lib/Config.ts @@ -23,6 +23,7 @@ class Config { public raiden: RaidenClientConfig; public orderthresholds: OrderBookThresholds; public webproxy: { port: number, disable: boolean }; + public debug: { raidenDirectChannelChecks: boolean }; public instanceid = 0; /** Whether to intialize a new database with default values. */ public initdb = true; @@ -96,6 +97,9 @@ class Config { disable: true, port: 8080, }; + this.debug = { + raidenDirectChannelChecks: true, + }; // TODO: add dynamic max/min price limits this.orderthresholds = { minQuantity: 0, // 0 = disabled diff --git a/lib/lndclient/LndClient.ts b/lib/lndclient/LndClient.ts index 7273d3d09..a0c6712ae 100644 --- a/lib/lndclient/LndClient.ts +++ b/lib/lndclient/LndClient.ts @@ -496,7 +496,7 @@ class LndClient extends SwapClient { return this.unaryCall('listChannels', new lndrpc.ListChannelsRequest()); } - public getRoutes = async (amount: number, destination: string, finalCltvDelta = this.cltvDelta): Promise => { + public getRoutes = async (amount: number, destination: string, _currency: string, finalCltvDelta = this.cltvDelta): Promise => { const request = new lndrpc.QueryRoutesRequest(); request.setAmt(amount); request.setFinalCltvDelta(finalCltvDelta); diff --git a/lib/orderbook/OrderBook.ts b/lib/orderbook/OrderBook.ts index 4e7e3543f..850301d0f 100644 --- a/lib/orderbook/OrderBook.ts +++ b/lib/orderbook/OrderBook.ts @@ -129,7 +129,7 @@ class OrderBook extends EventEmitter { return order.quantity >= minQuantity; } /** - * Checks that a currency advertised by a peer are known to us, have a swap client identifier, + * Checks that a currency advertised by a peer is known to us, has a swap client identifier, * and that their token identifier matches ours. */ private isPeerCurrencySupported = (peer: Peer, currency: string) => { diff --git a/lib/p2p/Peer.ts b/lib/p2p/Peer.ts index 8e121900a..b27eee7a9 100644 --- a/lib/p2p/Peer.ts +++ b/lib/p2p/Peer.ts @@ -64,10 +64,10 @@ class Peer extends EventEmitter { public active = false; /** Timer to periodically call getNodes #402 */ public discoverTimer?: NodeJS.Timer; - /** Currencies that we have verified that we can swap for this peer. */ + /** Currencies that we have verified we can swap with this peer. */ public verifiedCurrencies = new Set(); /** - * Currencies that we cannot swap because we are missing a swap client identifier or because our + * Currencies that we cannot swap because we are missing a swap client identifier or because the * peer's token identifier for this currency does not match ours - for example this may happen * because a peer is using a different raiden token contract address for a currency than we are. */ @@ -130,6 +130,10 @@ class Peer extends EventEmitter { return this.nodeState ? this.nodeState.addresses : undefined; } + public get raidenAddress(): string | undefined { + return this.nodeState ? this.nodeState.raidenAddress : undefined; + } + /** Returns a list of trading pairs advertised by this peer. */ public get advertisedPairs(): string[] { if (this.nodeState) { diff --git a/lib/raidenclient/RaidenClient.ts b/lib/raidenclient/RaidenClient.ts index 9775a574c..84f460856 100644 --- a/lib/raidenclient/RaidenClient.ts +++ b/lib/raidenclient/RaidenClient.ts @@ -51,13 +51,14 @@ class RaidenClient extends SwapClient { private disable: boolean; private unitConverter: UnitConverter; private maximumOutboundAmounts = new Map(); + private directChannelChecks: boolean; /** * Creates a raiden client. */ constructor( - { config, logger, unitConverter }: - { config: RaidenClientConfig, logger: Logger, unitConverter: UnitConverter }, + { config, logger, unitConverter, directChannelChecks = false }: + { config: RaidenClientConfig, logger: Logger, unitConverter: UnitConverter, directChannelChecks: boolean }, ) { super(logger); const { disable, host, port } = config; @@ -66,6 +67,7 @@ class RaidenClient extends SwapClient { this.host = host; this.disable = disable; this.unitConverter = unitConverter; + this.directChannelChecks = directChannelChecks; } /** @@ -201,12 +203,35 @@ class RaidenClient extends SwapClient { // not implemented, raiden does not use invoices } - public getRoutes = async (_amount: number, _destination: string) => { - // stub placeholder, query routes not currently implemented in raiden - // assume a fixed lock time of 100 Raiden's blocks - return [{ + public getRoutes = async (amount: number, destination: string, currency: string) => { + // a query routes call is not currently provided by raiden + + /** A placeholder route value that assumes a fixed lock time of 100 Raiden's blocks. */ + const placeholderRoute = { getTotalTimeLock: () => 101, - }]; + }; + + if (this.directChannelChecks) { + // temporary check for a direct channel in raiden + const tokenAddress = this.tokenAddresses.get(currency); + const channels = await this.getChannels(tokenAddress); + for (const channel of channels) { + if (channel.partner_address && channel.partner_address === destination) { + const balance = channel.balance; + if (balance >= amount) { + this.logger.debug(`found a direct channel for ${currency} to ${destination} with ${balance} balance`); + return [placeholderRoute]; + } else { + this.logger.warn(`direct channel found for ${currency} to ${destination} with balance of ${balance} is insufficient for ${amount})`); + return []; // we have a direct channel but it doesn't have enough balance, return no routes + } + } + } + this.logger.warn(`no direct channel found for ${currency} to ${destination}`); + return []; // no direct channels, return no routes + } else { + return [placeholderRoute]; + } } public getHeight = async () => { diff --git a/lib/raidenclient/types.ts b/lib/raidenclient/types.ts index 05c867af3..7dd629dc2 100644 --- a/lib/raidenclient/types.ts +++ b/lib/raidenclient/types.ts @@ -32,6 +32,7 @@ export type Channel = OpenChannelPayload & { channel_address: string; token_network_identifier: string; channel_identifier: number; + /** The balance of the channel denominated in the smallest units supported by the token. */ balance: number state: string; }; @@ -49,6 +50,7 @@ export type TokenPaymentResponse = TokenPaymentRequest & { export type TokenPaymentRequest = { token_address: string, target_address: string, + /** The amount of the payment request denominated in the smallest units supported by the token. */ amount: number, secret_hash: string, identifier?: number, diff --git a/lib/swaps/SwapClient.ts b/lib/swaps/SwapClient.ts index 6090a55fa..b9ea2fc17 100644 --- a/lib/swaps/SwapClient.ts +++ b/lib/swaps/SwapClient.ts @@ -13,7 +13,9 @@ enum ClientStatus { } type ChannelBalance = { + /** The cumulative balance of open channels denominated in satoshis. */ balance: number, + /** The cumulative balance of pending channels denominated in satoshis. */ pendingOpenBalance: number, }; @@ -107,12 +109,12 @@ abstract class SwapClient extends EventEmitter { public abstract async sendSmallestAmount(rHash: string, destination: string, currency: string): Promise; /** - * Gets routes for the given currency, amount and peerPubKey. - * @param amount the capacity of the route - * @param destination target node for the route + * Gets routes for the given currency, amount, and swap identifier. + * @param amount the capacity the route must support denominated in the smallest units supported by its currency + * @param destination the identifier for the receiving node * @returns routes */ - public abstract async getRoutes(amount: number, destination: string, finalCltvDelta?: number): Promise; + public abstract async getRoutes(amount: number, destination: string, currency: string, finalCltvDelta?: number): Promise; public abstract async addInvoice(rHash: string, amount: number, cltvExpiry: number): Promise; diff --git a/lib/swaps/SwapClientManager.ts b/lib/swaps/SwapClientManager.ts index fb3d479ef..f77c24204 100644 --- a/lib/swaps/SwapClientManager.ts +++ b/lib/swaps/SwapClientManager.ts @@ -52,6 +52,7 @@ class SwapClientManager extends EventEmitter { unitConverter, config: config.raiden, logger: loggers.raiden, + directChannelChecks: config.debug.raidenDirectChannelChecks, }); } diff --git a/lib/swaps/Swaps.ts b/lib/swaps/Swaps.ts index 36e266a95..f342211db 100644 --- a/lib/swaps/Swaps.ts +++ b/lib/swaps/Swaps.ts @@ -265,7 +265,7 @@ class Swaps extends EventEmitter { let routes; try { - routes = await swapClient.getRoutes(makerUnits, destination); + routes = await swapClient.getRoutes(makerUnits, destination, makerCurrency); } catch (err) { throw SwapFailureReason.UnexpectedClientError; } @@ -484,11 +484,10 @@ class Swaps extends EventEmitter { return false; } - const takerPubKey = peer.getIdentifier(takerSwapClient.type, takerCurrency)!; + const takerIdentifier = peer.getIdentifier(takerSwapClient.type, takerCurrency)!; const deal: SwapDeal = { ...requestBody, - takerPubKey, price, isBuy, quantity, @@ -498,7 +497,8 @@ class Swaps extends EventEmitter { takerCurrency, makerUnits, takerUnits, - destination: takerPubKey, + takerPubKey: takerIdentifier, + destination: takerIdentifier, peerPubKey: peer.nodePubKey!, localId: orderToAccept.localId, phase: SwapPhase.SwapCreated, @@ -526,7 +526,7 @@ class Swaps extends EventEmitter { } try { - deal.makerToTakerRoutes = await takerSwapClient.getRoutes(takerUnits, takerPubKey, deal.takerCltvDelta); + deal.makerToTakerRoutes = await takerSwapClient.getRoutes(takerUnits, takerIdentifier, deal.takerCurrency, deal.takerCltvDelta); } catch (err) { this.failDeal(deal, SwapFailureReason.UnexpectedClientError, err.message); await this.sendErrorToPeer({ diff --git a/test/jest/RaidenClient.spec.ts b/test/jest/RaidenClient.spec.ts index 1acabd5e0..5a35e2168 100644 --- a/test/jest/RaidenClient.spec.ts +++ b/test/jest/RaidenClient.spec.ts @@ -117,7 +117,7 @@ describe('RaidenClient', () => { describe('sendPayment', () => { test('it removes 0x from secret', async () => { - raiden = new RaidenClient({ unitConverter, config, logger: raidenLogger }); + raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger }); await raiden.init(currencyInstances as CurrencyInstance[]); const validTokenPaymentResponse: TokenPaymentResponse = getValidTokenPaymentResponse(); raiden['tokenPayment'] = jest.fn() @@ -129,7 +129,7 @@ describe('RaidenClient', () => { }); test('it rejects in case of empty secret response', async () => { - raiden = new RaidenClient({ unitConverter, config, logger: raidenLogger }); + raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger }); await raiden.init(currencyInstances as CurrencyInstance[]); const invalidTokenPaymentResponse: TokenPaymentResponse = { ...getValidTokenPaymentResponse(), @@ -157,7 +157,7 @@ describe('RaidenClient', () => { test('it fails when tokenAddress for currency not found', async () => { expect.assertions(1); - raiden = new RaidenClient({ unitConverter, config, logger: raidenLogger }); + raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger }); await raiden.init([] as CurrencyInstance[]); try { await raiden.openChannel({ @@ -172,7 +172,7 @@ describe('RaidenClient', () => { test('it throws when openChannel fails', async () => { expect.assertions(1); - raiden = new RaidenClient({ unitConverter, config, logger: raidenLogger }); + raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger }); const peerRaidenAddress = '0x10D8CCAD85C7dc123090B43aA1f98C00a303BFC5'; const currency = 'WETH'; const mockTokenAddresses = new Map(); @@ -195,7 +195,7 @@ describe('RaidenClient', () => { test('it opens a channel', async () => { expect.assertions(2); - raiden = new RaidenClient({ unitConverter, config, logger: raidenLogger }); + raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger }); const peerRaidenAddress = '0x10D8CCAD85C7dc123090B43aA1f98C00a303BFC5'; const currency = 'WETH'; const mockTokenAddresses = new Map(); @@ -219,7 +219,7 @@ describe('RaidenClient', () => { }); test('channelBalance calculates the total balance of open channels for a currency', async () => { - raiden = new RaidenClient({ unitConverter, config, logger: raidenLogger }); + raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger }); await raiden.init(currencyInstances as CurrencyInstance[]); raiden.tokenAddresses.get = jest.fn().mockReturnValue(channelBalanceTokenAddress); raiden['getChannels'] = jest.fn() diff --git a/test/jest/SwapClientManager.spec.ts b/test/jest/SwapClientManager.spec.ts index f0ca54ef8..06320c9eb 100644 --- a/test/jest/SwapClientManager.spec.ts +++ b/test/jest/SwapClientManager.spec.ts @@ -112,6 +112,9 @@ describe('Swaps.SwapClientManager', () => { host: 'localhost', port: 1234, }; + config.debug = { + raidenDirectChannelChecks: true, + }; db = new DB(loggers.db, config.dbpath); unitConverter = new UnitConverter(); unitConverter.init(); diff --git a/test/jest/integration/Swaps.spec.ts b/test/jest/integration/Swaps.spec.ts index 4d0075b6a..05fd9c4e5 100644 --- a/test/jest/integration/Swaps.spec.ts +++ b/test/jest/integration/Swaps.spec.ts @@ -260,6 +260,7 @@ describe('Swaps', () => { expect(lndBtc.getRoutes).toHaveBeenCalledWith( 1000, peerLndBtcPubKey, + takerCurrency, swapRequestBody.takerCltvDelta, ); expect(lndLtc.addInvoice).toHaveBeenCalledTimes(1);