From 5bc2705cb75d2295bb52495ef1f2a742315e83da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Vitor=20de=20Lima=20Matos?= Date: Tue, 14 Jul 2020 13:37:01 -0300 Subject: [PATCH 01/11] cli: Simplify GET /channels endpoing --- raiden-cli/src/routes/channels.ts | 79 ++++++++++++------------------- raiden-cli/src/utils/channels.ts | 17 ------- 2 files changed, 30 insertions(+), 66 deletions(-) diff --git a/raiden-cli/src/routes/channels.ts b/raiden-cli/src/routes/channels.ts index 6c116df314..28479bb96f 100644 --- a/raiden-cli/src/routes/channels.ts +++ b/raiden-cli/src/routes/channels.ts @@ -1,17 +1,13 @@ import { Router, Request, Response, NextFunction } from 'express'; -import { first } from 'rxjs/operators'; -import { ErrorCodes, RaidenError } from 'raiden-ts'; +import { first, pluck } from 'rxjs/operators'; +import { ErrorCodes, RaidenError, isntNil } from 'raiden-ts'; import { Cli } from '../types'; import { - validateAddressParameter, isInvalidParameterError, isTransactionWouldFailError, + validateOptionalAddressParameter, } from '../utils/validation'; -import { - flattenChannelDictionary, - transformSdkChannelFormatToApi, - filterChannels, -} from '../utils/channels'; +import { flattenChannelDictionary, transformSdkChannelFormatToApi } from '../utils/channels'; function isConflictError(error: RaidenError): boolean { return ( @@ -20,46 +16,39 @@ function isConflictError(error: RaidenError): boolean { ); } -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), - ); - 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(transformSdkChannelFormatToApi(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(transformSdkChannelFormatToApi)); } } 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(transformSdkChannelFormatToApi(channel)); } catch (error) { if (isInvalidParameterError(error)) { response.status(400).send(error.message); @@ -76,19 +65,11 @@ 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), - ); - router.get( - '/:tokenAddress/:partnerAddress', - validateAddressParameter.bind('tokenAddress'), - validateAddressParameter.bind('partnerAddress'), - getChannelsForTokenAndPartner.bind(this), + '/:tokenAddress?/:partnerAddress?', + validateOptionalAddressParameter.bind('tokenAddress'), + validateOptionalAddressParameter.bind('partnerAddress'), + getChannels.bind(this), ); router.put('/', openChannel.bind(this)); diff --git a/raiden-cli/src/utils/channels.ts b/raiden-cli/src/utils/channels.ts index cd8d4778e4..f308a5c943 100644 --- a/raiden-cli/src/utils/channels.ts +++ b/raiden-cli/src/utils/channels.ts @@ -9,23 +9,6 @@ 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); - } - - return filteredChannels; -} - export function transformSdkChannelFormatToApi(channel: RaidenChannel): ApiChannel { return { channel_identifier: channel.id, From a8ac1b6e6696479fc4e65a1947768d6a2c4008a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Vitor=20de=20Lima=20Matos?= Date: Tue, 14 Jul 2020 13:56:27 -0300 Subject: [PATCH 02/11] cli: Fix transform channel.state --- raiden-cli/src/types.ts | 8 +++++++- raiden-cli/src/utils/channels.ts | 25 ++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/raiden-cli/src/types.ts b/raiden-cli/src/types.ts index e27c287296..a7f0246ed1 100644 --- a/raiden-cli/src/types.ts +++ b/raiden-cli/src/types.ts @@ -19,6 +19,12 @@ 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; @@ -27,7 +33,7 @@ export interface ApiChannel { token_address: string; balance: string; total_deposit: string; - state: string; + state: ApiChannelState; settle_timeout: number; reveal_timeout: number; } diff --git a/raiden-cli/src/utils/channels.ts b/raiden-cli/src/utils/channels.ts index f308a5c943..b740d98760 100644 --- a/raiden-cli/src/utils/channels.ts +++ b/raiden-cli/src/utils/channels.ts @@ -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...} @@ -9,6 +9,25 @@ export function flattenChannelDictionary(channelDict: RaidenChannels): RaidenCha ); } +function transformSdkChannelStateToApi(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 apiState; +} + export function transformSdkChannelFormatToApi(channel: RaidenChannel): ApiChannel { return { channel_identifier: channel.id, @@ -17,7 +36,7 @@ export function transformSdkChannelFormatToApi(channel: RaidenChannel): ApiChann token_address: channel.token, balance: channel.balance.toString(), total_deposit: channel.ownDeposit.toString(), - state: channel.state, + state: transformSdkChannelStateToApi(channel.state), settle_timeout: channel.settleTimeout, reveal_timeout: 0, // FIXME: Not defined here. Python client handles reveal timeout differently, }; From 6dbb7a53a7c48978060d4df92a49b4924228d9c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Vitor=20de=20Lima=20Matos?= Date: Tue, 14 Jul 2020 14:01:32 -0300 Subject: [PATCH 03/11] cli: expose channel.total_withdraw and default reveal_timeout --- raiden-cli/src/utils/channels.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/raiden-cli/src/utils/channels.ts b/raiden-cli/src/utils/channels.ts index b740d98760..3b960ab6e2 100644 --- a/raiden-cli/src/utils/channels.ts +++ b/raiden-cli/src/utils/channels.ts @@ -36,8 +36,9 @@ export function transformSdkChannelFormatToApi(channel: RaidenChannel): ApiChann token_address: channel.token, balance: channel.balance.toString(), total_deposit: channel.ownDeposit.toString(), + total_withdraw: channel.ownWithdraw.toString(), state: transformSdkChannelStateToApi(channel.state), settle_timeout: channel.settleTimeout, - reveal_timeout: 0, // FIXME: Not defined here. Python client handles reveal timeout differently, + reveal_timeout: 50, // FIXME: Not defined here. Python client handles reveal timeout differently, }; } From 7192a26ad8cc897d7c128ea52538c8b865079755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Vitor=20de=20Lima=20Matos?= Date: Tue, 14 Jul 2020 15:53:05 -0300 Subject: [PATCH 04/11] sdk: ensure deposit amount decode raises proper error --- raiden-ts/src/raiden.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raiden-ts/src/raiden.ts b/raiden-ts/src/raiden.ts index 7a8361b6dd..7698005515 100644 --- a/raiden-ts/src/raiden.ts +++ b/raiden-ts/src/raiden.ts @@ -670,7 +670,7 @@ export class Raiden { assert(tokenNetwork, ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, this.log.info); assert(!subkey || this.deps.main, ErrorCodes.RDN_SUBKEY_NOT_SET, this.log.info); - const deposit = decode(UInt(32), amount); + const deposit = decode(UInt(32), amount, ErrorCodes.DTA_INVALID_DEPOSIT, this.log.info); const meta = { tokenNetwork, partner }; const promise = asyncActionToPromise(channelDeposit, meta, this.action$, true).then( ({ txHash }) => txHash, From 358ebb2c238d5afefdd1e186de4addace898f95f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Vitor=20de=20Lima=20Matos?= Date: Tue, 14 Jul 2020 15:58:28 -0300 Subject: [PATCH 05/11] cli: adjust channel interface --- raiden-cli/src/types.ts | 7 ++++--- raiden-cli/src/utils/channels.ts | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/raiden-cli/src/types.ts b/raiden-cli/src/types.ts index a7f0246ed1..3452068211 100644 --- a/raiden-cli/src/types.ts +++ b/raiden-cli/src/types.ts @@ -27,15 +27,16 @@ export enum ApiChannelState { // 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; + total_withdraw: string; state: ApiChannelState; - settle_timeout: number; - reveal_timeout: number; + settle_timeout: string; + reveal_timeout: string; } export enum ApiPaymentEvents { diff --git a/raiden-cli/src/utils/channels.ts b/raiden-cli/src/utils/channels.ts index 3b960ab6e2..84d535cde0 100644 --- a/raiden-cli/src/utils/channels.ts +++ b/raiden-cli/src/utils/channels.ts @@ -30,15 +30,15 @@ function transformSdkChannelStateToApi(state: ChannelState): ApiChannelState { export function transformSdkChannelFormatToApi(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(), total_withdraw: channel.ownWithdraw.toString(), state: transformSdkChannelStateToApi(channel.state), - settle_timeout: channel.settleTimeout, - reveal_timeout: 50, // FIXME: Not defined here. Python client handles reveal timeout differently, + settle_timeout: channel.settleTimeout.toString(), + reveal_timeout: '50', // FIXME: Not defined here. Python client handles reveal timeout differently, }; } From 612a8bdeee6cbf8d38075108c4bd9b78fd7cc087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Vitor=20de=20Lima=20Matos?= Date: Tue, 14 Jul 2020 15:59:21 -0300 Subject: [PATCH 06/11] cli: ensure more descriptive 500-errors on unknown exceptions --- raiden-cli/src/app.ts | 7 +++---- raiden-cli/src/utils/validation.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/raiden-cli/src/app.ts b/raiden-cli/src/app.ts index ce4632c926..482fc48c26 100644 --- a/raiden-cli/src/app.ts +++ b/raiden-cli/src/app.ts @@ -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'; @@ -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 { diff --git a/raiden-cli/src/utils/validation.ts b/raiden-cli/src/utils/validation.ts index 80efacf076..645b4f2cc1 100644 --- a/raiden-cli/src/utils/validation.ts +++ b/raiden-cli/src/utils/validation.ts @@ -53,5 +53,5 @@ export function isInvalidParameterError(error: RaidenError): boolean { * insufficient tokens funds for depositing. */ export function isTransactionWouldFailError(error: Error): boolean { - return /gas required exceeds allowance .* or always failing transaction/.test(error.message); + return /always failing transaction/.test(error.message); } From 960bbefbcd9fe5985f9489a262410a48706a923c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Vitor=20de=20Lima=20Matos?= Date: Tue, 14 Jul 2020 15:59:56 -0300 Subject: [PATCH 07/11] cli: add update/patch channel handlers --- raiden-cli/src/routes/channels.ts | 114 ++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 7 deletions(-) diff --git a/raiden-cli/src/routes/channels.ts b/raiden-cli/src/routes/channels.ts index 28479bb96f..3bf7531b57 100644 --- a/raiden-cli/src/routes/channels.ts +++ b/raiden-cli/src/routes/channels.ts @@ -1,7 +1,7 @@ import { Router, Request, Response, NextFunction } from 'express'; import { first, pluck } from 'rxjs/operators'; -import { ErrorCodes, RaidenError, isntNil } from 'raiden-ts'; -import { Cli } from '../types'; +import { ErrorCodes, isntNil, ChannelState } from 'raiden-ts'; +import { Cli, ApiChannelState } from '../types'; import { isInvalidParameterError, isTransactionWouldFailError, @@ -9,13 +9,25 @@ import { } from '../utils/validation'; import { flattenChannelDictionary, transformSdkChannelFormatToApi } 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) ); } +function isInsuficientFundsError(error: { message: string; code?: string | number }): boolean { + return ( + error.code === 'INSUFFICIENT_FUNDS' || + [ + ErrorCodes.DTA_INVALID_AMOUNT, + ErrorCodes.DTA_INVALID_DEPOSIT, + ErrorCodes.CNL_WITHDRAW_AMOUNT_TOO_LOW, + ErrorCodes.CNL_WITHDRAW_AMOUNT_TOO_HIGH, + ].includes(error.message) + ); +} + 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; @@ -52,7 +64,91 @@ async function openChannel(this: Cli, request: Request, response: Response, next } catch (error) { if (isInvalidParameterError(error)) { response.status(400).send(error.message); - } else if (error.code === 'INSUFFICIENT_FUNDS') { + } 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 = new Set(['state', 'total_deposit', 'total_withdraw']); +function validateChannelUpdateBody(request: Request, response: Response, next: NextFunction) { + const intersec = Object.keys(request.body).filter((k) => allowedUpdateKeys.has(k)); + if (intersec.length < 1) + return response + .status(400) + .send('one of "state" | "total_deposit" | "total_withdraw" operations required'); + else if (intersec.length > 1) + return response + .status(409) + .send('more than one of "state" | "total_deposit" | "total_withdraw" requested'); + if ( + request.body.state && + ![ApiChannelState.closed, ApiChannelState.settled].includes(request.body.state) + ) + return response + .status(400) + .send('invalid "state" requested: must be one of "closed" | "settled"'); + return next(); +} + +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) { + if (request.body.state === ApiChannelState.closed) { + await this.raiden.closeChannel(token, partner); + channel = await this.raiden.channels$ + .pipe( + pluck(token, partner), + first((channel) => + [ChannelState.closed, ChannelState.settleable, ChannelState.settling].includes( + channel.state, + ), + ), + ) + .toPromise(); + } else if (request.body.state === ApiChannelState.settled) { + const promise = this.raiden.settleChannel(token, partner); + channel = await this.raiden.channels$.pipe(pluck(token, partner), first()).toPromise(); + await promise; + } + } else if (request.body.total_deposit) { + await this.raiden.depositChannel( + token, + partner, + channel.ownDeposit.sub(request.body.total_deposit).mul(-1), + ); + channel = await this.raiden.channels$ + .pipe( + pluck(token, partner), + first((channel) => channel.ownDeposit.gte(request.body.total_deposit)), + ) + .toPromise(); + } else if (request.body.total_withdraw) { + await this.raiden.withdrawChannel( + token, + partner, + channel.ownWithdraw.sub(request.body.total_withdraw).mul(-1), + ); + channel = await this.raiden.channels$ + .pipe( + pluck(token, partner), + first((channel) => channel.ownWithdraw.gte(request.body.total_withdraw)), + ) + .toPromise(); + } + response.status(200).json(transformSdkChannelFormatToApi(channel)); + } catch (error) { + if (isInvalidParameterError(error)) { + response.status(400).send(error.message); + } else if (isInsuficientFundsError(error)) { response.status(402).send(error.message); } else if (isConflictError(error)) { response.status(409).send(error.message); @@ -74,9 +170,13 @@ export function makeChannelsRouter(this: Cli): Router { router.put('/', openChannel.bind(this)); - router.patch('/:tokenAddress/:partnerAddress', (_request: Request, response: Response) => { - response.status(404).send('Not implemented yet'); - }); + router.patch( + '/:tokenAddress/:partnerAddress', + validateOptionalAddressParameter.bind('tokenAddress'), + validateOptionalAddressParameter.bind('partnerAddress'), + validateChannelUpdateBody, + updateChannel.bind(this), + ); return router; } From dfce08ad150a0c9cb8717aacda0e4172614934de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Vitor=20de=20Lima=20Matos?= Date: Wed, 15 Jul 2020 10:39:36 -0300 Subject: [PATCH 08/11] cli: address review comments --- raiden-cli/src/routes/channels.ts | 139 +++++++++++++++++------------- raiden-cli/src/utils/channels.ts | 6 +- 2 files changed, 80 insertions(+), 65 deletions(-) diff --git a/raiden-cli/src/routes/channels.ts b/raiden-cli/src/routes/channels.ts index 3bf7531b57..e6799601a6 100644 --- a/raiden-cli/src/routes/channels.ts +++ b/raiden-cli/src/routes/channels.ts @@ -1,13 +1,14 @@ import { Router, Request, Response, NextFunction } from 'express'; import { first, pluck } from 'rxjs/operators'; -import { ErrorCodes, isntNil, ChannelState } from 'raiden-ts'; +import { ErrorCodes, isntNil, ChannelState, RaidenChannel } from 'raiden-ts'; import { Cli, ApiChannelState } from '../types'; import { isInvalidParameterError, isTransactionWouldFailError, validateOptionalAddressParameter, + validateAddressParameter, } from '../utils/validation'; -import { flattenChannelDictionary, transformSdkChannelFormatToApi } from '../utils/channels'; +import { flattenChannelDictionary, transformChannelFormatForApi } from '../utils/channels'; function isConflictError(error: Error): boolean { return ( @@ -20,8 +21,7 @@ function isInsuficientFundsError(error: { message: string; code?: string | numbe return ( error.code === 'INSUFFICIENT_FUNDS' || [ - ErrorCodes.DTA_INVALID_AMOUNT, - ErrorCodes.DTA_INVALID_DEPOSIT, + ErrorCodes.RDN_INSUFFICIENT_BALANCE, ErrorCodes.CNL_WITHDRAW_AMOUNT_TOO_LOW, ErrorCodes.CNL_WITHDRAW_AMOUNT_TOO_HIGH, ].includes(error.message) @@ -35,7 +35,7 @@ async function getChannels(this: Cli, request: Request, response: Response) { if (token && partner) { const channel = channelsDict[token]?.[partner]; if (channel) { - response.json(transformSdkChannelFormatToApi(channel)); + response.json(transformChannelFormatForApi(channel)); } else { response.status(404).send('The channel does not exist'); } @@ -43,7 +43,7 @@ async function getChannels(this: Cli, request: Request, response: Response) { 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(transformSdkChannelFormatToApi)); + response.json(channelsList.map(transformChannelFormatForApi)); } } @@ -60,7 +60,7 @@ async function openChannel(this: Cli, request: Request, response: Response, next const channel = await this.raiden.channels$ .pipe(pluck(token, partner), first(isntNil)) .toPromise(); - response.status(201).json(transformSdkChannelFormatToApi(channel)); + response.status(201).json(transformChannelFormatForApi(channel)); } catch (error) { if (isInvalidParameterError(error)) { response.status(400).send(error.message); @@ -74,27 +74,78 @@ async function openChannel(this: Cli, request: Request, response: Response, next } } -const allowedUpdateKeys = new Set(['state', 'total_deposit', 'total_withdraw']); +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.has(k)); + const intersec = Object.keys(request.body).filter((k) => allowedUpdateKeys.includes(k)); if (intersec.length < 1) - return response - .status(400) - .send('one of "state" | "total_deposit" | "total_withdraw" operations required'); + return response.status(400).send(`one of [${allowedUpdateKeys}] operations required`); else if (intersec.length > 1) - return response - .status(409) - .send('more than one of "state" | "total_deposit" | "total_withdraw" requested'); - if ( - request.body.state && - ![ApiChannelState.closed, ApiChannelState.settled].includes(request.body.state) - ) + 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 "closed" | "settled"'); + .send(`invalid "state" requested: must be one of [${allowedUpdateStates}]`); return next(); } +async function updateChannelState( + this: Cli, + channel: RaidenChannel, + newState: ApiChannelState.closed | ApiChannelState.settled, +): Promise { + 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 { + // 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 { + // 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; @@ -102,49 +153,13 @@ async function updateChannel(this: Cli, request: Request, response: Response, ne 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) { - if (request.body.state === ApiChannelState.closed) { - await this.raiden.closeChannel(token, partner); - channel = await this.raiden.channels$ - .pipe( - pluck(token, partner), - first((channel) => - [ChannelState.closed, ChannelState.settleable, ChannelState.settling].includes( - channel.state, - ), - ), - ) - .toPromise(); - } else if (request.body.state === ApiChannelState.settled) { - const promise = this.raiden.settleChannel(token, partner); - channel = await this.raiden.channels$.pipe(pluck(token, partner), first()).toPromise(); - await promise; - } + channel = await updateChannelState.call(this, channel, request.body.state); } else if (request.body.total_deposit) { - await this.raiden.depositChannel( - token, - partner, - channel.ownDeposit.sub(request.body.total_deposit).mul(-1), - ); - channel = await this.raiden.channels$ - .pipe( - pluck(token, partner), - first((channel) => channel.ownDeposit.gte(request.body.total_deposit)), - ) - .toPromise(); + channel = await updateChannelDeposit.call(this, channel, request.body.total_deposit); } else if (request.body.total_withdraw) { - await this.raiden.withdrawChannel( - token, - partner, - channel.ownWithdraw.sub(request.body.total_withdraw).mul(-1), - ); - channel = await this.raiden.channels$ - .pipe( - pluck(token, partner), - first((channel) => channel.ownWithdraw.gte(request.body.total_withdraw)), - ) - .toPromise(); + channel = await updateChannelWithdraw.call(this, channel, request.body.total_withdraw); } - response.status(200).json(transformSdkChannelFormatToApi(channel)); + response.status(200).json(transformChannelFormatForApi(channel)); } catch (error) { if (isInvalidParameterError(error)) { response.status(400).send(error.message); @@ -172,8 +187,8 @@ export function makeChannelsRouter(this: Cli): Router { router.patch( '/:tokenAddress/:partnerAddress', - validateOptionalAddressParameter.bind('tokenAddress'), - validateOptionalAddressParameter.bind('partnerAddress'), + validateAddressParameter.bind('tokenAddress'), + validateAddressParameter.bind('partnerAddress'), validateChannelUpdateBody, updateChannel.bind(this), ); diff --git a/raiden-cli/src/utils/channels.ts b/raiden-cli/src/utils/channels.ts index 84d535cde0..ce2d0e2ac5 100644 --- a/raiden-cli/src/utils/channels.ts +++ b/raiden-cli/src/utils/channels.ts @@ -9,7 +9,7 @@ export function flattenChannelDictionary(channelDict: RaidenChannels): RaidenCha ); } -function transformSdkChannelStateToApi(state: ChannelState): ApiChannelState { +function transformChannelStateForApi(state: ChannelState): ApiChannelState { let apiState; switch (state) { case ChannelState.open: @@ -28,7 +28,7 @@ function transformSdkChannelStateToApi(state: ChannelState): ApiChannelState { return apiState; } -export function transformSdkChannelFormatToApi(channel: RaidenChannel): ApiChannel { +export function transformChannelFormatForApi(channel: RaidenChannel): ApiChannel { return { channel_identifier: channel.id.toString(), token_network_address: channel.tokenNetwork, @@ -37,7 +37,7 @@ export function transformSdkChannelFormatToApi(channel: RaidenChannel): ApiChann balance: channel.capacity.toString(), total_deposit: channel.ownDeposit.toString(), total_withdraw: channel.ownWithdraw.toString(), - state: transformSdkChannelStateToApi(channel.state), + state: transformChannelStateForApi(channel.state), settle_timeout: channel.settleTimeout.toString(), reveal_timeout: '50', // FIXME: Not defined here. Python client handles reveal timeout differently, }; From 1875b0d62475e33d3b7d0e6502f85357a0c38991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Vitor=20de=20Lima=20Matos?= Date: Wed, 15 Jul 2020 12:30:05 -0300 Subject: [PATCH 09/11] sdk: check token balance before trying to deposit --- raiden-ts/CHANGELOG.md | 4 +- raiden-ts/src/channels/epics.ts | 43 ++++++++++++--------- raiden-ts/tests/e2e/raiden.spec.ts | 2 +- raiden-ts/tests/unit/epics/channels.spec.ts | 23 +++++++++++ raiden-ts/tests/unit/mocks.ts | 1 + 5 files changed, 52 insertions(+), 21 deletions(-) diff --git a/raiden-ts/CHANGELOG.md b/raiden-ts/CHANGELOG.md index e1e3b8a146..8a71e78da0 100644 --- a/raiden-ts/CHANGELOG.md +++ b/raiden-ts/CHANGELOG.md @@ -1,16 +1,16 @@ # Changelog ## [Unreleased] - ### Fixed ### Added ### Changed +- [#1905] Fail early if not enough tokens to deposit +[#1905]: https://github.com/raiden-network/light-client/issues/1905 ## [0.10.0] - 2020-07-13 - ### Fixed - [#1514] Fix handling of expired LockedTransfer and WithdrawRequest - [#1607] Fix settling when one side closes/updates with outdated BalanceProof diff --git a/raiden-ts/src/channels/epics.ts b/raiden-ts/src/channels/epics.ts index df36b3ea8b..2f2cf6c20b 100644 --- a/raiden-ts/src/channels/epics.ts +++ b/raiden-ts/src/channels/epics.ts @@ -618,29 +618,36 @@ const makeDeposit$ = ( ): Observable => { if (!deposit?.gt(Zero)) return EMPTY; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function stopRetryIfRejected(err: any, count: number) { + function stopRetryIfRejected(err: { message: string; code?: string | number }, count: number) { // 4001 is Metamask's code for rejected/denied signature prompt const metamaskRejectedErrorCode = 4001; return count >= 10 || err?.code === metamaskRejectedErrorCode; } - return defer(() => tokenContract.functions.allowance(sender, tokenNetworkContract.address)).pipe( - mergeMap((allowance) => - allowance.gte(deposit) - ? of(true) - : retryAsync$( - () => - tokenContract.functions.approve(tokenNetworkContract.address, deposit, { - nonce: approveNonce, - }), - (tokenContract.provider as JsonRpcProvider).pollingInterval, - stopRetryIfRejected, - ).pipe( - // if needed, send approveTx and wait/assert it before proceeding - assertTx('approve', ErrorCodes.CNL_APPROVE_TRANSACTION_FAILED, { log }), - ), - ), + return defer(() => + Promise.all([ + tokenContract.functions.balanceOf(sender), + tokenContract.functions.allowance(sender, tokenNetworkContract.address), + ]), + ).pipe( + mergeMap(([balance, allowance]) => { + assert(balance.gte(deposit), [ + ErrorCodes.RDN_INSUFFICIENT_BALANCE, + { current: balance.toString(), required: deposit.toString() }, + ]); + if (allowance.gte(deposit)) return of(true); + return retryAsync$( + () => + tokenContract.functions.approve(tokenNetworkContract.address, deposit, { + nonce: approveNonce, + }), + (tokenContract.provider as JsonRpcProvider).pollingInterval, + stopRetryIfRejected, + ).pipe( + // if needed, send approveTx and wait/assert it before proceeding + assertTx('approve', ErrorCodes.CNL_APPROVE_TRANSACTION_FAILED, { log }), + ); + }), mergeMapTo(channelId$), take(1), mergeMap((id) => diff --git a/raiden-ts/tests/e2e/raiden.spec.ts b/raiden-ts/tests/e2e/raiden.spec.ts index ff56185700..a16cab6c7d 100644 --- a/raiden-ts/tests/e2e/raiden.spec.ts +++ b/raiden-ts/tests/e2e/raiden.spec.ts @@ -1337,7 +1337,7 @@ describe('Raiden', () => { await expect(sub.openChannel(token, partner, { subkey: true })).resolves.toMatch(/^0x/); // first deposit fails, as subkey has only 200 tokens settled from previous channel await expect(sub.depositChannel(token, partner, 300, { subkey: true })).rejects.toThrow( - 'revert', + ErrorCodes.RDN_INSUFFICIENT_BALANCE, ); await expect(sub.depositChannel(token, partner, 80, { subkey: true })).resolves.toMatch(/^0x/); diff --git a/raiden-ts/tests/unit/epics/channels.spec.ts b/raiden-ts/tests/unit/epics/channels.spec.ts index c6eb8633e9..90a5c7e508 100644 --- a/raiden-ts/tests/unit/epics/channels.spec.ts +++ b/raiden-ts/tests/unit/epics/channels.spec.ts @@ -51,6 +51,7 @@ import { channelUniqueKey } from 'raiden-ts/channels/utils'; import { ChannelState } from 'raiden-ts/channels'; import { createBalanceHash, getBalanceProofFromEnvelopeMessage } from 'raiden-ts/messages'; import { getLocksroot } from 'raiden-ts/transfers/utils'; +import { ErrorCodes } from 'raiden-ts/utils/error'; test('channelSettleableEpic', async () => { expect.assertions(3); @@ -392,6 +393,28 @@ describe('channelDepositEpic', () => { ); }); + test('fails if not enough balance', async () => { + expect.assertions(1); + + const [raiden, partner] = await makeRaidens(2); + await ensureChannelIsOpen([raiden, partner]); + + const tokenContract = raiden.deps.getTokenContract(token); + tokenContract.functions.balanceOf.mockResolvedValue(deposit.sub(1)); + + raiden.store.dispatch( + channelDeposit.request({ deposit }, { tokenNetwork, partner: partner.address }), + ); + await waitBlock(); + + expect(raiden.output).toContainEqual( + channelDeposit.failure( + expect.objectContaining({ message: ErrorCodes.RDN_INSUFFICIENT_BALANCE }), + { tokenNetwork, partner: partner.address }, + ), + ); + }); + test('approve tx fails', async () => { expect.assertions(3); diff --git a/raiden-ts/tests/unit/mocks.ts b/raiden-ts/tests/unit/mocks.ts index de3cf9c6cb..556c3c67a3 100644 --- a/raiden-ts/tests/unit/mocks.ts +++ b/raiden-ts/tests/unit/mocks.ts @@ -813,6 +813,7 @@ export async function makeRaiden( } tokenContract.functions.approve.mockResolvedValue(makeTransaction()); tokenContract.functions.allowance.mockResolvedValue(Zero); + tokenContract.functions.balanceOf.mockResolvedValue(parseEther('1000')); return tokenContract; }, ); From 102820fdc94d422f806cceca9c50dd20b1fd6b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Vitor=20de=20Lima=20Matos?= Date: Wed, 15 Jul 2020 13:12:26 -0300 Subject: [PATCH 10/11] sdk: small fix on planUdcWithdraw value decoding --- raiden-ts/src/raiden.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/raiden-ts/src/raiden.ts b/raiden-ts/src/raiden.ts index 7698005515..ca2a98240b 100644 --- a/raiden-ts/src/raiden.ts +++ b/raiden-ts/src/raiden.ts @@ -1286,7 +1286,9 @@ export class Raiden { } public async planUdcWithdraw(value: BigNumberish): Promise { - const meta = { amount: bigNumberify(value) as UInt<32> }; + const meta = { + amount: decode(UInt(32), value, ErrorCodes.DTA_INVALID_AMOUNT, this.log.error), + }; const promise = asyncActionToPromise(udcWithdraw, meta, this.action$, true).then( ({ txHash }) => txHash!, ); From 36efe62fd4f25245c83f96ad9effc31506ba7680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Vitor=20de=20Lima=20Matos?= Date: Wed, 15 Jul 2020 18:06:50 -0300 Subject: [PATCH 11/11] cli: implement /shutdown endpoint (spec-undocumented) --- raiden-cli/src/index.ts | 10 +++++++--- raiden-cli/src/routes/api.v1.ts | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/raiden-cli/src/index.ts b/raiden-cli/src/index.ts index c1dc4e8ba6..6b9d4cf8ca 100644 --- a/raiden-cli/src/index.ts +++ b/raiden-cli/src/index.ts @@ -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(); @@ -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() { diff --git a/raiden-cli/src/routes/api.v1.ts b/raiden-cli/src/routes/api.v1.ts index 475722fa66..5235d9c355 100644 --- a/raiden-cli/src/routes/api.v1.ts +++ b/raiden-cli/src/routes/api.v1.ts @@ -37,5 +37,10 @@ export function makeApiV1Router(this: Cli): Router { }); }); + router.post('/shutdown', (_request: Request, response: Response) => { + this.raiden.stop(); + response.json({ status: 'shutdown' }); + }); + return router; }