From d8da02735f2ec1be48fb897749f050d4660a5bd2 Mon Sep 17 00:00:00 2001 From: Doron Zavelevsky Date: Wed, 10 Dec 2025 22:33:03 +0000 Subject: [PATCH 1/3] changed alchemy endpoint --- src/services/web3/liquidity/liquidity.ts | 13 ++++++++++++- src/services/web3/wallet/connectors.ts | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/services/web3/liquidity/liquidity.ts b/src/services/web3/liquidity/liquidity.ts index 09bbe3f5..14fe858b 100644 --- a/src/services/web3/liquidity/liquidity.ts +++ b/src/services/web3/liquidity/liquidity.ts @@ -164,10 +164,21 @@ export const removeLiquidity = async ( const liquidateFn = async () => { if (poolToken.version < 28) { + console.log('liquidate', { + poolToken, + amount: expandToken(poolToken.amount, poolToken.poolDecimals), + }); return await contract.liquidate( - expandToken(poolToken.amount, poolToken.poolDecimals) + expandToken(poolToken.amount, poolToken.poolDecimals), + { + gasLimit: 1000000, + } ); } else { + console.log('removeLiquidity', { + poolToken, + amount: expandToken(poolToken.amount, poolToken.poolDecimals), + }); return await contract.removeLiquidity( expandToken(poolToken.amount, poolToken.poolDecimals), [poolToken.bnt.token.address, poolToken.tkn.token.address], diff --git a/src/services/web3/wallet/connectors.ts b/src/services/web3/wallet/connectors.ts index 827ca73a..837f475e 100644 --- a/src/services/web3/wallet/connectors.ts +++ b/src/services/web3/wallet/connectors.ts @@ -7,7 +7,7 @@ import { WalletConnectConnector } from '@web3-react/walletconnect-connector'; import { TorusConnector } from '@web3-react/torus-connector'; import { SafeAppConnector } from '@gnosis.pm/safe-apps-web3-react'; -export const ALCHEMY_URL = `https://eth-mainnet.alchemyapi.io/v2/${ +export const ALCHEMY_URL = `https://eth-mainnet.g.alchemy.com/v2/${ process.env.REACT_APP_ALCHEMY_MAINNET as string }`; From 1b942fb66a56af9b0d4a13f14f3ab4e24c894d18 Mon Sep 17 00:00:00 2001 From: Doron Zavelevsky Date: Wed, 10 Dec 2025 22:33:57 +0000 Subject: [PATCH 2/3] undo changes --- src/store/liquidity/liquidity.ts | 692 +++++++++++++++++++------------ 1 file changed, 420 insertions(+), 272 deletions(-) diff --git a/src/store/liquidity/liquidity.ts b/src/store/liquidity/liquidity.ts index 15274098..09bbe3f5 100644 --- a/src/store/liquidity/liquidity.ts +++ b/src/store/liquidity/liquidity.ts @@ -1,295 +1,443 @@ -import { createSelector, createSlice } from '@reduxjs/toolkit'; -import { BigNumber } from 'bignumber.js'; -import { get } from 'lodash'; -import { Rewards, SnapshotRewards } from 'services/observables/liquidity'; -import { LockedAvailableBnt } from 'services/web3/lockedbnt/lockedbnt'; +import BigNumber from 'bignumber.js'; +import { sortBy } from 'lodash'; +import { first, take } from 'rxjs/operators'; import { - ProtectedPosition, - ProtectedPositionGrouped, -} from 'services/web3/protection/positions'; -import { PoolToken } from 'services/observables/pools'; -import { RootState } from 'store'; -import { bntToken } from 'services/web3/config'; -import { Dictionary } from 'services/web3/types'; -import MerkleTree from 'merkletreejs'; -import { getAddress, keccak256 } from 'ethers/lib/utils'; -import { generateLeaf } from 'services/web3/protection/rewards'; -import { calculatePercentageChange } from 'utils/formulas'; - -interface LiquidityState { - poolTokens: PoolToken[]; - lockedAvailableBNT: LockedAvailableBnt; - protectedPositions: ProtectedPosition[]; - rewards?: Rewards; - protocolBnBNTAmount: number; - loadingPositions: boolean; - loadingRewards: boolean; - loadingLockedBnt: boolean; - snapshots?: Dictionary; -} - -const initialState: LiquidityState = { - poolTokens: [], - lockedAvailableBNT: { - locked: [], - available: 0, - }, - protocolBnBNTAmount: 0, - protectedPositions: [], - rewards: undefined, - loadingPositions: false, - loadingRewards: false, - loadingLockedBnt: false, - snapshots: undefined, -}; + bancorConverterRegistry$, + liquidityProtection$, + settingsContractAddress$, +} from 'services/observables/contracts'; +import { Token } from 'services/observables/tokens'; +import { expandToken, shrinkToken } from 'utils/formulas'; +import { + calculateBntNeededToOpenSpace, + calculatePriceDeviationTooHigh, + decToPpm, +} from 'utils/helperFunctions'; +import { web3, writeWeb3 } from '..'; +import { + ConverterRegistry__factory, + Converter__factory, + LiquidityProtection, + LiquidityProtectionSettings, + LiquidityProtectionSettings__factory, + LiquidityProtectionSystemStore__factory, + LiquidityProtection__factory, +} from '../abis/types'; +import { MultiCall } from 'services/web3/multicall/multicall'; +import { + bntToken, + changeGas, + ethToken, + systemStore, + zeroAddress, +} from '../config'; +import { ErrorCode, EthNetworks, PoolType } from '../types'; +import { sendLiquidityEvent } from 'services/api/googleTagManager/liquidity'; +import { Pool, PoolToken } from 'services/observables/pools'; +import { Events } from 'services/api/googleTagManager'; -const liquiditySlice = createSlice({ - name: 'liquidity', - initialState, - reducers: { - setPoolTokens: (state, action) => { - state.poolTokens = action.payload; - }, - setLockedAvailableBNT: (state, action) => { - state.lockedAvailableBNT = action.payload; - }, - setProtectedPositions: (state, action) => { - state.protectedPositions = action.payload; - }, - setRewards: (state, action) => { - state.rewards = action.payload; - }, - setLoadingPositions: (state, action) => { - state.loadingPositions = action.payload; - }, - setLoadingRewards: (state, action) => { - state.loadingRewards = action.payload; - }, - setLoadingLockedBnt: (state, action) => { - state.loadingLockedBnt = action.payload; - }, - setProtocolBnBNTAmount: (state, action) => { - state.protocolBnBNTAmount = action.payload; - }, - setSnapshots: (state, action) => { - state.snapshots = action.payload; - }, - }, -}); +export const createPool = async ( + token: Token, + fee: string, + network: EthNetworks, + noPool: Function, + onHash: (txHash: string) => void, + onAccept: (txHash: string) => void, + onFee: (txHash: string) => void, + rejected: Function, + failed: Function +) => { + try { + const converterRegistryAddress = await bancorConverterRegistry$ + .pipe(take(1)) + .toPromise(); -export const getGroupedPositions = createSelector( - (state: RootState) => state.liquidity.protectedPositions, - (protectedPositions: ProtectedPosition[]) => { - return protectedPositions.reduce( - ((obj) => (acc: ProtectedPositionGrouped[], val: ProtectedPosition) => { - const symbol = val.reserveToken.symbol; - - const bnt = val.pool.reserves[1]; - const bntUSDPrice = bnt.usdPrice - ? new BigNumber(bnt.usdPrice) - : new BigNumber(0); - const poolId = val.pool.pool_dlt_id; - const groupId = `${poolId}-${symbol}`; - const filtered = protectedPositions.filter( - (pos) => - pos.pool.pool_dlt_id === poolId && - pos.reserveToken.symbol === symbol - ); + const regContract = ConverterRegistry__factory.connect( + converterRegistryAddress, + writeWeb3.signer + ); + + const reserves = [bntToken, token.address]; + const weights = ['500000', '500000']; - let item: ProtectedPositionGrouped = obj.get(groupId); - - if (!item) { - const calcSum = (key: string): string => { - return filtered - .map((pos) => new BigNumber(get(pos, key))) - .reduce((sum, current) => sum.plus(current), new BigNumber(0)) - .toString(); - }; - - const sumFees = calcSum('fees'); - const sumInitalStakeTkn = calcSum('initialStake.tknAmount'); - const sumInitalStakeUSD = calcSum('initialStake.usdAmount'); - - const claimableAmountTKN = calcSum('claimableAmount.tknAmount'); - - const sumRoi = new BigNumber(sumFees) - .div(sumInitalStakeTkn) - .toString(); - - const change = calculatePercentageChange( - Number(claimableAmountTKN), - Number(sumInitalStakeTkn) - ); - - item = { - groupId: groupId, - positionId: val.positionId, - pool: val.pool, - fees: sumFees, - initialStake: { - usdAmount: sumInitalStakeUSD, - tknAmount: sumInitalStakeTkn, - }, - protectedAmount: { - usdAmount: calcSum('protectedAmount.usdAmount'), - tknAmount: calcSum('protectedAmount.tknAmount'), - }, - claimableAmount: { - usdAmount: calcSum('claimableAmount.usdAmount'), - tknAmount: claimableAmountTKN, - }, - reserveToken: val.reserveToken, - roi: { - fees: sumRoi, - reserveRewards: new BigNumber(val.rewardsAmount) - .times(bntUSDPrice) - .div(sumInitalStakeUSD) - .toString(), - }, - aprs: val.aprs, - timestamps: val.timestamps, - currentCoveragePercent: val.currentCoveragePercent, - rewardsMultiplier: val.rewardsMultiplier, - rewardsAmount: val.rewardsAmount, - change, - subRows: [], - }; - - obj.set(groupId, item); - acc.push(item); - } - - if (filtered.length > 1) { - item.subRows.push(val); - } - return acc; - })(new Map()), - [] + const poolAddress = await regContract.getLiquidityPoolByConfig( + PoolType.Traditional, + reserves, + weights ); - } -); -export const getAllBntPositionsAndAmount = createSelector( - (state: RootState) => state.liquidity.protectedPositions, - (protectedPositions: ProtectedPosition[]) => { - const bntPositions = protectedPositions.filter( - (pos) => pos.reserveToken.address === bntToken + if (poolAddress !== zeroAddress) noPool(); + + const tx = await regContract.newConverter( + PoolType.Traditional, + token.name, + token.symbol, + token.decimals, + 50000, + reserves, + weights ); - const tknAmount = bntPositions - .map((x) => Number(x.protectedAmount.tknAmount)) - .reduce((sum, current) => sum + current, 0); - const usdAmount = bntPositions - .map((x) => Number(x.protectedAmount.usdAmount)) - .reduce((sum, current) => sum + current, 0); + onHash(tx.hash); + await tx.wait(); + + const converterAddress = await web3.provider.getTransactionReceipt(tx.hash); + const converter = Converter__factory.connect( + converterAddress.logs[0].address, + writeWeb3.signer + ); + const ownerShip = await converter.acceptOwnership(); + onAccept(ownerShip.hash); + await ownerShip.wait(); - return { tknAmount, usdAmount, bntPositions }; + const conversionFee = await converter.setConversionFee(decToPpm(fee)); + onFee(conversionFee.hash); + } catch (e: any) { + if (e.code === ErrorCode.DeniedTx) rejected(); + else failed(); } -); - -export const getPositionById = (id: string): any => - createSelector( - getGroupedPositions, - (positions: ProtectedPositionGrouped[]) => { - return positions.find((pos) => pos.groupId === id); - } - ); +}; -export interface MyStakeSummary { - protectedValue: number; - claimableValue: number; - fees: number; -} +export const addLiquidity = async ( + bntAmount: string, + bnt: Token, + tknAmount: string, + tkn: Token, + converterAddress: string, + onHash: (txHash: string) => void, + onCompleted: Function, + rejected: Function, + failed: (error: string) => void +) => { + try { + const contract = Converter__factory.connect( + converterAddress, + writeWeb3.signer + ); + const tknWei = expandToken(tknAmount, tkn.decimals); + const bntWei = expandToken(bntAmount, bnt.decimals); -export const getStakeSummary = createSelector( - (state: RootState) => state.liquidity.protectedPositions, - (protectedPositions: ProtectedPosition[]) => { - if (protectedPositions.length === 0) return; + const value = tkn.address === ethToken ? tknWei : undefined; - const initialStake = protectedPositions - .map((x) => Number(x.initialStake.usdAmount)) - .reduce((sum, current) => sum + current, 0); + // sendLiquidityEvent(Events.wallet_req); - const protectedValue = protectedPositions - .map((x) => Number(x.protectedAmount.usdAmount)) - .reduce((sum, current) => sum + current, 0); + const estimate = await contract.estimateGas.addLiquidity( + [bnt.address, tkn.address], + [bntWei, tknWei], + '1', + { value } + ); + const gasLimit = changeGas(estimate.toString()); - const claimableValue = protectedPositions - .map((x) => Number(x.claimableAmount.usdAmount)) - .reduce((sum, current) => sum + current, 0); + const tx = await contract.addLiquidity( + [bnt.address, tkn.address], + [bntWei, tknWei], + '1', + { value, gasLimit } + ); - const fees = protectedValue - initialStake; + sendLiquidityEvent(Events.wallet_confirm, tx.hash); - return { - protectedValue, - claimableValue, - fees, - }; + onHash(tx.hash); + + await tx.wait(); + onCompleted(); + } catch (e: any) { + console.error(e); + if (e.code === ErrorCode.DeniedTx) rejected(); + else failed(e.message); } -); - -export const getUserRewardsFromSnapshot = createSelector( - (state: RootState) => state.user.account, - (state: RootState) => state.liquidity.snapshots, - ( - account: string | null | undefined, - snapshots?: Dictionary - ) => { - const empty = { claimable: '0', totalClaimed: '0' }; - if (account && snapshots) { - if (snapshots[account]) { - return snapshots[account]; +}; + +export const removeLiquidity = async ( + poolToken: PoolToken, + onHash: (txHash: string) => void, + onCompleted: Function, + rejected: Function, + failed: (error: string) => void +) => { + try { + const contract = Converter__factory.connect( + poolToken.converter, + writeWeb3.signer + ); + + const liquidateFn = async () => { + if (poolToken.version < 28) { + return await contract.liquidate( + expandToken(poolToken.amount, poolToken.poolDecimals) + ); + } else { + return await contract.removeLiquidity( + expandToken(poolToken.amount, poolToken.poolDecimals), + [poolToken.bnt.token.address, poolToken.tkn.token.address], + ['1', '1'] + ); } - // fallback to key not found due to casing - const accAddress = getAddress(account); - const entry = Object.entries(snapshots).find( - ([address]) => getAddress(address) === accAddress - ); - return entry ? entry[1] : empty; - } - - return empty; + }; + sendLiquidityEvent(Events.wallet_req); + + const tx = await liquidateFn(); + sendLiquidityEvent(Events.wallet_confirm); + + onHash(tx.hash); + await tx.wait(); + onCompleted(); + } catch (e: any) { + console.error(e); + if (e.code === ErrorCode.DeniedTx) rejected(); + else failed(e.message); } -); - -export const getMerkleTree = createSelector( - (state: RootState) => state.liquidity.snapshots, - (snapshots?: Dictionary) => { - if (!snapshots) return null; - return new MerkleTree( - // Generate leafs - Object.entries(snapshots).map(([address, { claimable }]) => - generateLeaf(address, claimable) - ), - keccak256, - { sortPairs: true } +}; + +export const addLiquidityV2Single = async ( + pool: Pool, + token: Token, + amount: string, + onHash: (txHash: string) => void, + onCompleted: Function, + rejected: Function, + failed: (error: string) => void +) => { + try { + const liquidityProtectionContract = await liquidityProtection$ + .pipe(first()) + .toPromise(); + + const contract = LiquidityProtection__factory.connect( + liquidityProtectionContract, + writeWeb3.signer + ); + const fromIsEth = ethToken === token.address; + + sendLiquidityEvent(Events.wallet_req); + + const estimate = await contract.estimateGas.addLiquidity( + pool.pool_dlt_id, + token.address, + expandToken(amount, token.decimals), + { value: fromIsEth ? expandToken(amount, 18) : undefined } ); + const gasLimit = changeGas(estimate.toString()); + + const tx = await contract.addLiquidity( + pool.pool_dlt_id, + token.address, + expandToken(amount, token.decimals), + { value: fromIsEth ? expandToken(amount, 18) : undefined, gasLimit } + ); + onHash(tx.hash); + sendLiquidityEvent(Events.wallet_confirm, tx.hash); + + await tx.wait(); + + onCompleted(); + } catch (e: any) { + console.error(e); + if (e.code === ErrorCode.DeniedTx) rejected(); + else failed(e.message); } -); - -export const getUserRewardsProof = createSelector( - (state: RootState) => state.user.account, - getUserRewardsFromSnapshot, - getMerkleTree, - (account: string | null | undefined, userRewards, tree) => { - if (!account || !tree || userRewards.claimable === '0') return null; - const { claimable } = userRewards; - const leaf: Buffer = generateLeaf(account, claimable); - const proof: string[] = tree.getHexProof(leaf); - return proof; +}; + +export const addLiquidityV3Single = async () => {}; + +export const checkPriceDeviationTooHigh = async ( + pool: Pool, + selectedTkn: Token +): Promise => { + const converterContract = Converter__factory.connect( + pool.converter_dlt_id, + web3.provider + ); + + const settingsAddress = await settingsContractAddress$ + .pipe(take(1)) + .toPromise(); + + const settingsContract = LiquidityProtectionSettings__factory.connect( + settingsAddress, + web3.provider + ); + + const [primaryReserveAddress, secondaryReserveAddress] = sortBy( + pool.reserves, + [(o) => o.address !== selectedTkn.address] + ).map((x) => x.address); + + const [ + recentAverageRate, + averageRateMaxDeviation, + primaryReserveBalance, + secondaryReserveBalance, + ] = await Promise.all([ + converterContract.recentAverageRate(selectedTkn.address), + settingsContract.averageRateMaxDeviation(), + converterContract.reserveBalance(primaryReserveAddress), + converterContract.reserveBalance(secondaryReserveAddress), + ]); + + const averageRate = new BigNumber( + recentAverageRate['1'].toString() + ).dividedBy(new BigNumber(recentAverageRate['0'].toString())); + + if (averageRate.isNaN()) { + throw new Error( + 'Price deviation calculation failed. Please contact support.' + ); } -); - -export const { - setPoolTokens, - setLockedAvailableBNT, - setProtectedPositions, - setRewards, - setLoadingPositions, - setLoadingRewards, - setLoadingLockedBnt, - setProtocolBnBNTAmount, - setSnapshots, -} = liquiditySlice.actions; - -export const liquidity = liquiditySlice.reducer; + + return calculatePriceDeviationTooHigh( + averageRate, + new BigNumber(primaryReserveBalance.toString()), + new BigNumber(secondaryReserveBalance.toString()), + new BigNumber(averageRateMaxDeviation) + ); +}; + +export const getSpaceAvailable = async (id: string, tknDecimals: number) => { + const liquidityProtectionContract = await liquidityProtection$ + .pipe(first()) + .toPromise(); + const contract = LiquidityProtection__factory.connect( + liquidityProtectionContract, + web3.provider + ); + + const result = await contract.poolAvailableSpace(id); + + return { + bnt: shrinkToken(result['1'].toString(), 18), + tkn: shrinkToken(result['0'].toString(), tknDecimals), + }; +}; + +export const fetchBntNeededToOpenSpace = async ( + pool: Pool +): Promise => { + const settingsAddress = await settingsContractAddress$ + .pipe(take(1)) + .toPromise(); + const settingsContract = LiquidityProtectionSettings__factory.connect( + settingsAddress, + web3.provider + ); + + const systemStoreContract = LiquidityProtectionSystemStore__factory.connect( + systemStore, + web3.provider + ); + + const networkTokenMintingLimits = + await settingsContract.networkTokenMintingLimits(pool.pool_dlt_id); + + const networkTokensMinted = await systemStoreContract.networkTokensMinted( + pool.pool_dlt_id + ); + + const { tknBalance, bntBalance } = await fetchReserveBalances(pool); + + const bntNeeded = calculateBntNeededToOpenSpace( + bntBalance, + tknBalance, + networkTokensMinted.toString(), + networkTokenMintingLimits.toString() + ); + + return shrinkToken(bntNeeded, 18); +}; + +export const fetchReserveBalances = async ( + pool: Pool, + blockHeight?: number +) => { + const converterContract = Converter__factory.connect( + pool.converter_dlt_id, + web3.provider + ); + const tknBalance = ( + await converterContract.getConnectorBalance(pool.reserves[0].address, { + blockTag: blockHeight, + }) + ).toString(); + + const bntBalance = ( + await converterContract.getConnectorBalance(pool.reserves[1].address, { + blockTag: blockHeight, + }) + ).toString(); + + return { tknBalance, bntBalance }; +}; + +export const buildReserveBalancesCall = (pool: Pool): MultiCall[] => { + const contract = Converter__factory.connect( + pool.converter_dlt_id, + web3.provider + ); + const buildCall = (address: string): MultiCall => { + return { + contractAddress: contract.address, + interface: contract.interface, + methodName: 'getConnectorBalance', + methodParameters: [address], + }; + }; + + return [ + buildCall(pool.reserves[0].address), + buildCall(pool.reserves[1].address), + ]; +}; + +export const buildPoolROICall = ( + contract: LiquidityProtection, + poolToken: string, + reserveToken: string, + reserveAmount: string, + poolRateN: string, + poolRateD: string, + reserveRateN: string, + reserveRateD: string +): MultiCall => ({ + contractAddress: contract.address, + interface: contract.interface, + methodName: 'poolROI', + methodParameters: [ + poolToken, + reserveToken, + reserveAmount, + poolRateN, + poolRateD, + reserveRateN, + reserveRateD, + ], +}); + +export const buildRemoveLiquidityReturnCall = ( + contract: LiquidityProtection, + id: string, + portion: string, + removeTimestamp: number +): MultiCall => { + return { + contractAddress: contract.address, + interface: contract.interface, + methodName: 'removeLiquidityReturn', + methodParameters: [id, portion, String(removeTimestamp)], + }; +}; + +export const buildProtectionDelayCall = ( + contract: LiquidityProtectionSettings +): MultiCall[] => { + const buildCall = (methodName: string): MultiCall => { + return { + contractAddress: contract.address, + interface: contract.interface, + methodName: methodName, + methodParameters: [], + }; + }; + + return [buildCall('minProtectionDelay'), buildCall('maxProtectionDelay')]; +}; From e1c37c558070e23e1d77531a1c4a0fd949794913 Mon Sep 17 00:00:00 2001 From: Doron Zavelevsky Date: Wed, 10 Dec 2025 22:35:29 +0000 Subject: [PATCH 3/3] undo changes --- src/services/web3/liquidity/liquidity.ts | 13 +- src/store/liquidity/liquidity.ts | 692 +++++++++-------------- 2 files changed, 273 insertions(+), 432 deletions(-) diff --git a/src/services/web3/liquidity/liquidity.ts b/src/services/web3/liquidity/liquidity.ts index 14fe858b..09bbe3f5 100644 --- a/src/services/web3/liquidity/liquidity.ts +++ b/src/services/web3/liquidity/liquidity.ts @@ -164,21 +164,10 @@ export const removeLiquidity = async ( const liquidateFn = async () => { if (poolToken.version < 28) { - console.log('liquidate', { - poolToken, - amount: expandToken(poolToken.amount, poolToken.poolDecimals), - }); return await contract.liquidate( - expandToken(poolToken.amount, poolToken.poolDecimals), - { - gasLimit: 1000000, - } + expandToken(poolToken.amount, poolToken.poolDecimals) ); } else { - console.log('removeLiquidity', { - poolToken, - amount: expandToken(poolToken.amount, poolToken.poolDecimals), - }); return await contract.removeLiquidity( expandToken(poolToken.amount, poolToken.poolDecimals), [poolToken.bnt.token.address, poolToken.tkn.token.address], diff --git a/src/store/liquidity/liquidity.ts b/src/store/liquidity/liquidity.ts index 09bbe3f5..15274098 100644 --- a/src/store/liquidity/liquidity.ts +++ b/src/store/liquidity/liquidity.ts @@ -1,443 +1,295 @@ -import BigNumber from 'bignumber.js'; -import { sortBy } from 'lodash'; -import { first, take } from 'rxjs/operators'; +import { createSelector, createSlice } from '@reduxjs/toolkit'; +import { BigNumber } from 'bignumber.js'; +import { get } from 'lodash'; +import { Rewards, SnapshotRewards } from 'services/observables/liquidity'; +import { LockedAvailableBnt } from 'services/web3/lockedbnt/lockedbnt'; import { - bancorConverterRegistry$, - liquidityProtection$, - settingsContractAddress$, -} from 'services/observables/contracts'; -import { Token } from 'services/observables/tokens'; -import { expandToken, shrinkToken } from 'utils/formulas'; -import { - calculateBntNeededToOpenSpace, - calculatePriceDeviationTooHigh, - decToPpm, -} from 'utils/helperFunctions'; -import { web3, writeWeb3 } from '..'; -import { - ConverterRegistry__factory, - Converter__factory, - LiquidityProtection, - LiquidityProtectionSettings, - LiquidityProtectionSettings__factory, - LiquidityProtectionSystemStore__factory, - LiquidityProtection__factory, -} from '../abis/types'; -import { MultiCall } from 'services/web3/multicall/multicall'; -import { - bntToken, - changeGas, - ethToken, - systemStore, - zeroAddress, -} from '../config'; -import { ErrorCode, EthNetworks, PoolType } from '../types'; -import { sendLiquidityEvent } from 'services/api/googleTagManager/liquidity'; -import { Pool, PoolToken } from 'services/observables/pools'; -import { Events } from 'services/api/googleTagManager'; - -export const createPool = async ( - token: Token, - fee: string, - network: EthNetworks, - noPool: Function, - onHash: (txHash: string) => void, - onAccept: (txHash: string) => void, - onFee: (txHash: string) => void, - rejected: Function, - failed: Function -) => { - try { - const converterRegistryAddress = await bancorConverterRegistry$ - .pipe(take(1)) - .toPromise(); + ProtectedPosition, + ProtectedPositionGrouped, +} from 'services/web3/protection/positions'; +import { PoolToken } from 'services/observables/pools'; +import { RootState } from 'store'; +import { bntToken } from 'services/web3/config'; +import { Dictionary } from 'services/web3/types'; +import MerkleTree from 'merkletreejs'; +import { getAddress, keccak256 } from 'ethers/lib/utils'; +import { generateLeaf } from 'services/web3/protection/rewards'; +import { calculatePercentageChange } from 'utils/formulas'; + +interface LiquidityState { + poolTokens: PoolToken[]; + lockedAvailableBNT: LockedAvailableBnt; + protectedPositions: ProtectedPosition[]; + rewards?: Rewards; + protocolBnBNTAmount: number; + loadingPositions: boolean; + loadingRewards: boolean; + loadingLockedBnt: boolean; + snapshots?: Dictionary; +} + +const initialState: LiquidityState = { + poolTokens: [], + lockedAvailableBNT: { + locked: [], + available: 0, + }, + protocolBnBNTAmount: 0, + protectedPositions: [], + rewards: undefined, + loadingPositions: false, + loadingRewards: false, + loadingLockedBnt: false, + snapshots: undefined, +}; - const regContract = ConverterRegistry__factory.connect( - converterRegistryAddress, - writeWeb3.signer - ); +const liquiditySlice = createSlice({ + name: 'liquidity', + initialState, + reducers: { + setPoolTokens: (state, action) => { + state.poolTokens = action.payload; + }, + setLockedAvailableBNT: (state, action) => { + state.lockedAvailableBNT = action.payload; + }, + setProtectedPositions: (state, action) => { + state.protectedPositions = action.payload; + }, + setRewards: (state, action) => { + state.rewards = action.payload; + }, + setLoadingPositions: (state, action) => { + state.loadingPositions = action.payload; + }, + setLoadingRewards: (state, action) => { + state.loadingRewards = action.payload; + }, + setLoadingLockedBnt: (state, action) => { + state.loadingLockedBnt = action.payload; + }, + setProtocolBnBNTAmount: (state, action) => { + state.protocolBnBNTAmount = action.payload; + }, + setSnapshots: (state, action) => { + state.snapshots = action.payload; + }, + }, +}); - const reserves = [bntToken, token.address]; - const weights = ['500000', '500000']; +export const getGroupedPositions = createSelector( + (state: RootState) => state.liquidity.protectedPositions, + (protectedPositions: ProtectedPosition[]) => { + return protectedPositions.reduce( + ((obj) => (acc: ProtectedPositionGrouped[], val: ProtectedPosition) => { + const symbol = val.reserveToken.symbol; + + const bnt = val.pool.reserves[1]; + const bntUSDPrice = bnt.usdPrice + ? new BigNumber(bnt.usdPrice) + : new BigNumber(0); + const poolId = val.pool.pool_dlt_id; + const groupId = `${poolId}-${symbol}`; + const filtered = protectedPositions.filter( + (pos) => + pos.pool.pool_dlt_id === poolId && + pos.reserveToken.symbol === symbol + ); - const poolAddress = await regContract.getLiquidityPoolByConfig( - PoolType.Traditional, - reserves, - weights + let item: ProtectedPositionGrouped = obj.get(groupId); + + if (!item) { + const calcSum = (key: string): string => { + return filtered + .map((pos) => new BigNumber(get(pos, key))) + .reduce((sum, current) => sum.plus(current), new BigNumber(0)) + .toString(); + }; + + const sumFees = calcSum('fees'); + const sumInitalStakeTkn = calcSum('initialStake.tknAmount'); + const sumInitalStakeUSD = calcSum('initialStake.usdAmount'); + + const claimableAmountTKN = calcSum('claimableAmount.tknAmount'); + + const sumRoi = new BigNumber(sumFees) + .div(sumInitalStakeTkn) + .toString(); + + const change = calculatePercentageChange( + Number(claimableAmountTKN), + Number(sumInitalStakeTkn) + ); + + item = { + groupId: groupId, + positionId: val.positionId, + pool: val.pool, + fees: sumFees, + initialStake: { + usdAmount: sumInitalStakeUSD, + tknAmount: sumInitalStakeTkn, + }, + protectedAmount: { + usdAmount: calcSum('protectedAmount.usdAmount'), + tknAmount: calcSum('protectedAmount.tknAmount'), + }, + claimableAmount: { + usdAmount: calcSum('claimableAmount.usdAmount'), + tknAmount: claimableAmountTKN, + }, + reserveToken: val.reserveToken, + roi: { + fees: sumRoi, + reserveRewards: new BigNumber(val.rewardsAmount) + .times(bntUSDPrice) + .div(sumInitalStakeUSD) + .toString(), + }, + aprs: val.aprs, + timestamps: val.timestamps, + currentCoveragePercent: val.currentCoveragePercent, + rewardsMultiplier: val.rewardsMultiplier, + rewardsAmount: val.rewardsAmount, + change, + subRows: [], + }; + + obj.set(groupId, item); + acc.push(item); + } + + if (filtered.length > 1) { + item.subRows.push(val); + } + return acc; + })(new Map()), + [] ); + } +); - if (poolAddress !== zeroAddress) noPool(); - - const tx = await regContract.newConverter( - PoolType.Traditional, - token.name, - token.symbol, - token.decimals, - 50000, - reserves, - weights +export const getAllBntPositionsAndAmount = createSelector( + (state: RootState) => state.liquidity.protectedPositions, + (protectedPositions: ProtectedPosition[]) => { + const bntPositions = protectedPositions.filter( + (pos) => pos.reserveToken.address === bntToken ); - onHash(tx.hash); - await tx.wait(); - - const converterAddress = await web3.provider.getTransactionReceipt(tx.hash); - const converter = Converter__factory.connect( - converterAddress.logs[0].address, - writeWeb3.signer - ); - const ownerShip = await converter.acceptOwnership(); - onAccept(ownerShip.hash); - await ownerShip.wait(); + const tknAmount = bntPositions + .map((x) => Number(x.protectedAmount.tknAmount)) + .reduce((sum, current) => sum + current, 0); + const usdAmount = bntPositions + .map((x) => Number(x.protectedAmount.usdAmount)) + .reduce((sum, current) => sum + current, 0); - const conversionFee = await converter.setConversionFee(decToPpm(fee)); - onFee(conversionFee.hash); - } catch (e: any) { - if (e.code === ErrorCode.DeniedTx) rejected(); - else failed(); + return { tknAmount, usdAmount, bntPositions }; } -}; - -export const addLiquidity = async ( - bntAmount: string, - bnt: Token, - tknAmount: string, - tkn: Token, - converterAddress: string, - onHash: (txHash: string) => void, - onCompleted: Function, - rejected: Function, - failed: (error: string) => void -) => { - try { - const contract = Converter__factory.connect( - converterAddress, - writeWeb3.signer - ); - const tknWei = expandToken(tknAmount, tkn.decimals); - const bntWei = expandToken(bntAmount, bnt.decimals); +); + +export const getPositionById = (id: string): any => + createSelector( + getGroupedPositions, + (positions: ProtectedPositionGrouped[]) => { + return positions.find((pos) => pos.groupId === id); + } + ); - const value = tkn.address === ethToken ? tknWei : undefined; +export interface MyStakeSummary { + protectedValue: number; + claimableValue: number; + fees: number; +} - // sendLiquidityEvent(Events.wallet_req); +export const getStakeSummary = createSelector( + (state: RootState) => state.liquidity.protectedPositions, + (protectedPositions: ProtectedPosition[]) => { + if (protectedPositions.length === 0) return; - const estimate = await contract.estimateGas.addLiquidity( - [bnt.address, tkn.address], - [bntWei, tknWei], - '1', - { value } - ); - const gasLimit = changeGas(estimate.toString()); + const initialStake = protectedPositions + .map((x) => Number(x.initialStake.usdAmount)) + .reduce((sum, current) => sum + current, 0); - const tx = await contract.addLiquidity( - [bnt.address, tkn.address], - [bntWei, tknWei], - '1', - { value, gasLimit } - ); + const protectedValue = protectedPositions + .map((x) => Number(x.protectedAmount.usdAmount)) + .reduce((sum, current) => sum + current, 0); - sendLiquidityEvent(Events.wallet_confirm, tx.hash); + const claimableValue = protectedPositions + .map((x) => Number(x.claimableAmount.usdAmount)) + .reduce((sum, current) => sum + current, 0); - onHash(tx.hash); + const fees = protectedValue - initialStake; - await tx.wait(); - onCompleted(); - } catch (e: any) { - console.error(e); - if (e.code === ErrorCode.DeniedTx) rejected(); - else failed(e.message); + return { + protectedValue, + claimableValue, + fees, + }; } -}; - -export const removeLiquidity = async ( - poolToken: PoolToken, - onHash: (txHash: string) => void, - onCompleted: Function, - rejected: Function, - failed: (error: string) => void -) => { - try { - const contract = Converter__factory.connect( - poolToken.converter, - writeWeb3.signer - ); - - const liquidateFn = async () => { - if (poolToken.version < 28) { - return await contract.liquidate( - expandToken(poolToken.amount, poolToken.poolDecimals) - ); - } else { - return await contract.removeLiquidity( - expandToken(poolToken.amount, poolToken.poolDecimals), - [poolToken.bnt.token.address, poolToken.tkn.token.address], - ['1', '1'] - ); +); + +export const getUserRewardsFromSnapshot = createSelector( + (state: RootState) => state.user.account, + (state: RootState) => state.liquidity.snapshots, + ( + account: string | null | undefined, + snapshots?: Dictionary + ) => { + const empty = { claimable: '0', totalClaimed: '0' }; + if (account && snapshots) { + if (snapshots[account]) { + return snapshots[account]; } - }; - sendLiquidityEvent(Events.wallet_req); - - const tx = await liquidateFn(); - sendLiquidityEvent(Events.wallet_confirm); - - onHash(tx.hash); - await tx.wait(); - onCompleted(); - } catch (e: any) { - console.error(e); - if (e.code === ErrorCode.DeniedTx) rejected(); - else failed(e.message); + // fallback to key not found due to casing + const accAddress = getAddress(account); + const entry = Object.entries(snapshots).find( + ([address]) => getAddress(address) === accAddress + ); + return entry ? entry[1] : empty; + } + + return empty; } -}; - -export const addLiquidityV2Single = async ( - pool: Pool, - token: Token, - amount: string, - onHash: (txHash: string) => void, - onCompleted: Function, - rejected: Function, - failed: (error: string) => void -) => { - try { - const liquidityProtectionContract = await liquidityProtection$ - .pipe(first()) - .toPromise(); - - const contract = LiquidityProtection__factory.connect( - liquidityProtectionContract, - writeWeb3.signer - ); - const fromIsEth = ethToken === token.address; - - sendLiquidityEvent(Events.wallet_req); - - const estimate = await contract.estimateGas.addLiquidity( - pool.pool_dlt_id, - token.address, - expandToken(amount, token.decimals), - { value: fromIsEth ? expandToken(amount, 18) : undefined } +); + +export const getMerkleTree = createSelector( + (state: RootState) => state.liquidity.snapshots, + (snapshots?: Dictionary) => { + if (!snapshots) return null; + return new MerkleTree( + // Generate leafs + Object.entries(snapshots).map(([address, { claimable }]) => + generateLeaf(address, claimable) + ), + keccak256, + { sortPairs: true } ); - const gasLimit = changeGas(estimate.toString()); - - const tx = await contract.addLiquidity( - pool.pool_dlt_id, - token.address, - expandToken(amount, token.decimals), - { value: fromIsEth ? expandToken(amount, 18) : undefined, gasLimit } - ); - onHash(tx.hash); - sendLiquidityEvent(Events.wallet_confirm, tx.hash); - - await tx.wait(); - - onCompleted(); - } catch (e: any) { - console.error(e); - if (e.code === ErrorCode.DeniedTx) rejected(); - else failed(e.message); } -}; - -export const addLiquidityV3Single = async () => {}; - -export const checkPriceDeviationTooHigh = async ( - pool: Pool, - selectedTkn: Token -): Promise => { - const converterContract = Converter__factory.connect( - pool.converter_dlt_id, - web3.provider - ); - - const settingsAddress = await settingsContractAddress$ - .pipe(take(1)) - .toPromise(); - - const settingsContract = LiquidityProtectionSettings__factory.connect( - settingsAddress, - web3.provider - ); - - const [primaryReserveAddress, secondaryReserveAddress] = sortBy( - pool.reserves, - [(o) => o.address !== selectedTkn.address] - ).map((x) => x.address); - - const [ - recentAverageRate, - averageRateMaxDeviation, - primaryReserveBalance, - secondaryReserveBalance, - ] = await Promise.all([ - converterContract.recentAverageRate(selectedTkn.address), - settingsContract.averageRateMaxDeviation(), - converterContract.reserveBalance(primaryReserveAddress), - converterContract.reserveBalance(secondaryReserveAddress), - ]); - - const averageRate = new BigNumber( - recentAverageRate['1'].toString() - ).dividedBy(new BigNumber(recentAverageRate['0'].toString())); - - if (averageRate.isNaN()) { - throw new Error( - 'Price deviation calculation failed. Please contact support.' - ); +); + +export const getUserRewardsProof = createSelector( + (state: RootState) => state.user.account, + getUserRewardsFromSnapshot, + getMerkleTree, + (account: string | null | undefined, userRewards, tree) => { + if (!account || !tree || userRewards.claimable === '0') return null; + const { claimable } = userRewards; + const leaf: Buffer = generateLeaf(account, claimable); + const proof: string[] = tree.getHexProof(leaf); + return proof; } - - return calculatePriceDeviationTooHigh( - averageRate, - new BigNumber(primaryReserveBalance.toString()), - new BigNumber(secondaryReserveBalance.toString()), - new BigNumber(averageRateMaxDeviation) - ); -}; - -export const getSpaceAvailable = async (id: string, tknDecimals: number) => { - const liquidityProtectionContract = await liquidityProtection$ - .pipe(first()) - .toPromise(); - const contract = LiquidityProtection__factory.connect( - liquidityProtectionContract, - web3.provider - ); - - const result = await contract.poolAvailableSpace(id); - - return { - bnt: shrinkToken(result['1'].toString(), 18), - tkn: shrinkToken(result['0'].toString(), tknDecimals), - }; -}; - -export const fetchBntNeededToOpenSpace = async ( - pool: Pool -): Promise => { - const settingsAddress = await settingsContractAddress$ - .pipe(take(1)) - .toPromise(); - const settingsContract = LiquidityProtectionSettings__factory.connect( - settingsAddress, - web3.provider - ); - - const systemStoreContract = LiquidityProtectionSystemStore__factory.connect( - systemStore, - web3.provider - ); - - const networkTokenMintingLimits = - await settingsContract.networkTokenMintingLimits(pool.pool_dlt_id); - - const networkTokensMinted = await systemStoreContract.networkTokensMinted( - pool.pool_dlt_id - ); - - const { tknBalance, bntBalance } = await fetchReserveBalances(pool); - - const bntNeeded = calculateBntNeededToOpenSpace( - bntBalance, - tknBalance, - networkTokensMinted.toString(), - networkTokenMintingLimits.toString() - ); - - return shrinkToken(bntNeeded, 18); -}; - -export const fetchReserveBalances = async ( - pool: Pool, - blockHeight?: number -) => { - const converterContract = Converter__factory.connect( - pool.converter_dlt_id, - web3.provider - ); - const tknBalance = ( - await converterContract.getConnectorBalance(pool.reserves[0].address, { - blockTag: blockHeight, - }) - ).toString(); - - const bntBalance = ( - await converterContract.getConnectorBalance(pool.reserves[1].address, { - blockTag: blockHeight, - }) - ).toString(); - - return { tknBalance, bntBalance }; -}; - -export const buildReserveBalancesCall = (pool: Pool): MultiCall[] => { - const contract = Converter__factory.connect( - pool.converter_dlt_id, - web3.provider - ); - const buildCall = (address: string): MultiCall => { - return { - contractAddress: contract.address, - interface: contract.interface, - methodName: 'getConnectorBalance', - methodParameters: [address], - }; - }; - - return [ - buildCall(pool.reserves[0].address), - buildCall(pool.reserves[1].address), - ]; -}; - -export const buildPoolROICall = ( - contract: LiquidityProtection, - poolToken: string, - reserveToken: string, - reserveAmount: string, - poolRateN: string, - poolRateD: string, - reserveRateN: string, - reserveRateD: string -): MultiCall => ({ - contractAddress: contract.address, - interface: contract.interface, - methodName: 'poolROI', - methodParameters: [ - poolToken, - reserveToken, - reserveAmount, - poolRateN, - poolRateD, - reserveRateN, - reserveRateD, - ], -}); - -export const buildRemoveLiquidityReturnCall = ( - contract: LiquidityProtection, - id: string, - portion: string, - removeTimestamp: number -): MultiCall => { - return { - contractAddress: contract.address, - interface: contract.interface, - methodName: 'removeLiquidityReturn', - methodParameters: [id, portion, String(removeTimestamp)], - }; -}; - -export const buildProtectionDelayCall = ( - contract: LiquidityProtectionSettings -): MultiCall[] => { - const buildCall = (methodName: string): MultiCall => { - return { - contractAddress: contract.address, - interface: contract.interface, - methodName: methodName, - methodParameters: [], - }; - }; - - return [buildCall('minProtectionDelay'), buildCall('maxProtectionDelay')]; -}; +); + +export const { + setPoolTokens, + setLockedAvailableBNT, + setProtectedPositions, + setRewards, + setLoadingPositions, + setLoadingRewards, + setLoadingLockedBnt, + setProtocolBnBNTAmount, + setSnapshots, +} = liquiditySlice.actions; + +export const liquidity = liquiditySlice.reducer;