import { IAmountConfig, IFeeConfig, ISenderConfig, UIProperties, } from "./types"; import { TxChainSetter } from "./chain"; import { ChainGetter } from "@keplr-wallet/stores"; import { action, computed, makeObservable, observable } from "mobx"; import { AppCurrency } from "@keplr-wallet/types"; import { EmptyAmountError, InsufficientAmountError, InvalidNumberAmountError, NegativeAmountError, NotSupportedCurrencyError, ZeroAmountError, } from "./errors"; import { CoinPretty, Dec, DecUtils } from "@keplr-wallet/unit"; import { useState } from "react"; import { QueriesStore } from "./internal"; export class AmountConfig extends TxChainSetter implements IAmountConfig { @observable.ref protected _currency?: AppCurrency = undefined; @observable protected _value: string = ""; @observable protected _fraction: number = 0; @observable.ref protected _feeConfig: IFeeConfig | undefined = undefined; @observable protected _fractionSubFeeWeight: number = 0; @observable protected _disableSubFeeFromFaction: boolean = false; constructor( chainGetter: ChainGetter, protected readonly queriesStore: QueriesStore, initialChainId: string, protected readonly senderConfig: ISenderConfig, disableSubFeeFromFaction: boolean ) { super(chainGetter, initialChainId); this._disableSubFeeFromFaction = disableSubFeeFromFaction; makeObservable(this); } get fractionSubFeeWeight(): number { return this._fractionSubFeeWeight; } @action setFractionSubFeeWeight(fractionSubFeeWeight: number) { this._fractionSubFeeWeight = fractionSubFeeWeight; } get disableSubFeeFromFaction(): boolean { return this._disableSubFeeFromFaction; } @action setDisableSubFeeFromFaction(value: boolean) { this._disableSubFeeFromFaction = value; } get feeConfig(): IFeeConfig | undefined { return this._feeConfig; } @action setFeeConfig(feeConfig: IFeeConfig | undefined) { this._feeConfig = feeConfig; } @computed get value(): string { if (this.fraction > 0) { let result = this.queriesStore .get(this.chainId) .queryBalances.getQueryBech32Address(this.senderConfig.sender) .getBalanceFromCurrency(this.currency); if (this.feeConfig && !this.disableSubFeeFromFaction) { for (const fee of this.feeConfig.fees) { result = result.sub(fee); } } if (result.toDec().lte(new Dec(0))) { return "0"; } const maxValue = result .mul(new Dec(this.fraction)) .trim(true) .locale(false) .hideDenom(true); if ( this._fractionSubFeeWeight > 0 && this.feeConfig && this.feeConfig.fees.length > 0 ) { const subFee = this.feeConfig.fees[0].mul( new Dec(this._fractionSubFeeWeight) ); const finalValue = maxValue.sub(subFee); if (finalValue.toDec().lte(new Dec(0))) { return "0"; } return finalValue.toString(); } return maxValue.toString(); } return this._value; } @action setValue(value: string): void { if (value.startsWith(".")) { value = "0" + value; } this._value = value; this.setFraction(0); } @computed get amount(): CoinPretty[] { let amount: Dec; try { if (this.value.trim() === "") { amount = new Dec(0); } else { amount = new Dec(this.value); } } catch { amount = new Dec(0); } try { return [ new CoinPretty( this.currency, amount .mul(DecUtils.getTenExponentN(this.currency.coinDecimals)) .truncate() ), ]; } catch { return [new CoinPretty(this.currency, new Dec(0))]; } } @computed get currency(): AppCurrency { const chainInfo = this.modularChainInfo; if (this._currency) { const find = chainInfo.findCurrency(this._currency.coinMinimalDenom); if (find) { return find; } } if (chainInfo.currencies.length === 0) { throw new Error("Chain doesn't have the sendable currency informations"); } return chainInfo.currencies[0]; } @action setCurrency(currency: AppCurrency | undefined) { if (currency?.coinMinimalDenom !== this._currency?.coinMinimalDenom) { this._value = ""; this.setFraction(0); } this._currency = currency; } get fraction(): number { return this._fraction; } @action setFraction(fraction: number): void { this._fraction = fraction; } canUseCurrency(currency: AppCurrency): boolean { return ( this.modularChainInfo.findCurrency(currency.coinMinimalDenom) != null ); } @computed get uiProperties(): UIProperties { if (!this.currency) { return { error: new Error("Currency to send not set"), }; } if (this.value.trim() === "") { return { error: new EmptyAmountError("Amount is empty"), }; } try { const dec = new Dec(this.value); if (dec.equals(new Dec(0))) { return { error: new ZeroAmountError("Amount is zero"), }; } if (dec.lt(new Dec(0))) { return { error: new NegativeAmountError("Enter a positive number"), }; } // For checking if the amount is valid new CoinPretty( this.currency, dec.mul(DecUtils.getTenExponentN(this.currency.coinDecimals)).truncate() ); } catch { return { error: new InvalidNumberAmountError("Enter a valid number"), }; } for (const amount of this.amount) { const currency = amount.currency; if (!this.canUseCurrency(currency)) { return { error: new NotSupportedCurrencyError("Token not supported"), }; } const bal = this.queriesStore .get(this.chainId) .queryBalances.getQueryBech32Address(this.senderConfig.sender) .balances.find( (bal) => bal.currency.coinMinimalDenom === currency.coinMinimalDenom ); if (!bal) { return { warning: new Error( `Unable to load your ${currency.coinMinimalDenom} balance` ), }; } if (bal.error) { return { warning: new Error("Failed to fetch balance"), }; } if (!bal.response) { return { loadingState: "loading-block", }; } if (bal.balance.toDec().lt(amount.toDec())) { return { error: new InsufficientAmountError("Insufficient balance"), loadingState: bal.isFetching ? "loading" : undefined, }; } } return {}; } } export const useAmountConfig = ( chainGetter: ChainGetter, queriesStore: QueriesStore, chainId: string, senderConfig: ISenderConfig, disableSubFeeFromFaction: boolean, fractionSubFeeWeight?: number ) => { const [txConfig] = useState( () => new AmountConfig( chainGetter, queriesStore, chainId, senderConfig, disableSubFeeFromFaction ) ); txConfig.setChain(chainId); txConfig.setFractionSubFeeWeight(fractionSubFeeWeight ?? 0); txConfig.setDisableSubFeeFromFaction(disableSubFeeFromFaction); return txConfig; };