import { Injectable } from '@angular/core' ; import { BLOCKCHAIN_NAME } from '@shared/models/blockchain/blockchain-name' ; import { EthLikeWeb3Public } from 'src/app/core/services/blockchain/blockchain-adapters/eth-like/web3-public/eth-like-web3-public' ; import { HttpClient , HttpErrorResponse } from '@angular/common/http' ; import { WalletConnectorService } from 'src/app/core/services/blockchain/wallets/wallet-connector-service/wallet-connector.service' ; import { catchError , map , startWith , switchMap } from 'rxjs/operators' ; import InstantTradeToken from '@features/instant-trade/models/instant-trade-token' ; import InstantTrade from '@features/instant-trade/models/instant-trade' ; import { from , Observable , of } from 'rxjs' ; import { EthLikeWeb3PrivateService } from '@core/services/blockchain/blockchain-adapters/eth-like/web3-private/eth-like-web3-private.service' ; import { BlockchainsInfo } from 'src/app/core/services/blockchain/blockchain-info' ; import BigNumber from 'bignumber.js' ; import CustomError from 'src/app/core/errors/models/custom-error' ; import networks from 'src/app/shared/constants/blockchain/networks' ; import { PublicBlockchainAdapterService } from '@core/services/blockchain/blockchain-adapters/public-blockchain-adapter.service' ; import { AuthService } from 'src/app/core/services/auth/auth.service' ; import { TransactionOptions } from 'src/app/shared/models/blockchain/transaction-options' ; import { NATIVE_TOKEN_ADDRESS } from '@shared/constants/blockchain/native-token-address' ; import { ItSettingsForm , SettingsService } from 'src/app/features/swaps/services/settings-service/settings.service' ; import { TransactionReceipt } from 'web3-eth' ; import { ItOptions } from '@features/instant-trade/services/instant-trade-service/models/it-provider' ; import { OneinchSwapResponse } from '@features/instant-trade/services/instant-trade-service/providers/common/oneinch/common-oneinch/models/oneinch-swap-response' ; import { OneinchQuoteResponse } from '@features/instant-trade/services/instant-trade-service/providers/common/oneinch/common-oneinch/models/oneinch-quote-response' ; import { OneinchTokensResponse } from '@features/instant-trade/services/instant-trade-service/providers/common/oneinch/common-oneinch/models/oneinch-tokens-response' ; import { OneinchApproveResponse } from '@features/instant-trade/services/instant-trade-service/providers/common/oneinch/common-oneinch/models/oneinch-approve-response' ; import { OneinchQuoteRequest } from '@features/instant-trade/services/instant-trade-service/providers/common/oneinch/common-oneinch/models/oneinch-quote-request' ; import { OneinchSwapRequest } from '@features/instant-trade/services/instant-trade-service/providers/common/oneinch/common-oneinch/models/oneinch-swap-request' ; import { TokensService } from 'src/app/core/services/tokens/tokens.service' ; import { OneinchNotSupportedTokens } from 'src/app/core/errors/models/instant-trade/oneinch-not-supported-tokens' ; import { SymbolToken } from '@shared/models/tokens/symbol-token' ; import { Web3Pure } from '@core/services/blockchain/blockchain-adapters/common/web3-pure' ; import { TokenWithFeeError } from '@core/errors/models/common/token-with-fee-error' ; import InsufficientFundsOneinchError from '@core/errors/models/instant-trade/insufficient-funds-oneinch-error' ; import { OneinchQuoteError } from '@core/errors/models/provider/oneinch-quote-error' ; interface SupportedTokens { [ BLOCKCHAIN_NAME . ETHEREUM ] : string [ ] ; [ BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN ] : string [ ] ; [ BLOCKCHAIN_NAME . POLYGON ] : string [ ] ; } @ Injectable ( { providedIn : 'root' } ) export class CommonOneinchService { private readonly oneInchNativeAddress : string ; private readonly apiBaseUrl : string ; private readonly supportedTokens : SupportedTokens ; private walletAddress : string ; private settings : ItSettingsForm ; constructor ( private readonly httpClient , private readonly publicBlockchainAdapterService , private readonly web3Private , private readonly settingsService , private readonly walletConnectorService , private readonly authService , private readonly tokensService ) { this . oneInchNativeAddress = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' ; this . apiBaseUrl = 'https://api.1inch.exchange/v3.0/' ; this . supportedTokens = { [ BLOCKCHAIN_NAME . ETHEREUM ] : [ ] , [ BLOCKCHAIN_NAME . BINANCE_SMART_CHAIN ] : [ ] , [ BLOCKCHAIN_NAME . POLYGON ] : [ ] } ; this . authService . getCurrentUser ( ) . subscribe ( user => { this . walletAddress = user ?. address ; } ) ; this . settingsService . instantTradeValueChanges . pipe ( startWith ( this . settingsService . instantTradeValue ) ) . subscribe ( settingsForm => { this . settings = settingsForm ; } ) ; } private getOneInchTokenSpecificAddresses ( fromTokenAddress , toTokenAddress ) { if ( fromTokenAddress === NATIVE_TOKEN_ADDRESS ) { fromTokenAddress = this . oneInchNativeAddress ; } if ( toTokenAddress === NATIVE_TOKEN_ADDRESS ) { toTokenAddress = this . oneInchNativeAddress ; } return { fromTokenAddress , toTokenAddress } ; } private async getSupportedTokensByBlockchain ( blockchain ) { blockchain = blockchain as keyof SupportedTokens ; if ( this . supportedTokens [ blockchain ] . length ) { return this . supportedTokens [ blockchain ] ; } const blockchainId = BlockchainsInfo . getBlockchainByName ( blockchain ) . id ; const supportedTokensByBlockchain = await this . httpClient . get ( `${ this . apiBaseUrl }${ blockchainId }/tokens` ) . pipe ( map ( ( response ) => Object . keys ( response . tokens ) . map ( tokenAddress => tokenAddress . toLowerCase ( ) ) ) ) . toPromise ( ) ; this . supportedTokens [ blockchain ] = supportedTokensByBlockchain ; return supportedTokensByBlockchain ; } private loadApproveAddress ( blockchainId ) { return this . httpClient . get ( `${ this . apiBaseUrl }${ blockchainId }/approve/spender` ) . pipe ( map ( ( response ) => response . address ) ) ; } public getAllowance ( blockchain , tokenAddress ) { if ( BlockchainsInfo . getBlockchainType ( blockchain ) !== 'ethLike' ) { throw new CustomError ( 'Wrong blockchain error' ) ; } const blockchainAdapter = this . publicBlockchainAdapterService [ blockchain ] as EthLikeWeb3Public ; if ( blockchainAdapter . isNativeAddress ( tokenAddress ) ) { return of ( new BigNumber ( Infinity ) ) ; } return this . loadApproveAddress ( BlockchainsInfo . getBlockchainByName ( blockchain ) . id ) . pipe ( switchMap ( address => from ( blockchainAdapter . getAllowance ( { tokenAddress , ownerAddress : this . walletAddress , spenderAddress : address } ) ) ) ) ; } public async approve ( blockchain , tokenAddress , options ) { this . walletConnectorService . checkSettings ( blockchain ) ; const approveAddress = await this . loadApproveAddress ( BlockchainsInfo . getBlockchainByName ( blockchain ) . id ) . toPromise ( ) ; await this . web3Private . approveTokens ( tokenAddress , approveAddress , 'infinity' , options ) ; } public async calculateTrade ( blockchain , fromToken , fromAmount , toToken , shouldCalculateGas ) { const { fromTokenAddress , toTokenAddress } = this . getOneInchTokenSpecificAddresses ( fromToken . address , toToken . address ) ; const supportedTokensAddresses = await this . getSupportedTokensByBlockchain ( blockchain ) ; if ( ! supportedTokensAddresses . includes ( fromTokenAddress . toLowerCase ( ) ) || ! supportedTokensAddresses . includes ( toTokenAddress . toLowerCase ( ) ) ) { throw new OneinchNotSupportedTokens ( ) ; } const amountAbsolute = Web3Pure . toWei ( fromAmount , fromToken . decimals ) ; const { estimatedGas , toTokenAmount , path } = await this . getTradeInfo ( blockchain , fromTokenAddress , toTokenAddress , amountAbsolute ) ; const instantTrade = { blockchain , from : { token : fromToken , amount : fromAmount } , to : { token : toToken , amount : Web3Pure . fromWei ( toTokenAmount , toToken . decimals ) } , path } ; if ( ! shouldCalculateGas ) { return instantTrade ; } if ( BlockchainsInfo . getBlockchainType ( blockchain ) !== 'ethLike' ) { throw new CustomError ( 'Wrong blockchain error' ) ; } const blockchainAdapter = this . publicBlockchainAdapterService [ blockchain ] as EthLikeWeb3Public ; const gasPrice = await blockchainAdapter . getGasPrice ( ) ; const gasPriceInEth = Web3Pure . fromWei ( gasPrice ) ; const gasFeeInEth = gasPriceInEth . multipliedBy ( estimatedGas ) ; const ethPrice = await this . tokensService . getNativeCoinPriceInUsd ( blockchain ) ; const gasFeeInUsd = gasFeeInEth . multipliedBy ( ethPrice ) ; return { ... instantTrade , gasLimit : estimatedGas . toFixed ( 0 ) , gasPrice , gasFeeInUsd , gasFeeInEth } ; } private async getTradeInfo ( blockchain , fromTokenAddress , toTokenAddress , amountAbsolute ) { const blockchainId = BlockchainsInfo . getBlockchainByName ( blockchain ) . id ; const quoteTradeParams = { params : { fromTokenAddress , toTokenAddress , amount : amountAbsolute } } ; if ( this . settings . disableMultihops ) { quoteTradeParams . params . mainRouteParts = '1' ; } let oneInchTrade ; let estimatedGas ; let toTokenAmount ; try { if ( ! this . walletAddress ) { throw new Error ( 'User has not connected' ) ; } if ( fromTokenAddress !== this . oneInchNativeAddress ) { const allowance = await this . getAllowance ( blockchain , fromTokenAddress ) . toPromise ( ) ; if ( new BigNumber ( amountAbsolute ) . gt ( allowance ) ) { throw new Error ( 'User have no allowance' ) ; } } const swapTradeParams = { params : { ... quoteTradeParams . params , slippage : this . settings . slippageTolerance . toString ( ) , fromAddress : this . walletAddress } } ; oneInchTrade = await this . httpClient . get < OneinchSwapResponse > ( `${ this . apiBaseUrl }${ blockchainId }/swap` , swapTradeParams ) . toPromise ( ) ; estimatedGas = new BigNumber ( oneInchTrade . tx . gas ) ; toTokenAmount = oneInchTrade . toTokenAmount ; } catch ( _err ) { oneInchTrade = await this . httpClient . get < OneinchQuoteResponse > ( `${ this . apiBaseUrl }${ blockchainId }/quote` , quoteTradeParams ) . toPromise ( ) ; if ( oneInchTrade . hasOwnProperty ( 'errors' ) || ! oneInchTrade . toTokenAmount ) { throw new OneinchQuoteError ( ) ; } estimatedGas = new BigNumber ( oneInchTrade . estimatedGas ) ; toTokenAmount = oneInchTrade . toTokenAmount ; } const path = await this . extractPath ( blockchain , fromTokenAddress , toTokenAddress , oneInchTrade ) ; return { estimatedGas , toTokenAmount , path } ; } return private async extractPath ( blockchain , fromTokenAddress , toTokenAddress , oneInchTrade ) { const addressesPath = [ fromTokenAddress ] ; addressesPath . push ( ... oneInchTrade . protocols [ 0 ] . map ( protocol => protocol [ 0 ] . toTokenAddress ) ) ; addressesPath . pop ( ) ; addressesPath . push ( toTokenAddress ) ; const promises = addressesPath . map ( async ( wrappedTokenAddress , index ) => { const tokenAddress = wrappedTokenAddress === this . oneInchNativeAddress ? NATIVE_TOKEN_ADDRESS : wrappedTokenAddress ; let symbol = await this . tokensService . getTokenSymbol ( blockchain , tokenAddress ) ; if ( index !== 0 && index !== addressesPath . length - 1 && tokenAddress === NATIVE_TOKEN_ADDRESS ) { symbol = `W${ symbol }` ; } return symbol ; } ) ; try { const symbols = await Promise . all ( promises ) ; return symbols . map ( ( symbol , index ) => ( { address : addressesPath [ index ] , symbol } ) ) ; } catch ( _err ) { return [ ] ; } } public async createTrade ( trade , options ) { const { blockchain } = trade ; this . walletConnectorService . checkSettings ( blockchain ) ; const web3Public = this . publicBlockchainAdapterService [ trade . blockchain ] ; await web3Public . checkBalance ( trade . from . token , trade . from . amount , this . walletAddress ) ; const { fromTokenAddress , toTokenAddress } = this . getOneInchTokenSpecificAddresses ( trade . from . token . address , trade . to . token . address ) ; const fromAmountAbsolute = Web3Pure . toWei ( trade . from . amount , trade . from . token . decimals ) ; const blockchainId = BlockchainsInfo . getBlockchainByName ( blockchain ) . id ; const swapTradeParams = { params : { fromTokenAddress , toTokenAddress , amount : fromAmountAbsolute , slippage : this . settings . slippageTolerance . toString ( ) , fromAddress : this . walletAddress } } ; if ( this . settings . disableMultihops ) { swapTradeParams . params . mainRouteParts = '1' ; } const oneInchTrade = ( await this . httpClient . get ( `${ this . apiBaseUrl }${ blockchainId }/swap` , swapTradeParams ) . pipe ( catchError ( ( err ) => this . specifyError ( err as HttpErrorResponse , blockchain ) ) ) . toPromise ( ) ) as OneinchSwapResponse ; const trxOptions = { onTransactionHash : options . onConfirm , data : oneInchTrade . tx . data , gas : oneInchTrade . tx . gas . toString ( ) , inWei : fromTokenAddress === this . oneInchNativeAddress || undefined , ... ( trade . gasPrice && { gasPrice : trade . gasPrice } ) } ; return this . web3Private . trySendTransaction ( oneInchTrade . tx . to , fromTokenAddress !== this . oneInchNativeAddress ? '0' : fromAmountAbsolute , trxOptions ) ; } private specifyError ( err , blockchain ) { if ( err instanceof HttpErrorResponse ) { if ( err . error . message ?. includes ( 'cannot estimate' ) ) { const nativeToken = networks . find ( el => el . name === blockchain ) . nativeCoin . symbol ; const message = `1inch sets increased costs on gas fee. For transaction enter less ${ nativeToken } amount or top up your ${ nativeToken } balance.` ; throw new CustomError ( message ) ; } if ( err . error . message ?. includes ( 'insufficient funds for transfer' ) ) { const nativeToken = networks . find ( el => el . name === blockchain ) . nativeCoin . symbol ; throw new InsufficientFundsOneinchError ( nativeToken ) ; } if ( err . error . description ?. includes ( 'cannot estimate' ) ) { throw new TokenWithFeeError ( ) ; } throw new CustomError ( err . error . message ) ; } if ( err instanceof Error ) { throw new CustomError ( err . message ) ; } throw new CustomError ( err ?. toString ( ) ) ; } }