import { Observable , combineLatest } from 'rxjs' ; import { map } from 'rxjs/operators' ; import { assert } from '@polkadot/util' ; import { FixedPointNumber , forceToCurrencyId , MaybeCurrency , Token } from '@acala-network/sdk-core' ; import { CurrencyId , Position } from '@acala-network/types/interfaces' ; import { DerivedLoanType } from '@acala-network/api-derive' ; import { ApiRx } from '@polkadot/api' ; import { WalletRx } from '@acala-network/sdk-wallet' ; import { memoize } from 'lodash' ; import { ModuleLoansPosition } from '@polkadot/types/lookup' ; export interface LoanParams { debitExchangeRate : FixedPointNumber ; globalInterestRatePerSec : FixedPointNumber ; liquidationRatio : FixedPointNumber ; requiredCollateralRatio : FixedPointNumber ; interestRatePerSec : FixedPointNumber ; } export interface LoanPosition extends LoanParams { collateral : FixedPointNumber ; debit : FixedPointNumber ; debitAmount : FixedPointNumber ; collateralAmount : FixedPointNumber ; collateralRatio : FixedPointNumber ; requiredCollateral : FixedPointNumber ; stableFeeAPR : FixedPointNumber ; liquidationPrice : FixedPointNumber ; liquidationRatio : FixedPointNumber ; canGenerate : FixedPointNumber ; canPayBack : FixedPointNumber ; canWithdraw : FixedPointNumber ; maxGenerate : FixedPointNumber ; } const YEAR_SECONDS = 365 * 24 * 60 * 60 ; export class LoanRx { private api : ApiRx ; private readonly currency : CurrencyId ; private collateralToken : Token ; private stableCoinToken : Token ; private readonly address : string ; private wallet : WalletRx ; private loanPosition$ : Observable < Position > ; private readonly loanParams$ : Observable < LoanParams > ; constructor ( api , currency , address , wallet ) { const collateralToken = wallet . getToken ( currency ) ; const stableCoinToken = wallet . getToken ( api . consts . cdpEngine . getStableCurrencyId ) ; assert ( collateralToken && stableCoinToken , `init the loan sdk failed, can't find useable token in currency chain` ) ; this . api = api ; this . currency = forceToCurrencyId ( api , currency ) ; this . address = address ; this . collateralToken = collateralToken ; this . stableCoinToken = stableCoinToken ; this . wallet = wallet ; this . loanPosition$ = this . getLoanPosition ( ) ; this . loanParams$ = this . getLoanParams ( ) ; } get position ( ) : Observable < LoanPosition > { return this . getPositionWithChanged ( FixedPointNumber . ZERO , FixedPointNumber . ZERO ) ; } get params ( ) : Observable < LoanParams > { return this . loanParams$ ; } public updatePosition ( debitAmountChange , collateralChange ) { return this . getPositionWithChanged ( debitAmountChange , collateralChange ) ; } /**
     * get the position information of the loan
     */ private getPositionWithChanged = memoize ( ( debitAmountChange , collateralChange ) : Observable < LoanPosition > => { return combineLatest ( [ this . loanParams$ , this . loanPosition$ , this . wallet . queryPrice ( this . currency ) ] ) . pipe ( map ( ( [ params , position , price ] ) => { const { debit , collateral } = position ; const { debitExchangeRate , requiredCollateralRatio , interestRatePerSec , liquidationRatio , globalInterestRatePerSec } = params ; const _debit = FixedPointNumber . fromInner ( debit . toString ( ) , this . stableCoinToken . decimals ) ; const _collateral = FixedPointNumber . fromInner ( collateral . toString ( ) , this . collateralToken . decimals ) . plus ( collateralChange ) ; const debitAmount = _debit . times ( debitExchangeRate ) . plus ( debitAmountChange ) ; const collateralAmount = _collateral . times ( price . price ) ; const requiredCollateral = this . getRequiredCollateral ( debitAmount , requiredCollateralRatio , price . price ) ; const canGenerate = this . getCanGenerate ( collateralAmount , debitAmount , requiredCollateralRatio , FixedPointNumber . ONE , FixedPointNumber . ZERO ) ; const maxGenerate = this . getMaxGenerate ( collateralAmount , requiredCollateralRatio ) ; return { collateral : _collateral , debit : _debit , debitAmount : debitAmount , collateralAmount : collateralAmount , collateralRatio : collateralAmount . div ( debitAmount ) , requiredCollateral , stableFeeAPR : this . getStableFeeAPR ( interestRatePerSec . plus ( globalInterestRatePerSec ) ) , liquidationPrice : this . getLiquidationPrice ( _collateral , debitAmount , liquidationRatio ) , canGenerate , canPayBack : debitAmount , canWithdraw : _collateral . minus ( requiredCollateral ) . min ( FixedPointNumber . ZERO ) , maxGenerate , ... params } ; } ) ) ; } ) ; private getMaxGenerate ( collateralAmount , requiredCollateralRatio ) { return collateralAmount . div ( requiredCollateralRatio ) ; } private getRequiredCollateral ( debitAmount , requiredCollateralRatio , price ) { const result = debitAmount . times ( requiredCollateralRatio ) . div ( price ) ; if ( result . isLessThan ( FixedPointNumber . ZERO ) || ! result . isFinaite ( ) ) { return FixedPointNumber . ZERO ; } return result ; } private getStableFeeAPR ( interestRate ) { return new FixedPointNumber ( Math . pow ( Number ( interestRate . plus ( FixedPointNumber . ONE ) . toString ( ) ) , YEAR_SECONDS ) - 1 ) ; } private getLiquidationPrice ( collateral , debitAmount , liquidationRatio ) { const result = debitAmount . times ( liquidationRatio ) . div ( collateral ) ; if ( result . isLessThan ( FixedPointNumber . ZERO ) || result . isEqualTo ( FixedPointNumber . ZERO ) || ! result . isFinaite ( ) ) { return new FixedPointNumber ( NaN ) ; } return result ; } private getLoanPosition ( ) { return this . api . query . loans . positions ( this . currency , this . address ) as Observable < ModuleLoansPosition > ; } private getCanGenerate ( collateralAmount , currentDebitAmount , requiredCollateralRatio , stableCoinPrice , slippage = FixedPointNumber . ZERO ) { const result = collateralAmount . div ( requiredCollateralRatio ) . minus ( currentDebitAmount ) . div ( stableCoinPrice ) . minus ( slippage ) ; if ( result . isLessThan ( FixedPointNumber . ZERO ) || ! result . isFinaite ( ) ) return FixedPointNumber . ZERO ; return result ; } private getLoanParams ( ) { return ( ( this . api . derive as any ) . loan . loanType ( this . currency ) as Observable < DerivedLoanType > ) . pipe ( map ( ( params ) : LoanParams => { return { debitExchangeRate : FixedPointNumber . fromInner ( params . debitExchangeRate . toString ( ) ) , globalInterestRatePerSec : FixedPointNumber . fromInner ( params . globalInterestRatePerSec . toString ( ) ) , liquidationRatio : FixedPointNumber . fromInner ( params . liquidationRatio . toString ( ) ) , requiredCollateralRatio : FixedPointNumber . fromInner ( params . requiredCollateralRatio . toString ( ) ) , interestRatePerSec : FixedPointNumber . fromInner ( params . interestRatePerSec . toString ( ) ) } ; } ) ) ; } }