Skip to content

Commit e07f606

Browse files
author
Karl Ranna
committed
feat(raiden): add sendPayment support
1 parent e59e96e commit e07f606

File tree

6 files changed

+131
-42
lines changed

6 files changed

+131
-42
lines changed

lib/Xud.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Logger from './Logger';
44
import Config from './Config';
55
import DB from './db/DB';
66
import OrderBook from './orderbook/OrderBook';
7+
import BaseClient from './BaseClient';
78
import LndClient from './lndclient/LndClient';
89
import RaidenClient from './raidenclient/RaidenClient';
910
import GrpcServer from './grpc/GrpcServer';
@@ -74,25 +75,30 @@ class Xud extends EventEmitter {
7475
this.db = new DB(loggers.db, this.config.dbpath);
7576
await this.db.init(this.config.network, this.config.initdb);
7677

78+
const swapClients = new Map<string, BaseClient>();
7779
// setup LND clients and initialize
7880
for (const currency in this.config.lnd) {
7981
const lndConfig = this.config.lnd[currency]!;
8082
if (!lndConfig.disable) {
8183
const lndClient = new LndClient(lndConfig, currency, loggers.lnd);
8284
this.lndClients[currency] = lndClient;
85+
swapClients.set(currency, lndClient);
8386
initPromises.push(lndClient.init());
8487
}
8588
}
8689

8790
// setup raiden client and connect if configured
88-
this.raidenClient = new RaidenClient(this.config.raiden, loggers.raiden);
91+
this.raidenClient = new RaidenClient(this.config.raiden, loggers.raiden, this.db.models);
8992
if (!this.raidenClient.isDisabled()) {
90-
initPromises.push(this.raidenClient.init());
93+
await this.raidenClient.init();
94+
for (const currency of this.raidenClient.tokenAddresses.keys()) {
95+
swapClients.set(currency, this.raidenClient);
96+
}
9197
}
9298

9399
this.pool = new Pool(this.config.p2p, this.config.network, loggers.p2p, this.db.models);
94100

95-
this.swaps = new Swaps(loggers.swaps, this.db.models, this.pool, this.lndClients);
101+
this.swaps = new Swaps(loggers.swaps, this.db.models, this.pool, swapClients);
96102
initPromises.push(this.swaps.init());
97103

98104
this.orderBook = new OrderBook(loggers.orderbook, this.db.models, this.config.nomatching, this.pool, this.swaps);

lib/constants/enums.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ export enum SwapPhase {
6262
/** The terms of the swap have been agreed to, and we will attempt to execute it. */
6363
SwapAgreed = 2,
6464
/**
65-
* We have commanded lnd to send payment according to the agreed terms. The payment (and swap)
65+
* We have commanded swap client to send payment according to the agreed terms. The payment (and swap)
6666
* could still fail due to no route with sufficient capacity, lack of cooperation from the
67-
* receiver or any intermediary node along the route, or an unexpected error from lnd.
67+
* receiver or any intermediary node along the route, or an unexpected error from swap client.
6868
*/
6969
SendingAmount = 3,
7070
/** We have received the agreed amount of the swap, and the preimage is now known to both sides. */

lib/raidenclient/RaidenClient.ts

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import Logger from '../Logger';
33
import BaseClient, { ClientStatus, ChannelBalance } from '../BaseClient';
44
import errors from './errors';
55
import { SwapDeal } from '../swaps/types';
6-
import { SwapClient } from '../constants/enums';
6+
import { SwapClient, SwapState, SwapRole } from '../constants/enums';
7+
import assert from 'assert';
8+
import OrderBookRepository from '../orderbook/OrderBookRepository';
9+
import { Models } from '../db/DB';
710

811
/**
912
* A utility function to parse the payload from an http response.
@@ -68,26 +71,34 @@ type ChannelEvent = {
6871
amount?: number;
6972
};
7073

74+
type TokenPaymentResponse = {
75+
secret: string;
76+
};
77+
7178
/**
7279
* A class representing a client to interact with raiden.
7380
*/
7481
class RaidenClient extends BaseClient {
7582
public readonly type = SwapClient.Raiden;
76-
public readonly cltvDelta: number = 0;
83+
public readonly cltvDelta: number = 1;
7784
public address = '';
85+
public tokenAddresses = new Map<string, string>();
7886
private port: number;
7987
private host: string;
88+
private repository: OrderBookRepository;
8089

8190
/**
8291
* Creates a raiden client.
8392
*/
84-
constructor(config: RaidenClientConfig, logger: Logger) {
93+
constructor(config: RaidenClientConfig, logger: Logger, models: Models) {
8594
super(logger);
8695
const { disable, host, port } = config;
8796

8897
this.port = port;
8998
this.host = host;
9099

100+
this.repository = new OrderBookRepository(logger, models);
101+
91102
if (disable) {
92103
this.setStatus(ClientStatus.Disabled);
93104
}
@@ -101,11 +112,27 @@ class RaidenClient extends BaseClient {
101112
this.logger.error(`can't init raiden. raiden is disabled`);
102113
return;
103114
}
115+
// associate the client with all currencies that have a contract address
116+
await this.setCurrencies();
104117
// set status as disconnected until we can verify the connection
105118
this.setStatus(ClientStatus.Disconnected);
106119
await this.verifyConnection();
107120
}
108121

122+
private setCurrencies = async () => {
123+
try {
124+
const currencies = await this.repository.getCurrencies();
125+
const raidenCurrencies = currencies.filter((currency) => {
126+
const tokenAddress = currency.getDataValue('tokenAddress');
127+
if (tokenAddress) {
128+
this.tokenAddresses.set(currency.getDataValue('id'), tokenAddress);
129+
}
130+
});
131+
} catch (e) {
132+
this.logger.error('failed to set tokenAddresses for Raiden', e);
133+
}
134+
}
135+
109136
/**
110137
* Verifies that Raiden REST service can be reached by attempting a `getAddress` call.
111138
*/
@@ -134,8 +161,32 @@ class RaidenClient extends BaseClient {
134161
}
135162
}
136163

137-
public sendPayment = async (_deal: SwapDeal): Promise<string> => {
138-
return ''; // stub placeholder
164+
public sendPayment = async (deal: SwapDeal): Promise<string> => {
165+
assert(deal.state === SwapState.Active);
166+
assert(deal.destination);
167+
let amount = 0;
168+
let tokenAddress;
169+
if (deal.role === SwapRole.Maker) {
170+
// we are the maker paying the taker
171+
amount = deal.takerAmount;
172+
tokenAddress = this.tokenAddresses.get(deal.takerCurrency);
173+
} else {
174+
// we are the taker paying the maker
175+
amount = deal.makerAmount;
176+
tokenAddress = this.tokenAddresses.get(deal.makerCurrency);
177+
}
178+
try {
179+
if (!tokenAddress) {
180+
throw(errors.TOKEN_ADDRESS_NOT_FOUND);
181+
}
182+
// TODO: Secret hash. Depending on sha256 <-> keccak256 problem:
183+
// https://github.com/ExchangeUnion/xud/issues/870
184+
const tokenPaymentResponse = await this.tokenPayment(tokenAddress, deal.destination!, amount);
185+
return tokenPaymentResponse.secret;
186+
} catch (e) {
187+
this.logger.error(`Got exception from RaidenClient.tokenPayment:`, e);
188+
throw e;
189+
}
139190
}
140191

141192
public getRoutes = async (_amount: number, _destination: string) => {
@@ -305,15 +356,25 @@ class RaidenClient extends BaseClient {
305356
* @param amount
306357
* @param secret_hash optional; provide your own secret hash
307358
*/
308-
private tokenPayment = async (token_address: string, target_address: string, amount: number, secret_hash?: string): Promise<{}> => {
359+
private tokenPayment = async (
360+
token_address: string,
361+
target_address: string,
362+
amount: number,
363+
secret_hash?: string,
364+
): Promise<TokenPaymentResponse> => {
309365
const endpoint = `payments/${token_address}/${target_address}`;
310-
let payload = { amount };
366+
let payload = {
367+
amount,
368+
// Raiden payment will timeout without an error if the identifier
369+
// is not specified.
370+
identifier: Math.round(Math.random() * (Number.MAX_SAFE_INTEGER - 1) + 1),
371+
};
311372
if (secret_hash) {
312373
payload = Object.assign(payload, { secret_hash });
313374
}
314375
const res = await this.sendRequest(endpoint, 'POST', payload);
315376
const body = await parseResponseBody(res);
316-
return body;
377+
return body as TokenPaymentResponse;
317378
}
318379

319380
/**

lib/raidenclient/errors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const errorCodes = {
88
INVALID: codesPrefix.concat('.4'),
99
SERVER_ERROR: codesPrefix.concat('.5'),
1010
UNEXPECTED: codesPrefix.concat('.6'),
11+
TOKEN_ADDRESS_NOT_FOUND: codesPrefix.concat('.7'),
1112
};
1213

1314
const errors = {
@@ -35,6 +36,10 @@ const errors = {
3536
message: 'unexpected error during raiden request',
3637
code: errorCodes.UNEXPECTED,
3738
},
39+
TOKEN_ADDRESS_NOT_FOUND: {
40+
message: 'raiden token address not found',
41+
code: errorCodes.TOKEN_ADDRESS_NOT_FOUND,
42+
},
3843
};
3944

4045
export { errorCodes };

lib/swaps/Swaps.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class Swaps extends EventEmitter {
4848
constructor(private logger: Logger,
4949
private models: Models,
5050
private pool: Pool,
51-
private swapClients: { [currency: string]: BaseClient | undefined },
51+
private swapClients: Map<string, BaseClient>,
5252
) {
5353
super();
5454
this.repository = new SwapRepository(this.models);
@@ -116,8 +116,8 @@ class Swaps extends EventEmitter {
116116
*/
117117
public isPairSupported = (pairId: string): boolean => {
118118
const currencies = pairId.split('/');
119-
const baseCurrencyClient = this.swapClients[currencies[0]];
120-
const quoteCurrencyClient = this.swapClients[currencies[1]];
119+
const baseCurrencyClient = this.swapClients.get(currencies[0]);
120+
const quoteCurrencyClient = this.swapClients.get(currencies[1]);
121121
return baseCurrencyClient !== undefined && baseCurrencyClient.isConnected() &&
122122
quoteCurrencyClient !== undefined && quoteCurrencyClient.isConnected();
123123
}
@@ -140,13 +140,20 @@ class Swaps extends EventEmitter {
140140
* @returns undefined if the setup is verified, otherwise an error message
141141
*/
142142
private checkPeerIdentifiers = (deal: SwapDeal, peer: Peer) => {
143-
// TODO: this verification should happen before we accept orders from the peer and should check Raiden as well
144-
if (!peer.getIdentifier(SwapClient.Lnd, deal.takerCurrency)) {
145-
return 'peer did not provide an LND PubKey for ' + deal.takerCurrency;
143+
// TODO: this verification should happen before we accept orders from the peer
144+
const takerSwapClient = this.swapClients.get(deal.takerCurrency);
145+
if (!takerSwapClient) {
146+
return `unable to get swap client for ${deal.takerCurrency}`;
146147
}
147-
148-
if (!peer.getIdentifier(SwapClient.Lnd, deal.makerCurrency)) {
149-
return 'peer did not provide an LND PubKey for ' + deal.makerCurrency;
148+
if (!peer.getIdentifier(takerSwapClient.type, deal.takerCurrency)) {
149+
return `peer did not provide an identifier for ${deal.takerCurrency}`;
150+
}
151+
const makerSwapClient = this.swapClients.get(deal.makerCurrency);
152+
if (!makerSwapClient) {
153+
return `unable to get swap client for ${deal.makerCurrency}`;
154+
}
155+
if (!peer.getIdentifier(makerSwapClient.type, deal.makerCurrency)) {
156+
return `peer did not provide an identifier for ${deal.makerCurrency}`;
150157
}
151158
return;
152159
}
@@ -197,7 +204,7 @@ class Swaps extends EventEmitter {
197204
const { makerCurrency } = Swaps.deriveCurrencies(maker.pairId, maker.isBuy);
198205
const { makerAmount } = Swaps.calculateSwapAmounts(taker.quantity, maker.price, maker.isBuy, maker.pairId);
199206

200-
const swapClient = this.swapClients[makerCurrency];
207+
const swapClient = this.swapClients.get(makerCurrency);
201208
if (!swapClient) throw new Error('swap client not found');
202209

203210
const peer = this.pool.getPeer(maker.peerPubKey);
@@ -263,10 +270,10 @@ class Swaps extends EventEmitter {
263270

264271
const quantity = Math.min(maker.quantity, taker.quantity);
265272
const { makerAmount, takerAmount } = Swaps.calculateSwapAmounts(quantity, maker.price, maker.isBuy, maker.pairId);
266-
const clientType = this.swapClients[makerCurrency]!.type;
273+
const clientType = this.swapClients.get(makerCurrency)!.type;
267274
const destination = peer.getIdentifier(clientType, makerCurrency)!;
268275

269-
const takerCltvDelta = this.swapClients[takerCurrency]!.cltvDelta;
276+
const takerCltvDelta = this.swapClients.get(takerCurrency)!.cltvDelta;
270277

271278
const preimage = await randomBytes(32);
272279

@@ -337,7 +344,7 @@ class Swaps extends EventEmitter {
337344
const { makerAmount, takerAmount } = Swaps.calculateSwapAmounts(quantity, price, isBuy, requestBody.pairId);
338345
const { makerCurrency, takerCurrency } = Swaps.deriveCurrencies(requestBody.pairId, isBuy);
339346

340-
const swapClient = this.swapClients[takerCurrency];
347+
const swapClient = this.swapClients.get(takerCurrency);
341348
if (!swapClient) {
342349
await this.sendErrorToPeer(peer, rHash, SwapFailureReason.SwapClientNotSetup, 'Unsupported taker currency', requestPacket.header.id);
343350
return false;
@@ -414,8 +421,8 @@ class Swaps extends EventEmitter {
414421

415422
const routeCltvDelta = deal.makerToTakerRoutes[0].getTotalTimeLock() - height;
416423

417-
const makerClientCltvDelta = this.swapClients[makerCurrency]!.cltvDelta;
418-
const takerClientCltvDelta = this.swapClients[takerCurrency]!.cltvDelta;
424+
const makerClientCltvDelta = this.swapClients.get(makerCurrency)!.cltvDelta;
425+
const takerClientCltvDelta = this.swapClients.get(takerCurrency)!.cltvDelta;
419426

420427
// cltvDelta can't be zero for swap clients (checked in constructor)
421428
const cltvDeltaFactor = makerClientCltvDelta / takerClientCltvDelta;
@@ -426,7 +433,7 @@ class Swaps extends EventEmitter {
426433
}
427434

428435
const responseBody: packets.SwapAcceptedPacketBody = {
429-
makerCltvDelta: deal.makerCltvDelta || 0,
436+
makerCltvDelta: deal.makerCltvDelta || 1,
430437
rHash: requestBody.rHash,
431438
quantity: requestBody.proposedQuantity,
432439
};
@@ -480,7 +487,7 @@ class Swaps extends EventEmitter {
480487
}
481488
}
482489

483-
const swapClient = this.swapClients[deal.makerCurrency];
490+
const swapClient = this.swapClients.get(deal.makerCurrency);
484491
if (!swapClient) {
485492
// We checked that we had a swap client for both currencies involved when the swap was initiated. Still...
486493
return;
@@ -576,7 +583,7 @@ class Swaps extends EventEmitter {
576583
// As the maker, I need to forward the payment to the other chain
577584
this.logger.debug('Executing maker code');
578585

579-
const swapClient = this.swapClients[deal.takerCurrency]!;
586+
const swapClient = this.swapClients.get(deal.takerCurrency)!;
580587

581588
try {
582589
this.setDealPhase(deal, SwapPhase.SendingAmount);

test/integration/Swaps.spec.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe('Swaps.Integration', () => {
8989
let db: DB;
9090
let pool: Pool;
9191
let swaps: Swaps;
92-
const swapClients: { [currency: string]: BaseClient | undefined } = {};
92+
const swapClients = new Map<string, BaseClient>();
9393
let peer: Peer;
9494
let sandbox: SinonSandbox;
9595
let getRoutesResponse;
@@ -120,13 +120,15 @@ describe('Swaps.Integration', () => {
120120
} as any);
121121
};
122122
// lnd btc
123-
swapClients.BTC = sandbox.createStubInstance(BaseClient) as any;
124-
swapClients.BTC!.getRoutes = getRoutesResponse;
125-
swapClients.BTC!.isConnected = () => true;
123+
const btcSwapClient = sandbox.createStubInstance(BaseClient) as any;
124+
btcSwapClient.getRoutes = getRoutesResponse;
125+
btcSwapClient.isConnected = () => true;
126+
swapClients.set('BTC', btcSwapClient);
126127
// lnd ltc
127-
swapClients.LTC = sandbox.createStubInstance(BaseClient) as any;
128-
swapClients.LTC!.isConnected = () => true;
129-
swapClients.LTC!.getRoutes = getRoutesResponse;
128+
const ltcSwapClient = sandbox.createStubInstance(BaseClient) as any;
129+
ltcSwapClient.isConnected = () => true;
130+
ltcSwapClient.getRoutes = getRoutesResponse;
131+
swapClients.set('LTC', ltcSwapClient);
130132
swaps = new Swaps(loggers.swaps, db.models, pool, swapClients);
131133
});
132134

@@ -184,15 +186,23 @@ describe('Swaps.Integration', () => {
184186
const noRoutesFound = () => {
185187
return Promise.resolve([]);
186188
};
187-
swapClients.BTC!.getRoutes = noRoutesFound;
188-
swapClients.LTC!.getRoutes = noRoutesFound;
189+
let btcSwapClient = swapClients.get('BTC');
190+
btcSwapClient!.getRoutes = noRoutesFound;
191+
swapClients.set('BTC', btcSwapClient!);
192+
let ltcSwapClient = swapClients.get('LTC');
193+
ltcSwapClient!.getRoutes = noRoutesFound;
194+
swapClients.set('LTC', ltcSwapClient!);
189195
await expect(swaps.executeSwap(validMakerOrder(), validTakerOrder()))
190196
.to.eventually.be.rejected.and.equal(SwapFailureReason.NoRouteFound);
191197
const rejectsWithUnknownError = () => {
192198
return Promise.reject('UNKNOWN');
193199
};
194-
swapClients.BTC!.getRoutes = rejectsWithUnknownError;
195-
swapClients.LTC!.getRoutes = rejectsWithUnknownError;
200+
btcSwapClient = swapClients.get('BTC');
201+
btcSwapClient!.getRoutes = rejectsWithUnknownError;
202+
swapClients.set('BTC', btcSwapClient!);
203+
ltcSwapClient = swapClients.get('LTC');
204+
ltcSwapClient!.getRoutes = rejectsWithUnknownError;
205+
swapClients.set('LTC', ltcSwapClient!);
196206
await expect(swaps.executeSwap(validMakerOrder(), validTakerOrder()))
197207
.to.eventually.be.rejected.and.equal(SwapFailureReason.UnexpectedClientError);
198208
});

0 commit comments

Comments
 (0)