raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
258 lines • 13.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ensureApprovedBalance$ = exports.transact = exports.groupChannel = exports.channelAmounts = exports.channelUniqueKey = exports.channelKey = void 0;
const constants_1 = require("@ethersproject/constants");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const helpers_1 = require("../helpers");
const types_1 = require("../messages/types");
const error_1 = require("../utils/error");
const rx_1 = require("../utils/rx");
const types_2 = require("../utils/types");
/**
* Returns a key (string) for a channel unique per tokenNetwork+partner
*
* @param channel - Either a Channel or a { tokenNetwork, partner } pair of addresses
* @param channel.tokenNetwork - TokenNetwork address
* @param channel.partner - Partner address
* @returns A string, for now
*/
function channelKey({ tokenNetwork, partner, }) {
const partnerAddr = typeof partner === 'string' ? partner : partner.address;
return `${tokenNetwork}@${partnerAddr}`;
}
exports.channelKey = channelKey;
/**
* Returns a unique key (string) for a channel per tokenNetwork+partner+id
*
* @param channel - Either a Channel or a { tokenNetwork, partner } pair of addresses
* @returns A string, for now
*/
function channelUniqueKey(channel) {
if ('_id' in channel && channel._id)
return channel._id;
return `${channelKey(channel)}#${channel.id.toString().padStart(9, '0')}`;
}
exports.channelUniqueKey = channelUniqueKey;
/**
* Calculates and returns partial and total amounts of given channel state
*
* @param channel - A Channel state to calculate amounts from
* @returns An object holding own&partner's deposit, withdraw, transferred, locked, balance and
* capacity.
*/
function channelAmounts(channel) {
const ownWithdraw = channel.own.withdraw, partnerWithdraw = channel.partner.withdraw, ownTransferred = channel.own.balanceProof.transferredAmount, partnerTransferred = channel.partner.balanceProof.transferredAmount, ownOnchainUnlocked = channel.own.locks
.filter((lock) => lock.registered)
.reduce((acc, lock) => acc.add(lock.amount), constants_1.Zero), partnerOnchainUnlocked = channel.partner.locks
.filter((lock) => lock.registered)
.reduce((acc, lock) => acc.add(lock.amount), constants_1.Zero), ownUnlocked = ownTransferred.add(ownOnchainUnlocked), partnerUnlocked = partnerTransferred.add(partnerOnchainUnlocked), ownLocked = channel.own.balanceProof.lockedAmount.sub(ownOnchainUnlocked), partnerLocked = channel.partner.balanceProof.lockedAmount.sub(partnerOnchainUnlocked), ownBalance = partnerUnlocked.sub(ownUnlocked), partnerBalance = ownUnlocked.sub(partnerUnlocked), // == -ownBalance
_ownPendingWithdraw = (0, types_2.bnMax)(
// get maximum between actual and pending withdraws (as it's a total)
ownWithdraw, ...channel.own.pendingWithdraws
.filter((req) => req.type === types_1.MessageType.WITHDRAW_REQUEST)
.map((req) => req.total_withdraw)), _partnerPendingWithdraw = (0, types_2.bnMax)(partnerWithdraw, ...channel.partner.pendingWithdraws
.filter((req) => req.type === types_1.MessageType.WITHDRAW_REQUEST)
.map((req) => req.total_withdraw)), ownCapacity = channel.own.deposit
.sub(_ownPendingWithdraw) // pending withdraws reduce capacity
.sub(ownLocked)
.add(ownBalance), partnerCapacity = channel.partner.deposit
.sub(_partnerPendingWithdraw)
.sub(partnerLocked)
.add(partnerBalance), ownTotalWithdrawable = channel.own.deposit.add(ownBalance).sub(ownLocked), ownWithdrawable = ownTotalWithdrawable.sub(ownWithdraw), partnerTotalWithdrawable = channel.partner.deposit
.add(partnerBalance)
.sub(partnerLocked), partnerWithdrawable = partnerTotalWithdrawable.sub(partnerWithdraw), totalCapacity = channel.own.deposit
.sub(channel.own.withdraw)
.add(channel.partner.deposit)
.sub(channel.partner.withdraw);
return {
ownDeposit: channel.own.deposit,
ownWithdraw,
ownTransferred,
ownLocked,
ownBalance,
ownCapacity,
ownOnchainUnlocked,
ownUnlocked,
partnerDeposit: channel.partner.deposit,
partnerWithdraw,
partnerTransferred,
partnerLocked,
partnerBalance,
partnerCapacity,
partnerOnchainUnlocked,
partnerUnlocked,
ownTotalWithdrawable,
ownWithdrawable,
partnerTotalWithdrawable,
partnerWithdrawable,
totalCapacity,
};
}
exports.channelAmounts = channelAmounts;
/**
* Custom operator to wait & assert transaction success
*
* @param method - method name to use in logs
* @param error - ErrorCode to throw if transaction fails
* @param deps - object containing logger
* @param deps.log - Logger instance
* @param deps.provider - Eth provider
* @returns operator function to wait for transaction and output hash
*/
function assertTx(method, error, { log, provider }) {
return (tx$) => tx$.pipe((0, operators_1.tap)((tx) => log.debug(`sent ${method} tx "${tx.hash}" to "${tx.to}"`)), (0, operators_1.mergeMap)((tx) => (0, rx_1.retryAsync$)(() => tx.wait(), provider.pollingInterval, { onErrors: error_1.networkErrors }).pipe((0, operators_1.map)((txReceipt) => [tx, txReceipt]))), (0, operators_1.map)(([tx, receipt]) => {
if (!receipt.status || !receipt.transactionHash || !receipt.blockNumber)
throw new error_1.RaidenError(error, {
status: receipt.status ?? null,
transactionHash: receipt.transactionHash ?? null,
blockNumber: receipt.blockNumber ?? null,
});
log.debug(`${method} tx "${receipt.transactionHash}" successfuly mined!`);
return [tx, receipt];
}));
}
/**
* Reactively on state, emits grouped observables per channel which emits respective channel
* states and completes when channel is settled.
* Can be used either passing input directly or as an operator
*
* @returns Tuple containing grouped Observable and { key, id }: { ChannelKey, number } values
*/
function groupChannel() {
return (state$) => state$.pipe((0, operators_1.pluck)('channels'), (0, rx_1.distinctRecordValues)(), (0, operators_1.pluck)(1),
// grouped$ output will be backed by a ReplaySubject(1), so will emit latest channel state
// immediately if resubscribed or withLatestFrom'd
(0, operators_1.groupBy)(channelUniqueKey, { connector: () => new rxjs_1.ReplaySubject(1) }), (0, operators_1.map)((grouped$) => {
const [key, _id] = grouped$.key.split('#');
const id = +_id;
return grouped$.pipe((0, operators_1.takeUntil)(state$.pipe(
// takeUntil first time state's channelId differs from this observable's
// e.g. when channel is settled and gone (channel.id will be undefined)
(0, operators_1.filter)(({ channels }) => channels[key]?.id !== id))));
}));
}
exports.groupChannel = groupChannel;
const feeDataCache = new WeakMap();
/**
* provider.getFeeData, but caches result per provider and per blockNumber (i.e. invalidates cache
* on each block)
*
* @param provider - JsonRpcProvider instance to getFeeData from
* @returns cached promise to feeData
*/
const getFeeData = Object.assign(function getFeeData_(provider) {
const cached = feeDataCache.get(provider);
if (cached?.[0] === provider.blockNumber)
return cached[1];
const promise = provider.getFeeData().catch((err) => {
feeDataCache.delete(provider);
throw err; // re-throw
});
feeDataCache.set(provider, [provider.blockNumber, promise]);
return promise;
}, { cache: feeDataCache });
/**
* Performs a contract transaction with retries
* It automatically choose gasPrice from latest provider.getFeeData, and choose which account to
* use for call depending on opts.subkey or config.subkey (defaults to main account, if available),
* it also adds a +5% margin over estimated gasLimit;
* this function errors if tx doesn't succeed; retry must be implemented by caller
*
* @param contract - Contract instance
* @param method - Method name (string)
* @param parameters - Parameters array for contract method
* @param deps - Epics dependencies
* @param opts - transact options
* @param opts.subkey - whether to force use of subkey (true) or main key (false); null uses
* contract instance as is
* @param opts.error - error to throw if tx can't go through
* @returns Observable returning [transaction, receipt] tuple
*/
function transact(contract, method, parameters, deps, { subkey, error = error_1.ErrorCodes.RDN_TRANSACTION_FAILED, } = {}) {
const { provider, config$ } = deps;
return (0, rxjs_1.defer)(async () => getFeeData(provider)).pipe((0, operators_1.withLatestFrom)(config$), (0, operators_1.mergeMap)(([feeData, { gasPriceFactor, subkey: configSubkey }]) => {
let gasPrice;
if (!gasPriceFactor || gasPriceFactor === 1.0)
gasPrice = undefined;
else if (feeData.maxPriorityFeePerGas && feeData.maxFeePerGas) {
// post-EIP1559, we apply gasPriceFactor only over maxPriorityFeePerGas, and it allows
// to decrease the default priority fee if <1
const addedPriorityFee = feeData.maxPriorityFeePerGas
.mul(Math.round((gasPriceFactor - 1.0) * 1e6))
.div(1e6);
gasPrice = {
// default ethers maxPriorityFeePerGas is constant 2.5Gwei
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas.add(addedPriorityFee),
maxFeePerGas: feeData.maxFeePerGas.add(addedPriorityFee),
};
}
else if (feeData.gasPrice) {
gasPrice = {
gasPrice: feeData.gasPrice.mul(Math.round(gasPriceFactor * 1e6)).div(1e6),
};
}
let contractWithSigner = contract;
if (subkey !== null) {
const { signer: onchainSigner } = (0, helpers_1.chooseOnchainAccount)(deps, subkey ?? configSubkey);
contractWithSigner = (0, helpers_1.getContractWithSigner)(contract, onchainSigner);
}
return (0, rxjs_1.defer)(async () => contractWithSigner.estimateGas[method](...parameters)).pipe((0, operators_1.mergeMap)(async (gasLimit) => {
gasLimit = gasLimit.add(gasLimit.mul(5).div(100)); // add +5% gasLimit
let paramsWithOverrides;
if (parameters.length === contractWithSigner[method].length)
paramsWithOverrides = parameters
.slice(0, -1)
.concat({ ...(0, types_2.last)(parameters), ...gasPrice, gasLimit });
else
paramsWithOverrides = parameters.concat({ ...gasPrice, gasLimit });
return contractWithSigner[method](...paramsWithOverrides);
}));
}), assertTx(method, error, deps));
}
exports.transact = transact;
/**
* Approves spender to transfer up to 'deposit' from our tokens; skips if already allowed
* Errors if sender doesn't have enough balance, or transaction fails (may be retried)
*
* @param tokenContract - Token contract instance already connected to sender's signer
* @param spender - Spender address
* @param amount - Amount to be required to be approved
* @param deps - Epics dependencies
* @param deps.log - Logger instance
* @param deps.config$ - Config observable
* @param deps.latest$ - Latest observable
* @returns Observable of true (if already approved) or approval receipt
*/
function ensureApprovedBalance$(tokenContract, spender, amount, deps) {
const { config$ } = deps;
return config$.pipe((0, operators_1.first)(), (0, operators_1.mergeMap)(async ({ subkey }) => {
const { address: sender } = (0, helpers_1.chooseOnchainAccount)(deps, subkey);
return Promise.all([
tokenContract.callStatic.balanceOf(sender),
tokenContract.callStatic.allowance(sender, spender),
]);
}), (0, operators_1.withLatestFrom)(config$), (0, operators_1.mergeMap)(([[balance, allowance], { minimumAllowance }]) => {
(0, error_1.assert)(balance.gte(amount), [
error_1.ErrorCodes.RDN_INSUFFICIENT_BALANCE,
{ current: balance, required: amount },
]);
if (allowance.gte(amount))
return (0, rxjs_1.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$ = (0, rxjs_1.of)(true);
if (!allowance.isZero())
resetAllowance$ = transact(tokenContract, 'approve', [spender, 0], deps, {
error: error_1.ErrorCodes.RDN_APPROVE_TRANSACTION_FAILED,
}).pipe((0, operators_1.mapTo)(true));
// if needed, send approveTx and wait/assert it before proceeding; 'amount' could be enough,
// but we send 'prevAllowance + amount' in case there's a pending amount
// default minimumAllowance=MaxUint256 allows to approve once and for all
return resetAllowance$.pipe((0, operators_1.mergeMapTo)(transact(tokenContract, 'approve', [spender, (0, types_2.bnMax)(minimumAllowance, amount)], deps, {
error: error_1.ErrorCodes.RDN_APPROVE_TRANSACTION_FAILED,
})), (0, operators_1.pluck)(1));
}));
}
exports.ensureApprovedBalance$ = ensureApprovedBalance$;
//# sourceMappingURL=utils.js.map