import { DenomHelper } from "@keplr-wallet/common"; import { QueryError, QueryResponse, QuerySharedContext, StoreUtils, } from "../../../common"; import { ChainGetter, findERC20CosmosBankBalance, shouldQueryERC20WithCosmosBank, } from "../../../chain"; import { computed, makeObservable } from "mobx"; import { CoinPretty, Int } from "@keplr-wallet/unit"; import { BalanceRegistry, IObservableQueryBalanceImpl } from "../../balances"; import { ObservableChainQuery } from "../../chain-query"; import { Balances } from "./types"; import { AppCurrency } from "@keplr-wallet/types"; import { Bech32Address } from "@keplr-wallet/cosmos"; export class ObservableQueryCosmosBalancesImplParent extends ObservableChainQuery { // XXX: See comments below. // The reason why this field is here is that I don't know if it's mobx's bug or intention, // but fetch can be executed twice by observation of parent and child by `onBecomeObserved`, // so fetch should not be overridden in this parent class. public duplicatedFetchResolver?: Promise; constructor( sharedContext: QuerySharedContext, chainId: string, chainGetter: ChainGetter, protected readonly bech32Address: string ) { super( sharedContext, chainId, chainGetter, `/cosmos/bank/v1beta1/balances/${bech32Address}?pagination.limit=1000` ); makeObservable(this); } protected override canFetch(): boolean { // If bech32 address is empty, it will always fail, so don't need to fetch it. return this.bech32Address.length > 0; } protected override onReceiveResponse( response: Readonly> ) { super.onReceiveResponse(response); const mcInfo2 = this.chainGetter.getModularChain(this.chainId); const balances = response.data.balances ?? []; const denoms = balances.map((coin) => coin.denom); mcInfo2.addUnknownDenoms(...denoms); } } export class ObservableQueryCosmosBalancesImpl implements IObservableQueryBalanceImpl { constructor( protected readonly parent: ObservableQueryCosmosBalancesImplParent, protected readonly chainId: string, protected readonly chainGetter: ChainGetter, protected readonly denomHelper: DenomHelper ) { makeObservable(this); } @computed get balance(): CoinPretty { const currency = this.currency; if (!this.response) { return new CoinPretty(currency, new Int(0)).ready(false); } if (this.denomHelper.type === "erc20") { const matchedBalance = findERC20CosmosBankBalance( this.response.data.balances, currency.coinMinimalDenom ); return StoreUtils.getBalanceFromCurrency( currency, matchedBalance ? [{ ...matchedBalance, denom: currency.coinMinimalDenom }] : [] ); } return StoreUtils.getBalanceFromCurrency( currency, this.response.data.balances ?? [] ); } @computed get currency(): AppCurrency { const denom = this.denomHelper.denom; const mcInfo2 = this.chainGetter.getModularChain(this.chainId); return mcInfo2.forceFindCurrency(denom); } get error(): Readonly> | undefined { return this.parent.error; } get isFetching(): boolean { return this.parent.isFetching; } get isObserved(): boolean { return this.parent.isObserved; } get isStarted(): boolean { return this.parent.isStarted; } get response(): Readonly> | undefined { return this.parent.response; } fetch(): Promise { // XXX: The balances of cosmos-sdk can share the result of one endpoint. // This class is implemented for this optimization. // But the problem is that the query store can't handle these process properly right now. // Currently, this is the only use-case, // so We'll manually implement this here. // In the case of fetch(), even if it is executed multiple times, // the actual logic should be processed only once. // So some sort of debouncing is needed. if (!this.parent.duplicatedFetchResolver) { this.parent.duplicatedFetchResolver = new Promise( (resolve, reject) => { (async () => { try { await this.parent.fetch(); this.parent.duplicatedFetchResolver = undefined; resolve(); } catch (e) { this.parent.duplicatedFetchResolver = undefined; reject(e); } })(); } ); return this.parent.duplicatedFetchResolver; } return this.parent.duplicatedFetchResolver; } async waitFreshResponse(): Promise< Readonly> | undefined > { return await this.parent.waitFreshResponse(); } async waitResponse(): Promise> | undefined> { return await this.parent.waitResponse(); } } export class ObservableQueryCosmosBalanceRegistry implements BalanceRegistry { protected parentMap: Map = new Map(); constructor(protected readonly sharedContext: QuerySharedContext) {} getBalanceImpl( chainId: string, chainGetter: ChainGetter, bech32Address: string, minimalDenom: string ): ObservableQueryCosmosBalancesImpl | undefined { const denomHelper = new DenomHelper(minimalDenom); if (denomHelper.type !== "native") { if (denomHelper.type !== "erc20") { return; } const mcInfo = chainGetter.getModularChain(chainId); if ( mcInfo.type !== "ethermint" || !shouldQueryERC20WithCosmosBank(mcInfo.chainIdentifier) ) { return; } } try { Bech32Address.validate(bech32Address); } catch { return; } const key = `${chainId}/${bech32Address}`; if (!this.parentMap.has(key)) { this.parentMap.set( key, new ObservableQueryCosmosBalancesImplParent( this.sharedContext, chainId, chainGetter, bech32Address ) ); } return new ObservableQueryCosmosBalancesImpl( this.parentMap.get(key)!, chainId, chainGetter, denomHelper ); } }