Skip to content

Commit

Permalink
sdk: review comments, split approveIfNeeded$
Browse files Browse the repository at this point in the history
  • Loading branch information
andrevmatos committed Aug 3, 2020
1 parent 94041ab commit ad639d9
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 86 deletions.
46 changes: 12 additions & 34 deletions raiden-ts/src/channels/epics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import {
retryTx,
txNonceErrors,
txFailErrors,
approveIfNeeded$,
} from './utils';

/**
Expand Down Expand Up @@ -689,43 +690,20 @@ function makeDeposit$(
// retryTx from here
return defer(() =>
Promise.all([
tokenContract.functions.balanceOf(sender),
tokenContract.functions.allowance(sender, tokenNetworkContract.address),
tokenContract.functions.balanceOf(sender) as Promise<UInt<32>>,
tokenContract.functions.allowance(sender, tokenNetworkContract.address) as Promise<UInt<32>>,
]),
).pipe(
withLatestFrom(config$),
mergeMap(([[balance, allowance], { minimumAllowance }]) => {
assert(balance.gte(deposit), [
ErrorCodes.RDN_INSUFFICIENT_BALANCE,
{ current: balance.toString(), required: deposit.toString() },
]);

if (allowance.gte(deposit)) return of(true); // if allowance already enough

// secure ERC20 tokens require changing allowance only from or to Zero
// see https://github.com/raiden-network/light-client/issues/2010
let resetAllowance$: Observable<true> = of(true);
if (!allowance.isZero())
resetAllowance$ = defer(() =>
tokenContract.functions.approve(tokenNetworkContract.address, 0),
).pipe(
assertTx('approve', ErrorCodes.CNL_APPROVE_TRANSACTION_FAILED, { log }),
mapTo(true),
);

// if needed, send approveTx and wait/assert it before proceeding; 'deposit' could be enough,
// but we send 'prevAllowance + deposit' in case there's a pending deposit
// default minimumAllowance=MaxUint256 allows to approve once and for all
return resetAllowance$.pipe(
mergeMap(() =>
tokenContract.functions.approve(
tokenNetworkContract.address,
bnMax(minimumAllowance, deposit),
),
),
assertTx('approve', ErrorCodes.CNL_APPROVE_TRANSACTION_FAILED, { log }),
);
}),
mergeMap(([[balance, allowance], { minimumAllowance }]) =>
approveIfNeeded$(
[balance, allowance, bnMax(minimumAllowance, deposit)],
tokenContract,
tokenNetworkContract.address as Address,
ErrorCodes.CNL_APPROVE_TRANSACTION_FAILED,
{ log },
),
),
mergeMapTo(channelId$),
take(1),
mergeMap((id) =>
Expand Down
68 changes: 65 additions & 3 deletions raiden-ts/src/channels/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,29 @@ import {
throwError,
timer,
MonoTypeOperatorFunction,
of,
defer,
} from 'rxjs';
import { tap, mergeMap, map, pluck, filter, groupBy, takeUntil, retryWhen } from 'rxjs/operators';
import {
tap,
mergeMap,
map,
pluck,
filter,
groupBy,
takeUntil,
retryWhen,
mapTo,
} from 'rxjs/operators';
import { Zero } from 'ethers/constants';
import { ContractTransaction, ContractReceipt } from 'ethers/contract';
import logging from 'loglevel';
import logging, { Logger } from 'loglevel';

import { HumanStandardToken } from '../contracts/HumanStandardToken';
import { RaidenState } from '../state';
import { RaidenEpicDeps } from '../types';
import { UInt, Address, Hash, Int, bnMax } from '../utils/types';
import { RaidenError } from '../utils/error';
import { RaidenError, assert, ErrorCodes } from '../utils/error';
import { distinctRecordValues } from '../utils/rx';
import { MessageType } from '../messages/types';
import { Channel, ChannelBalances } from './state';
Expand Down Expand Up @@ -245,3 +258,52 @@ export function groupChannel$(state$: Observable<RaidenState>) {
}),
);
}

/* eslint-disable jsdoc/valid-types */
/**
* Approves spender to transfer up to 'deposit' from our tokens; skips if already allowed
*
* @param amounts - Tuple of amounts
* @param amounts.0 - Our current token balance
* @param amounts.1 - Spender's current allowance
* @param amounts.2 - The new desired allowance for spender
* @param tokenContract - Token contract instance
* @param spender - Spender address
* @param approveError - ErrorCode of approve transaction errors
* @param opts - Options object
* @param opts.log - Logger instance for asserTx
* @returns Cold observable to perform approve transactions
*/
export function approveIfNeeded$(
[balance, allowance, amount]: [UInt<32>, UInt<32>, UInt<32>],
tokenContract: HumanStandardToken,
spender: Address,
approveError: string = ErrorCodes.RDN_APPROVE_TRANSACTION_FAILED,
{ log }: { log: Logger } = { log: logging },
): Observable<true | ContractReceipt> {
assert(balance.gte(amount), [
ErrorCodes.RDN_INSUFFICIENT_BALANCE,
{ current: balance.toString(), required: amount.toString() },
]);

if (allowance.gte(amount)) return of(true); // if allowance already enough

// secure ERC20 tokens require changing allowance only from or to Zero
// see https://github.com/raiden-network/light-client/issues/2010
let resetAllowance$: Observable<true> = of(true);
if (!allowance.isZero())
resetAllowance$ = defer(() => tokenContract.functions.approve(spender, 0)).pipe(
assertTx('approve', approveError, { log }),
mapTo(true),
);

// if needed, send approveTx and wait/assert it before proceeding; 'deposit' could be enough,
// but we send 'prevAllowance + deposit' in case there's a pending deposit
// default minimumAllowance=MaxUint256 allows to approve once and for all
return resetAllowance$.pipe(
mergeMap(() => tokenContract.functions.approve(spender, amount)),
assertTx('approve', approveError, { log }),
pluck(1),
);
}
/* eslint-enable jsdoc/valid-types */
20 changes: 18 additions & 2 deletions raiden-ts/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@ import { Wallet } from 'ethers/wallet';
import { Contract, ContractReceipt, ContractTransaction } from 'ethers/contract';
import { Network, toUtf8Bytes, sha256 } from 'ethers/utils';
import { JsonRpcProvider } from 'ethers/providers';
import { MaxUint256 } from 'ethers/constants';
import { Observable, defer } from 'rxjs';
import { filter, map, pluck, withLatestFrom, first, exhaustMap, mergeMap } from 'rxjs/operators';
import logging from 'loglevel';

import { RaidenState } from './state';
import { ContractsInfo, RaidenEpicDeps } from './types';
import { ContractsInfo, RaidenEpicDeps, Latest } from './types';
import { assert } from './utils';
import { raidenTransfer } from './transfers/utils';
import { RaidenTransfer } from './transfers/state';
import { channelAmounts } from './channels/utils';
import { RaidenChannels, RaidenChannel } from './channels/state';
import { pluckDistinct, distinctRecordValues } from './utils/rx';
import { Address, PrivateKey, isntNil, Hash } from './utils/types';
import { Address, PrivateKey, isntNil, Hash, UInt } from './utils/types';
import { getNetworkName } from './utils/ethers';
import { RaidenError, ErrorCodes } from './utils/error';

Expand Down Expand Up @@ -389,3 +390,18 @@ export async function fetchContractsInfo(
OneToN: { address: oneToN, block_number: firstBlock },
};
}

/**
* Resolves to our current UDC balance, as seen from [[monitorUdcBalanceEpic]]
*
* @param latest$ - Latest observable
* @returns Promise to our current UDC balance
*/
export async function getUdcBalance(latest$: Observable<Latest>): Promise<UInt<32>> {
return latest$
.pipe(
pluck('udcBalance'),
first((balance) => !!balance && balance.lt(MaxUint256)),
)
.toPromise();
}
17 changes: 4 additions & 13 deletions raiden-ts/src/raiden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { createLogger } from 'redux-logger';
import constant from 'lodash/constant';
import memoize from 'lodash/memoize';
import { Observable, AsyncSubject, merge, defer, EMPTY, ReplaySubject, of } from 'rxjs';
import { first, filter, map, mergeMap, skip, pluck } from 'rxjs/operators';
import { first, filter, map, mergeMap, skip } from 'rxjs/operators';
import logging from 'loglevel';

import { TokenNetworkRegistryFactory } from './contracts/TokenNetworkRegistryFactory';
Expand Down Expand Up @@ -76,6 +76,7 @@ import {
waitConfirmation,
callAndWaitMined,
fetchContractsInfo,
getUdcBalance,
} from './helpers';
import { RaidenError, ErrorCodes } from './utils/error';

Expand Down Expand Up @@ -1107,12 +1108,7 @@ export class Raiden {
* @returns Promise to UDC remaining capacity
*/
public async getUDCCapacity(): Promise<BigNumber> {
const balance = await this.deps.latest$
.pipe(
pluck('udcBalance'),
first((balance) => !!balance && balance.lt(MaxUint256)),
)
.toPromise();
const balance = await getUdcBalance(this.deps.latest$);
const blockNumber = this.state.blockNumber;
const owedAmount = Object.values(this.state.iou)
.reduce(
Expand Down Expand Up @@ -1155,12 +1151,7 @@ export class Raiden {

const deposit = decode(UInt(32), amount, ErrorCodes.DTA_INVALID_DEPOSIT, this.log.info);
assert(deposit.gt(Zero), ErrorCodes.DTA_NON_POSITIVE_NUMBER, this.log.info);
const balance = await this.deps.latest$
.pipe(
pluck('udcBalance'),
first((balance) => !!balance && balance.lt(MaxUint256)),
)
.toPromise();
const balance = await getUdcBalance(this.deps.latest$);
const meta = { totalDeposit: balance.add(deposit) as UInt<32> };

const mined = asyncActionToPromise(udcDeposit, meta, this.action$).then((res) => res?.txHash);
Expand Down
51 changes: 17 additions & 34 deletions raiden-ts/src/services/epics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ import { messageGlobalSend } from '../messages/actions';
import { MessageType, PFSCapacityUpdate, PFSFeeUpdate, MonitorRequest } from '../messages/types';
import { MessageTypeId, signMessage, createBalanceHash } from '../messages/utils';
import { ChannelState, Channel } from '../channels/state';
import { channelAmounts, groupChannel$, assertTx, retryTx } from '../channels/utils';
import {
channelAmounts,
groupChannel$,
assertTx,
retryTx,
approveIfNeeded$,
} from '../channels/utils';
import {
Address,
decode,
Expand Down Expand Up @@ -462,8 +468,8 @@ function makeUdcDeposit$(
) {
return defer(() =>
Promise.all([
tokenContract.functions.balanceOf(sender),
tokenContract.functions.allowance(sender, userDepositContract.address),
tokenContract.functions.balanceOf(sender) as Promise<UInt<32>>,
tokenContract.functions.allowance(sender, userDepositContract.address) as Promise<UInt<32>>,
userDepositContract.functions.effectiveBalance(address),
]),
).pipe(
Expand All @@ -472,35 +478,12 @@ function makeUdcDeposit$(
ErrorCodes.UDC_DEPOSIT_OUTDATED,
{ requested: totalDeposit.toString(), current: deposited.toString() },
]);
assert(balance.gte(deposit), [
ErrorCodes.RDN_INSUFFICIENT_BALANCE,
{ current: balance.toString(), required: deposit.toString() },
]);

if (allowance.gte(deposit)) return of(true); // if allowance already enough

// secure ERC20 tokens require changing allowance only from or to Zero
// see https://github.com/raiden-network/light-client/issues/2010
let resetAllowance$: Observable<true> = of(true);
if (!allowance.isZero())
resetAllowance$ = defer(() =>
tokenContract.functions.approve(userDepositContract.address, 0),
).pipe(
assertTx('approve', ErrorCodes.RDN_APPROVE_TRANSACTION_FAILED, { log }),
mapTo(true),
);

// if needed, send approveTx and wait/assert it before proceeding; 'deposit' could be enough,
// but we send 'prevAllowance + deposit' in case there's a pending deposit
// default minimumAllowance=MaxUint256 allows to approve once and for all
return resetAllowance$.pipe(
mergeMap(() =>
tokenContract.functions.approve(
userDepositContract.address,
bnMax(minimumAllowance, deposit),
),
),
assertTx('approve', ErrorCodes.RDN_APPROVE_TRANSACTION_FAILED, { log }),
return approveIfNeeded$(
[balance, allowance, bnMax(minimumAllowance, deposit)],
tokenContract,
userDepositContract.address as Address,
ErrorCodes.RDN_APPROVE_TRANSACTION_FAILED,
{ log },
);
}),
// send setTotalDeposit transaction
Expand Down Expand Up @@ -532,11 +515,11 @@ export const udcDepositEpic = (
{}: Observable<RaidenState>,
{ userDepositContract, getTokenContract, address, log, signer, main, config$ }: RaidenEpicDeps,
): Observable<udcDeposit.failure | udcDeposit.success> => {
const svtToken = userDepositContract.functions.token() as Promise<Address>;
const serviceToken = userDepositContract.functions.token() as Promise<Address>;
return action$.pipe(
filter(udcDeposit.request.is),
concatMap((action) =>
defer(() => svtToken).pipe(
defer(() => serviceToken).pipe(
withLatestFrom(config$),
mergeMap(([token, config]) => {
const { signer: onchainSigner, address: onchainAddress } = chooseOnchainAccount(
Expand Down

0 comments on commit ad639d9

Please sign in to comment.