Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactive UDC withdraw #2635

Merged
merged 10 commits into from
Mar 25, 2021
3 changes: 2 additions & 1 deletion raiden-ts/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ export const RaidenEvents = [
RaidenActions.newBlock,
RaidenActions.matrixPresence.success,
RaidenActions.tokenMonitored,
RaidenActions.udcWithdrawPlan.success,
RaidenActions.udcWithdrawPlan.failure,
RaidenActions.udcWithdraw.success,
RaidenActions.udcWithdraw.failure,
RaidenActions.udcWithdrawn,
RaidenActions.msBalanceProofSent,
RaidenActions.channelSettle.success,
RaidenActions.channelSettle.failure,
Expand Down
6 changes: 3 additions & 3 deletions raiden-ts/src/raiden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ import {
} from './helpers';
import { createPersisterMiddleware } from './persister';
import { raidenReducer } from './reducer';
import { pathFind, udcDeposit, udcWithdraw } from './services/actions';
import { pathFind, udcDeposit, udcWithdrawPlan } from './services/actions';
import type { IOU, RaidenPaths, RaidenPFS, SuggestedPartner } from './services/types';
import { Paths, PFS, PfsMode, SuggestedPartners } from './services/types';
import { pfsListInfo } from './services/utils';
Expand Down Expand Up @@ -1392,10 +1392,10 @@ export class Raiden {
const meta = {
amount: decode(UInt(32), value, ErrorCodes.DTA_INVALID_AMOUNT, this.log.error),
};
const promise = asyncActionToPromise(udcWithdraw, meta, this.action$, true).then(
const promise = asyncActionToPromise(udcWithdrawPlan, meta, this.action$, true).then(
({ txHash }) => txHash!,
);
this.store.dispatch(udcWithdraw.request(undefined, meta));
this.store.dispatch(udcWithdrawPlan.request(undefined, meta));
return promise;
}

Expand Down
33 changes: 19 additions & 14 deletions raiden-ts/src/services/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,36 +66,41 @@ const UdcWithdrawId = t.type({
amount: UInt(32),
});

export const udcWithdraw = createAsyncAction(
export const udcWithdrawPlan = createAsyncAction(
UdcWithdrawId,
'udc/withdraw/request',
'udc/withdraw/success',
'udc/withdraw/failure',
'udc/withdraw/plan/request',
'udc/withdraw/plan/success',
'udc/withdraw/plan/failure',
t.undefined,
t.intersection([
t.type({ block: t.number }),
t.partial({ txHash: Hash, txBlock: t.number, confirmed: t.union([t.undefined, t.boolean]) }),
]),
);

export namespace udcWithdraw {
export interface request extends ActionType<typeof udcWithdraw.request> {}
export interface success extends ActionType<typeof udcWithdraw.success> {}
export interface failure extends ActionType<typeof udcWithdraw.failure> {}
export namespace udcWithdrawPlan {
export interface request extends ActionType<typeof udcWithdrawPlan.request> {}
export interface success extends ActionType<typeof udcWithdrawPlan.success> {}
export interface failure extends ActionType<typeof udcWithdrawPlan.failure> {}
}

export const udcWithdrawn = createAction(
'udc/withdrawn',
export const udcWithdraw = createAsyncAction(
UdcWithdrawId,
'udc/withdraw/request',
'udc/withdraw/success',
'udc/withdraw/failure',
t.undefined,
t.type({
withdrawal: UInt(32),
txHash: Hash,
txBlock: t.number,
confirmed: t.union([t.undefined, t.boolean]),
}),
UdcWithdrawId,
);

export interface udcWithdrawn extends ActionType<typeof udcWithdrawn> {}
export namespace udcWithdraw {
export interface request extends ActionType<typeof udcWithdraw.request> {}
export interface success extends ActionType<typeof udcWithdraw.success> {}
export interface failure extends ActionType<typeof udcWithdraw.failure> {}
}

export const msBalanceProofSent = createAction(
'ms/balanceProof/sent',
Expand Down
67 changes: 38 additions & 29 deletions raiden-ts/src/services/epics/udc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import type { RaidenEpicDeps } from '../../types';
import { assert, commonTxErrors, ErrorCodes, networkErrors } from '../../utils/error';
import { pluckDistinct, retryAsync$, retryWhile } from '../../utils/rx';
import type { Address, UInt } from '../../utils/types';
import { udcDeposit, udcWithdraw, udcWithdrawn } from '../actions';
import { udcDeposit, udcWithdraw, udcWithdrawPlan } from '../actions';

/**
* Monitors the balance of UDC and emits udcDeposited, made available in Latest['udcBalance']
Expand Down Expand Up @@ -88,27 +88,36 @@ function makeUdcDeposit$(
tokenContract.callStatic.allowance(sender, userDepositContract.address) as Promise<
UInt<32>
>,
userDepositContract.callStatic.total_deposit(address),
]),
pollingInterval,
{ onErrors: networkErrors },
).pipe(
mergeMap(([balance, allowance, deposited]) => {
assert(deposited.add(deposit).eq(totalDeposit), [
ErrorCodes.UDC_DEPOSIT_OUTDATED,
{ requested: totalDeposit.toString(), current: deposited.toString() },
]);
return approveIfNeeded$(
mergeMap(([balance, allowance]) =>
approveIfNeeded$(
[balance, allowance, deposit],
tokenContract,
userDepositContract.address as Address,
ErrorCodes.RDN_APPROVE_TRANSACTION_FAILED,
{ provider },
{ log, minimumAllowance },
);
}),
// send setTotalDeposit transaction
mergeMap(() => userDepositContract.deposit(address, totalDeposit)),
),
),
mergeMap(() =>
retryAsync$(
async () => userDepositContract.callStatic.total_deposit(address),
pollingInterval,
{ onErrors: networkErrors },
).pipe(
mergeMap(async (deposited) => {
assert(deposited.add(deposit).eq(totalDeposit), [
ErrorCodes.UDC_DEPOSIT_OUTDATED,
{ requested: totalDeposit.toString(), current: deposited.toString() },
]);
weilbith marked this conversation as resolved.
Show resolved Hide resolved
// send setTotalDeposit transaction
return userDepositContract.deposit(address, totalDeposit);
}),
),
),
assertTx('deposit', ErrorCodes.RDN_DEPOSIT_TRANSACTION_FAILED, { log, provider }),
// retry also txFail errors, since estimateGas can lag behind just-opened channel or
// just-approved allowance
Expand Down Expand Up @@ -199,7 +208,7 @@ export function udcDepositEpic(
/**
* Handle a UDC withdraw request and send plan transaction
*
* @param action$ - Observable of udcWithdraw.request actions
* @param action$ - Observable of udcWithdrawPlan.request actions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.userDepositContract - UDC contract instance
Expand All @@ -208,15 +217,15 @@ export function udcDepositEpic(
* @param deps.signer - Signer instance
* @param deps.provider - Provider instance
* @param deps.config$ - Config observable
* @returns Observable of udcWithdraw.success|udcWithdraw.failure actions
* @returns Observable of udcWithdrawPlan.success|udcWithdrawPlan.failure actions
*/
export function udcWithdrawRequestEpic(
export function udcWithdrawPlanRequestEpic(
action$: Observable<RaidenAction>,
state$: Observable<RaidenState>,
{ userDepositContract, address, log, signer, provider, config$ }: RaidenEpicDeps,
): Observable<udcWithdraw.success | udcWithdraw.failure> {
): Observable<udcWithdrawPlan.success | udcWithdrawPlan.failure> {
return action$.pipe(
filter(udcWithdraw.request.is),
filter(udcWithdrawPlan.request.is),
mergeMap((action) =>
retryAsync$(
() =>
Expand Down Expand Up @@ -262,7 +271,7 @@ export function udcWithdrawRequestEpic(
),
first(({ amount }) => amount.gte(action.meta.amount)),
map(({ amount, withdraw_block }) =>
udcWithdraw.success(
udcWithdrawPlan.success(
{
block: withdraw_block.toNumber(),
txHash,
Expand All @@ -274,28 +283,28 @@ export function udcWithdrawRequestEpic(
),
),
),
catchError((err) => of(udcWithdraw.failure(err, action.meta))),
catchError((err) => of(udcWithdrawPlan.failure(err, action.meta))),
);
}),
);
}

/**
* At startup, check if there was a previous plan and re-emit udcWithdraw.success action
* At startup, check if there was a previous plan and re-emit udcWithdrawPlan.success action
*
* @param action$ - Observable of RaidenActions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.userDepositContract - UDC contract instance
* @param deps.address - Our address
* @param deps.config$ - Config observable
* @returns Observable of udcWithdraw.success actions
* @returns Observable of udcWithdrawPlan.success actions
*/
export function udcCheckWithdrawPlannedEpic(
{}: Observable<RaidenAction>,
{}: Observable<RaidenState>,
{ userDepositContract, address, config$ }: RaidenEpicDeps,
): Observable<udcWithdraw.success> {
): Observable<udcWithdrawPlan.success> {
return config$.pipe(
first(),
mergeMap(({ pollingInterval }) =>
Expand All @@ -305,7 +314,7 @@ export function udcCheckWithdrawPlannedEpic(
),
filter((value) => value.withdraw_block.gt(Zero)),
map(({ amount, withdraw_block }) =>
udcWithdraw.success(
udcWithdrawPlan.success(
{ block: withdraw_block.toNumber(), confirmed: true },
{ amount: amount as UInt<32> },
),
Expand All @@ -316,7 +325,7 @@ export function udcCheckWithdrawPlannedEpic(
/**
* When a plan is detected (done on this session or previous), wait until timeout and withdraw
*
* @param action$ - Observable of udcWithdraw.success actions
* @param action$ - Observable of udcWithdrawPlan.success actions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.log - Logger instance
Expand All @@ -325,15 +334,15 @@ export function udcCheckWithdrawPlannedEpic(
* @param deps.signer - Signer instance
* @param deps.provider - Provider instance
* @param deps.config$ - Config observable
* @returns Observable of udcWithdrawn|udcWithdraw.failure actions
* @returns Observable of udcWithdraw.success|udcWithdrawPlan.failure actions
*/
export function udcWithdrawPlannedEpic(
action$: Observable<RaidenAction>,
state$: Observable<RaidenState>,
{ log, userDepositContract, address, signer, provider, config$ }: RaidenEpicDeps,
): Observable<udcWithdrawn | udcWithdraw.failure> {
): Observable<udcWithdraw.success | udcWithdrawPlan.failure> {
return action$.pipe(
filter(udcWithdraw.success.is),
filter(udcWithdrawPlan.success.is),
filter((action) => action.payload.confirmed === true),
mergeMap((action) =>
state$.pipe(
Expand Down Expand Up @@ -377,7 +386,7 @@ export function udcWithdrawPlannedEpic(
filter((newBalance) => newBalance.lt(balance)),
take(1),
map((newBalance) =>
udcWithdrawn(
udcWithdraw.success(
{
withdrawal: balance.sub(newBalance) as UInt<32>,
txHash: transactionHash,
Expand All @@ -389,7 +398,7 @@ export function udcWithdrawPlannedEpic(
),
),
),
catchError((err) => of(udcWithdraw.failure(err, action.meta))),
catchError((err) => of(udcWithdrawPlan.failure(err, action.meta))),
);
}),
);
Expand Down
26 changes: 13 additions & 13 deletions raiden-ts/tests/unit/epics/udc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { BigNumber } from '@ethersproject/bignumber';
import { Zero } from '@ethersproject/constants';
import { parseEther } from '@ethersproject/units';

import { udcWithdraw, udcWithdrawn } from '@/services/actions';
import { udcWithdraw, udcWithdrawPlan } from '@/services/actions';
import type { Hash, UInt } from '@/utils/types';

describe('udcWithdraw', () => {
describe('udcWithdrawPlan', () => {
test('planned withdraw picked on startup', async () => {
const [raiden] = await makeRaidens(1, false);
const amount = parseEther('5');
Expand All @@ -18,7 +18,7 @@ describe('udcWithdraw', () => {
);
await raiden.start();
expect(raiden.output).toContainEqual(
udcWithdraw.success(
udcWithdrawPlan.success(
{ block: withdrawBlock.toNumber(), confirmed: true },
{ amount: amount as UInt<32> },
),
Expand All @@ -43,12 +43,12 @@ describe('udcWithdraw', () => {
blockNumber: raiden.deps.provider.blockNumber,
});

raiden.store.dispatch(udcWithdraw.request(undefined, { amount: amount as UInt<32> }));
raiden.store.dispatch(udcWithdrawPlan.request(undefined, { amount: amount as UInt<32> }));

await waitBlock(raiden.deps.provider.blockNumber + confirmationBlocks);
await waitBlock();
expect(raiden.output).toContainEqual(
udcWithdraw.success(
udcWithdrawPlan.success(
{
block: withdrawBlock.toNumber(),
txHash: planTx.hash as Hash,
Expand All @@ -62,20 +62,20 @@ describe('udcWithdraw', () => {

test('withdraw require: zero amount', async () => {
const [raiden] = await makeRaidens(1);
raiden.store.dispatch(udcWithdraw.request(undefined, { amount: Zero as UInt<32> }));
raiden.store.dispatch(udcWithdrawPlan.request(undefined, { amount: Zero as UInt<32> }));
await waitBlock();
expect(raiden.output).toContainEqual(
udcWithdraw.failure(expect.any(Error), { amount: Zero as UInt<32> }),
udcWithdrawPlan.failure(expect.any(Error), { amount: Zero as UInt<32> }),
);
});

test('withdraw require: not enough balance', async () => {
const [raiden] = await makeRaidens(1);
const amount = parseEther('500');
raiden.store.dispatch(udcWithdraw.request(undefined, { amount: amount as UInt<32> }));
raiden.store.dispatch(udcWithdrawPlan.request(undefined, { amount: amount as UInt<32> }));
await waitBlock();
expect(raiden.output).toContainEqual(
udcWithdraw.failure(expect.any(Error), { amount: amount as UInt<32> }),
udcWithdrawPlan.failure(expect.any(Error), { amount: amount as UInt<32> }),
);
});

Expand All @@ -89,12 +89,12 @@ describe('udcWithdraw', () => {
.mockResolvedValueOnce(parseEther('5'))
.mockResolvedValueOnce(Zero);
raiden.store.dispatch(
udcWithdraw.success({ block: 200, confirmed: true }, { amount: amount as UInt<32> }),
udcWithdrawPlan.success({ block: 200, confirmed: true }, { amount: amount as UInt<32> }),
);
await waitBlock(200);
await waitBlock();
expect(raiden.output).toContainEqual(
udcWithdrawn(
udcWithdraw.success(
{
withdrawal: amount as UInt<32>,
txHash: withdrawTx.hash as Hash,
Expand All @@ -113,12 +113,12 @@ describe('udcWithdraw', () => {
raiden.deps.userDepositContract.withdraw.mockResolvedValue(withdrawTx);
raiden.deps.userDepositContract.balances.mockClear().mockResolvedValueOnce(Zero);
raiden.store.dispatch(
udcWithdraw.success({ block: 200, confirmed: true }, { amount: amount as UInt<32> }),
udcWithdrawPlan.success({ block: 200, confirmed: true }, { amount: amount as UInt<32> }),
);
await waitBlock(200);
await waitBlock();
expect(raiden.output).toContainEqual(
udcWithdraw.failure(expect.any(Error), { amount: amount as UInt<32> }),
udcWithdrawPlan.failure(expect.any(Error), { amount: amount as UInt<32> }),
);
});
});