Skip to content

Commit

Permalink
feat: http endpoint for raiden hash resolver
Browse files Browse the repository at this point in the history
This commit adds a new http server and endpoint to listen for hash
resolver requests from Raiden. It replaces the previous method of a grpc
interface for listening to resolve requests.

Closes #931.
  • Loading branch information
sangaman committed May 14, 2019
1 parent 9a2b75b commit 3d1f4de
Show file tree
Hide file tree
Showing 11 changed files with 270 additions and 7 deletions.
4 changes: 4 additions & 0 deletions bin/xud
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ const { argv } = require('yargs')
type: 'string',
alias: 'x',
},
'http.port': {
describe: 'Port to listen for http requests',
type: 'number',
},
'lnd.[currency].certpath': {
describe: 'Path to the SSL certificate for lnd',
type: 'string',
Expand Down
6 changes: 5 additions & 1 deletion lib/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import toml from 'toml';
import { deepMerge } from './utils/utils';
import { promises as fs } from 'fs';
import { LndClientConfig } from './lndclient/LndClient';
import { RaidenClientConfig } from './raidenclient/RaidenClient';
import { Level } from './Logger';
import { XuNetwork } from './constants/enums';
import { PoolConfig } from './p2p/types';
import { RaidenClientConfig } from './raidenclient/types';

class Config {
public p2p: PoolConfig;
Expand All @@ -17,6 +17,7 @@ class Config {
public logdateformat: string;
public network: XuNetwork;
public rpc: { disable: boolean, host: string, port: number };
public http: { port: number };
public lnd: { [currency: string]: LndClientConfig | undefined } = {};
public raiden: RaidenClientConfig;
public webproxy: { port: number, disable: boolean };
Expand Down Expand Up @@ -80,6 +81,9 @@ class Config {
host: 'localhost',
port: 8886,
};
this.http = {
port: 8887,
};
this.webproxy = {
disable: true,
port: 8080,
Expand Down
3 changes: 3 additions & 0 deletions lib/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export enum Context {
Lnd = 'LND',
Raiden = 'RAIDEN',
Swaps = 'SWAPS',
Http = 'HTTP',
}

type Loggers = {
Expand All @@ -41,6 +42,7 @@ type Loggers = {
lnd: Logger,
raiden: Logger,
swaps: Logger,
http: Logger,
};

class Logger {
Expand Down Expand Up @@ -94,6 +96,7 @@ class Logger {
lnd: new Logger({ ...object, context: Context.Lnd }),
raiden: new Logger({ ...object, context: Context.Raiden }),
swaps: new Logger({ ...object, context: Context.Swaps }),
http: new Logger({ ...object, context: Context.Http }),
};
}

Expand Down
11 changes: 11 additions & 0 deletions lib/Xud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import NodeKey from './nodekey/NodeKey';
import Service from './service/Service';
import { EventEmitter } from 'events';
import Swaps from './swaps/Swaps';
import HttpServer from './http/HttpServer';

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

Expand All @@ -35,6 +36,7 @@ class Xud extends EventEmitter {
private pool!: Pool;
private orderBook!: OrderBook;
private rpcServer?: GrpcServer;
private httpServer?: HttpServer;
private nodeKey!: NodeKey;
private grpcAPIProxy?: GrpcWebProxyServer;
private swaps!: Swaps;
Expand Down Expand Up @@ -133,6 +135,11 @@ class Xud extends EventEmitter {
shutdown: this.beginShutdown,
});

if (!this.raidenClient.isDisabled()) {
this.httpServer = new HttpServer(loggers.http, this.service);
await this.httpServer.listen(this.config.http.port);
}

// start rpc server last
if (!this.config.rpc.disable) {
this.rpcServer = new GrpcServer(loggers.rpc, this.service);
Expand Down Expand Up @@ -203,9 +210,13 @@ class Xud extends EventEmitter {
if (!this.raidenClient.isDisabled()) {
this.raidenClient.close();
}

// TODO: ensure we are not in the middle of executing any trades
const closePromises: Promise<void>[] = [];

if (this.httpServer) {
closePromises.push(this.httpServer.close());
}
if (this.pool) {
closePromises.push(this.pool.disconnect());
}
Expand Down
108 changes: 108 additions & 0 deletions lib/http/HttpServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import http from 'http';
import Service from '../service/Service';
import HttpService from './HttpService';
import Logger from '../Logger';
import errors from '../service/errors';

const reqToJson = (req: http.IncomingMessage) => {
return new Promise<object>((resolve, reject) => {
const body: any[] = [];
req.on('data', (chunk) => {
body.push(chunk);
}).on('end', () => {
const bodyStr = Buffer.concat(body).toString();
const reqContentLength = parseInt(req.headers['content-length'] || '', 10);

if (bodyStr.length !== reqContentLength) {
reject('invalid content-length header');
return;
}

try {
resolve(JSON.parse(bodyStr));
} catch (err) {
reject('cannot parse request json');
}
});
});
};

class HttpServer {
private server: http.Server;
private httpService: HttpService;

constructor(private logger: Logger, service: Service) {
this.server = http.createServer(this.requestListener);
this.httpService = new HttpService(service);
}

private requestListener: http.RequestListener = async (req, res) => {
if (!req.headers['content-length']) {
res.writeHead(411);
return;
}

let reqJson: any;
try {
reqJson = await reqToJson(req);
} catch (err) {
res.writeHead(400);
res.end(err);
return;
}

let resJson: any;
try {
switch (req.url) {
case '/resolveraiden':
resJson = await this.httpService.resolveHashRaiden(reqJson);
break;
default:
res.writeHead(404);
res.end();
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(resJson));
} catch (err) {
if (err.code === errors.INVALID_ARGUMENT) {
res.writeHead(400);
res.end(err.message);
} else {
res.writeHead(500);
res.end(JSON.stringify(err));
}
}
}

/**
* Starts the server and begins listening on the provided port.
* @returns true if the server started listening successfully, false otherwise
*/
public listen = async (port: number) => {
return new Promise<void>((resolve, reject) => {
const listenErrHandler = (err: Error) => {
reject(err);
};

this.server.listen(port, '127.0.0.1').once('listening', () => {
this.logger.info(`http server listening on 127.0.0.1:${port}`);
this.server.removeListener('error', listenErrHandler);
resolve();
}).on('error', listenErrHandler);
});
}

/**
* Stops listening for requests.
*/
public close = () => {
return new Promise<void>((resolve) => {
this.server.close(() => {
this.logger.info('http server has closed');
resolve();
});
});
}
}

export default HttpServer;
16 changes: 16 additions & 0 deletions lib/http/HttpService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Service from '../service/Service';
import { RaidenResolveRequest, RaidenResolveResponse } from '../raidenclient/types';

class HttpService {
constructor(private service: Service) {}

public resolveHashRaiden = async (resolveRequest: RaidenResolveRequest): Promise<RaidenResolveResponse> => {
const secret = await this.service.resolveHash({
amount: resolveRequest.amount,
rHash: resolveRequest.secret_hash,
});
return { secret };
}
}

export default HttpService;
4 changes: 2 additions & 2 deletions lib/p2p/Pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 +824,7 @@ class Pool extends EventEmitter {
}

/**
* Start listening for incoming p2p connections on the configured host and port. If `this.listenPort` is 0 or undefined,
* Starts listening for incoming p2p connections on the configured host and port. If `this.listenPort` is 0 or undefined,
* a random available port is used and will be assigned to `this.listenPort`.
* @return a promise that resolves once the server is listening, or rejects if it fails to listen
*/
Expand All @@ -850,7 +850,7 @@ class Pool extends EventEmitter {
}

/**
* Stop listening for incoming p2p connections.
* Stops listening for incoming p2p connections.
* @return a promise that resolves once the server is no longer listening
*/
private unlisten = () => {
Expand Down
1 change: 0 additions & 1 deletion lib/raidenclient/RaidenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,4 +367,3 @@ class RaidenClient extends SwapClient {
}

export default RaidenClient;
export { RaidenClientConfig, RaidenInfo, OpenChannelPayload };
23 changes: 22 additions & 1 deletion lib/raidenclient/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type RaidenInfo = {
};

/**
* The payload for the [[openChannel]] call.
* The payload for the openChannel call.
*/
export type OpenChannelPayload = {
partner_address: string;
Expand Down Expand Up @@ -59,3 +59,24 @@ export type TokenPaymentRequest = {
secret_hash: string,
identifier?: number,
};

export type RaidenResolveRequest = {
/** The token address for the resolve request in hex. */
token: string;
/** The payment hash in hex. */
secret_hash: string;
/** The amount of the incoming payment pending resolution, in the smallest units supported by the token. */
amount: number;
// unused fields on the raiden request listed below, taken from raiden codebase
// 'payment_identifier': secret_request_event.payment_identifier,
// 'payment_sender': to_hex(secret_request_event.recipient),
// 'expiration': secret_request_event.expiration,
// 'payment_recipient': to_hex(raiden.address),
// 'reveal_timeout': raiden.config['reveal_timeout'],
// 'settle_timeout': raiden.config['settle_timeout'],
};

export type RaidenResolveResponse = {
/** The preimage in hex format. */
secret: string,
};
9 changes: 7 additions & 2 deletions lib/service/Service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Pool from '../p2p/Pool';
import OrderBook from '../orderbook/OrderBook';
import LndClient, { LndInfo } from '../lndclient/LndClient';
import RaidenClient, { RaidenInfo } from '../raidenclient/RaidenClient';
import RaidenClient from '../raidenclient/RaidenClient';
import { EventEmitter } from 'events';
import errors from './errors';
import { SwapClientType, OrderSide, SwapRole } from '../constants/enums';
Expand All @@ -11,6 +11,7 @@ import { Order, OrderPortion, PlaceOrderEvent } from '../orderbook/types';
import Swaps from '../swaps/Swaps';
import { OrderSidesArrays } from '../orderbook/TradingPair';
import { SwapSuccess, SwapFailure, ResolveRequest } from '../swaps/types';
import { RaidenInfo } from '../raidenclient/types';

/**
* The components required by the API service layer.
Expand Down Expand Up @@ -46,6 +47,8 @@ const argChecks = {
if (nodePubKey === '') throw errors.INVALID_ARGUMENT('nodePubKey must be specified');
},
HAS_PAIR_ID: ({ pairId }: { pairId: string }) => { if (pairId === '') throw errors.INVALID_ARGUMENT('pairId must be specified'); },
HAS_RHASH: ({ rHash }: { rHash: string }) => { if (rHash === '') throw errors.INVALID_ARGUMENT('rHash must be specified'); },
POSITIVE_AMOUNT: ({ amount }: { amount: number }) => { if (amount <= 0) throw errors.INVALID_ARGUMENT('amount must be greater than 0'); },
POSITIVE_QUANTITY: ({ quantity }: { quantity: number }) => { if (quantity <= 0) throw errors.INVALID_ARGUMENT('quantity must be greater than 0'); },
PRICE_NON_NEGATIVE: ({ price }: { price: number }) => { if (price < 0) throw errors.INVALID_ARGUMENT('price cannot be negative'); },
VALID_CURRENCY: ({ currency }: { currency: string }) => {
Expand Down Expand Up @@ -420,9 +423,11 @@ class Service extends EventEmitter {
}

/**
* resolveHash resolve hash to preimage.
* Resolves a hash to its preimage.
*/
public resolveHash = async (request: ResolveRequest) => {
argChecks.HAS_RHASH(request);
argChecks.POSITIVE_AMOUNT(request);
return this.swaps.handleResolveRequest(request);
}
}
Expand Down
Loading

0 comments on commit 3d1f4de

Please sign in to comment.