UNPKG

raiden-ts

Version:

Raiden Light Client Typescript/Javascript SDK

258 lines 13.5 kB
"use strict"; 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