Skip to content

Commit

Permalink
feat(raiden): add sendPayment support
Browse files Browse the repository at this point in the history
  • Loading branch information
Karl Ranna committed Apr 16, 2019
1 parent e59e96e commit e07f606
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 42 deletions.
12 changes: 9 additions & 3 deletions lib/Xud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Logger from './Logger';
import Config from './Config';
import DB from './db/DB';
import OrderBook from './orderbook/OrderBook';
import BaseClient from './BaseClient';
import LndClient from './lndclient/LndClient';
import RaidenClient from './raidenclient/RaidenClient';
import GrpcServer from './grpc/GrpcServer';
Expand Down Expand Up @@ -74,25 +75,30 @@ class Xud extends EventEmitter {
this.db = new DB(loggers.db, this.config.dbpath);
await this.db.init(this.config.network, this.config.initdb);

const swapClients = new Map<string, BaseClient>();
// setup LND clients and initialize
for (const currency in this.config.lnd) {
const lndConfig = this.config.lnd[currency]!;
if (!lndConfig.disable) {
const lndClient = new LndClient(lndConfig, currency, loggers.lnd);
this.lndClients[currency] = lndClient;
swapClients.set(currency, lndClient);
initPromises.push(lndClient.init());
}
}

// setup raiden client and connect if configured
this.raidenClient = new RaidenClient(this.config.raiden, loggers.raiden);
this.raidenClient = new RaidenClient(this.config.raiden, loggers.raiden, this.db.models);
if (!this.raidenClient.isDisabled()) {
initPromises.push(this.raidenClient.init());
await this.raidenClient.init();
for (const currency of this.raidenClient.tokenAddresses.keys()) {
swapClients.set(currency, this.raidenClient);
}
}

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

this.swaps = new Swaps(loggers.swaps, this.db.models, this.pool, this.lndClients);
this.swaps = new Swaps(loggers.swaps, this.db.models, this.pool, swapClients);
initPromises.push(this.swaps.init());

this.orderBook = new OrderBook(loggers.orderbook, this.db.models, this.config.nomatching, this.pool, this.swaps);
Expand Down
4 changes: 2 additions & 2 deletions lib/constants/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ export enum SwapPhase {
/** The terms of the swap have been agreed to, and we will attempt to execute it. */
SwapAgreed = 2,
/**
* We have commanded lnd to send payment according to the agreed terms. The payment (and swap)
* We have commanded swap client to send payment according to the agreed terms. The payment (and swap)
* could still fail due to no route with sufficient capacity, lack of cooperation from the
* receiver or any intermediary node along the route, or an unexpected error from lnd.
* receiver or any intermediary node along the route, or an unexpected error from swap client.
*/
SendingAmount = 3,
/** We have received the agreed amount of the swap, and the preimage is now known to both sides. */
Expand Down
77 changes: 69 additions & 8 deletions lib/raidenclient/RaidenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import Logger from '../Logger';
import BaseClient, { ClientStatus, ChannelBalance } from '../BaseClient';
import errors from './errors';
import { SwapDeal } from '../swaps/types';
import { SwapClient } from '../constants/enums';
import { SwapClient, SwapState, SwapRole } from '../constants/enums';
import assert from 'assert';
import OrderBookRepository from '../orderbook/OrderBookRepository';
import { Models } from '../db/DB';

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

type TokenPaymentResponse = {
secret: string;
};

/**
* A class representing a client to interact with raiden.
*/
class RaidenClient extends BaseClient {
public readonly type = SwapClient.Raiden;
public readonly cltvDelta: number = 0;
public readonly cltvDelta: number = 1;
public address = '';
public tokenAddresses = new Map<string, string>();
private port: number;
private host: string;
private repository: OrderBookRepository;

/**
* Creates a raiden client.
*/
constructor(config: RaidenClientConfig, logger: Logger) {
constructor(config: RaidenClientConfig, logger: Logger, models: Models) {
super(logger);
const { disable, host, port } = config;

this.port = port;
this.host = host;

this.repository = new OrderBookRepository(logger, models);

if (disable) {
this.setStatus(ClientStatus.Disabled);
}
Expand All @@ -101,11 +112,27 @@ class RaidenClient extends BaseClient {
this.logger.error(`can't init raiden. raiden is disabled`);
return;
}
// associate the client with all currencies that have a contract address
await this.setCurrencies();
// set status as disconnected until we can verify the connection
this.setStatus(ClientStatus.Disconnected);
await this.verifyConnection();
}

private setCurrencies = async () => {
try {
const currencies = await this.repository.getCurrencies();
const raidenCurrencies = currencies.filter((currency) => {
const tokenAddress = currency.getDataValue('tokenAddress');
if (tokenAddress) {
this.tokenAddresses.set(currency.getDataValue('id'), tokenAddress);
}
});
} catch (e) {
this.logger.error('failed to set tokenAddresses for Raiden', e);
}
}

/**
* Verifies that Raiden REST service can be reached by attempting a `getAddress` call.
*/
Expand Down Expand Up @@ -134,8 +161,32 @@ class RaidenClient extends BaseClient {
}
}

public sendPayment = async (_deal: SwapDeal): Promise<string> => {
return ''; // stub placeholder
public sendPayment = async (deal: SwapDeal): Promise<string> => {
assert(deal.state === SwapState.Active);
assert(deal.destination);
let amount = 0;
let tokenAddress;
if (deal.role === SwapRole.Maker) {
// we are the maker paying the taker
amount = deal.takerAmount;
tokenAddress = this.tokenAddresses.get(deal.takerCurrency);
} else {
// we are the taker paying the maker
amount = deal.makerAmount;
tokenAddress = this.tokenAddresses.get(deal.makerCurrency);
}
try {
if (!tokenAddress) {
throw(errors.TOKEN_ADDRESS_NOT_FOUND);
}
// TODO: Secret hash. Depending on sha256 <-> keccak256 problem:
// https://github.com/ExchangeUnion/xud/issues/870
const tokenPaymentResponse = await this.tokenPayment(tokenAddress, deal.destination!, amount);
return tokenPaymentResponse.secret;
} catch (e) {
this.logger.error(`Got exception from RaidenClient.tokenPayment:`, e);
throw e;
}
}

public getRoutes = async (_amount: number, _destination: string) => {
Expand Down Expand Up @@ -305,15 +356,25 @@ class RaidenClient extends BaseClient {
* @param amount
* @param secret_hash optional; provide your own secret hash
*/
private tokenPayment = async (token_address: string, target_address: string, amount: number, secret_hash?: string): Promise<{}> => {
private tokenPayment = async (
token_address: string,
target_address: string,
amount: number,
secret_hash?: string,
): Promise<TokenPaymentResponse> => {
const endpoint = `payments/${token_address}/${target_address}`;
let payload = { amount };
let payload = {
amount,
// Raiden payment will timeout without an error if the identifier
// is not specified.
identifier: Math.round(Math.random() * (Number.MAX_SAFE_INTEGER - 1) + 1),
};
if (secret_hash) {
payload = Object.assign(payload, { secret_hash });
}
const res = await this.sendRequest(endpoint, 'POST', payload);
const body = await parseResponseBody(res);
return body;
return body as TokenPaymentResponse;
}

/**
Expand Down
5 changes: 5 additions & 0 deletions lib/raidenclient/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const errorCodes = {
INVALID: codesPrefix.concat('.4'),
SERVER_ERROR: codesPrefix.concat('.5'),
UNEXPECTED: codesPrefix.concat('.6'),
TOKEN_ADDRESS_NOT_FOUND: codesPrefix.concat('.7'),
};

const errors = {
Expand Down Expand Up @@ -35,6 +36,10 @@ const errors = {
message: 'unexpected error during raiden request',
code: errorCodes.UNEXPECTED,
},
TOKEN_ADDRESS_NOT_FOUND: {
message: 'raiden token address not found',
code: errorCodes.TOKEN_ADDRESS_NOT_FOUND,
},
};

export { errorCodes };
Expand Down
43 changes: 25 additions & 18 deletions lib/swaps/Swaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class Swaps extends EventEmitter {
constructor(private logger: Logger,
private models: Models,
private pool: Pool,
private swapClients: { [currency: string]: BaseClient | undefined },
private swapClients: Map<string, BaseClient>,
) {
super();
this.repository = new SwapRepository(this.models);
Expand Down Expand Up @@ -116,8 +116,8 @@ class Swaps extends EventEmitter {
*/
public isPairSupported = (pairId: string): boolean => {
const currencies = pairId.split('/');
const baseCurrencyClient = this.swapClients[currencies[0]];
const quoteCurrencyClient = this.swapClients[currencies[1]];
const baseCurrencyClient = this.swapClients.get(currencies[0]);
const quoteCurrencyClient = this.swapClients.get(currencies[1]);
return baseCurrencyClient !== undefined && baseCurrencyClient.isConnected() &&
quoteCurrencyClient !== undefined && quoteCurrencyClient.isConnected();
}
Expand All @@ -140,13 +140,20 @@ class Swaps extends EventEmitter {
* @returns undefined if the setup is verified, otherwise an error message
*/
private checkPeerIdentifiers = (deal: SwapDeal, peer: Peer) => {
// TODO: this verification should happen before we accept orders from the peer and should check Raiden as well
if (!peer.getIdentifier(SwapClient.Lnd, deal.takerCurrency)) {
return 'peer did not provide an LND PubKey for ' + deal.takerCurrency;
// TODO: this verification should happen before we accept orders from the peer
const takerSwapClient = this.swapClients.get(deal.takerCurrency);
if (!takerSwapClient) {
return `unable to get swap client for ${deal.takerCurrency}`;
}

if (!peer.getIdentifier(SwapClient.Lnd, deal.makerCurrency)) {
return 'peer did not provide an LND PubKey for ' + deal.makerCurrency;
if (!peer.getIdentifier(takerSwapClient.type, deal.takerCurrency)) {
return `peer did not provide an identifier for ${deal.takerCurrency}`;
}
const makerSwapClient = this.swapClients.get(deal.makerCurrency);
if (!makerSwapClient) {
return `unable to get swap client for ${deal.makerCurrency}`;
}
if (!peer.getIdentifier(makerSwapClient.type, deal.makerCurrency)) {
return `peer did not provide an identifier for ${deal.makerCurrency}`;
}
return;
}
Expand Down Expand Up @@ -197,7 +204,7 @@ class Swaps extends EventEmitter {
const { makerCurrency } = Swaps.deriveCurrencies(maker.pairId, maker.isBuy);
const { makerAmount } = Swaps.calculateSwapAmounts(taker.quantity, maker.price, maker.isBuy, maker.pairId);

const swapClient = this.swapClients[makerCurrency];
const swapClient = this.swapClients.get(makerCurrency);
if (!swapClient) throw new Error('swap client not found');

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

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

const takerCltvDelta = this.swapClients[takerCurrency]!.cltvDelta;
const takerCltvDelta = this.swapClients.get(takerCurrency)!.cltvDelta;

const preimage = await randomBytes(32);

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

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

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

const makerClientCltvDelta = this.swapClients[makerCurrency]!.cltvDelta;
const takerClientCltvDelta = this.swapClients[takerCurrency]!.cltvDelta;
const makerClientCltvDelta = this.swapClients.get(makerCurrency)!.cltvDelta;
const takerClientCltvDelta = this.swapClients.get(takerCurrency)!.cltvDelta;

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

const responseBody: packets.SwapAcceptedPacketBody = {
makerCltvDelta: deal.makerCltvDelta || 0,
makerCltvDelta: deal.makerCltvDelta || 1,
rHash: requestBody.rHash,
quantity: requestBody.proposedQuantity,
};
Expand Down Expand Up @@ -480,7 +487,7 @@ class Swaps extends EventEmitter {
}
}

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

const swapClient = this.swapClients[deal.takerCurrency]!;
const swapClient = this.swapClients.get(deal.takerCurrency)!;

try {
this.setDealPhase(deal, SwapPhase.SendingAmount);
Expand Down
32 changes: 21 additions & 11 deletions test/integration/Swaps.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('Swaps.Integration', () => {
let db: DB;
let pool: Pool;
let swaps: Swaps;
const swapClients: { [currency: string]: BaseClient | undefined } = {};
const swapClients = new Map<string, BaseClient>();
let peer: Peer;
let sandbox: SinonSandbox;
let getRoutesResponse;
Expand Down Expand Up @@ -120,13 +120,15 @@ describe('Swaps.Integration', () => {
} as any);
};
// lnd btc
swapClients.BTC = sandbox.createStubInstance(BaseClient) as any;
swapClients.BTC!.getRoutes = getRoutesResponse;
swapClients.BTC!.isConnected = () => true;
const btcSwapClient = sandbox.createStubInstance(BaseClient) as any;
btcSwapClient.getRoutes = getRoutesResponse;
btcSwapClient.isConnected = () => true;
swapClients.set('BTC', btcSwapClient);
// lnd ltc
swapClients.LTC = sandbox.createStubInstance(BaseClient) as any;
swapClients.LTC!.isConnected = () => true;
swapClients.LTC!.getRoutes = getRoutesResponse;
const ltcSwapClient = sandbox.createStubInstance(BaseClient) as any;
ltcSwapClient.isConnected = () => true;
ltcSwapClient.getRoutes = getRoutesResponse;
swapClients.set('LTC', ltcSwapClient);
swaps = new Swaps(loggers.swaps, db.models, pool, swapClients);
});

Expand Down Expand Up @@ -184,15 +186,23 @@ describe('Swaps.Integration', () => {
const noRoutesFound = () => {
return Promise.resolve([]);
};
swapClients.BTC!.getRoutes = noRoutesFound;
swapClients.LTC!.getRoutes = noRoutesFound;
let btcSwapClient = swapClients.get('BTC');
btcSwapClient!.getRoutes = noRoutesFound;
swapClients.set('BTC', btcSwapClient!);
let ltcSwapClient = swapClients.get('LTC');
ltcSwapClient!.getRoutes = noRoutesFound;
swapClients.set('LTC', ltcSwapClient!);
await expect(swaps.executeSwap(validMakerOrder(), validTakerOrder()))
.to.eventually.be.rejected.and.equal(SwapFailureReason.NoRouteFound);
const rejectsWithUnknownError = () => {
return Promise.reject('UNKNOWN');
};
swapClients.BTC!.getRoutes = rejectsWithUnknownError;
swapClients.LTC!.getRoutes = rejectsWithUnknownError;
btcSwapClient = swapClients.get('BTC');
btcSwapClient!.getRoutes = rejectsWithUnknownError;
swapClients.set('BTC', btcSwapClient!);
ltcSwapClient = swapClients.get('LTC');
ltcSwapClient!.getRoutes = rejectsWithUnknownError;
swapClients.set('LTC', ltcSwapClient!);
await expect(swaps.executeSwap(validMakerOrder(), validTakerOrder()))
.to.eventually.be.rejected.and.equal(SwapFailureReason.UnexpectedClientError);
});
Expand Down

0 comments on commit e07f606

Please sign in to comment.