diff --git a/src/app/core/services/tokens/balance-loader.service.ts b/src/app/core/services/tokens/balance-loader.service.ts index 2a7c00bb29..768b39dd36 100644 --- a/src/app/core/services/tokens/balance-loader.service.ts +++ b/src/app/core/services/tokens/balance-loader.service.ts @@ -1,47 +1,117 @@ import { Injectable } from '@angular/core'; import { Token } from '@app/shared/models/tokens/token'; import { TokenAmount } from '@app/shared/models/tokens/token-amount'; -import { isTokenAmount } from '@app/shared/utils/is-token'; import BigNumber from 'bignumber.js'; import { List } from 'immutable'; import { BlockchainName, BlockchainsInfo, Injector, Web3Public, Web3Pure } from 'rubic-sdk'; import { AuthService } from '../auth/auth.service'; import pTimeout from 'rubic-sdk/lib/common/utils/p-timeout'; +import { ChainsToLoadFirstly, isTopChain } from './constants/first-loaded-chains'; +import { TokensUpdaterService } from './tokens-updater.service'; +import { AssetType } from '@app/features/trade/models/asset'; +import { BehaviorSubject } from 'rxjs'; + +type TokensListOfTopChainsWithOtherChains = { + [key in BlockchainName]: Token[]; +} & { + TOP_CHAINS: { + [key in ChainsToLoadFirstly]: Token[]; + }; +}; @Injectable({ providedIn: 'root' }) export class BalanceLoaderService { - constructor(private readonly authService: AuthService) {} + constructor( + private readonly authService: AuthService, + private readonly tokensUpdaterService: TokensUpdaterService + ) {} - public async getTokensWithBalancesAndPatchImmediately( + public updateAllChainsTokensWithBalances( tokensList: List, - _patchTokensInGeneralList: ( + patchTokensInGeneralList: ( tokensWithBalances: List, patchAllChains: boolean - ) => void - ): Promise> { - const tokensByChain: Record = {} as Record< - BlockchainName | 'TOP_CHAINS', - Token[] - >; - // @TODO handle TOP_CHAINS firstly + ) => void, + isBalanceAlreadyCalculatedForChain: Record, + isBalanceLoading$: Record> + ): void { + const tokensByChain: TokensListOfTopChainsWithOtherChains = { + TOP_CHAINS: {} + } as TokensListOfTopChainsWithOtherChains; for (const token of tokensList) { - const chainTokensList = tokensByChain[token.blockchain]; - if (!Array.isArray(chainTokensList)) { - tokensByChain[token.blockchain] = [] as Token[]; - } - if (isTokenAmount(token)) { - tokensByChain[token.blockchain].push(token); + if (isTopChain(token.blockchain)) { + if (!Array.isArray(tokensByChain.TOP_CHAINS[token.blockchain])) { + tokensByChain.TOP_CHAINS[token.blockchain] = [] as Token[]; + } + tokensByChain.TOP_CHAINS[token.blockchain].push(token); } else { - tokensByChain[token.blockchain].push( - this.getTokensWithNullBalances(List([token]), false).get(0) - ); + if (!Array.isArray(tokensByChain[token.blockchain])) { + tokensByChain[token.blockchain] = [] as Token[]; + } + tokensByChain[token.blockchain].push(token); } } - return List([]); + for (const key in tokensByChain) { + if (key === 'TOP_CHAINS') { + const topChainsTokens = tokensByChain.TOP_CHAINS; + const promises = Object.entries(topChainsTokens).map( + ([chain, tokens]: [BlockchainName, Token[]]) => { + if (!this.isChainSupportedByWallet(chain)) return tokens.map(() => new BigNumber(NaN)); + + const web3Public = Injector.web3PublicService.getWeb3Public(chain) as Web3Public; + return web3Public + .getTokensBalances( + this.authService.userAddress, + tokens.map(t => t.address) + ) + .catch(() => tokens.map(() => new BigNumber(NaN))); + } + ); + + Promise.all(promises).then(balances => { + const flattenBalances = balances.flat(); + const flattenTokens = Object.values(topChainsTokens).flat(); + const tokensWithBalances = flattenTokens.map((token, idx) => ({ + ...token, + amount: flattenBalances[idx] + ? Web3Pure.fromWei(flattenBalances[idx], token.decimals) + : new BigNumber(NaN) + })) as TokenAmount[]; + + patchTokensInGeneralList(List(tokensWithBalances), true); + this.tokensUpdaterService.triggerUpdateTokens(); + + isBalanceAlreadyCalculatedForChain.allChains = true; + isBalanceLoading$.allChains.next(false); + }); + } else { + const chain = key as Exclude; + const chainTokens = tokensByChain[chain]; + const web3Public = Injector.web3PublicService.getWeb3Public(chain) as Web3Public; + + web3Public + .getTokensBalances( + this.authService.userAddress, + chainTokens.map(t => t.address) + ) + .catch(() => chainTokens.map(() => new BigNumber(NaN))) + .then(balances => { + const tokensWithBalances = chainTokens.map((token, idx) => ({ + ...token, + amount: balances[idx] + ? Web3Pure.fromWei(balances[idx], token.decimals) + : new BigNumber(NaN) + })) as TokenAmount[]; + + patchTokensInGeneralList(List(tokensWithBalances), true); + this.tokensUpdaterService.triggerUpdateTokens(); + }); + } + } } /** @@ -58,22 +128,13 @@ export class BalanceLoaderService { if (!Array.isArray(chainTokensList)) { tokensByChain[token.blockchain] = [] as Token[]; } - if (isTokenAmount(token)) { - tokensByChain[token.blockchain].push(token); - } else { - tokensByChain[token.blockchain].push( - this.getTokensWithNullBalances(List([token]), false).get(0) - ); - } + tokensByChain[token.blockchain].push(token); } try { const promises = Object.entries(tokensByChain).map( ([chain, tokens]: [BlockchainName, Token[]]) => { - const doesWalletSupportsTokenChain = - BlockchainsInfo.getChainType(chain) === this.authService.userChainType; - // if EVM-address used -> it will fetch only evm address etc. - if (!doesWalletSupportsTokenChain) return tokens.map(() => new BigNumber(NaN)); + if (!this.isChainSupportedByWallet(chain)) return tokens.map(() => new BigNumber(NaN)); const web3Public = Injector.web3PublicService.getWeb3Public(chain) as Web3Public; const chainBalancesPromise = web3Public.getTokensBalances( @@ -121,4 +182,9 @@ export class BalanceLoaderService { favorite: isFavorite })); } + + /* if EVM-address used -> it will fetch only evm address etc. */ + private isChainSupportedByWallet(chain: BlockchainName): boolean { + return BlockchainsInfo.getChainType(chain) === this.authService.userChainType; + } } diff --git a/src/app/core/services/tokens/constants/first-loaded-chains.ts b/src/app/core/services/tokens/constants/first-loaded-chains.ts index 3fd7e2f4e0..5cd9de190c 100644 --- a/src/app/core/services/tokens/constants/first-loaded-chains.ts +++ b/src/app/core/services/tokens/constants/first-loaded-chains.ts @@ -1,4 +1,4 @@ -import { BLOCKCHAIN_NAME } from 'rubic-sdk'; +import { BLOCKCHAIN_NAME, BlockchainName } from 'rubic-sdk'; export const CHAINS_TO_LOAD_FIRSTLY = [ BLOCKCHAIN_NAME.ETHEREUM, @@ -8,3 +8,9 @@ export const CHAINS_TO_LOAD_FIRSTLY = [ BLOCKCHAIN_NAME.BASE, BLOCKCHAIN_NAME.ZK_SYNC ] as const; + +export type ChainsToLoadFirstly = (typeof CHAINS_TO_LOAD_FIRSTLY)[number]; + +export function isTopChain(blockchain: BlockchainName): blockchain is ChainsToLoadFirstly { + return CHAINS_TO_LOAD_FIRSTLY.some(topChain => topChain === blockchain); +} diff --git a/src/app/core/services/tokens/tokens-store.service.ts b/src/app/core/services/tokens/tokens-store.service.ts index b216a157b5..bc99ba02fc 100644 --- a/src/app/core/services/tokens/tokens-store.service.ts +++ b/src/app/core/services/tokens/tokens-store.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject, firstValueFrom, forkJoin, from, iif, Observable, of } from 'rxjs'; +import { BehaviorSubject, firstValueFrom, forkJoin, from, Observable, of } from 'rxjs'; import { List } from 'immutable'; import { TokenAmount } from '@shared/models/tokens/token-amount'; -import { catchError, debounceTime, first, map, switchMap, tap } from 'rxjs/operators'; +import { map, switchMap, tap } from 'rxjs/operators'; import { TokensApiService } from '@core/services/backend/tokens-api/tokens-api.service'; import { AuthService } from '@core/services/auth/auth.service'; import { Token } from '@shared/models/tokens/token'; @@ -180,7 +180,45 @@ export class TokensStoreService { ).flat(); } - public startBalanceCalculating(blockchain: AssetType): void { + // public startBalanceCalculating(blockchain: AssetType): void { + // if (this.isBalanceAlreadyCalculatedForChain[blockchain]) { + // console.log( + // `%cSKIPPED FOR ${blockchain}`, + // 'color: red; font-size: 20px;', + // this.isBalanceAlreadyCalculatedForChain + // ); + // return; + // } + + // const tokensList$: Observable> = iif( + // () => blockchain === 'allChains', + // this.allChainsTokens$, + // this.tokens$.pipe( + // first(v => Boolean(v)), + // map(tokens => tokens.filter(t => t.blockchain === blockchain)) + // ) + // ); + + // forkJoin([firstValueFrom(tokensList$), firstValueFrom(this.authService.currentUser$)]) + // .pipe( + // switchMap(([tokens, user]) => { + // if (!user) return of(this.balanceLoaderService.getTokensWithNullBalances(tokens, false)); + + // this._isBalanceLoading$[blockchain].next(true); + // this.isBalanceAlreadyCalculatedForChain[blockchain] = true; + + // return this.balanceLoaderService.getTokensWithBalance(tokens); + // }), + // catchError(() => of(List())) + // ) + // .subscribe((tokensWithBalances: List) => { + // this.patchTokensBalances(tokensWithBalances, blockchain === 'allChains'); + // this.tokensUpdaterService.triggerUpdateTokens(); + // this._isBalanceLoading$[blockchain].next(false); + // }); + // } + + public async startBalanceCalculating(blockchain: AssetType): Promise { if (this.isBalanceAlreadyCalculatedForChain[blockchain]) { console.log( `%cSKIPPED FOR ${blockchain}`, @@ -189,34 +227,33 @@ export class TokensStoreService { ); return; } + const tokensList = + blockchain === 'allChains' + ? this.allChainsTokens + : this.tokens.filter(t => t.blockchain === blockchain); + + if (!this.authService.user) { + const nullTokens = this.balanceLoaderService.getTokensWithNullBalances(tokensList, false); + this.patchTokensBalances(nullTokens, blockchain === 'allChains'); + this.tokensUpdaterService.triggerUpdateTokens(); + return; + } - const tokensList$: Observable> = iif( - () => blockchain === 'allChains', - this.allChainsTokens$, - this.tokens$.pipe( - first(v => Boolean(v)), - map(tokens => tokens.filter(t => t.blockchain === blockchain)) - ) - ); - - forkJoin([firstValueFrom(tokensList$), firstValueFrom(this.authService.currentUser$)]) - .pipe( - debounceTime(500), - switchMap(([tokens, user]) => { - if (!user) return of(this.balanceLoaderService.getTokensWithNullBalances(tokens, false)); - - this._isBalanceLoading$[blockchain].next(true); - this.isBalanceAlreadyCalculatedForChain[blockchain] = true; - - return this.balanceLoaderService.getTokensWithBalance(tokens); - }), - catchError(() => of(List())) - ) - .subscribe((tokensWithBalances: List) => { - this.patchTokensBalances(tokensWithBalances, blockchain === 'allChains'); - this.tokensUpdaterService.triggerUpdateTokens(); - this._isBalanceLoading$[blockchain].next(false); - }); + this._isBalanceLoading$[blockchain].next(true); + if (blockchain === 'allChains') { + this.balanceLoaderService.updateAllChainsTokensWithBalances( + tokensList, + this.patchTokensBalances.bind(this), + this.isBalanceAlreadyCalculatedForChain, + this._isBalanceLoading$ + ); + } else { + const tokens = await this.balanceLoaderService.getTokensWithBalance(tokensList); + this.patchTokensBalances(tokens, false); + this.tokensUpdaterService.triggerUpdateTokens(); + this._isBalanceLoading$[blockchain].next(false); + this.isBalanceAlreadyCalculatedForChain[blockchain] = true; + } } public resetBalanceCalculatingStatuses(): void {