import { DefaultGasPriceStep, FeeType, IBaseAmountConfig, IFeeConfig, IGasConfig, ISenderConfig, UIProperties, } from "./types"; import { TxChainSetter } from "./chain"; import { ChainGetter } from "@keplr-wallet/stores"; import { action, computed, makeObservable, observable } from "mobx"; import { CoinPretty, Dec, DecUtils, Int } from "@keplr-wallet/unit"; import { Currency, FeeCurrency, StdFee, isEthSignChain, } from "@keplr-wallet/types"; import { computedFn } from "mobx-utils"; import { useState } from "react"; import { InsufficientFeeError, InsufficientAmountError, ShouldTopUpWarning, } from "./errors"; import { QueriesStore } from "./internal"; import { DenomHelper } from "@keplr-wallet/common"; export class FeeConfig extends TxChainSetter implements IFeeConfig { /** * This hooks package is for cosmos/ethermint chains. * feeCurrencies exist on cosmos and ethermint types. */ protected get _cosmosFeeCurrencies(): FeeCurrency[] { const u = this.modularChainInfo.unwrapped; if (u.type === "cosmos" || u.type === "ethermint") { return u.cosmos.feeCurrencies; } return []; } @observable.ref protected _fee: | { type: FeeType | "custom"; currency: Currency; } | CoinPretty[] | undefined = undefined; /** * `additionAmountToNeedFee` indicated that the fee config should consider the amount config's amount * when checking that the fee is sufficient to send tx. * If this value is true and if the amount + fee is not sufficient to send tx, it will return error. * Else, only consider the fee without addition the amount. * @protected */ @observable protected additionAmountToNeedFee: boolean = true; @observable protected computeTerraClassicTax: boolean = false; @observable protected _disableBalanceCheck: boolean = false; @observable protected _l1DataFee: Dec | undefined = undefined; @observable protected forceUseAtoneTokenAsFee: boolean = false; @observable protected forceTopUp: boolean = false; constructor( chainGetter: ChainGetter, protected readonly queriesStore: QueriesStore, initialChainId: string, protected readonly senderConfig: ISenderConfig, protected readonly amountConfig: IBaseAmountConfig, protected readonly gasConfig: IGasConfig, additionAmountToNeedFee: boolean = true, computeTerraClassicTax: boolean = false, forceUseAtoneTokenAsFee: boolean = false, forceTopUp: boolean = false ) { super(chainGetter, initialChainId); this.additionAmountToNeedFee = additionAmountToNeedFee; this.computeTerraClassicTax = computeTerraClassicTax; this.forceUseAtoneTokenAsFee = forceUseAtoneTokenAsFee; this.forceTopUp = forceTopUp; makeObservable(this); } @action setAdditionAmountToNeedFee(additionAmountToNeedFee: boolean) { this.additionAmountToNeedFee = additionAmountToNeedFee; } @action setComputeTerraClassicTax(computeTerraClassicTax: boolean) { this.computeTerraClassicTax = computeTerraClassicTax; } @action setForceUseAtoneTokenAsFee(forceUseAtoneTokenAsFee: boolean) { this.forceUseAtoneTokenAsFee = forceUseAtoneTokenAsFee; } @action setForceTopUp(forceTopUp: boolean) { this.forceTopUp = forceTopUp; } @action setDisableBalanceCheck(bool: boolean) { this._disableBalanceCheck = bool; } get disableBalanceCheck(): boolean { return this._disableBalanceCheck; } get type(): FeeType | "manual" | "custom" { if (!this.fee) { return "manual"; } if ("type" in this.fee) { return this.fee.type; } return "manual"; } @computed protected get fee(): | { type: FeeType | "custom"; currency: Currency; } | CoinPretty[] | undefined { if (!this._fee) { return undefined; } if ("type" in this._fee) { const coinMinimalDenom = this._fee.currency.coinMinimalDenom; const feeCurrency = this._cosmosFeeCurrencies.find( (cur) => cur.coinMinimalDenom === coinMinimalDenom ); const currency = this.modularChainInfo.forceFindCurrency(coinMinimalDenom); return { type: this._fee.type, currency: { ...feeCurrency, ...currency, }, }; } return this._fee.map((coin) => { const coinMinimalDenom = coin.currency.coinMinimalDenom; const feeCurrency = this._cosmosFeeCurrencies.find( (cur) => cur.coinMinimalDenom === coinMinimalDenom ); const currency = this.modularChainInfo.forceFindCurrency(coinMinimalDenom); return new CoinPretty( { ...feeCurrency, ...currency, }, coin.toCoin().amount ); }); } @action setFee( fee: | { type: FeeType | "custom"; currency: Currency; } | CoinPretty | CoinPretty[] | undefined ): void { if (fee && "type" in fee) { // Destruct it to ensure ref update. this._fee = { ...fee, }; } else if (fee) { if ("length" in fee) { this._fee = fee; } else { this._fee = [fee]; } } else { this._fee = undefined; } } @computed get selectableFeeCurrencies(): FeeCurrency[] { const u = this.modularChainInfo.unwrapped; if (isEthSignChain(u)) { return this._cosmosFeeCurrencies.slice(0, 1); } if (this.modularChainInfo.chainId === "atomone-1") { //현재 atomone에서는 MsgMintPhoton를 제외하면 ATONE을 fee로 사용해서 안됨, 그래서 하드코딩으로 옵션을 적용 const feeCurrenciesWithoutAtone = this._cosmosFeeCurrencies.filter( (cur) => cur.coinMinimalDenom !== "uatone" ); if ( feeCurrenciesWithoutAtone.length > 0 && !this.forceUseAtoneTokenAsFee ) { return feeCurrenciesWithoutAtone; } } if (this.canOsmosisTxFeesAndReady()) { const queryOsmosis = this.queriesStore.get(this.chainId).osmosis; if (queryOsmosis) { const txFees = queryOsmosis.queryTxFeesFeeTokens; const exists: { [denom: string]: boolean | undefined } = {}; // To reduce the confusion, add the priority to native (not ibc token) currency. // And, put the most priority to the base denom. // Remainings are sorted in alphabetical order. return this._cosmosFeeCurrencies .concat(txFees.feeCurrencies) .filter((cur) => { if (!exists[cur.coinMinimalDenom]) { exists[cur.coinMinimalDenom] = true; return true; } return false; }) .sort((cur1, cur2) => { if ( cur1.coinMinimalDenom === queryOsmosis.queryTxFeesBaseDenom.baseDenom ) { return -1; } if ( cur2.coinMinimalDenom === queryOsmosis.queryTxFeesBaseDenom.baseDenom ) { return 1; } const cur1IsIBCToken = cur1.coinMinimalDenom.startsWith("ibc/"); const cur2IsIBCToken = cur2.coinMinimalDenom.startsWith("ibc/"); if (cur1IsIBCToken && !cur2IsIBCToken) { return 1; } if (!cur1IsIBCToken && cur2IsIBCToken) { return -1; } return cur1.coinMinimalDenom < cur2.coinMinimalDenom ? -1 : 1; }); } } else if (this.canFeeMarketTxFeesAndReady()) { if (this.modularChainInfo.hasFeature("initia-dynamicfee")) { return this._cosmosFeeCurrencies.slice(0, 1); } if (this.modularChainInfo.hasFeature("evm-feemarket")) { return this._cosmosFeeCurrencies.slice(0, 1); } const queryCosmos = this.queriesStore.get(this.chainId).cosmos; if (queryCosmos) { const gasPrices = queryCosmos.queryFeeMarketGasPrices.gasPrices; const found: FeeCurrency[] = []; for (const gasPrice of gasPrices) { const cur = this.modularChainInfo.findCurrency(gasPrice.denom); if (cur) { found.push(cur); } } const firstFeeDenom = this._cosmosFeeCurrencies.length > 0 ? this._cosmosFeeCurrencies[0].coinMinimalDenom : ""; return found.sort((cur1, cur2) => { // firstFeeDenom should be the first. // others should be sorted in alphabetical order. if (cur1.coinMinimalDenom === firstFeeDenom) { return -1; } if (cur2.coinMinimalDenom === firstFeeDenom) { return 1; } return cur1.coinDenom < cur2.coinDenom ? -1 : 1; }); } } const res: FeeCurrency[] = []; for (const feeCurrency of this._cosmosFeeCurrencies) { const cur = this.modularChainInfo.findCurrency( feeCurrency.coinMinimalDenom ); if (cur) { res.push({ ...feeCurrency, ...cur, }); } } return res; } toStdFee(): StdFee { const primitive = this.getFeePrimitive(); return { gas: this.gasConfig.gas.toString(), amount: primitive.map((p) => { return { amount: p.amount, denom: p.currency.coinMinimalDenom, }; }), }; } @computed get fees(): CoinPretty[] { const primitives = this.getFeePrimitive(); return primitives.map((p) => { return new CoinPretty(p.currency, p.amount); }); } getFeePrimitive(): { amount: string; currency: FeeCurrency; }[] { let res: { amount: string; currency: FeeCurrency; }[] = []; // If there is no fee currency, just return with empty fee amount. if (!this.fee) { res = []; } else if ("type" in this.fee) { res = [ { amount: this.getFeeTypePrettyForFeeCurrency( this.fee.currency, this.fee.type ).toCoin().amount, currency: this.fee.currency, }, ]; } else { res = this.fee.map((fee) => { return { amount: fee.toCoin().amount, currency: fee.currency, }; }); } if ( res.length > 0 && this.computeTerraClassicTax && this.modularChainInfo.hasFeature("terra-classic-fee") ) { const etcQueries = this.queriesStore.get(this.chainId).keplrETC; if ( etcQueries && etcQueries.queryTerraClassicTaxRate.response && etcQueries.queryTerraClassicTaxCaps.response ) { const taxRate = etcQueries.queryTerraClassicTaxRate.taxRate; if (taxRate && taxRate.toDec().gt(new Dec(0))) { const sendAmount = this.amountConfig.amount; for (const sendAmt of sendAmount) { if ( new DenomHelper(sendAmt.currency.coinMinimalDenom).type === "native" ) { let tax = sendAmt .toDec() .mul(DecUtils.getTenExponentN(sendAmt.currency.coinDecimals)) .mul(taxRate.toDec()); const taxCap = etcQueries.queryTerraClassicTaxCaps.getTaxCaps( sendAmt.currency.coinMinimalDenom ); if (taxCap && tax.roundUp().gt(taxCap)) { tax = taxCap.toDec(); } const taxAmount = tax.roundUp(); if (taxAmount.isPositive()) { const i = res.findIndex( (f) => f.currency.coinMinimalDenom === sendAmt.currency.coinMinimalDenom ); if (i >= 0) { res[i] = { amount: new Int(res[i].amount).add(taxAmount).toString(), currency: res[i].currency, }; } else { res.push({ amount: taxAmount.toString(), currency: sendAmt.currency, }); } } } } } } } return res; } protected canOsmosisTxFeesAndReady(): boolean { if (this.modularChainInfo.hasFeature("osmosis-txfees")) { const queries = this.queriesStore.get(this.chainId); if (!queries.osmosis) { console.log( "Chain has osmosis-txfees feature. But no osmosis queries provided." ); return false; } const queryBaseDenom = queries.osmosis.queryTxFeesBaseDenom; if ( queryBaseDenom.baseDenom && this._cosmosFeeCurrencies.find( (cur) => cur.coinMinimalDenom === queryBaseDenom.baseDenom ) ) { return true; } } return false; } protected canFeeMarketTxFeesAndReady(): boolean { if (this.modularChainInfo.chainId.startsWith("cheqd-mainnet-")) { return false; } if (this.modularChainInfo.hasFeature("feemarket")) { const queries = this.queriesStore.get(this.chainId); if (!queries.cosmos) { console.log( "Chain has feemarket feature. But no cosmos queries provided." ); return false; } const queryFeeMarketGasPrices = queries.cosmos.queryFeeMarketGasPrices; if (queryFeeMarketGasPrices.gasPrices.length === 0) { return false; } for (let i = 0; i < queryFeeMarketGasPrices.gasPrices.length; i++) { const gasPrice = queryFeeMarketGasPrices.gasPrices[i]; // 일단 모든 currency에 대해서 find를 시도한다. this.modularChainInfo.findCurrency(gasPrice.denom); } return ( queryFeeMarketGasPrices.gasPrices.find((gasPrice) => this.modularChainInfo.findCurrency(gasPrice.denom) ) != null ); } if (this.modularChainInfo.hasFeature("evm-feemarket")) { const queries = this.queriesStore.get(this.chainId); if (!queries.cosmos) { console.log( "Chain has evm-feemarket feature. But no cosmos queries provided." ); return false; } const queryEvmFeeMarketBaseFee = queries.cosmos.queryEvmFeeMarketBaseFee; return queryEvmFeeMarketBaseFee.baseFee != null; } if (this.modularChainInfo.hasFeature("initia-dynamicfee")) { const queries = this.queriesStore.get(this.chainId); if (!queries.keplrETC) { console.log( "Chain has initia-dynamicfee feature. But no initia queries provided." ); return false; } const queryInitiaDynamicFee = queries.keplrETC.queryInitiaDynamicFee; if (!queryInitiaDynamicFee.baseGasPrice) { return false; } return true; } return false; } get l1DataFee(): Dec | undefined { return this._l1DataFee; } @action setL1DataFee(fee: Dec) { this._l1DataFee = fee; } readonly getFeeTypePrettyForFeeCurrency = computedFn( (feeCurrency: FeeCurrency, feeType: FeeType | "custom") => { const gas = this.gasConfig.gas; const gasPrice = this.getGasPriceForFeeCurrency(feeCurrency, feeType); const feeAmount = gasPrice.mul(new Dec(gas)); return new CoinPretty(feeCurrency, feeAmount.roundUp()).maxDecimals( feeCurrency.coinDecimals ); } ); readonly getGasPriceForFeeCurrency = computedFn( (feeCurrency: FeeCurrency, feeType: FeeType | "custom"): Dec => { if ( this.modularChainInfo.hasFeature("osmosis-base-fee-beta") || this.canOsmosisTxFeesAndReady() ) { const queryOsmosis = this.queriesStore.get(this.chainId).osmosis; if (queryOsmosis) { const baseDenom = queryOsmosis.queryTxFeesBaseDenom.baseDenom; let baseFeeCurrency = this.selectableFeeCurrencies.find( (c) => c.coinMinimalDenom === baseDenom ) || this._cosmosFeeCurrencies[0]; if (this.modularChainInfo.hasFeature("osmosis-base-fee-beta")) { const remoteBaseFeeStep = this.queriesStore.simpleQuery.queryGet<{ low?: number; average?: number; high?: number; }>( "https://config-lambda.keplr.app/osmosis/osmosis-base-fee-beta.json" ); const baseFee = queryOsmosis.queryBaseFee.baseFee; if (baseFee) { const low = remoteBaseFeeStep.response?.data.low ? parseFloat( baseFee .mul(new Dec(remoteBaseFeeStep.response.data.low)) .toString(8) ) : baseFeeCurrency.gasPriceStep?.low ?? DefaultGasPriceStep.low; const average = Math.max( low, remoteBaseFeeStep.response?.data.average ? parseFloat( baseFee .mul(new Dec(remoteBaseFeeStep.response.data.average)) .toString(8) ) : baseFeeCurrency.gasPriceStep?.average ?? DefaultGasPriceStep.average ); const high = Math.max( average, remoteBaseFeeStep.response?.data.high ? parseFloat( baseFee .mul(new Dec(remoteBaseFeeStep.response.data.high)) .toString(8) ) : baseFeeCurrency.gasPriceStep?.high ?? DefaultGasPriceStep.high ); baseFeeCurrency = { ...baseFeeCurrency, gasPriceStep: { low, average, high, }, }; } } if (this.canOsmosisTxFeesAndReady()) { if ( feeCurrency.coinMinimalDenom !== baseDenom && queryOsmosis.queryTxFeesFeeTokens.isTxFeeToken( feeCurrency.coinMinimalDenom ) ) { const baseGasPriceStep = baseFeeCurrency.gasPriceStep ?? DefaultGasPriceStep; const resolvedFeeType = feeType === "custom" ? "average" : feeType; const baseGasPrice = new Dec( baseGasPriceStep[resolvedFeeType].toString() ); const spotPriceDec = queryOsmosis.queryTxFeesSpotPriceByDenom.getQueryDenom( feeCurrency.coinMinimalDenom ).spotPriceDec; if (spotPriceDec.gt(new Dec(0))) { // If you calculate only the spot price, slippage cannot be considered // However, rather than performing the actual calculation here, // the slippage problem is avoided by simply giving an additional value of 1%. return baseGasPrice.quo(spotPriceDec).mul(new Dec(1.01)); } else { return new Dec(0); } } } if ( feeCurrency.coinMinimalDenom === baseFeeCurrency.coinMinimalDenom ) { return this.populateGasPriceStep(baseFeeCurrency, feeType); } } } else if (this.canFeeMarketTxFeesAndReady()) { if (this.modularChainInfo.hasFeature("initia-dynamicfee")) { const queryEtc = this.queriesStore.get(this.chainId).keplrETC; if (queryEtc) { const gasPrice = queryEtc.queryInitiaDynamicFee.baseGasPrice; if (gasPrice) { const multiplication = this.getMultiplication(); const resolvedType = feeType === "custom" ? "average" : feeType; switch (resolvedType) { case "low": return new Dec(multiplication.low).mul(new Dec(gasPrice)); case "average": return new Dec(multiplication.average).mul(new Dec(gasPrice)); case "high": return new Dec(multiplication.high).mul(new Dec(gasPrice)); } } } } else if (this.modularChainInfo.hasFeature("evm-feemarket")) { const queryCosmos = this.queriesStore.get(this.chainId).cosmos; if (queryCosmos) { const baseFee = queryCosmos.queryEvmFeeMarketBaseFee.baseFee; if (baseFee && baseFee.amount) { const multiplication = this.getMultiplication(); const resolvedType = feeType === "custom" ? "average" : feeType; switch (resolvedType) { case "low": return new Dec(multiplication.low).mul(baseFee.amount); case "average": return new Dec(multiplication.average).mul(baseFee.amount); case "high": return new Dec(multiplication.high).mul(baseFee.amount); } } } } else { const queryCosmos = this.queriesStore.get(this.chainId).cosmos; if (queryCosmos) { const gasPrices = queryCosmos.queryFeeMarketGasPrices.gasPrices; const gasPrice = gasPrices.find( (gasPrice) => gasPrice.denom === feeCurrency.coinMinimalDenom ); if (gasPrice) { const multiplication = this.getMultiplication(); const resolvedType = feeType === "custom" ? "average" : feeType; switch (resolvedType) { case "low": return new Dec(multiplication.low).mul(gasPrice.amount); case "average": return new Dec(multiplication.average).mul(gasPrice.amount); case "high": return new Dec(multiplication.high).mul(gasPrice.amount); } } } } } // TODO: Handle terra classic fee return this.populateGasPriceStep(feeCurrency, feeType); } ); protected populateGasPriceStep( feeCurrency: FeeCurrency, feeType: FeeType | "custom" ): Dec { const gasPriceStep = feeCurrency.gasPriceStep ?? DefaultGasPriceStep; const resolvedFeeType = feeType === "custom" ? "average" : feeType; let gasPrice = new Dec(0); switch (resolvedFeeType) { case "low": { gasPrice = new Dec(gasPriceStep.low); break; } case "average": { gasPrice = new Dec(gasPriceStep.average); break; } case "high": { gasPrice = new Dec(gasPriceStep.high); break; } default: { throw new Error(`Unknown fee type: ${resolvedFeeType}`); } } return gasPrice; } @computed get _uiProperties(): UIProperties { if (this.disableBalanceCheck) { return {}; } const fee = this.getFeePrimitive(); if (!fee) { return {}; } let priorWarning: Error | undefined = undefined; let priorIsLoadingState = false; const makeReturn = (uiProperties: UIProperties): UIProperties => { return { ...uiProperties, ...(() => { if (priorIsLoadingState) { if (uiProperties.loadingState === "loading-block") { return { loadingState: "loading-block", }; } else { return { loadingState: "loading", }; } } return {}; })(), ...(() => { if (priorWarning) { if (uiProperties.error) { return {}; } else { return { warning: priorWarning, }; } } return {}; })(), }; }; if ( fee.length > 0 && this.computeTerraClassicTax && this.modularChainInfo.hasFeature("terra-classic-fee") ) { const etcQueries = this.queriesStore.get(this.chainId).keplrETC; if (etcQueries) { if ( etcQueries.queryTerraClassicTaxRate.error || etcQueries.queryTerraClassicTaxRate.isFetching ) { return makeReturn({ error: (() => { if (etcQueries.queryTerraClassicTaxRate.error) { return new Error("Failed to fetch tax rate"); } })(), loadingState: etcQueries.queryTerraClassicTaxRate.isFetching ? "loading-block" : undefined, }); } if ( etcQueries.queryTerraClassicTaxCaps.error || etcQueries.queryTerraClassicTaxCaps.isFetching ) { return makeReturn({ error: (() => { if (etcQueries.queryTerraClassicTaxCaps.error) { return new Error("Failed to fetch tax rate"); } })(), loadingState: etcQueries.queryTerraClassicTaxCaps.isFetching ? "loading-block" : undefined, }); } } } if (this.modularChainInfo.hasFeature("osmosis-base-fee-beta")) { const queryOsmosis = this.queriesStore.get(this.chainId).osmosis; if (queryOsmosis) { const queryBaseFee = queryOsmosis.queryBaseFee; const baseFee = queryBaseFee.baseFee; if (queryBaseFee.isFetching) { priorIsLoadingState = true; } if (queryBaseFee.error) { priorWarning = new Error("Failed to fetch base fee"); } if (!baseFee) { return makeReturn({ loadingState: "loading-block", }); } } } else if (this.canFeeMarketTxFeesAndReady()) { if (this.modularChainInfo.hasFeature("initia-dynamicfee")) { const queryEtc = this.queriesStore.get(this.chainId).keplrETC; if (queryEtc) { const queryInitiaDynamicFee = queryEtc.queryInitiaDynamicFee; if (queryInitiaDynamicFee.error) { priorWarning = new Error("Failed to fetch gas prices"); } if (!queryInitiaDynamicFee.response) { return makeReturn({ loadingState: "loading-block", }); } if (queryInitiaDynamicFee.isFetching) { priorIsLoadingState = true; } } } else if (this.modularChainInfo.hasFeature("evm-feemarket")) { const queryCosmos = this.queriesStore.get(this.chainId).cosmos; if (queryCosmos) { const queryEvmFeeMarketBaseFee = queryCosmos.queryEvmFeeMarketBaseFee; if (queryEvmFeeMarketBaseFee.error) { priorWarning = new Error("Failed to fetch base fee"); } if (!queryEvmFeeMarketBaseFee.response) { return makeReturn({ loadingState: "loading-block", }); } if (queryEvmFeeMarketBaseFee.isFetching) { priorIsLoadingState = true; } } } else { const queryCosmos = this.queriesStore.get(this.chainId).cosmos; if (queryCosmos) { const queryFeeMarketGasPrices = queryCosmos.queryFeeMarketGasPrices; if (queryFeeMarketGasPrices.error) { priorWarning = new Error("Failed to fetch gas prices"); } if (!queryFeeMarketGasPrices.response) { return makeReturn({ loadingState: "loading-block", }); } if (queryFeeMarketGasPrices.isFetching) { priorIsLoadingState = true; } } } } if (this.canOsmosisTxFeesAndReady()) { const queryOsmosis = this.queriesStore.get(this.chainId).osmosis; if (queryOsmosis && this.getFeePrimitive().length > 0) { const baseDenom = queryOsmosis.queryTxFeesBaseDenom.baseDenom; const feeCurrency = this.getFeePrimitive()[0].currency; if ( feeCurrency.coinMinimalDenom !== baseDenom && queryOsmosis.queryTxFeesFeeTokens.isTxFeeToken( feeCurrency.coinMinimalDenom ) ) { const spotPrice = queryOsmosis.queryTxFeesSpotPriceByDenom.getQueryDenom( feeCurrency.coinMinimalDenom ); const error = (() => { if (spotPrice.error) { return new Error("Failed to fetch spot price"); } })(); const loadingState = (() => { if (!spotPrice.response) { return "loading-block"; } if (spotPrice.isFetching) { return "loading"; } })(); // Return only needed. // There is proceeding logic to validate the balance. if (loadingState === "loading") { priorIsLoadingState = true; } if (error || loadingState) { return makeReturn({ error, loadingState, }); } } } } // TODO: 여기서 terra classic 관련 무슨 처리를 해야하는데 나중에 하자... const amount = this.amountConfig.amount; const needs = fee.slice(); if (this.additionAmountToNeedFee) { for (let i = 0; i < needs.length; i++) { const need = needs[i]; for (const amt of amount) { if ( need.currency.coinMinimalDenom === amt.currency.coinMinimalDenom ) { needs[i] = { ...need, amount: new Int(need.amount) .add(new Int(amt.toCoin().amount)) .toString(), }; } } } } for (let i = 0; i < needs.length; i++) { const need = needs[i]; if (new Int(need.amount).lte(new Int(0))) { continue; } const bal = this.queriesStore .get(this.chainId) .queryBalances.getQueryBech32Address(this.senderConfig.value) .balances.find( (bal) => bal.currency.coinMinimalDenom === need.currency.coinMinimalDenom ); if (!bal) { priorWarning = new Error( `Unable to load your ${need.currency.coinMinimalDenom} balance` ); } if (bal) { // XXX: AtomOne에서 fee top-up 시에 bal.error 또는 bal.response에 접근해도 // bal이 observed 상태가 되지 않아서 fetch가 이뤄지지 않는 문제가 있음 // 아무리봐도 원인을 찾을 수 없기 때문에 일단 강제로 observed 상태로 만든다. bal.waitResponse(); if (bal.error) { priorWarning = new Error("Failed to fetch balance"); } if (!bal.response) { return makeReturn({ loadingState: "loading-block", }); } if (new Int(bal.balance.toCoin().amount).lt(new Int(need.amount))) { if (bal.isFetching) { priorIsLoadingState = true; } return makeReturn({ error: new InsufficientFeeError("Insufficient fee"), }); } } } return makeReturn({}); } @computed get uiProperties(): UIProperties { if ( this.forceTopUp || this._uiProperties.error instanceof InsufficientFeeError ) { const topUpStatus = this.getTopUpStatus(); if ( topUpStatus.shouldTopUp && (topUpStatus.isTopUpAvailable || topUpStatus.remainingTimeMs != null) ) { return { warning: new ShouldTopUpWarning("Should top up"), }; } } return this._uiProperties; } readonly getTopUpStatus = computedFn(() => { const keplrETCQueries = this.queriesStore.get(this.chainId).keplrETC; if (!keplrETCQueries || !this.senderConfig.sender) { return { isTopUpAvailable: false, remainingTimeMs: undefined, shouldTopUp: false, topUpOverrideStdFee: undefined, }; } const topUpStatus = keplrETCQueries.queryTopUpStatus.getTopUpStatus( this.senderConfig.sender ).topUpStatus; if (topUpStatus != null) { const { isTopUpAvailable, remainingTimeMs, stakingChainId, validatorAddress, coinDenom, coinMinimalDenom, requiredStaking, additionalStakingNeeded, } = topUpStatus; // 모든 fee currency가 부족할 경우에만 topup 사용이 가능 const shouldTopUp = (() => { // amount 자체가 잔액을 초과하는 경우는 topup 대상이 아님 if ( this.amountConfig.uiProperties.error instanceof InsufficientAmountError ) { return false; } const queryBalances = this.queriesStore .get(this.chainId) .queryBalances.getQueryBech32Address(this.senderConfig.sender); for (const feeCurrency of this.selectableFeeCurrencies) { const requiredFee = this.getFeeTypePrettyForFeeCurrency( feeCurrency, this.type === "manual" ? "average" : this.type ); const totalNeed = (() => { let need = requiredFee; for (const amt of this.amountConfig.amount) { if ( amt.currency.coinMinimalDenom === feeCurrency.coinMinimalDenom ) { need = need.add(amt); } } return need; })(); const bal = queryBalances.getBalance(feeCurrency)?.balance; if (!bal || bal.toDec().lte(new Dec(0))) { continue; } if (bal.toDec().gte(totalNeed.toDec())) { return false; } } return this.selectableFeeCurrencies.length > 0; })(); let topUpOverrideStdFee: StdFee | undefined = undefined; if (this.forceTopUp || shouldTopUp) { const baseFeeCurrency = this._cosmosFeeCurrencies[0]; if (baseFeeCurrency) { const feeAmount = this.getFeeTypePrettyForFeeCurrency( baseFeeCurrency, "average" ); topUpOverrideStdFee = { gas: this.gasConfig.gas.toString(), amount: [ { amount: feeAmount.toCoin().amount, denom: baseFeeCurrency.coinMinimalDenom, }, ], }; } } return { shouldTopUp, isTopUpAvailable, remainingTimeMs, topUpOverrideStdFee, stakingChainId, validatorAddress, coinDenom, coinMinimalDenom, requiredStaking, additionalStakingNeeded, }; } return { isTopUpAvailable: false, remainingTimeMs: undefined, shouldTopUp: false, topUpOverrideStdFee: undefined, }; }); @computed get topUpStatus(): { shouldTopUp: boolean; remainingTimeMs?: number; topUpOverrideStdFee?: StdFee; isTopUpAvailable: boolean; stakingChainId?: string; validatorAddress?: string; coinDenom?: string; coinMinimalDenom?: string; requiredStaking?: number; additionalStakingNeeded?: number; } { // always call getTopUpStatus() to ensure topUpQuery is observed const status = this.getTopUpStatus(); if (this.uiProperties.warning instanceof ShouldTopUpWarning) { return { ...status, shouldTopUp: this.forceTopUp || status.shouldTopUp, }; } return { shouldTopUp: false, remainingTimeMs: undefined, topUpOverrideStdFee: undefined, isTopUpAvailable: false, stakingChainId: status.stakingChainId, validatorAddress: status.validatorAddress, coinDenom: status.coinDenom, coinMinimalDenom: status.coinMinimalDenom, requiredStaking: status.requiredStaking, additionalStakingNeeded: status.additionalStakingNeeded, }; } refreshTopUpStatus(): void { const keplrETCQueries = this.queriesStore.get(this.chainId).keplrETC; if (!keplrETCQueries || !this.senderConfig.sender) { return; } const topUpQuery = keplrETCQueries.queryTopUpStatus.getTopUpStatus( this.senderConfig.sender ); if (topUpQuery.topUpStatus != null) { topUpQuery.waitFreshResponse(); } } private getMultiplication(): { low: number; average: number; high: number } { let multiplication = { low: 1.1, average: 1.2, high: 1.3, }; const multificationConfig = this.queriesStore.simpleQuery.queryGet<{ [str: string]: | { low: number; average: number; high: number; } | undefined; }>("https://config-lambda.keplr.app", "/feemarket/info.json"); if (multificationConfig.response) { const _default = multificationConfig.response.data["__default__"]; if ( _default && _default.low != null && typeof _default.low === "number" && _default.average != null && typeof _default.average === "number" && _default.high != null && typeof _default.high === "number" ) { multiplication = { low: _default.low, average: _default.average, high: _default.high, }; } const specific = multificationConfig.response.data[ this.modularChainInfo.chainIdentifier ]; if ( specific && specific.low != null && typeof specific.low === "number" && specific.average != null && typeof specific.average === "number" && specific.high != null && typeof specific.high === "number" ) { multiplication = { low: specific.low, average: specific.average, high: specific.high, }; } } return multiplication; } } export const useFeeConfig = ( chainGetter: ChainGetter, queriesStore: QueriesStore, chainId: string, senderConfig: ISenderConfig, amountConfig: IBaseAmountConfig, gasConfig: IGasConfig, opts: { additionAmountToNeedFee?: boolean; computeTerraClassicTax?: boolean; forceUseAtoneTokenAsFee?: boolean; forceTopUp?: boolean; } = {} ) => { const [config] = useState( () => new FeeConfig( chainGetter, queriesStore, chainId, senderConfig, amountConfig, gasConfig, opts.additionAmountToNeedFee ?? true, opts.computeTerraClassicTax ?? false, opts.forceUseAtoneTokenAsFee ?? false, opts.forceTopUp ?? false ) ); config.setChain(chainId); config.setAdditionAmountToNeedFee(opts.additionAmountToNeedFee ?? true); config.setComputeTerraClassicTax(opts.computeTerraClassicTax ?? false); config.setForceUseAtoneTokenAsFee(opts.forceUseAtoneTokenAsFee ?? false); config.setForceTopUp(opts.forceTopUp ?? false); return config; };