Skip to content

Commit

Permalink
Merge branch 'master' into feature/cli_contracts_version
Browse files Browse the repository at this point in the history
  • Loading branch information
andrevmatos authored Jul 16, 2020
2 parents bd34bb9 + 36efe62 commit 35736c0
Show file tree
Hide file tree
Showing 13 changed files with 254 additions and 107 deletions.
7 changes: 3 additions & 4 deletions raiden-cli/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import express, { Express, Request, Response, NextFunction, Errback } from 'express';
import express, { Express, Request, Response, NextFunction } from 'express';
import createError from 'http-errors';
import logger from 'morgan';
import { Cli } from './types';
Expand All @@ -10,13 +10,12 @@ function notFoundHandler(_request: Request, _response: Response, next: NextFunct

function internalErrorHandler(
this: Cli,
error: Errback,
error: Error,
_request: Request,
_response: Response,
next: NextFunction,
) {
this.log.error(error);
next(createError(500, 'Internal Raiden node error'));
next(createError(500, error.message));
}

export function makeApp(this: Cli): Express {
Expand Down
10 changes: 7 additions & 3 deletions raiden-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@ function createLocalStorage(name: string): LocalStorage {
return localStorage;
}

function shutdown(this: Cli): void {
function shutdownServer(this: Cli): void {
if (this.server?.listening) {
this.log.info('Closing server...');
this.server.close();
}
}

function shutdownRaiden(this: Cli): void {
if (this.raiden.started) {
this.log.info('Stopping raiden...');
this.raiden.stop();
Expand All @@ -86,8 +88,10 @@ function shutdown(this: Cli): void {
}

function registerShutdownHooks(this: Cli): void {
process.on('SIGINT', shutdown.bind(this));
process.on('SIGTERM', shutdown.bind(this));
// raiden shutdown triggers server shutdown
this.raiden.state$.subscribe(undefined, shutdownServer.bind(this), shutdownServer.bind(this));
process.on('SIGINT', shutdownRaiden.bind(this));
process.on('SIGTERM', shutdownRaiden.bind(this));
}

async function main() {
Expand Down
5 changes: 5 additions & 0 deletions raiden-cli/src/routes/api.v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,10 @@ export function makeApiV1Router(this: Cli): Router {
});
});

router.post('/shutdown', (_request: Request, response: Response) => {
this.raiden.stop();
response.json({ status: 'shutdown' });
});

return router;
}
196 changes: 146 additions & 50 deletions raiden-cli/src/routes/channels.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,169 @@
import { Router, Request, Response, NextFunction } from 'express';
import { first } from 'rxjs/operators';
import { ErrorCodes, RaidenError } from 'raiden-ts';
import { Cli } from '../types';
import { first, pluck } from 'rxjs/operators';
import { ErrorCodes, isntNil, ChannelState, RaidenChannel } from 'raiden-ts';
import { Cli, ApiChannelState } from '../types';
import {
validateAddressParameter,
isInvalidParameterError,
isTransactionWouldFailError,
validateOptionalAddressParameter,
validateAddressParameter,
} from '../utils/validation';
import {
flattenChannelDictionary,
transformSdkChannelFormatToApi,
filterChannels,
} from '../utils/channels';
import { flattenChannelDictionary, transformChannelFormatForApi } from '../utils/channels';

function isConflictError(error: RaidenError): boolean {
function isConflictError(error: Error): boolean {
return (
[ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, ErrorCodes.CNL_INVALID_STATE].includes(error.message) ||
isTransactionWouldFailError(error)
);
}

async function getAllChannels(this: Cli, _request: Request, response: Response) {
const channelDictionary = await this.raiden.channels$.pipe(first()).toPromise();
const channelList = flattenChannelDictionary(channelDictionary);
const formattedChannels = channelList.map((channel) => transformSdkChannelFormatToApi(channel));
response.json(formattedChannels);
}

async function getChannelsForToken(this: Cli, request: Request, response: Response) {
const channelDictionary = await this.raiden.channels$.pipe(first()).toPromise();
const channelList = flattenChannelDictionary(channelDictionary);
const filteredChannels = filterChannels(channelList, request.params.tokenAddress);
const formattedChannels = filteredChannels.map((channel) =>
transformSdkChannelFormatToApi(channel),
function isInsuficientFundsError(error: { message: string; code?: string | number }): boolean {
return (
error.code === 'INSUFFICIENT_FUNDS' ||
[
ErrorCodes.RDN_INSUFFICIENT_BALANCE,
ErrorCodes.CNL_WITHDRAW_AMOUNT_TOO_LOW,
ErrorCodes.CNL_WITHDRAW_AMOUNT_TOO_HIGH,
].includes(error.message)
);
response.json(formattedChannels);
}

async function getChannelsForTokenAndPartner(this: Cli, request: Request, response: Response) {
const channelDictionary = await this.raiden.channels$.pipe(first()).toPromise();
const channel = channelDictionary[request.params.tokenAddress]?.[request.params.partnerAddress];

if (channel) {
response.json(transformSdkChannelFormatToApi(channel));
async function getChannels(this: Cli, request: Request, response: Response) {
const channelsDict = await this.raiden.channels$.pipe(first()).toPromise();
const token: string | undefined = request.params.tokenAddress;
const partner: string | undefined = request.params.partnerAddress;
if (token && partner) {
const channel = channelsDict[token]?.[partner];
if (channel) {
response.json(transformChannelFormatForApi(channel));
} else {
response.status(404).send('The channel does not exist');
}
} else {
response.status(404).send('The channel does not exist');
let channelsList = flattenChannelDictionary(channelsDict);
if (token) channelsList = channelsList.filter((channel) => channel.token === token);
if (partner) channelsList = channelsList.filter((channel) => channel.token === partner);
response.json(channelsList.map(transformChannelFormatForApi));
}
}

async function openChannel(this: Cli, request: Request, response: Response, next: NextFunction) {
const token: string = request.body.token_address;
const partner: string = request.body.partner_address;
try {
// TODO: We ignore the provided `reveal_timeout` until #1656 provides
// a better solution.
await this.raiden.openChannel(request.body.token_address, request.body.partner_address, {
await this.raiden.openChannel(token, partner, {
settleTimeout: request.body.settle_timeout,
deposit: request.body.total_deposit,
});
const channelDictionary = await this.raiden.channels$.pipe(first()).toPromise();
const newChannel =
channelDictionary[request.body.token_address]?.[request.body.partner_address];
response.status(201).json(transformSdkChannelFormatToApi(newChannel));
const channel = await this.raiden.channels$
.pipe(pluck(token, partner), first(isntNil))
.toPromise();
response.status(201).json(transformChannelFormatForApi(channel));
} catch (error) {
if (isInvalidParameterError(error)) {
response.status(400).send(error.message);
} else if (isInvalidParameterError(error)) {
response.status(402).send(error.message);
} else if (isConflictError(error)) {
response.status(409).send(error.message);
} else {
next(error);
}
}
}

const allowedUpdateKeys = ['state', 'total_deposit', 'total_withdraw'];
const allowedUpdateStates = [ApiChannelState.closed, ApiChannelState.settled];
function validateChannelUpdateBody(request: Request, response: Response, next: NextFunction) {
const intersec = Object.keys(request.body).filter((k) => allowedUpdateKeys.includes(k));
if (intersec.length < 1)
return response.status(400).send(`one of [${allowedUpdateKeys}] operations required`);
else if (intersec.length > 1)
return response.status(409).send(`more than one of [${allowedUpdateKeys}] requested`);
if (request.body.state && !allowedUpdateStates.includes(request.body.state))
return response
.status(400)
.send(`invalid "state" requested: must be one of [${allowedUpdateStates}]`);
return next();
}

async function updateChannelState(
this: Cli,
channel: RaidenChannel,
newState: ApiChannelState.closed | ApiChannelState.settled,
): Promise<RaidenChannel> {
if (newState === ApiChannelState.closed) {
await this.raiden.closeChannel(channel.token, channel.partner);
const closedStates = [ChannelState.closed, ChannelState.settleable, ChannelState.settling];
channel = await this.raiden.channels$
.pipe(
pluck(channel.token, channel.partner),
first((channel) => closedStates.includes(channel.state)),
)
.toPromise();
} else if (newState === ApiChannelState.settled) {
const promise = this.raiden.settleChannel(channel.token, channel.partner);
const newChannel = await this.raiden.channels$
.pipe(pluck(channel.token, channel.partner), first())
.toPromise();
await promise;
if (newChannel) channel = newChannel; // channel may have been cleared
}
return channel;
}

async function updateChannelDeposit(
this: Cli,
channel: RaidenChannel,
totalDeposit: string,
): Promise<RaidenChannel> {
// amount = new deposit - previous deposit == (previous deposit - new deposit) * -1
const depositAmount = channel.ownDeposit.sub(totalDeposit).mul(-1);
await this.raiden.depositChannel(channel.token, channel.partner, depositAmount);
return await this.raiden.channels$
.pipe(
pluck(channel.token, channel.partner),
first((channel) => channel.ownDeposit.gte(totalDeposit)),
)
.toPromise();
}

async function updateChannelWithdraw(
this: Cli,
channel: RaidenChannel,
totalWithdraw: string,
): Promise<RaidenChannel> {
// amount = new withdraw - previous withdraw == (previous withdraw - new withdraw) * -1
const withdrawAmount = channel.ownWithdraw.sub(totalWithdraw).mul(-1);
await this.raiden.withdrawChannel(channel.token, channel.partner, withdrawAmount);
return await this.raiden.channels$
.pipe(
pluck(channel.token, channel.partner),
first((channel) => channel.ownWithdraw.gte(totalWithdraw)),
)
.toPromise();
}

async function updateChannel(this: Cli, request: Request, response: Response, next: NextFunction) {
const token: string = request.params.tokenAddress;
const partner: string = request.params.partnerAddress;
try {
let channel = await this.raiden.channels$.pipe(first(), pluck(token, partner)).toPromise();
if (!channel) return response.status(404).send('channel not found');
if (request.body.state) {
channel = await updateChannelState.call(this, channel, request.body.state);
} else if (request.body.total_deposit) {
channel = await updateChannelDeposit.call(this, channel, request.body.total_deposit);
} else if (request.body.total_withdraw) {
channel = await updateChannelWithdraw.call(this, channel, request.body.total_withdraw);
}
response.status(200).json(transformChannelFormatForApi(channel));
} catch (error) {
if (isInvalidParameterError(error)) {
response.status(400).send(error.message);
} else if (error.code === 'INSUFFICIENT_FUNDS') {
} else if (isInsuficientFundsError(error)) {
response.status(402).send(error.message);
} else if (isConflictError(error)) {
response.status(409).send(error.message);
Expand All @@ -76,26 +176,22 @@ async function openChannel(this: Cli, request: Request, response: Response, next
export function makeChannelsRouter(this: Cli): Router {
const router = Router();

router.get('/', getAllChannels.bind(this));

router.get(
'/:tokenAddress',
validateAddressParameter.bind('tokenAddress'),
getChannelsForToken.bind(this),
'/:tokenAddress?/:partnerAddress?',
validateOptionalAddressParameter.bind('tokenAddress'),
validateOptionalAddressParameter.bind('partnerAddress'),
getChannels.bind(this),
);

router.get(
router.put('/', openChannel.bind(this));

router.patch(
'/:tokenAddress/:partnerAddress',
validateAddressParameter.bind('tokenAddress'),
validateAddressParameter.bind('partnerAddress'),
getChannelsForTokenAndPartner.bind(this),
validateChannelUpdateBody,
updateChannel.bind(this),
);

router.put('/', openChannel.bind(this));

router.patch('/:tokenAddress/:partnerAddress', (_request: Request, response: Response) => {
response.status(404).send('Not implemented yet');
});

return router;
}
15 changes: 11 additions & 4 deletions raiden-cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,24 @@ export interface Cli {
server?: Server;
}

export enum ApiChannelState {
opened = 'opened',
closed = 'closed',
settled = 'settled',
}

// Data structures as exchanged over the API
export interface ApiChannel {
channel_identifier: number;
channel_identifier: string;
token_network_address: string;
partner_address: string;
token_address: string;
balance: string;
total_deposit: string;
state: string;
settle_timeout: number;
reveal_timeout: number;
total_withdraw: string;
state: ApiChannelState;
settle_timeout: string;
reveal_timeout: string;
}

export enum ApiPaymentEvents {
Expand Down
47 changes: 25 additions & 22 deletions raiden-cli/src/utils/channels.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RaidenChannel, RaidenChannels } from 'raiden-ts';
import { ApiChannel } from '../types';
import { RaidenChannel, RaidenChannels, ChannelState } from 'raiden-ts';
import { ApiChannel, ApiChannelState } from '../types';

export function flattenChannelDictionary(channelDict: RaidenChannels): RaidenChannel[] {
// To flatten structure {token: {partner: [channel..], partner:...}, token...}
Expand All @@ -9,33 +9,36 @@ export function flattenChannelDictionary(channelDict: RaidenChannels): RaidenCha
);
}

export function filterChannels(
channels: RaidenChannel[],
tokenAddress?: string,
partnerAddress?: string,
): RaidenChannel[] {
let filteredChannels = channels;

if (tokenAddress) {
filteredChannels = filteredChannels.filter((channel) => channel.token === tokenAddress);
}
if (partnerAddress) {
filteredChannels = filteredChannels.filter((channel) => channel.token === partnerAddress);
function transformChannelStateForApi(state: ChannelState): ApiChannelState {
let apiState;
switch (state) {
case ChannelState.open:
case ChannelState.closing:
apiState = ApiChannelState.opened;
break;
case ChannelState.closed:
case ChannelState.settleable:
case ChannelState.settling:
apiState = ApiChannelState.closed;
break;
case ChannelState.settled:
apiState = ApiChannelState.settled;
break;
}

return filteredChannels;
return apiState;
}

export function transformSdkChannelFormatToApi(channel: RaidenChannel): ApiChannel {
export function transformChannelFormatForApi(channel: RaidenChannel): ApiChannel {
return {
channel_identifier: channel.id,
channel_identifier: channel.id.toString(),
token_network_address: channel.tokenNetwork,
partner_address: channel.partner,
token_address: channel.token,
balance: channel.balance.toString(),
balance: channel.capacity.toString(),
total_deposit: channel.ownDeposit.toString(),
state: channel.state,
settle_timeout: channel.settleTimeout,
reveal_timeout: 0, // FIXME: Not defined here. Python client handles reveal timeout differently,
total_withdraw: channel.ownWithdraw.toString(),
state: transformChannelStateForApi(channel.state),
settle_timeout: channel.settleTimeout.toString(),
reveal_timeout: '50', // FIXME: Not defined here. Python client handles reveal timeout differently,
};
}
Loading

0 comments on commit 35736c0

Please sign in to comment.