import { PCacheable } from 'ts-cacheable' ; import { EthLikeContractData } from '@features/cross-chain-routing/services/cross-chain-routing-service/contracts-data/contract-data/eth-like-contract-data' ; import InsufficientFundsGasPriceValueError from '@core/errors/models/cross-chain-routing/insufficient-funds-gas-price-value' ; import { CrossChainRoutingApiService } from '@core/services/backend/cross-chain-routing-api/cross-chain-routing-api.service' ; import { SolanaWeb3PrivateService } from '@core/services/blockchain/blockchain-adapters/solana/solana-web3-private.service' ; import { BLOCKCHAIN_NAME } from '@shared/models/blockchain/blockchain-name' ; import { SwapFormService } from '@features/swaps/services/swaps-form-service/swap-form.service' ; import InstantTrade from '@features/instant-trade/models/instant-trade' ; import { GasService } from '@core/services/gas-service/gas.service' ; import { AuthService } from '@core/services/auth/auth.service' ; import { ContractExecutorFacadeService } from '@features/cross-chain-routing/services/cross-chain-routing-service/contract-executor/contract-executor-facade.service' ; import { from , Observable } from 'rxjs' ; import CrossChainIsUnavailableWarning from '@core/errors/models/cross-chain-routing/cross-chainIs-unavailable-warning' ; import InstantTradeToken from '@features/instant-trade/models/instant-trade-token' ; import { Web3Pure } from '@core/services/blockchain/blockchain-adapters/common/web3-pure' ; import UnsupportedTokenCCR from '@core/errors/models/cross-chain-routing/unsupported-token-ccr' ; import { WalletConnectorService } from '@core/services/blockchain/wallets/wallet-connector-service/wallet-connector.service' ; import { BlockchainToken } from '@shared/models/tokens/blockchain-token' ; import { PublicBlockchainAdapterService } from '@core/services/blockchain/blockchain-adapters/public-blockchain-adapter.service' ; import { Injectable } from '@angular/core' ; import { EthLikeWeb3PrivateService } from '@core/services/blockchain/blockchain-adapters/eth-like/web3-private/eth-like-web3-private.service' ; import InsufficientLiquidityError from '@core/errors/models/instant-trade/insufficient-liquidity-error' ; import { PriceImpactService } from '@core/services/price-impact/price-impact.service' ; import { SUPPORTED_CROSS_CHAIN_BLOCKCHAINS , SupportedCrossChainBlockchain } from '@features/cross-chain-routing/services/cross-chain-routing-service/models/supported-cross-chain-blockchain' ; import { SolanaContractExecutorService } from '@features/cross-chain-routing/services/cross-chain-routing-service/contract-executor/solana-contract-executor.service' ; import CustomError from '@core/errors/models/custom-error' ; import FailedToCheckForTransactionReceiptError from '@core/errors/models/common/failed-to-check-for-transaction-receipt-error' ; import { TransactionOptions } from '@shared/models/blockchain/transaction-options' ; import MaxGasPriceOverflowWarning from '@core/errors/models/common/max-gas-price-overflow-warning' ; import { CrossChainTrade } from '@features/cross-chain-routing/services/cross-chain-routing-service/models/cross-chain-trade' ; import { EthLikeContractExecutorService } from '@features/cross-chain-routing/services/cross-chain-routing-service/contract-executor/eth-like-contract-executor.service' ; import { ContractsDataService } from '@features/cross-chain-routing/services/cross-chain-routing-service/contracts-data/contracts-data.service' ; import { SettingsService } from '@features/swaps/services/settings-service/settings.service' ; import { TokensService } from '@core/services/tokens/tokens.service' ; import { compareAddresses } from '@shared/utils/utils' ; import BigNumber from 'bignumber.js' ; import { TransactionReceipt } from 'web3-eth' ; import { CrossChainTradeInfo } from '@features/cross-chain-routing/services/cross-chain-routing-service/models/cross-chain-trade-info' ; import { TokenAmount } from '@shared/models/tokens/token-amount' ; interface TradeAndToAmount { trade : InstantTrade | null ; toAmount : BigNumber ; } interface IndexedTradeAndToAmount { providerIndex : number ; tradeAndToAmount : TradeAndToAmount ; } const CACHEABLE_MAX_AGE = 15000 ; @ Injectable ( { providedIn : 'root' } ) export class CrossChainRoutingService { public static isSupportedBlockchain ( blockchain ) { return ! ! SUPPORTED_CROSS_CHAIN_BLOCKCHAINS . find ( supportedBlockchain => supportedBlockchain === blockchain ) ; } private readonly contracts = this . contractsDataService . contracts ; private currentCrossChainTrade : CrossChainTrade ; /**
     * Gets slippage, selected in settings, divided by 100%.
     */ private get slippageTolerance ( ) : number { return this . settingsService . crossChainRoutingValue . slippageTolerance / 100 ; } constructor ( private readonly contractsDataService , private readonly walletConnectorService , private readonly authService , private readonly settingsService , private readonly publicBlockchainAdapterService , private readonly web3PrivateService , private readonly swapFormService , private readonly tokensService , private readonly apiService , private readonly gasService , private readonly contractExecutorFacade , private readonly ethLikeContractExecutor , private readonly solanaPrivateAdapter ) { } private async needApprove ( fromBlockchain , fromToken ) { const blockchainAdapter = this . publicBlockchainAdapterService [ fromBlockchain ] ; if ( blockchainAdapter . isNativeAddress ( fromToken . address ) ) { return false ; } const contractAddress = this . contracts [ fromBlockchain ] . address ; return blockchainAdapter . getAllowance ( { tokenAddress : fromToken . address , ownerAddress : this . authService . userAddress , spenderAddress : contractAddress } ) . then ( allowance => allowance . eq ( 0 ) ) ; } public approve ( options = { } ) { const { fromBlockchain , tokenIn : fromToken } = this . currentCrossChainTrade ; const contractAddress = this . contracts [ fromBlockchain ] . address ; return from ( this . web3PrivateService . approveTokens ( fromToken . address , contractAddress , 'infinity' , options ) ) ; } public async calculateTrade ( calculateNeedApprove = false ) { const { fromToken , fromAmount , toToken } = this . swapFormService . inputValue ; const fromBlockchain = fromToken . blockchain ; const toBlockchain = toToken . blockchain ; if ( ! CrossChainRoutingService . isSupportedBlockchain ( fromBlockchain ) || ! CrossChainRoutingService . isSupportedBlockchain ( toBlockchain ) ) { throw Error ( 'Not supported blockchains' ) ; } const fromTransitToken = this . contracts [ fromBlockchain ] . transitToken ; const toTransitToken = this . contracts [ toBlockchain ] . transitToken ; const fromSlippage = 1 - this . slippageTolerance / 2 ; const toSlippage = 1 - this . slippageTolerance / 2 ; const { providerIndex : fromProviderIndex , tradeAndToAmount : { trade : fromTrade , toAmount : fromTransitTokenAmount } } = await this . getBestProviderIndex ( fromBlockchain , fromToken , fromAmount , fromTransitToken ) ; const { toTransitTokenAmount , feeInPercents } = await this . getToTransitTokenAmount ( toBlockchain , fromTransitTokenAmount , fromTrade === null , fromSlippage ) ; const { providerIndex : toProviderIndex , tradeAndToAmount : { trade : toTrade , toAmount } } = await this . getBestProviderIndex ( toBlockchain , toTransitToken , toTransitTokenAmount , toToken ) ; const cryptoFee = await this . getCryptoFee ( fromBlockchain , toBlockchain ) ; this . currentCrossChainTrade = { fromBlockchain , fromProviderIndex , tokenIn : fromToken , tokenInAmount : fromAmount , fromTransitTokenAmount , fromSlippage , fromTrade , toBlockchain , toProviderIndex , toTransitTokenAmount , tokenOut : toToken , tokenOutAmount : toAmount , toSlippage , toTrade , transitTokenFee : feeInPercents , cryptoFee } ; const [ gasData , minMaxErrors , needApprove ] = await Promise . all ( [ this . getGasData ( this . currentCrossChainTrade ) , this . checkMinMaxErrors ( this . currentCrossChainTrade ) , calculateNeedApprove ? this . needApprove ( fromBlockchain , fromToken ) : undefined ] ) ; this . currentCrossChainTrade = { ... this . currentCrossChainTrade , ... gasData } ; const toAmountWithoutSlippage = toAmount . dividedBy ( fromSlippage ) ; return { toAmount : toAmountWithoutSlippage , ... minMaxErrors , needApprove } ; } /**
     * Gets the best provider index in blockchain, based on profit of uniswap provider.
     */ private async getBestProviderIndex ( blockchain , fromToken , fromAmount , toToken ) { const promises = this . contracts [ blockchain ] . providersData . map ( async ( _ , providerIndex ) => ( { providerIndex , tradeAndToAmount : await this . getTradeAndToAmount ( blockchain , providerIndex , fromToken , fromAmount , toToken ) } ) ) ; return Promise . allSettled ( promises ) . then ( results => { const sortedResults = results . filter ( result => result . status === 'fulfilled' ) . map ( ( result ) => result . value ) . sort ( ( a , b ) => b . tradeAndToAmount . toAmount . comparedTo ( a . tradeAndToAmount . toAmount ) ) ; if ( ! sortedResults . length ) { throw ( results [ 0 ] as PromiseRejectedResult ) . reason ; } return sortedResults [ 0 ] ; } ) ; } param blockchain param providerIndex param fromToken param fromAmount param toToken private async getTradeAndToAmount ( blockchain , providerIndex , fromToken , fromAmount , toToken ) { if ( ! compareAddresses ( fromToken . address , toToken . address ) ) { try { const instantTrade = await this . contracts [ blockchain ] . getProvider ( providerIndex ) . calculateTrade ( fromToken , fromAmount , toToken , false ) ; return { trade : instantTrade , toAmount : instantTrade . to . amount } ; } catch ( err ) { if ( err instanceof InsufficientLiquidityError ) { throw new InsufficientLiquidityError ( 'CrossChainRouting' ) ; } throw err ; } } return { trade : null , toAmount : fromAmount } ; } /**
     * Compares min and max amounts, permitted in source contract, with current trade's value.
     */ private async checkMinMaxErrors ( trade ) { const { fromBlockchain , fromTransitTokenAmount , fromSlippage } = trade ; const { minAmount : minTransitTokenAmount , maxAmount : maxTransitTokenAmount } = await this . getMinMaxTransitTokenAmounts ( fromBlockchain , fromSlippage ) ; if ( fromTransitTokenAmount . lt ( minTransitTokenAmount ) ) { const minAmount = await this . getFromTokenAmount ( fromBlockchain , trade . fromProviderIndex , trade . tokenIn , minTransitTokenAmount ) ; if ( ! minAmount ?. isFinite ( ) ) { throw new InsufficientLiquidityError ( 'CrossChainRouting' ) ; } return { minAmountError : minAmount } ; } if ( fromTransitTokenAmount . gt ( maxTransitTokenAmount ) ) { const maxAmount = await this . getFromTokenAmount ( fromBlockchain , trade . fromProviderIndex , trade . tokenIn , maxTransitTokenAmount ) ; return { maxAmountError : maxAmount } ; } return { } ; } param fromBlockchain param fromSlippage private async getMinMaxTransitTokenAmounts ( fromBlockchain , fromSlippage ) { const fromTransitToken = this . contracts [ fromBlockchain ] . transitToken ; const getAmount = async ( type ) : Promise < BigNumber > => { const contract = this . contracts [ fromBlockchain ] ; const fromTransitTokenAmountAbsolute = type === 'minAmount' ? await contract . minTokenAmount ( ) : await contract . maxTokenAmount ( ) ; const fromTransitTokenAmount = Web3Pure . fromWei ( fromTransitTokenAmountAbsolute , fromTransitToken . decimals ) ; if ( type === 'minAmount' ) { return fromTransitTokenAmount . dividedBy ( fromSlippage ) ; } return fromTransitTokenAmount ; } ; return Promise . all ( [ getAmount ( 'minAmount' ) , getAmount ( 'maxAmount' ) ] ) . then ( ( [ minAmount , maxAmount ] ) => ( { minAmount , maxAmount } ) ) ; } param blockchain param providerIndex param fromToken param transitTokenAmount private async getFromTokenAmount ( blockchain , providerIndex , fromToken , transitTokenAmount ) { const transitToken = this . contracts [ blockchain ] . transitToken ; if ( compareAddresses ( fromToken . address , transitToken . address ) ) { return transitTokenAmount ; } if ( transitTokenAmount . eq ( 0 ) ) { return new BigNumber ( 0 ) ; } return ( await this . contracts [ blockchain ] . getProvider ( providerIndex ) . calculateTrade ( transitToken , transitTokenAmount , fromToken , false ) ) . to . amount ; } param toBlockchain param fromTransitTokenAmount param isDirectTrade param fromSlippage private async getToTransitTokenAmount ( toBlockchain , fromTransitTokenAmount , isDirectTrade , fromSlippage ) { const feeInPercents = await this . getFeeInPercents ( toBlockchain ) ; let toTransitTokenAmount = fromTransitTokenAmount . multipliedBy ( 100 - feeInPercents ) . dividedBy ( 100 ) ; if ( ! isDirectTrade ) { toTransitTokenAmount = toTransitTokenAmount . multipliedBy ( fromSlippage ) ; } return { toTransitTokenAmount , feeInPercents } ; } param toBlockchain private async getFeeInPercents ( toBlockchain ) { const feeOfToBlockchainAbsolute = await this . contracts [ toBlockchain ] . feeAmountOfBlockchain ( ) ; return parseInt ( feeOfToBlockchainAbsolute ) / 10000 ; } param fromBlockchain param toBlockchain return private async getCryptoFee ( fromBlockchain , toBlockchain ) { return this . contracts [ fromBlockchain ] . blockchainCryptoFee ( this . contracts [ toBlockchain ] . numOfBlockchain ) ; } /**
     * Calculates gas limit and gas price in source network, if possible to calculate.
     */ private async getGasData ( trade ) { const { fromBlockchain } = trade ; const walletAddress = this . authService . userAddress ; if ( fromBlockchain !== BLOCKCHAIN_NAME . ETHEREUM || ! walletAddress ) { return null ; } try { const { contractAddress , contractAbi , methodName , methodArguments , value } = await this . ethLikeContractExecutor . getContractParams ( trade , walletAddress ) ; const web3Public = this . publicBlockchainAdapterService [ fromBlockchain ] ; const gasLimit = await web3Public . getEstimatedGas ( contractAbi , contractAddress , methodName , methodArguments , walletAddress , value ) ; const gasPrice = Web3Pure . toWei ( await this . gasService . getGasPriceInEthUnits ( fromBlockchain ) ) ; return { gasLimit , gasPrice } ; } catch ( _err ) { return null ; } } /**
     * Gets trade info to show in transaction info panel.
     */ public async getTradeInfo ( ) { if ( ! this . currentCrossChainTrade ) { return null ; } const trade = this . currentCrossChainTrade ; const { fromBlockchain , toBlockchain , tokenIn , tokenOut , tokenInAmount , tokenOutAmount , fromTransitTokenAmount , toTransitTokenAmount } = trade ; const firstTransitToken = this . contracts [ fromBlockchain ] . transitToken ; const secondTransitToken = this . contracts [ toBlockchain ] . transitToken ; const feePercent = trade . transitTokenFee ; const fee = feePercent / 100 ; const feeAmount = trade . toTransitTokenAmount . multipliedBy ( fee ) . dividedBy ( 1 - fee ) ; const estimatedGas = trade . gasLimit ?. multipliedBy ( Web3Pure . fromWei ( trade . gasPrice ) ) ; const [ priceImpactFrom , priceImpactTo ] = await Promise . all ( [ this . calculatePriceImpact ( tokenIn , firstTransitToken , tokenInAmount , fromTransitTokenAmount , 'from' ) , this . calculatePriceImpact ( tokenOut , secondTransitToken , tokenOutAmount , toTransitTokenAmount , 'to' ) ] ) ; const fromProvider = this . contracts [ fromBlockchain ] . getProvider ( trade . fromProviderIndex ) . providerType ; const toProvider = this . contracts [ toBlockchain ] . getProvider ( trade . toProviderIndex ) . providerType ; const fromPath = trade . fromTrade ? trade . fromTrade . path . map ( token => token . symbol ) : null ; const toPath = trade . toTrade ? trade . toTrade . path . map ( token => token . symbol ) : null ; return { feePercent , feeAmount , feeTokenSymbol : secondTransitToken . symbol , cryptoFee : trade . cryptoFee , estimatedGas , priceImpactFrom , priceImpactTo , fromProvider , toProvider , fromPath , toPath } ; } param token param transitToken param tokenAmount param transitTokenAmount param type return private async calculatePriceImpact ( token , transitToken , tokenAmount , transitTokenAmount , type ) { if ( ! compareAddresses ( token . address , transitToken . address ) ) { const transitTokenPrice = await this . tokensService . getAndUpdateTokenPrice ( { address : transitToken . address , blockchain : token . blockchain } ) ; const priceImpactArguments = type === 'from' ? [ token . price , transitTokenPrice , tokenAmount , transitTokenAmount ] : [ transitTokenPrice , token . price , transitTokenAmount , tokenAmount ] ; return PriceImpactService . calculatePriceImpact ( ... priceImpactArguments ) ; } return 0 ; } /**
     * Checks that contracts are not paused.
     */ private async checkIfPaused ( ) { const { fromBlockchain , toBlockchain } = this . currentCrossChainTrade ; const [ isFromPaused , isToPaused ] = await Promise . all ( [ this . contracts [ fromBlockchain ] . isPaused ( ) , this . contracts [ toBlockchain ] . isPaused ( ) ] ) ; if ( isFromPaused || isToPaused ) { throw new CrossChainIsUnavailableWarning ( ) ; } if ( fromBlockchain === BLOCKCHAIN_NAME . SOLANA || toBlockchain === BLOCKCHAIN_NAME . SOLANA ) { const isSolanaWorking = await SolanaContractExecutorService . checkHealth ( this . solanaPrivateAdapter ) ; if ( ! isSolanaWorking ) { throw new CustomError ( 'Solana blockchain mainnet is not stable right now. Please, try again later.' ) ; } } } /**
     * Checks that in target blockchain current gas price is less than or equal to max gas price.
     */ private async checkGasPrice ( ) { const { toBlockchain } = this . currentCrossChainTrade ; if ( toBlockchain !== BLOCKCHAIN_NAME . ETHEREUM ) { return ; } const maxGasPrice = await ( this . contracts [ toBlockchain ] as EthLikeContractData ) . maxGasPrice ( ) ; const currentGasPrice = Web3Pure . toWei ( await this . gasService . getGasPriceInEthUnits ( toBlockchain ) ) ; if ( new BigNumber ( maxGasPrice ) . lt ( currentGasPrice ) ) { throw new MaxGasPriceOverflowWarning ( toBlockchain ) ; } } /**
     * Checks that in target blockchain tokens' pool's balance is enough.
     */ @ PCacheable ( { maxAge : CACHEABLE_MAX_AGE } ) private async checkContractBalance ( ) { const { toBlockchain , toTransitTokenAmount } = this . currentCrossChainTrade ; const contractAddress = this . contracts [ toBlockchain ] . address ; const secondTransitToken = this . contracts [ toBlockchain ] . transitToken ; const blockchainAdapter = this . publicBlockchainAdapterService [ toBlockchain ] ; const contractBalanceAbsolute = await blockchainAdapter . getTokenBalance ( contractAddress , secondTransitToken . address ) ; const contractBalance = Web3Pure . fromWei ( contractBalanceAbsolute , secondTransitToken . decimals ) ; if ( toTransitTokenAmount . gt ( contractBalance ) ) { throw new CrossChainIsUnavailableWarning ( ) ; } } /**
     * Checks contracts' state and user's balance.
     */ private async checkTradeParameters ( ) { this . walletConnectorService . checkSettings ( this . currentCrossChainTrade . fromBlockchain ) ; const { fromBlockchain , tokenIn , tokenInAmount } = this . currentCrossChainTrade ; const blockchainAdapter = this . publicBlockchainAdapterService [ fromBlockchain ] ; await Promise . all ( [ this . checkIfPaused ( ) , this . checkGasPrice ( ) , this . checkContractBalance ( ) , blockchainAdapter . checkBalance ( tokenIn , tokenInAmount , this . authService . userAddress ) ] ) ; } public createTrade ( options = { } ) { return from ( ( async ( ) => { await this . checkTradeParameters ( ) ; let transactionHash ; try { transactionHash = await this . contractExecutorFacade . executeTrade ( this . currentCrossChainTrade , options , this . authService . userAddress ) ; await this . postCrossChainTrade ( transactionHash ) ; } catch ( err ) { if ( err instanceof FailedToCheckForTransactionReceiptError ) { await this . postCrossChainTrade ( transactionHash ) ; return ; } const errMessage = err . message || err . toString ?. ( ) ; if ( errMessage ?. includes ( 'swapContract: Not enough amount of tokens' ) ) { throw new CrossChainIsUnavailableWarning ( ) ; } if ( errMessage ?. includes ( 'err: insufficient funds for gas * price + value' ) ) { throw new InsufficientFundsGasPriceValueError ( this . currentCrossChainTrade . tokenIn . symbol ) ; } const unsupportedTokenErrors = [ 'execution reverted: TransferHelper: TRANSFER_FROM_FAILED' , 'execution reverted: UniswapV2: K' , 'execution reverted: UniswapV2:  TRANSFER_FAILED' , 'execution reverted: Pancake: K' , 'execution reverted: Pancake:  TRANSFER_FAILED' , 'execution reverted: Solarbeam: K' , 'execution reverted: Solarbeam:  TRANSFER_FAILED' ] ; if ( unsupportedTokenErrors . some ( errText => errMessage . toLowerCase ( ) . includes ( errText . toLocaleLowerCase ( ) ) ) ) { throw new UnsupportedTokenCCR ( ) ; } throw err ; } } ) ( ) ) ; } param transactionHash private async postCrossChainTrade ( transactionHash ) { const settings = this . settingsService . crossChainRoutingValue ; await this . apiService . postTrade ( transactionHash , this . currentCrossChainTrade . fromBlockchain , settings . promoCode ?. status === 'accepted' ? settings . promoCode . text : undefined ) ; } public calculateTokenOutAmountMin ( ) { return ContractExecutorFacadeService . calculateTokenOutAmountMin ( this . currentCrossChainTrade ) ; } }