/ src / store / v3MigrationSelectors.ts
v3MigrationSelectors.ts
  1  import {
  2    InterestRate,
  3    PoolBaseCurrencyHumanized,
  4    ReserveDataHumanized,
  5    ReservesIncentiveDataHumanized,
  6    UserReserveDataHumanized,
  7    valueToWei,
  8  } from '@aave/contract-helpers';
  9  import {
 10    MigrationRepayAsset,
 11    MigrationSupplyAsset,
 12    V3MigrationHelperSignedCreditDelegationPermit,
 13    V3MigrationHelperSignedPermit,
 14  } from '@aave/contract-helpers/dist/esm/v3-migration-contract/v3MigrationTypes';
 15  import {
 16    ComputedUserReserve,
 17    formatReservesAndIncentives,
 18    FormatReserveUSDResponse,
 19    formatUserSummary,
 20    FormatUserSummaryResponse,
 21    valueToBigNumber,
 22  } from '@aave/math-utils';
 23  import {
 24    CalculateReserveIncentivesResponse,
 25    ReserveIncentiveResponse,
 26  } from '@aave/math-utils/dist/esm/formatters/incentive/calculate-reserve-incentives';
 27  import { SignatureLike } from '@ethersproject/bytes';
 28  import { BigNumber } from 'bignumber.js';
 29  import { BigNumberish } from 'ethers';
 30  import { Approval } from 'src/helpers/useTransactionHandler';
 31  import {
 32    BorrowMigrationReserve,
 33    SupplyMigrationReserve,
 34    UserMigrationReserves,
 35  } from 'src/hooks/migration/useUserMigrationReserves';
 36  import { UserSummaryForMigration } from 'src/hooks/migration/useUserSummaryForMigration';
 37  import { FormattedUserReserves } from 'src/hooks/pool/useUserSummaryAndIncentives';
 38  
 39  import { RootStore } from './root';
 40  import {
 41    MigrationException,
 42    MigrationSelectedAsset,
 43    MigrationSelectedBorrowAsset,
 44  } from './v3MigrationSlice';
 45  
 46  export const selectIsolationModeForMigration = (
 47    poolReserveV3Summary: Pick<
 48      FormatUserSummaryResponse<ReserveDataHumanized & FormatReserveUSDResponse>,
 49      'totalCollateralMarketReferenceCurrency' | 'isolatedReserve'
 50    >
 51  ) => {
 52    if (poolReserveV3Summary.totalCollateralMarketReferenceCurrency !== '0') {
 53      return poolReserveV3Summary.isolatedReserve;
 54    }
 55    return undefined;
 56  };
 57  
 58  export const selectMigrationSelectedSupplyIndex = (
 59    selectedMigrationSupplyAssets: MigrationSelectedAsset[],
 60    underlyingAsset: string
 61  ) => {
 62    return selectedMigrationSupplyAssets.findIndex(
 63      (supplyAsset) => supplyAsset.underlyingAsset == underlyingAsset
 64    );
 65  };
 66  
 67  export const selectMigrationSelectedBorrowIndex = (
 68    selectedBorrowAssets: MigrationSelectedBorrowAsset[],
 69    borrowAsset: MigrationSelectedBorrowAsset
 70  ) => {
 71    return selectedBorrowAssets.findIndex((asset) => asset.debtKey == borrowAsset.debtKey);
 72  };
 73  
 74  export type MigrationUserReserve = FormattedUserReserves & {
 75    increasedVariableBorrows: string;
 76    interestRate: InterestRate;
 77    debtKey: string;
 78    usageAsCollateralEnabledOnUserV3?: boolean;
 79    isolatedOnV3?: boolean;
 80    canBeEnforced?: boolean;
 81    migrationDisabled?: MigrationDisabled;
 82    V3Rates?: V3Rates;
 83  };
 84  
 85  export type V3Rates = {
 86    variableBorrowAPY: string;
 87    supplyAPY: string;
 88    aIncentivesData?: ReserveIncentiveResponse[];
 89    vIncentivesData?: ReserveIncentiveResponse[];
 90    priceInUSD: string;
 91    ltv?: string;
 92    liquidationThreshold?: string;
 93  };
 94  
 95  type ReserveDebtApprovalPayload = {
 96    [underlyingAsset: string]: {
 97      variableDebtTokenAddress: string;
 98      decimals: number;
 99      variableDebtAmount: string;
100    };
101  };
102  
103  export const selectSplittedBorrowsForMigration = (userReserves: FormattedUserReserves[]) => {
104    const splittedUserReserves: MigrationUserReserve[] = [];
105    userReserves.forEach((userReserve) => {
106      if (userReserve.variableBorrows !== '0') {
107        splittedUserReserves.push({
108          ...userReserve,
109          interestRate: InterestRate.Variable,
110          increasedVariableBorrows: userReserve.variableBorrows,
111          debtKey: userReserve.reserve.variableDebtTokenAddress,
112        });
113      }
114    });
115    return splittedUserReserves;
116  };
117  
118  export const selectDefinitiveSupplyAssetForMigration = (
119    selectedMigrationSupplyAssets: MigrationSelectedAsset[],
120    migrationExceptions: Record<string, MigrationException>,
121    userReservesV3Map: Record<
122      string,
123      ComputedUserReserve<ReserveDataHumanized & FormatReserveUSDResponse>
124    >
125  ) => {
126    const enforcedAssets = selectedMigrationSupplyAssets.filter(
127      (supplyAsset) => supplyAsset.enforced
128    );
129  
130    if (enforcedAssets.length > 0) {
131      return enforcedAssets;
132    }
133  
134    const nonIsolatedAssets = selectedMigrationSupplyAssets.filter((supplyAsset) => {
135      const underlyingAssetAddress = selectMigrationUnderlyingAssetWithExceptions(
136        migrationExceptions,
137        supplyAsset
138      );
139      const v3UserReserve = userReservesV3Map[underlyingAssetAddress];
140      const v3ReserveBalanceWithExceptions = selectMigrationAssetBalanceWithExceptions(
141        migrationExceptions,
142        v3UserReserve
143      );
144      if (v3UserReserve) {
145        return v3ReserveBalanceWithExceptions == '0' && !v3UserReserve.reserve.isIsolated;
146      } else {
147        return false;
148      }
149    });
150  
151    if (nonIsolatedAssets.length > 0) {
152      return nonIsolatedAssets;
153    }
154  
155    const isolatedAssets = selectedMigrationSupplyAssets.filter((supplyAsset) => {
156      const underlyingAssetAddress = selectMigrationUnderlyingAssetWithExceptions(
157        migrationExceptions,
158        supplyAsset
159      );
160      const v3UserReserve = userReservesV3Map[underlyingAssetAddress];
161      const v3ReserveBalanceWithExceptions = selectMigrationAssetBalanceWithExceptions(
162        migrationExceptions,
163        v3UserReserve
164      );
165      return v3ReserveBalanceWithExceptions == '0' && v3UserReserve.reserve.isIsolated;
166    });
167  
168    return isolatedAssets;
169  };
170  
171  export const selectUserReservesMapFromUserReserves = (
172    userReservesData: ComputedUserReserve<
173      ReserveDataHumanized & FormatReserveUSDResponse & Partial<CalculateReserveIncentivesResponse>
174    >[]
175  ) => {
176    const v3ReservesMap = userReservesData.reduce((obj, item) => {
177      obj[item.underlyingAsset] = item;
178      return obj;
179    }, {} as Record<string, ComputedUserReserve<ReserveDataHumanized & FormatReserveUSDResponse & Partial<CalculateReserveIncentivesResponse>>>);
180  
181    return v3ReservesMap;
182  };
183  
184  export enum MigrationDisabled {
185    IsolationModeBorrowDisabled,
186    EModeBorrowDisabled,
187    V3AssetMissing,
188    InsufficientLiquidity,
189    AssetNotFlashloanable,
190    ReserveFrozen,
191    NotEnoughtSupplies,
192  }
193  
194  export const selectMigrationUnderlyingAssetWithExceptions = (
195    migrationExceptions: Record<string, MigrationException>,
196    reserve: {
197      underlyingAsset: string;
198    }
199  ): string => {
200    const defaultUnderlyingAsset = reserve?.underlyingAsset;
201    if (migrationExceptions[defaultUnderlyingAsset]) {
202      return migrationExceptions[defaultUnderlyingAsset].v3UnderlyingAsset;
203    }
204    return defaultUnderlyingAsset;
205  };
206  
207  export const selectMigrationUnderluingAssetWithExceptionsByV3Key = (
208    migrationExceptions: Record<string, MigrationException>,
209    reserveV3: {
210      underlyingAsset: string;
211    }
212  ) => {
213    const exceptionItem = Object.values(migrationExceptions).find(
214      (exception) => exception.v3UnderlyingAsset == reserveV3.underlyingAsset
215    );
216    return exceptionItem?.v2UnderlyingAsset || reserveV3.underlyingAsset;
217  };
218  
219  export const selectMigrationAssetBalanceWithExceptions = (
220    migrationExceptions: Record<string, MigrationException>,
221    reserve: {
222      underlyingAsset: string;
223      underlyingBalance: string;
224    }
225  ) => {
226    const underlyingAssetAddress = selectMigrationUnderlyingAssetWithExceptions(
227      migrationExceptions,
228      reserve
229    );
230    const exceptionAsset = migrationExceptions[underlyingAssetAddress];
231    if (exceptionAsset) {
232      return exceptionAsset.amount;
233    }
234    return reserve.underlyingBalance;
235  };
236  
237  export type IsolatedReserve = FormatReserveUSDResponse & { enteringIsolationMode?: boolean };
238  
239  export const selectedUserSupplyReservesForMigration = (
240    selectedMigrationSupplyAssets: MigrationSelectedAsset[],
241    supplyReserves: SupplyMigrationReserve[],
242    isolatedReserveV3: IsolatedReserve | undefined
243  ) => {
244    const selectedUserReserves = supplyReserves.filter(
245      (userReserve) =>
246        selectMigrationSelectedSupplyIndex(
247          selectedMigrationSupplyAssets,
248          userReserve.underlyingAsset
249        ) >= 0
250    );
251    selectedUserReserves.sort((userReserve) => {
252      if (!isolatedReserveV3) {
253        if (userReserve.isolatedOnV3) {
254          return 1;
255        }
256        return -1;
257      } else {
258        if (isolatedReserveV3.underlyingAsset == userReserve.underlyingAsset) {
259          return -1;
260        } else {
261          return 1;
262        }
263      }
264    });
265  
266    return selectedUserReserves;
267  };
268  
269  export const selectUserSupplyIncreasedReservesForMigrationPermits = (
270    store: RootStore,
271    supplyReserves: SupplyMigrationReserve[],
272    isolatedReserveV3: IsolatedReserve | undefined
273  ) => {
274    return selectedUserSupplyReservesForMigration(
275      store.selectedMigrationSupplyAssets,
276      supplyReserves,
277      isolatedReserveV3
278    ).map((userReserve) => {
279      const increasedAmount = addPercent(userReserve.underlyingBalance);
280      const valueInWei = valueToWei(increasedAmount, userReserve.reserve.decimals);
281      return { ...userReserve, increasedAmount: valueInWei };
282    });
283  };
284  
285  export const selectUserSupplyAssetsForMigrationNoPermit = (
286    store: RootStore,
287    supplyReserves: SupplyMigrationReserve[],
288    isolatedReserveV3: IsolatedReserve | undefined
289  ): MigrationSupplyAsset[] => {
290    const selectedUserSupplyReserves = selectUserSupplyIncreasedReservesForMigrationPermits(
291      store,
292      supplyReserves,
293      isolatedReserveV3
294    );
295    return selectedUserSupplyReserves.map(({ underlyingAsset, reserve, increasedAmount }) => {
296      const deadline = Math.floor(Date.now() / 1000 + 3600);
297      return {
298        amount: increasedAmount,
299        aToken: reserve.aTokenAddress,
300        underlyingAsset: underlyingAsset,
301        deadline,
302      };
303    });
304  };
305  
306  export const selectMigrationRepayAssets = (
307    store: RootStore,
308    borrowReserves: BorrowMigrationReserve[]
309  ): MigrationRepayAsset[] => {
310    const deadline = Math.floor(Date.now() / 1000 + 3600);
311    return selectSelectedBorrowReservesForMigration(
312      borrowReserves,
313      store.selectedMigrationBorrowAssets
314    ).map((userReserve) => ({
315      underlyingAsset: userReserve.underlyingAsset,
316      amount: userReserve.increasedVariableBorrows,
317      deadline,
318      debtToken: userReserve.debtKey,
319      rateMode: userReserve.interestRate,
320    }));
321  };
322  
323  export const selectMigrationSignedPermits = (
324    store: RootStore,
325    signatures: SignatureLike[],
326    deadline: BigNumberish
327  ): {
328    supplyPermits: V3MigrationHelperSignedPermit[];
329    creditDelegationPermits: V3MigrationHelperSignedCreditDelegationPermit[];
330  } => {
331    const approvalsWithSignatures = store.approvalPermitsForMigrationAssets.map((approval, index) => {
332      return {
333        ...approval,
334        signedPermit: signatures[index],
335      };
336    });
337  
338    const supplyPermits: V3MigrationHelperSignedPermit[] = approvalsWithSignatures
339      .filter((approval) => approval.permitType === 'SUPPLY_MIGRATOR_V3')
340      .map(({ signedPermit, underlyingAsset, amount }) => ({
341        deadline,
342        aToken: underlyingAsset,
343        value: amount,
344        signedPermit,
345      }));
346  
347    const creditDelegationPermits: V3MigrationHelperSignedCreditDelegationPermit[] =
348      approvalsWithSignatures
349        .filter((approval) => approval.permitType === 'BORROW_MIGRATOR_V3')
350        .map(({ amount, signedPermit, underlyingAsset }) => ({
351          deadline,
352          debtToken: underlyingAsset,
353          signedPermit,
354          value: amount,
355        }));
356  
357    return {
358      supplyPermits,
359      creditDelegationPermits,
360    };
361  };
362  
363  const addPercent = (amount: string) => {
364    const convertedAmount = valueToBigNumber(amount);
365    return convertedAmount.plus(convertedAmount.div(1000)).toString();
366  };
367  
368  export const selectSelectedBorrowReservesForMigration = (
369    borrowReserves: BorrowMigrationReserve[],
370    selectedMigrationBorrowAssets: MigrationSelectedBorrowAsset[]
371  ) => {
372    return borrowReserves.filter(
373      (userReserve) =>
374        selectMigrationSelectedBorrowIndex(selectedMigrationBorrowAssets, userReserve) >= 0
375    );
376  };
377  
378  export const selectSelectedBorrowReservesForMigrationV3 = (
379    selectedMigrationBorrowAssets: MigrationSelectedBorrowAsset[],
380    toUserSummary: UserSummaryForMigration,
381    userMigrationReserves: UserMigrationReserves
382  ) => {
383    const { userReservesData: userReservesDataV3 } = toUserSummary;
384    const selectedUserReserves = selectSelectedBorrowReservesForMigration(
385      userMigrationReserves.borrowReserves,
386      selectedMigrationBorrowAssets
387    )
388      .filter((userReserve) => userReserve.migrationDisabled === undefined)
389      // debtKey should be mapped for v3Migration
390      .map((borrowReserve) => {
391        let debtKey = borrowReserve.debtKey;
392        const borrowReserveV3 = userReservesDataV3.find(
393          (userReserve) => userReserve.underlyingAsset == borrowReserve.underlyingAsset
394        );
395  
396        if (borrowReserveV3) {
397          debtKey = borrowReserveV3.reserve.variableDebtTokenAddress;
398        }
399  
400        return {
401          ...borrowReserve,
402          debtKey,
403        };
404      });
405  
406    return selectedUserReserves;
407  };
408  
409  export const selectFormatUserSummaryForMigration = (
410    reserves: ReserveDataHumanized[] = [],
411    reserveIncentives: ReservesIncentiveDataHumanized[] = [],
412    userReserves: UserReserveDataHumanized[] = [],
413    baseCurrencyData: PoolBaseCurrencyHumanized,
414    currentTimestamp: number,
415    userEmodeCategoryId = 0
416  ) => {
417    const { marketReferenceCurrencyDecimals, marketReferenceCurrencyPriceInUsd } = baseCurrencyData;
418    const formattedReserves = formatReservesAndIncentives({
419      reserves: reserves,
420      reserveIncentives,
421      currentTimestamp,
422      marketReferenceCurrencyDecimals: marketReferenceCurrencyDecimals,
423      marketReferencePriceInUsd: marketReferenceCurrencyPriceInUsd,
424    });
425  
426    const formattedSummary = formatUserSummary({
427      currentTimestamp,
428      formattedReserves,
429      marketReferenceCurrencyDecimals: marketReferenceCurrencyDecimals,
430      marketReferencePriceInUsd: marketReferenceCurrencyPriceInUsd,
431      userReserves,
432      userEmodeCategoryId,
433    });
434  
435    return formattedSummary;
436  };
437  
438  /**
439   * Returns the required approval/permit payload to migrate the selected borrow reserves.
440   * The amount to migrate is the sum of the variable debt and the stable debt positions for an asset.
441   * Since all debt migrated to V3 becomes variable, the credit delegation approval/permit will be for the variable debt token address.
442   * @param store - root store
443   * @param timestamp - current timestamp
444   * @returns array of approval payloads
445   */
446  export const selectMigrationBorrowPermitPayloads = (
447    store: RootStore,
448    toUserSummary: UserSummaryForMigration,
449    borrowReserves: BorrowMigrationReserve[],
450    buffer?: boolean
451  ): Approval[] => {
452    const { userReservesData: userReservesDataV3 } = toUserSummary;
453    const selectedUserReserves = selectSelectedBorrowReservesForMigration(
454      borrowReserves,
455      store.selectedMigrationBorrowAssets
456    );
457  
458    const reserveDebts: ReserveDebtApprovalPayload = {};
459  
460    selectedUserReserves
461      .filter((userReserve) => userReserve.migrationDisabled === undefined)
462      .forEach((userReserve) => {
463        const borrowReserveV3 = userReservesDataV3.find(
464          (v3Reserve) => v3Reserve.underlyingAsset === userReserve.underlyingAsset
465        );
466        if (!borrowReserveV3) {
467          return;
468        }
469  
470        if (!reserveDebts[userReserve.underlyingAsset]) {
471          reserveDebts[userReserve.underlyingAsset] = {
472            variableDebtTokenAddress: borrowReserveV3.reserve.variableDebtTokenAddress,
473            decimals: borrowReserveV3.reserve.decimals,
474            variableDebtAmount: '0',
475          };
476        }
477  
478        const debt = reserveDebts[userReserve.underlyingAsset];
479  
480        debt.variableDebtAmount = valueToBigNumber(debt.variableDebtAmount)
481          .plus(valueToBigNumber(userReserve.increasedVariableBorrows))
482          .toString();
483      });
484  
485    return Object.keys(reserveDebts).map<Approval>((key) => {
486      const debt = reserveDebts[key];
487      const totalDebt = valueToBigNumber(debt.variableDebtAmount);
488      const combinedAmountInWei = valueToWei(totalDebt.toString(), debt.decimals);
489      let bufferedAmount = combinedAmountInWei;
490      if (buffer) {
491        const amountBN = new BigNumber(bufferedAmount);
492        const tenPercent = amountBN.dividedBy(10);
493        bufferedAmount = amountBN.plus(tenPercent).toFixed(0);
494      }
495  
496      return {
497        amount: bufferedAmount,
498        underlyingAsset: debt.variableDebtTokenAddress,
499        permitType: 'BORROW_MIGRATOR_V3',
500      };
501    });
502  };
503  
504  export const selectIsMigrationAvailable = (store: RootStore) => {
505    return Boolean(store.currentMarketData.addresses.V3_MIGRATOR);
506  };
507  
508  export type MigrationReserve = FormattedUserReserves & { migrationDisabled?: MigrationDisabled };
509  type MigrationSelectedReserve = {
510    underlyingAsset: string;
511    enforced?: boolean;
512    debtKey?: string;
513    interestRate?: InterestRate;
514  };
515  type ComputedMigrationSelections = {
516    activeSelections: MigrationReserve[];
517    activeUnselected: MigrationReserve[];
518  };
519  export const assetSelected = (
520    reserve: MigrationReserve,
521    selectedAssets: MigrationSelectedReserve[]
522  ) => {
523    const selectedReserve = selectedAssets.find(
524      (selectedAsset: MigrationSelectedReserve) =>
525        selectedAsset.underlyingAsset === reserve.underlyingAsset
526    );
527    return selectedReserve !== undefined;
528  };
529  export const assetEnabled = (selected: MigrationSelectedReserve, reserves: MigrationReserve[]) => {
530    const selectedReserve = reserves.find(
531      (reserve: MigrationReserve) => selected.underlyingAsset === reserve.underlyingAsset
532    );
533    return selectedReserve !== undefined && selectedReserve.migrationDisabled === undefined;
534  };
535  
536  export const computeSelections = (
537    reserves: MigrationReserve[],
538    selections: MigrationSelectedReserve[]
539  ): ComputedMigrationSelections => {
540    const enabledReserves = reserves.filter((reserve) => reserve.migrationDisabled === undefined);
541  
542    const selectedEnabledReserves = enabledReserves.filter((reserve) =>
543      assetSelected(reserve, selections)
544    );
545    const unselectedEnabledReserves = enabledReserves.filter(
546      (reserve) => !assetSelected(reserve, selections)
547    );
548  
549    return {
550      activeSelections: selectedEnabledReserves,
551      activeUnselected: unselectedEnabledReserves,
552    };
553  };