import { Inject , Injectable , Injector } from '@angular/core' ; import { BehaviorSubject , combineLatest , EMPTY , forkJoin , from , interval , Observable , of } from 'rxjs' ; import { catchError , filter , finalize , first , map , startWith , switchMap , take , tap } from 'rxjs/operators' ; import BigNumber from 'bignumber.js' ; import { TuiDialogService } from '@taiga-ui/core' ; import { BLOCKCHAIN_NAME } from '@shared/models/blockchain/blockchain-name' ; import { STAKING_CONTRACT_ABI } from 'src/app/features/staking/constants/xbrbc-contract-abi' ; import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' ; import { TransactionReceipt } from 'web3-eth' ; import { StakingApiService } from '@features/staking/services/staking-api.service' ; import { AuthService } from '@app/core/services/auth/auth.service' ; import { ErrorsService } from '@core/errors/errors.service' ; import { NotificationsService } from '@core/services/notifications/notifications.service' ; import { PublicBlockchainAdapterService } from '@core/services/blockchain/blockchain-adapters/public-blockchain-adapter.service' ; import { PrivateBlockchainAdapterService } from '@core/services/blockchain/blockchain-adapters/private-blockchain-adapter.service' ; import { BinancePolygonRubicBridgeProviderService } from '@features/bridge/services/bridge-service/blockchains-bridge-provider/binance-polygon-bridge-provider/binance-polygon-rubic-bridge-provider/binance-polygon-rubic-bridge-provider.service' ; import { EthereumBinanceRubicBridgeProviderService } from '@features/bridge/services/bridge-service/blockchains-bridge-provider/ethereum-binance-bridge-provider/rubic-bridge-provider/ethereum-binance-rubic-bridge-provider.service' ; import { TokensApiService } from '@core/services/backend/tokens-api/tokens-api.service' ; import { ENVIRONMENT } from 'src/environments/environment' ; import { Web3Pure } from '@core/services/blockchain/blockchain-adapters/common/web3-pure' ; import { SwapModalComponent } from '@features/staking/components/swap-modal/swap-modal.component' ; import { ERROR_TYPE } from '@core/errors/models/error-type' ; import { BRIDGE_PROVIDER } from '@shared/models/bridge/bridge-provider' ; import { BridgeTrade } from '@features/bridge/models/bridge-trade' ; import { MinimalToken } from '@shared/models/tokens/minimal-token' ; import { TokensService } from '@core/services/tokens/tokens.service' ; import { RubicError } from '@core/errors/models/rubic-error' ; import { TOKEN_RANK } from '@shared/models/tokens/token-rank' ; import { STAKING_TOKENS } from '@features/staking/constants/staking-tokens' ; @ Injectable ( ) export class StakingService { /**
     * User's wallet address.
     */ private walletAddress : string ; /**
     * Staking contract address.
     */ private readonly stakingContractAddress = ENVIRONMENT . staking . stakingContractAddress ; /**
     * Contract address for staking via bridge [from api].
     */ private bridgeContractAddress : string ; /**
     * Amount with rewards [from contract].
     */ private readonly _amountWithRewards$ = new BehaviorSubject < BigNumber > ( new BigNumber ( 0 ) ) ; public readonly amountWithRewards$ = this . _amountWithRewards$ . asObservable ( ) ; /**
     * Current APR [from api].
     */ private readonly _apr$ = new BehaviorSubject < number > ( 0 ) ; public readonly apr$ = this . _apr$ . asObservable ( ) ; /**
     * Staking refill time [currently unused].
     */ private readonly _refillTime$ = new BehaviorSubject < string > ( '' ) ; public readonly refillTime$ = this . _refillTime$ . asObservable ( ) ; /**
     * How much RBC user already staked [from contract].
     */ private readonly _userEnteredAmount$ = new BehaviorSubject < number > ( 0 ) ; public readonly userEnteredAmount$ = this . _userEnteredAmount$ . asObservable ( ) ; /**
     * Total RBC amount in stake [from contract].
     */ private readonly _totalRBCEntered$ = new BehaviorSubject < number > ( 0 ) ; public readonly totalRBCEntered$ = this . _totalRBCEntered$ . asObservable ( ) ; /**
     * Grouped totalRbcEntered$ and userEnteredAnount$ [from contract].
     */ public readonly stakingProgress$ = combineLatest ( [ this . _totalRBCEntered$ , this . _userEnteredAmount$ ] ) . pipe ( map ( ( [ totalRbcEntered , userEnteredAmount ] ) => ( { totalRbcEntered , userEnteredAmount } ) ) ) ; /**
     * User's xBRBC balance [from contract].
     */ private readonly _stakingTokenBalance$ = new BehaviorSubject < BigNumber > ( new BigNumber ( 0 ) ) ; public readonly stakingTokenBalance$ = this . _stakingTokenBalance$ . asObservable ( ) ; /**
     * Earned rewards [from contract].
     */ private readonly _earnedRewards$ = new BehaviorSubject < BigNumber > ( new BigNumber ( 0 ) ) ; public readonly earnedRewards$ = this . _earnedRewards$ . asObservable ( ) ; /**
     * Current token selected for stake.
     */ private readonly _selectedToken$ = new BehaviorSubject < MinimalToken > ( undefined ) ; public readonly selectedToken$ = this . _selectedToken$ . asObservable ( ) ; get selectedToken ( ) : MinimalToken { return this . _selectedToken$ . getValue ( ) ; } /**
     * User's max amount for unstake [from contract].
     */ private readonly _maxAmountForWithdraw$ = new BehaviorSubject < BigNumber > ( new BigNumber ( 0 ) ) ; public readonly maxAmountForWithdraw$ = this . _maxAmountForWithdraw$ . asObservable ( ) ; /**
     * Users deposit [from api].
     */ private readonly _usersTotalDeposit$ = new BehaviorSubject < BigNumber > ( new BigNumber ( 0 ) ) ; /**
     * Utility Subj to trigger token balance update.
     */ private readonly updateTokenBalance$ = new BehaviorSubject < void > ( null ) ; /**
     * BRBC usd price [from api].
     */ private BRBCUsdPrice : number ; /**
     * BRBC token usd price update time.
     */ private BRBC_USD_PRICE_UPDATE_TIME = 600000 ; /**
     * Is user need to connect wallet.
     */ public readonly needLogin$ = this . authService . getCurrentUser ( ) . pipe ( map ( user => ! user ?. address ) ) ; /**
     * Loading state for whole progress block.
     */ public readonly stakingProgressLoading$ = new BehaviorSubject < boolean > ( true ) ; /**
     * Loading state for whole statistics block.
     */ public readonly stakingStatisticsLoading$ = new BehaviorSubject < boolean > ( false ) ; /**
     * Balance of token selected for stake [from contract].
     */ public readonly selectedTokenBalance$ = combineLatest ( [ this . selectedToken$ , this . needLogin$ , this . updateTokenBalance$ . asObservable ( ) ] ) . pipe ( switchMap ( ( [ selectedToken , needLogin ] ) => { if ( needLogin ) { this . _amountWithRewards$ . next ( new BigNumber ( 0 ) ) ; this . _earnedRewards$ . next ( new BigNumber ( 0 ) ) ; this . _stakingTokenBalance$ . next ( new BigNumber ( 0 ) ) ; return of ( new BigNumber ( 0 ) ) ; } return this . getSelectedTokenBalance ( selectedToken ) ; } ) ) ; constructor ( private readonly web3PublicService , private readonly web3PrivateService , private readonly authService , private readonly stakingApiService , private readonly tokensApiService , private readonly tokensService , private readonly errorService , private readonly notificationsService , private readonly polygonBinanceBridge , private readonly ethereumBinanceBridge , @ Inject ( TuiDialogService ) private readonly dialogService , @ Inject ( Injector ) private readonly injector ) { forkJoin ( [ this . getTotalRBCEntered ( ) , this . getApr ( ) , this . getRefillTime ( ) ] ) . subscribe ( ( ) => this . stakingProgressLoading$ . next ( false ) ) ; this . authService . getCurrentUser ( ) . pipe ( filter ( Boolean ) , take ( 1 ) , tap ( ( { address } ) => ( this . walletAddress = address ) ) , switchMap ( ( ) => { return forkJoin ( [ this . getStakingTokenBalance ( ) . pipe ( switchMap ( stakingTokenBalance => this . getAmountWithRewards ( stakingTokenBalance ) ) ) , this . getUserEnteredAmount ( ) , this . getMaxAmountForWithdraw ( ) ] ) ; } ) , switchMap ( ( [ amountWithRewards ] ) => this . getEarnedRewards ( amountWithRewards ) ) ) . subscribe ( ( ) => this . stakingStatisticsLoading$ . next ( false ) ) ; this . stakingApiService . getBridgeContractAddress ( ) . subscribe ( address => { this . bridgeContractAddress = address ; } ) ; } param token public setToken ( token ) { this . _selectedToken$ . next ( token ) ; } param amount return public enterStake ( amount ) { const tokenBlockchain = this . selectedToken . blockchain ; const needSwap = tokenBlockchain !== BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN_TESTNET && tokenBlockchain !== BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN ; const amountInWei = Web3Pure . toWei ( amount ) ; if ( needSwap ) { return this . openSwapModal ( amount , tokenBlockchain ) ; } else { return from ( this . web3PrivateService [ BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN ] . tryExecuteContractMethod ( this . stakingContractAddress , STAKING_CONTRACT_ABI , 'enter' , [ amountInWei ] ) ) . pipe ( catchError ( ( error ) => { this . errorService . catchAnyError ( error as Error ) ; return EMPTY ; } ) , switchMap ( receipt => this . updateUsersDeposit ( amountInWei , receipt . transactionHash ) ) , switchMap ( ( ) => this . reloadStakingProgress ( ) ) , switchMap ( ( ) => this . reloadStakingStatistics ( ) ) , tap ( ( ) => this . updateTokenBalance$ . next ( ) ) ) ; } } param amount return public leaveStake ( amount ) { const amountInWei = Web3Pure . toWei ( amount ) ; return from ( this . web3PrivateService [ BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN ] . tryExecuteContractMethod ( this . stakingContractAddress , STAKING_CONTRACT_ABI , 'leave' , [ amountInWei ] ) ) . pipe ( switchMap ( receipt => this . updateUsersDepositAfterWithdraw ( amountInWei , receipt . transactionHash ) ) , switchMap ( ( ) => forkJoin ( [ this . reloadStakingStatistics ( ) , this . reloadStakingProgress ( ) ] ) ) , switchMap ( ( ) => this . getMaxAmountForWithdraw ( ) ) ) ; } param amount return public needApprove ( amount ) { return from ( this . web3PublicService [ this . selectedToken . blockchain ] . getAllowance ( { tokenAddress : this . selectedToken . address , ownerAddress : this . walletAddress , spenderAddress : this . stakingContractAddress } ) ) . pipe ( map ( allowance => allowance . lt ( Web3Pure . fromWei ( amount ) ) ) ) ; } return public approveTokens ( ) { return from ( this . web3PrivateService [ BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN ] . approveTokens ( this . selectedToken . address , this . stakingContractAddress , 'infinity' ) ) . pipe ( catchError ( ( error ) => { this . errorService . catch ( error as RubicError < ERROR_TYPE . TEXT > ) ; return EMPTY ; } ) ) ; } return public getStakingTokenBalance ( ) { return from ( this . web3PublicService [ BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN ] . getTokenBalance ( this . walletAddress , this . stakingContractAddress ) ) . pipe ( catchError ( ( error ) => { this . errorService . catch ( error as RubicError < ERROR_TYPE . TEXT > ) ; return of ( new BigNumber ( '0' ) ) ; } ) , tap ( balance => this . _stakingTokenBalance$ . next ( Web3Pure . fromWei ( balance ) ) ) ) ; } return public getMaxAmountForWithdraw ( ) { return from ( this . web3PublicService [ BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN ] . callContractMethod ( this . stakingContractAddress , STAKING_CONTRACT_ABI , 'actualBalanceOf' , { methodArguments : [ this . walletAddress ] , from : this . walletAddress } ) ) . pipe ( map ( actualBalance => Web3Pure . fromWei ( actualBalance ) ) , tap ( actualBalance => this . _maxAmountForWithdraw$ . next ( actualBalance ) ) ) ; } return public reloadStakingStatistics ( ) { this . stakingStatisticsLoading$ . next ( true ) ; this . getApr ( ) . subscribe ( ) ; return this . needLogin$ . pipe ( take ( 1 ) , switchMap ( needLogin => { if ( needLogin ) { return EMPTY ; } return this . getStakingTokenBalance ( ) . pipe ( switchMap ( stakingTokenBalance => this . getAmountWithRewards ( stakingTokenBalance ) ) , switchMap ( ( ) => this . getEarnedRewards ( ) ) ) ; } ) , finalize ( ( ) => this . stakingStatisticsLoading$ . next ( false ) ) ) ; } return public reloadStakingProgress ( ) { this . stakingProgressLoading$ . next ( true ) ; return this . needLogin$ . pipe ( take ( 1 ) , switchMap ( needLogin => { if ( needLogin ) { return this . getTotalRBCEntered ( ) ; } return forkJoin ( [ this . getTotalRBCEntered ( ) , this . getUserEnteredAmount ( ) ] ) ; } ) , finalize ( ( ) => this . stakingProgressLoading$ . next ( false ) ) ) ; } param token return private getSelectedTokenBalance ( token ) { return from ( this . web3PublicService [ token . blockchain ] . getTokenBalance ( this . walletAddress , token . address ) ) . pipe ( catchError ( ( error ) => { this . errorService . catch ( error as RubicError < ERROR_TYPE . TEXT > ) ; return EMPTY ; } ) , map ( balance => Web3Pure . fromWei ( balance ) ) ) ; } param amount return public calculateLeaveReward ( amount ) { if ( amount . isZero ( ) ) { return of ( new BigNumber ( 0 ) ) ; } return from ( this . web3PublicService [ BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN ] . callContractMethod ( this . stakingContractAddress , STAKING_CONTRACT_ABI , 'canReceive' , { methodArguments : [ amount . toFixed ( 0 ) ] , from : this . walletAddress } ) ) . pipe ( catchError ( ( ) => EMPTY ) , map ( res => Web3Pure . fromWei ( res ) ) ) ; } param amount return public enterStakeViaBridge ( amount ) { const fromBlockchain = this . selectedToken . blockchain ; const bridgeTrade = this . getBridgeTradeObject ( fromBlockchain , amount ) ; return this . getRubicBridge ( fromBlockchain ) . createTrade ( bridgeTrade ) ; } param amount return public needBridgeApprove ( amount ) { const fromBlockchain = this . selectedToken . blockchain ; const bridgeTrade = this . getBridgeTradeObject ( fromBlockchain , amount ) ; return this . getRubicBridge ( fromBlockchain ) . needApprove ( bridgeTrade ) ; } param amount return public approveBridgeTokens ( amount ) { const fromBlockchain = this . selectedToken . blockchain ; const bridgeTrade = this . getBridgeTradeObject ( fromBlockchain , amount ) ; return this . getRubicBridge ( fromBlockchain ) . approve ( bridgeTrade ) ; } param amount public calculateBRBCUsdPrice ( amount ) { return amount . multipliedBy ( this . BRBCUsdPrice ) ; } param stakingTokenBalance return private getAmountWithRewards ( stakingTokenBalance ) { return this . calculateLeaveReward ( stakingTokenBalance ) . pipe ( catchError ( ( error ) => { this . errorService . catchAnyError ( error as RubicError < ERROR_TYPE . TEXT > ) ; return of ( new BigNumber ( 0 ) ) ; } ) , tap ( actualBalance => { this . _amountWithRewards$ . next ( actualBalance ) ; } ) ) ; } return private getEarnedRewards ( amountWithRewards ? ) { return combineLatest ( [ this . getUsersDeposit ( ) , amountWithRewards ? of ( amountWithRewards ) : this . _amountWithRewards$ ] ) . pipe ( first ( ) , map ( ( [ usersDeposit , totalAmount ] ) => { const usersDepositInTokens = Web3Pure . fromWei ( usersDeposit ) ; const earnedRewards = totalAmount . minus ( usersDepositInTokens ) ; if ( earnedRewards . s === - 1 || earnedRewards . s === null ) { return new BigNumber ( 0 ) ; } return earnedRewards ; } ) , tap ( earnedRewards => this . _earnedRewards$ . next ( earnedRewards ) ) ) ; } return private getUserEnteredAmount ( ) { return from ( this . web3PublicService [ BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN ] . callContractMethod ( this . stakingContractAddress , STAKING_CONTRACT_ABI , 'userEnteredAmount' , { methodArguments : [ this . walletAddress ] } ) ) . pipe ( catchError ( ( error ) => { console . debug ( 'user entered amount' ) ; this . errorService . catch ( error as RubicError < ERROR_TYPE . TEXT > ) ; return EMPTY ; } ) , map ( amount => Web3Pure . fromWei ( amount ) . toNumber ( ) ) , tap ( userEnteredAmount => this . _userEnteredAmount$ . next ( userEnteredAmount ) ) ) ; } return private getTotalRBCEntered ( ) { return from ( this . web3PublicService [ BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN ] . callContractMethod ( this . stakingContractAddress , STAKING_CONTRACT_ABI , 'totalRBCEntered' ) ) . pipe ( catchError ( ( error ) => { console . debug ( 'get total rbc entered' ) ; this . errorService . catch ( error as RubicError < ERROR_TYPE . TEXT > ) ; return EMPTY ; } ) , tap ( totalRbcEntered => this . _totalRBCEntered$ . next ( Web3Pure . fromWei ( + totalRbcEntered ) . toNumber ( ) ) ) ) ; } return private getApr ( ) { return this . stakingApiService . getApr ( ) . pipe ( catchError ( ( err ) => { console . debug ( err ) ; return EMPTY ; } ) , tap ( apr => this . _apr$ . next ( apr ) ) ) ; } return private getRefillTime ( ) { return this . stakingApiService . getRefillTime ( ) . pipe ( tap ( refillTime => this . _refillTime$ . next ( refillTime ) ) ) ; } return private getUsersDeposit ( ) { return this . stakingApiService . getUsersDeposit ( this . walletAddress ) . pipe ( tap ( deposit => this . _usersTotalDeposit$ . next ( new BigNumber ( deposit ) ) ) ) ; } param amount param txHash return private updateUsersDeposit ( amount , txHash ) { return this . stakingApiService . updateUsersDeposit ( { walletAddress : this . walletAddress , amount , txHash , network : 'binance-smart-chain' } ) ; } param amount param txHash return private updateUsersDepositAfterWithdraw ( amount , txHash ) { return this . stakingApiService . updateUsersDepositAfterWithdraw ( { walletAddress : this . walletAddress , amount , txHash , network : 'binance-smart-chain' } ) ; } param amount param blockchain return private openSwapModal ( amount , blockchain ) { return this . dialogService . open ( new PolymorpheusComponent ( SwapModalComponent , this . injector ) , { size : 'l' , data : { amount , blockchain } } ) ; } param fromBlockchain param amount return private getBridgeTradeObject ( fromBlockchain , amount ) { switch ( fromBlockchain ) { case BLOCKCHAIN_NAME . POLYGON : return { provider : BRIDGE_PROVIDER . SWAP_RBC , token : { symbol : 'RBC' , image : 'assets/images/icons/staking/rbc-pos.svg' , rank : TOKEN_RANK . HIGH , tokenByBlockchain : { [ BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN ] : { blockchain : BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN , address : '0x8E3BCC334657560253B83f08331d85267316e08a' , name : 'BRBC' , symbol : 'BRBC' , decimals : 18 , minAmount : 100 , maxAmount : 100000 } , [ BLOCKCHAIN_NAME . POLYGON ] : { blockchain : BLOCKCHAIN_NAME . POLYGON , address : '0xc3cFFDAf8F3fdF07da6D5e3A89B8723D5E385ff8' , name : 'Rubic (pos)' , symbol : 'RBC' , decimals : 18 , minAmount : 100 , maxAmount : 100100 } } } , fromBlockchain , toBlockchain : BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN , amount , toAddress : this . bridgeContractAddress , onTransactionHash : async ( txHash ) => { await this . stakingApiService . sendBridgeTxHash ( { txHash , network : 'polygon' } ) . toPromise ( ) ; } } ; case BLOCKCHAIN_NAME . ETHEREUM : return { provider : BRIDGE_PROVIDER . SWAP_RBC , token : { symbol : 'RBC' , image : 'assets/images/icons/staking/rbc-eth.svg' , rank : TOKEN_RANK . HIGH , tokenByBlockchain : { [ BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN ] : { blockchain : BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN , address : '0x8E3BCC334657560253B83f08331d85267316e08a' , name : 'BRBC' , symbol : 'BRBC' , decimals : 18 , minAmount : 200 , maxAmount : 100000 } , [ BLOCKCHAIN_NAME . ETHEREUM ] : { blockchain : BLOCKCHAIN_NAME . ETHEREUM , address : '0xA4EED63db85311E22dF4473f87CcfC3DaDCFA3E3' , name : 'Rubic' , symbol : 'RBC' , decimals : 18 , minAmount : 200 , maxAmount : 100100 } } } , fromBlockchain , toBlockchain : BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN , amount , toAddress : this . bridgeContractAddress , onTransactionHash : async ( txHash ) => { await this . stakingApiService . sendBridgeTxHash ( { txHash , network : 'ethereum' } ) . toPromise ( ) ; } } ; } } param blockchain return private getRubicBridge ( blockchain ) { if ( blockchain === BLOCKCHAIN_NAME . POLYGON ) { return this . polygonBinanceBridge ; } else if ( blockchain === BLOCKCHAIN_NAME . ETHEREUM ) { return this . ethereumBinanceBridge ; } } return public watchBRBCUsdPrice ( ) { const { blockchain , address } = STAKING_TOKENS [ 0 ] ; return interval ( this . BRBC_USD_PRICE_UPDATE_TIME ) . pipe ( startWith ( 0 ) , switchMap ( ( ) => from ( this . tokensService . getAndUpdateTokenPrice ( { address , blockchain } , true ) ) ) , tap ( response => ( this . BRBCUsdPrice = response ) ) ) ; } }