import { Injectable } from '@angular/core' ; import { BehaviorSubject , forkJoin , from , Observable , of , Subject } from 'rxjs' ; import { List } from 'immutable' ; import { TokenAmount } from '@shared/models/tokens/token-amount' ; import { COINGECKO_TEST_TOKENS } from 'src/test/tokens/test-tokens' ; import { AuthService } from 'src/app/core/services/auth/auth.service' ; import { UseTestingModeService } from 'src/app/core/services/use-testing-mode/use-testing-mode.service' ; import { TokensApiService } from 'src/app/core/services/backend/tokens-api/tokens-api.service' ; import { BLOCKCHAIN_NAME } from '@shared/models/blockchain/blockchain-name' ; import { Token } from '@shared/models/tokens/token' ; import BigNumber from 'bignumber.js' ; import { PublicBlockchainAdapterService } from '@core/services/blockchain/blockchain-adapters/public-blockchain-adapter.service' ; import { EthLikeWeb3Public } from 'src/app/core/services/blockchain/blockchain-adapters/eth-like/web3-public/eth-like-web3-public' ; import { catchError , map , switchMap , tap } from 'rxjs/operators' ; import { CoingeckoApiService } from 'src/app/core/services/external-api/coingecko-api/coingecko-api.service' ; import { NATIVE_TOKEN_ADDRESS , NATIVE_SOLANA_MINT_ADDRESS } from '@shared/constants/blockchain/native-token-address' ; import { TOKENS_PAGINATION } from '@core/services/tokens/tokens-pagination' ; import { TokensRequestQueryOptions } from 'src/app/core/services/backend/tokens-api/models/tokens' ; import { PAGINATED_BLOCKCHAIN_NAME , TokensNetworkState } from 'src/app/shared/models/tokens/paginated-tokens' ; import { DEFAULT_TOKEN_IMAGE } from '@shared/constants/tokens/default-token-image' ; import { compareAddresses , compareTokens } from '@shared/utils/utils' ; import { ErrorsService } from '@core/errors/errors.service' ; import { WalletConnectorService } from '@core/services/blockchain/wallets/wallet-connector-service/wallet-connector.service' ; import { BlockchainsInfo } from '@core/services/blockchain/blockchain-info' ; import { MinimalToken } from '@shared/models/tokens/minimal-token' ; import { Web3Pure } from '@core/services/blockchain/blockchain-adapters/common/web3-pure' ; /**
 * Service that contains actions (transformations and fetch) with tokens.
 */ @ Injectable ( { providedIn : 'root' } ) export class TokensService { /**
     * Current tokens list state.
     */ private readonly _tokens$ = new BehaviorSubject < List < TokenAmount > > ( List ( [ ] ) ) ; public readonly tokens$ = this . _tokens$ . asObservable ( ) ; /**
     * Current favorite tokens list state.
     */ public readonly _favoriteTokens$ = new BehaviorSubject < List < TokenAmount > > ( List ( ) ) ; public readonly favoriteTokens$ = this . _favoriteTokens$ . asObservable ( ) ; /**
     * Current tokens request options state.
     */ private readonly _tokensRequestParameters$ = new Subject < { [ p ] : unknown ; } > ( ) ; /**
     * Current tokens network state.
     */ private readonly _tokensNetworkState$ = new BehaviorSubject < TokensNetworkState > ( TOKENS_PAGINATION ) ; public readonly tokensNetworkState$ = this . _tokensNetworkState$ . asObservable ( ) ; /**
     * Current user address.
     */ private userAddress : string ; /**
     * Is testing mode currently activated.
     */ private isTestingMode = false ; /**
     * Amount of test tokens.
     */ private readonly testTokensNumber : number ; /**
     * Current tokens list.
     */ get tokens ( ) : List < TokenAmount > { return this . _tokens$ . getValue ( ) ; } /**
     * Current favorite tokens list.
     */ get favoriteTokens ( ) : List < TokenAmount > { return this . _favoriteTokens$ . getValue ( ) ; } /**
     * Sets new tokens request options.
     */ set tokensRequestParameters ( parameters ) { this . _tokensRequestParameters$ . next ( parameters ) ; } /**
     * Checks if two tokens are equal.
     */ public static areTokensEqual ( token0 , token1 ) { return ( token0 ?. blockchain === token1 ?. blockchain && token0 ?. address . toLowerCase ( ) === token1 ?. address . toLowerCase ( ) ) ; } constructor ( private readonly tokensApiService , private readonly authService , private readonly publicBlockchainAdapterService , private readonly useTestingMode , private readonly coingeckoApiService , private readonly errorsService , private readonly walletConnectorService ) { this . testTokensNumber = COINGECKO_TEST_TOKENS . length ; this . setupSubscriptions ( ) ; } /**
     * Setups service subscriptions.
     */ private setupSubscriptions ( ) { this . _tokensRequestParameters$ . pipe ( switchMap ( params => this . tokensApiService . getTokensList ( params ) ) , switchMap ( tokens => { if ( ! this . isTestingMode ) { const newTokens = this . setDefaultTokensParams ( tokens , false ) ; return this . calculateTokensBalancesByType ( 'default' , newTokens ) ; } return of ( ) ; } ) , catchError ( ( err ) => { console . error ( 'Error retrieving tokens' , err ) ; return of ( ) ; } ) ) . subscribe ( ) ; this . authService . getCurrentUser ( ) . subscribe ( async ( user ) => { this . userAddress = user ?. address ; await this . calculateTokensBalancesByType ( 'default' ) ; if ( this . userAddress ) { this . fetchFavoriteTokens ( ) ; } else { this . _favoriteTokens$ . next ( List ( [ ] ) ) ; } } ) ; this . useTestingMode . isTestingMode . subscribe ( async ( isTestingMode ) => { if ( isTestingMode ) { this . isTestingMode = true ; this . _tokens$ . next ( List ( COINGECKO_TEST_TOKENS ) ) ; await this . calculateTokensBalancesByType ( 'default' ) ; } } ) ; this . _tokensRequestParameters$ . next ( undefined ) ; } public fetchFavoriteTokens ( ) { this . tokensApiService . fetchFavoriteTokens ( ) . subscribe ( tokens => this . calculateTokensBalancesByType ( 'favorite' , tokens ) ) ; } param tokens param isFavorite private setDefaultTokensParams ( tokens , isFavorite ) { return tokens . map ( token => ( { ... token , amount : new BigNumber ( NaN ) , favorite : isFavorite } ) ) ; } /**
     * Calculates tokens balances for default and favorite tokens.
     */ public async calculateTokensBalances ( ) { await this . calculateTokensBalancesByType ( 'default' ) ; await this . calculateTokensBalancesByType ( 'favorite' ) ; } param type param oldTokens public async calculateTokensBalancesByType ( type , oldTokens ? ) { const subject$ = type === 'favorite' ? this . _favoriteTokens$ : this . _tokens$ ; const tokens = oldTokens || subject$ . value ; if ( type === 'default' ) { if ( ! tokens . size ) { return ; } if ( ! this . userAddress ) { this . _tokens$ . next ( this . setDefaultTokensParams ( tokens , false ) ) ; return ; } } else if ( ! tokens . size || ! this . userAddress ) { this . _favoriteTokens$ . next ( List ( [ ] ) ) ; return ; } const newTokens = this . setDefaultTokensParams ( tokens , type === 'favorite' ) ; const tokensWithBalance = await this . getTokensWithBalance ( newTokens as List < TokenAmount > ) ; if ( ! this . isTestingMode || ( this . isTestingMode && tokens . size <= this . testTokensNumber ) ) { const updatedTokens = tokens . map ( token => { const currentToken = this . tokens . find ( t => TokensService . areTokensEqual ( token , t ) ) ; const balance = tokensWithBalance . find ( tWithBalance => TokensService . areTokensEqual ( token , tWithBalance ) ) ?. amount ; return { ... token , ... currentToken , amount : balance || new BigNumber ( NaN ) } ; } ) ; subject$ . next ( List ( updatedTokens ) ) ; } } param tokens return private async getTokensWithBalance ( tokens ) { const blockchains = this . walletConnectorService . getBlockchainsBasedOnWallet ( ) ; const balances$ = blockchains . map ( blockchain => { const tokensAddresses = tokens . filter ( token => token . blockchain === blockchain ) . map ( token => token . address ) . toArray ( ) ; return this . publicBlockchainAdapterService [ blockchain ] . getTokensBalances ( this . userAddress , tokensAddresses ) ; } ) ; const balancesSettled = await Promise . allSettled ( balances$ ) ; return blockchains . map ( ( blockchain , blockchainIndex ) => { if ( balancesSettled [ blockchainIndex ] . status === 'fulfilled' ) { const balances = ( balancesSettled [ blockchainIndex ] as PromiseFulfilledResult < BigNumber [ ] > ) . value ; return tokens . filter ( token => token . blockchain === blockchain ) . map ( ( token , tokenIndex ) => ( { ... token , amount : Web3Pure . fromWei ( balances [ tokenIndex ] , token . decimals ) || undefined } ) ) . toArray ( ) ; } return null ; } ) . filter ( t => t !== null ) . flat ( ) ; } param address param blockchain return public addTokenByAddress ( address , blockchain ) { const blockchainAdapter = this . publicBlockchainAdapterService [ blockchain ] ; const balance$ = this . userAddress ? from ( blockchainAdapter . getTokenBalance ( this . userAddress , address ) ) : of ( null ) ; return forkJoin ( [ blockchainAdapter . getTokenInfo ( address ) , balance$ ] ) . pipe ( map ( ( [ tokenInfo , amount ] ) => ( { blockchain , address , name : tokenInfo . name , symbol : tokenInfo . symbol , decimals : tokenInfo . decimals , image : '' , rank : 1 , price : null , usedInIframe : true , amount : amount || new BigNumber ( NaN ) } ) ) , tap ( ( token ) => this . _tokens$ . next ( this . tokens . push ( token ) ) ) ) ; } param token public addToken ( token ) { if ( ! this . tokens . find ( t => TokensService . areTokensEqual ( t , token ) ) ) { this . _tokens$ . next ( this . tokens . push ( token ) ) ; } } param token public patchToken ( token ) { this . _tokens$ . next ( this . tokens . filter ( t => ! TokensService . areTokensEqual ( t , token ) ) . push ( token ) ) ; } param $event param token public onTokenImageError ( $event , token = null ) { const target = $event . target as HTMLImageElement ; if ( target . src !== DEFAULT_TOKEN_IMAGE ) { target . src = DEFAULT_TOKEN_IMAGE ; if ( token ) { const newToken = { ... token , image : DEFAULT_TOKEN_IMAGE } ; this . patchToken ( newToken ) ; } } } param blockchain public getNativeCoinPriceInUsd ( blockchain ) { let nativeCoinAddress ; const blockchainType = BlockchainsInfo . getBlockchainType ( blockchain ) ; if ( blockchainType === 'solana' ) { nativeCoinAddress = NATIVE_SOLANA_MINT_ADDRESS ; } else if ( blockchainType === 'ethLike' ) { nativeCoinAddress = NATIVE_TOKEN_ADDRESS ; } const nativeCoin = this . tokens . find ( token => TokensService . areTokensEqual ( token , { blockchain , address : nativeCoinAddress } ) ) ; return this . coingeckoApiService . getNativeCoinPrice ( blockchain ) . pipe ( map ( price => price || nativeCoin ?. price ) ) . toPromise ( ) ; } param token param searchBackend public getAndUpdateTokenPrice ( token , searchBackend = false ) { return this . coingeckoApiService . getCommonTokenOrNativeCoinPrice ( token ) . pipe ( map ( tokenPrice => { if ( tokenPrice ) { return tokenPrice ; } const foundToken = this . tokens . find ( t => TokensService . areTokensEqual ( t , token ) ) ; return foundToken ?. price ; } ) , switchMap ( tokenPrice => { if ( ! tokenPrice && searchBackend ) { return this . fetchQueryTokens ( token . address , token . blockchain as PAGINATED_BLOCKCHAIN_NAME ) . pipe ( map ( backendTokens => backendTokens . get ( 0 ) ?. price ) ) ; } return of ( tokenPrice ) ; } ) , tap ( tokenPrice => { if ( tokenPrice ) { const foundToken = this . tokens . find ( t => TokensService . areTokensEqual ( t , token ) ) ; if ( foundToken && tokenPrice !== foundToken . price ) { const newToken = { ... foundToken , price : tokenPrice } ; this . _tokens$ . next ( this . tokens . filter ( tokenAmount => ! TokensService . areTokensEqual ( tokenAmount , token ) ) . push ( newToken ) ) ; } } } ) ) . toPromise ( ) ; } param token public async getAndUpdateTokenBalance ( token ) { if ( ! this . userAddress ) { return null ; } try { const blockchainAdapter = this . publicBlockchainAdapterService [ token . blockchain ] ; const balanceInWei = await blockchainAdapter . getTokenOrNativeBalance ( this . userAddress , token . address ) ; const foundToken = this . tokens . find ( t => TokensService . areTokensEqual ( t , token ) ) ; if ( ! foundToken ) { return new BigNumber ( NaN ) ; } const balance = Web3Pure . fromWei ( balanceInWei , foundToken . decimals ) ; if ( ! foundToken . amount . eq ( balance ) ) { const newToken = { ... foundToken , amount : balance } ; this . _tokens$ . next ( this . tokens . filter ( tokenAmount => ! TokensService . areTokensEqual ( tokenAmount , token ) ) . push ( newToken ) ) ; } return new BigNumber ( balance ) ; } catch ( err ) { console . debug ( err ) ; const foundToken = this . tokens . find ( t => TokensService . areTokensEqual ( t , token ) ) ; return foundToken ?. amount ; } } param token param searchBackend public async getTokenByAddress ( token , searchBackend = true ) { const foundToken = this . tokens . find ( t => TokensService . areTokensEqual ( t , token ) ) ; if ( foundToken ) { return foundToken ; } if ( searchBackend ) { return this . fetchQueryTokens ( token . address , token . blockchain as PAGINATED_BLOCKCHAIN_NAME ) . pipe ( map ( backendTokens => backendTokens . get ( 0 ) ) ) . toPromise ( ) ; } return null ; } param network param next private updateNetworkPage ( network , next ) { const oldState = this . _tokensNetworkState$ . value ; const newState = { ... oldState , [ network ] : { ... oldState [ network ] , page : oldState [ network ] . page + 1 , maxPage : next ? oldState [ network ] . maxPage + 1 : oldState [ network ] . maxPage } } ; this . _tokensNetworkState$ . next ( newState ) ; } param network public fetchNetworkTokens ( network ) { this . tokensApiService . fetchSpecificBackendTokens ( { network , page : this . _tokensNetworkState$ . value [ network ] . page } ) . pipe ( tap ( tokensResponse => this . updateNetworkPage ( network , tokensResponse . next ) ) , map ( tokensResponse => ( { ... tokensResponse , result : tokensResponse . result . map ( token => ( { ... token , amount : new BigNumber ( NaN ) , favorite : false } ) ) } ) ) , switchMap ( tokens => { return this . userAddress ? this . getTokensWithBalance ( tokens . result ) : of ( tokens . result ) ; } ) ) . subscribe ( ( tokens ) => { this . _tokens$ . next ( this . tokens . concat ( tokens ) ) ; } ) ; } param query param network public fetchQueryTokens ( query , network ) { const isAddress = query . includes ( '0x' ) ; const params = { network , ... ( ! isAddress && { symbol : query } ) , ... ( isAddress && { address : query } ) } ; return this . tokensApiService . fetchQueryTokens ( params ) ; } param favoriteToken public addFavoriteToken ( favoriteToken ) { return this . tokensApiService . addFavoriteToken ( favoriteToken ) . pipe ( tap ( ( ) => { if ( ! this . _favoriteTokens$ . value . some ( token => compareTokens ( token , favoriteToken ) ) ) { this . _favoriteTokens$ . next ( this . _favoriteTokens$ . value . push ( favoriteToken ) ) ; } } ) ) ; } param token public removeFavoriteToken ( token ) { const filteredTokens = this . _favoriteTokens$ . value . filter ( el => ! TokensService . areTokensEqual ( el , token ) ) ; return this . tokensApiService . deleteFavoriteToken ( token ) . pipe ( tap ( ( ) => { if ( this . _favoriteTokens$ . value . some ( favoriteToken => compareTokens ( token , favoriteToken ) ) ) { this . _favoriteTokens$ . next ( filteredTokens ) ; } } ) ) ; } /**
     * Gets symbol of token, using currently stored tokens or blockchain request.
     */ public async getTokenSymbol ( blockchain , tokenAddress ) { const foundToken = this . tokens . find ( token => token . blockchain === blockchain && compareAddresses ( token . address , tokenAddress ) ) ; if ( foundToken ) { return foundToken ?. symbol ; } BlockchainsInfo . checkIsEthLike ( blockchain ) ; const blockchainAdapter = this . publicBlockchainAdapterService [ blockchain ] as EthLikeWeb3Public ; return blockchainAdapter . getTokenSymbol ( tokenAddress ) ; } }