diff --git a/packages/stores/src/account/base.ts b/packages/stores/src/account/base.ts index b09fc7b4ac..ba5a5ab90d 100644 --- a/packages/stores/src/account/base.ts +++ b/packages/stores/src/account/base.ts @@ -216,7 +216,7 @@ export class AccountStore[] = []> { makeObservable(this); autorun(async () => { - const isOneClickTradingEnabled = await this.getShouldUseOneClickTrading(); + const isOneClickTradingEnabled = await this.isOneClickTradingEnabled(); const oneClickTradingInfo = await this.getOneClickTradingInfo(); const hasUsedOneClickTrading = await this.getHasUsedOneClickTrading(); runInAction(() => { @@ -1472,7 +1472,7 @@ export class AccountStore[] = []> { }: { messages: readonly EncodeObject[]; }): Promise { - const isOneClickTradingEnabled = await this.isOneCLickTradingEnabled(); + const isOneClickTradingEnabled = await this.isOneClickTradingEnabled(); const oneClickTradingInfo = await this.getOneClickTradingInfo(); if (!oneClickTradingInfo || !isOneClickTradingEnabled) { @@ -1535,7 +1535,7 @@ export class AccountStore[] = []> { }); } - async isOneCLickTradingEnabled(): Promise { + async isOneClickTradingEnabled(): Promise { const oneClickTradingInfo = await this.getOneClickTradingInfo(); if (isNil(oneClickTradingInfo)) return false; diff --git a/packages/web/.env b/packages/web/.env index d0f78286ff..cf30c16291 100644 --- a/packages/web/.env +++ b/packages/web/.env @@ -48,4 +48,7 @@ TWITTER_API_URL=https://api.twitter.com/ BLOCKAID_BASE_URL=http://api.blockaid.io:80 # BLOCKAID_API_KEY= -NEXT_PUBLIC_SPEND_LIMIT_CONTRACT_ADDRESS=osmo10xqv8rlpkflywm92k5wdmplzy7khtasl9c2c08psmvlu543k724sy94k74 \ No newline at end of file +NEXT_PUBLIC_SPEND_LIMIT_CONTRACT_ADDRESS=osmo10xqv8rlpkflywm92k5wdmplzy7khtasl9c2c08psmvlu543k724sy94k74 + +# Disable TRPC logs in development +# NEXT_PUBLIC_TRPC_LOGS=off diff --git a/packages/web/components/alert/__tests__/prettify.spec.ts b/packages/web/components/alert/__tests__/prettify.spec.ts index 68a35e5f09..90e0bb343c 100644 --- a/packages/web/components/alert/__tests__/prettify.spec.ts +++ b/packages/web/components/alert/__tests__/prettify.spec.ts @@ -1,6 +1,11 @@ +import { Dec } from "@keplr-wallet/unit"; import cases from "jest-in-case"; -import { isOverspendErrorMessage, isRejectedTxErrorMessage } from "../prettify"; +import { + getParametersFromOverspendErrorMessage, + isOverspendErrorMessage, + isRejectedTxErrorMessage, +} from "../prettify"; cases( "isOverspendErrorMessage", @@ -45,6 +50,53 @@ cases( ] ); +cases( + "getParametersFromOverspendErrorMessage", + ({ message, result }) => { + expect(getParametersFromOverspendErrorMessage(message)).toEqual(result); + }, + [ + { + name: "should extract parameters from valid overspend error message", + message: + "Fetch error. Spend limit error: Overspend: 2000000 has been spent but limit is 1000000.", + result: { + wouldSpendTotal: new Dec("2000000", 6), + limit: new Dec("1000000", 6), + }, + }, + { + name: "should extract parameters from complex overspend error message", + message: + "Fetch error. execution blocked by authenticator (account = osmo1sh8lreekwcytxpqr6lxmw5cl7kdrfsdfat2ujlvz, authenticator id = 208, msg index = 0, msg type url = /osmosis.poolmanager.v1beta1.MsgSwapExactAmountIn): Spend limit error: Overspend: 50065777 has been spent but limit is 1000000: execute wasm contract failed", + result: { + wouldSpendTotal: new Dec("50065777", 6), + limit: new Dec("1000000", 6), + }, + }, + { + name: "should handle empty message", + message: "", + result: undefined, + }, + { + name: "should handle undefined message", + message: undefined, + result: undefined, + }, + { + name: "should return undefined for non-overspend error message", + message: "execution succeeded", + result: undefined, + }, + { + name: "should return undefined for invalid overspend error format", + message: "Spend limit error: Invalid format", + result: undefined, + }, + ] +); + cases( "isRejectedTxErrorMessage", ({ message, result }) => { diff --git a/packages/web/components/alert/prettify.ts b/packages/web/components/alert/prettify.ts index 7b53c3ea30..7f4038cdfd 100644 --- a/packages/web/components/alert/prettify.ts +++ b/packages/web/components/alert/prettify.ts @@ -1,5 +1,5 @@ import { AppCurrency } from "@keplr-wallet/types"; -import { CoinPretty, Int } from "@keplr-wallet/unit"; +import { CoinPretty, Dec, Int } from "@keplr-wallet/unit"; import { isInsufficientFeeError, isSlippageErrorMessage, @@ -27,6 +27,34 @@ const regexRejectedTx = /Request rejected/; const regexOverspendError = /Spend limit error: Overspend: (\d+) has been spent but limit is (\d+)/; +export function getParametersFromOverspendErrorMessage( + message: string | undefined +): { wouldSpendTotal: Dec; limit: Dec } | undefined { + if (!message) return; + + const match = message.match(regexOverspendError); + if (!match) return; + + const [, wouldSpendTotal, limit] = match; + + if (!wouldSpendTotal || !limit) return; + + try { + // Validate that extracted values are valid numbers + if (isNaN(Number(wouldSpendTotal)) || isNaN(Number(limit))) { + return; + } + + return { + wouldSpendTotal: new Dec(wouldSpendTotal, 6), + limit: new Dec(limit, 6), + }; + } catch (error) { + console.error("Failed to parse overspend error parameters:", error); + return; + } +} + export function isOverspendErrorMessage({ message, }: { diff --git a/packages/web/components/one-click-trading/one-click-remaining-time.tsx b/packages/web/components/one-click-trading/one-click-remaining-time.tsx index c4ffe12bff..d2fc8146e7 100644 --- a/packages/web/components/one-click-trading/one-click-remaining-time.tsx +++ b/packages/web/components/one-click-trading/one-click-remaining-time.tsx @@ -8,7 +8,8 @@ import { humanizeTime } from "~/utils/date"; export const OneClickTradingRemainingTime: FunctionComponent<{ className?: string; -}> = ({ className }) => { + useShortTimeUnits?: boolean; +}> = ({ className, useShortTimeUnits }) => { const { oneClickTradingInfo, isOneClickTradingExpired } = useOneClickTradingSession(); const { t } = useTranslation(); @@ -24,7 +25,8 @@ export const OneClickTradingRemainingTime: FunctionComponent<{ humanizeTime( dayjs.unix( unixNanoSecondsToSeconds(oneClickTradingInfo.sessionPeriod.end) - ) + ), + useShortTimeUnits ) ); }; @@ -39,7 +41,7 @@ export const OneClickTradingRemainingTime: FunctionComponent<{ ); return () => clearInterval(intervalId); - }, [oneClickTradingInfo]); + }, [oneClickTradingInfo, useShortTimeUnits]); if (isOneClickTradingExpired) { return ( diff --git a/packages/web/components/one-click-trading/one-click-trading-settings.tsx b/packages/web/components/one-click-trading/one-click-trading-settings.tsx index 84940d2219..a29fc5d18f 100644 --- a/packages/web/components/one-click-trading/one-click-trading-settings.tsx +++ b/packages/web/components/one-click-trading/one-click-trading-settings.tsx @@ -1,4 +1,4 @@ -import { Dec, PricePretty } from "@keplr-wallet/unit"; +import { Dec } from "@keplr-wallet/unit"; import { makeRemoveAuthenticatorMsg } from "@osmosis-labs/tx"; import { OneClickTradingTransactionParams } from "@osmosis-labs/types"; import { noop, runIfFn } from "@osmosis-labs/utils"; @@ -26,15 +26,16 @@ import { Button, buttonVariants, GoBackButton } from "~/components/ui/button"; import { Switch } from "~/components/ui/switch"; import { EventName } from "~/config"; import { + OneClickTradingParamsChanges, useAmplitudeAnalytics, useDisclosure, useOneClickTradingSession, useTranslation, } from "~/hooks"; +import { formatSpendLimit } from "~/hooks/one-click-trading/use-one-click-trading-session-manager"; import { useEstimateTxFees } from "~/hooks/use-estimate-tx-fees"; import { ModalBase, ModalCloseButton } from "~/modals"; import { useStore } from "~/stores"; -import { trimPlaceholderZeros } from "~/utils/number"; import { api } from "~/utils/trpc"; type Classes = "root"; @@ -45,14 +46,13 @@ enum SettingsScreens { SessionPeriod = "sessionPeriod", } -interface OneClickTradingSettingsProps { +type BaseOneClickTradingSettingsProps = { classes?: Partial>; onGoBack: () => void; transaction1CTParams: OneClickTradingTransactionParams | undefined; setTransaction1CTParams: Dispatch< SetStateAction >; - onStartTrading: () => void; onEndSession?: () => void; onClose: () => void; isLoading?: boolean; @@ -60,7 +60,37 @@ interface OneClickTradingSettingsProps { isEndingSession?: boolean; hideBackButton?: boolean; hasExistingSession?: boolean; -} +}; + +type StandaloneProps = BaseOneClickTradingSettingsProps & { + /** + * Whether component is in standalone mode. + * When true or undefined, the component acts as a full settings page with start/edit functionality + */ + standalone?: true; + /** Required callback when user clicks start/edit trading button */ + onStartTrading: () => void; + /** In standalone mode the changes are tracked internally. Initial changes are not accepted */ + externalChanges?: undefined; + /** Not needed in standalone mode */ + setExternalChanges?: Dispatch>; +}; + +type NonStandaloneProps = BaseOneClickTradingSettingsProps & { + /** + * Whether component is in standalone mode. + * When false, the component acts as a settings panel within another component + */ + standalone: false; + /** Start trading callback is not allowed in non-standalone mode */ + onStartTrading?: never; + /** Initial changes to be applied to the settings */ + externalChanges: OneClickTradingParamsChanges; + /** Callback to set initial changes */ + setExternalChanges: (value: OneClickTradingParamsChanges) => void; +}; + +type OneClickTradingSettingsProps = StandaloneProps | NonStandaloneProps; /** * Compares the changes between two sets of OneClickTradingTransactionParams. @@ -70,26 +100,28 @@ export function compare1CTTransactionParams({ prevParams, nextParams, }: { - prevParams: OneClickTradingTransactionParams; - nextParams: OneClickTradingTransactionParams; -}): Array<"spendLimit" | "resetPeriod" | "sessionPeriod"> { - let changes = new Set<"spendLimit" | "resetPeriod" | "sessionPeriod">(); + prevParams?: OneClickTradingTransactionParams; + nextParams?: OneClickTradingTransactionParams; +}): OneClickTradingParamsChanges { + let changes: OneClickTradingParamsChanges = []; if (prevParams?.spendLimit.toString() !== nextParams?.spendLimit.toString()) { - changes.add("spendLimit"); + changes.push("spendLimit"); } if (prevParams?.sessionPeriod.end !== nextParams?.sessionPeriod.end) { - changes.add("sessionPeriod"); + changes.push("sessionPeriod"); } - return Array.from(changes); -} + if (prevParams?.isOneClickEnabled !== nextParams?.isOneClickEnabled) { + changes.push("isEnabled"); + } + + if (prevParams?.networkFeeLimit !== nextParams?.networkFeeLimit) { + changes.push("networkFeeLimit"); + } -function formatSpendLimit(spendLimit: PricePretty | undefined) { - return `${spendLimit?.symbol}${trimPlaceholderZeros( - spendLimit?.toDec().toString(2) ?? "" - )}`; + return changes; } export const OneClickTradingSettings = ({ @@ -105,11 +137,16 @@ export const OneClickTradingSettings = ({ hasExistingSession, onEndSession, onClose, + standalone = true, + externalChanges = [], + setExternalChanges, }: OneClickTradingSettingsProps) => { const { t } = useTranslation(); - const [changes, setChanges] = useState< - Array<"spendLimit" | "resetPeriod" | "sessionPeriod"> - >([]); + + const [changes, setChanges] = useState([]); + + const [initialChanges, setInitialChanges] = + useState([]); const [initialTransaction1CTParams, setInitialTransaction1CTParams] = useState(); @@ -147,7 +184,9 @@ export const OneClickTradingSettings = ({ useEffect(() => { if (!transaction1CTParams || initialTransaction1CTParams) return; setInitialTransaction1CTParams(transaction1CTParams); - }, [initialTransaction1CTParams, transaction1CTParams]); + setChanges(externalChanges); + setInitialChanges(externalChanges); + }, [externalChanges, initialTransaction1CTParams, transaction1CTParams]); const { isOpen: isDiscardDialogOpen, @@ -170,11 +209,30 @@ export const OneClickTradingSettings = ({ ) => { setTransaction1CTParamsProp((prevParams) => { const nextParams = runIfFn(newParamsOrFn, prevParams); + + if (!initialTransaction1CTParams || !nextParams) { + console.error("Transaction params are undefined"); + return prevParams; + } + setChanges( - compare1CTTransactionParams({ - prevParams: initialTransaction1CTParams!, - nextParams: nextParams!, - }) + Array.from( + new Set([ + /** + * Spreading the changes and then merging with the new changes + * ensures that the changes from the previous modal open are not overwritten + * + * If I have already made a change, and returned to the review order modal, + * then I start editing again I expect my previous changes to be in the state + * and the previously changed parameters displayed the same way as current change + */ + ...changes, + ...compare1CTTransactionParams({ + prevParams: initialTransaction1CTParams!, + nextParams: nextParams!, + }), + ]) + ) ); return nextParams; @@ -193,6 +251,8 @@ export const OneClickTradingSettings = ({ )} / ${formatSpendLimit(initialTransaction1CTParams.spendLimit)}` : undefined; + const changesWithoutIsEnabled = changes.filter((c) => c !== "isEnabled"); + return ( <> { - if (changes.length > 0) { - return openCloseConfirmDialog(); + if (standalone) { + if (changesWithoutIsEnabled.length > 0) { + return openCloseConfirmDialog(); + } + } else { + setTransaction1CTParams(initialTransaction1CTParams); + setExternalChanges?.(initialChanges); } onClose(); @@ -228,7 +293,7 @@ export const OneClickTradingSettings = ({ { - if (changes.length > 0) { + if (standalone && changesWithoutIsEnabled.length > 0) { return onOpenDiscardDialog(); } onGoBack(); @@ -268,46 +333,51 @@ export const OneClickTradingSettings = ({
- { - if (hasExistingSession) onEndSession?.(); - setTransaction1CTParams((params) => { - if (!params) throw new Error("1CT Params is undefined"); - - const nextValue = !params.isOneClickEnabled; - if (nextValue) { - logEvent([ - EventName.OneClickTrading.enableOneClickTrading, - ]); - } - - return { - ...params, - isOneClickEnabled: nextValue, - }; - }); - }} - content={ -
- {hasExistingSession && isEndingSession && ( -

- {t("oneClickTrading.settings.endingSession")} -

- )} - {(isLoading || isEndingSession) && ( - - )}{" "} - { + if (hasExistingSession) onEndSession?.(); + setTransaction1CTParams((params) => { + if (!params) + throw new Error("1CT Params is undefined"); + + const nextValue = !params.isOneClickEnabled; + if (nextValue) { + logEvent([ + EventName.OneClickTrading.enableOneClickTrading, + ]); } - /> -
- } - isDisabled={isSendingTx || isEndingSession} - /> + + return { + ...params, + isOneClickEnabled: nextValue, + }; + }); + }} + content={ +
+ {hasExistingSession && isEndingSession && ( +

+ {t("oneClickTrading.settings.endingSession")} +

+ )} + {(isLoading || isEndingSession) && ( + + )}{" "} + +
+ } + isDisabled={isSendingTx || isEndingSession} + /> + )} setCurrentScreen(SettingsScreens.SpendLimit)} @@ -378,8 +448,9 @@ export const OneClickTradingSettings = ({ />
- {hasExistingSession && - changes.length > 0 && + {standalone && + hasExistingSession && + changes.filter((c) => c !== "isEnabled").length > 0 && (!isSendingTx || !isEndingSession) && (
- - ); - } - const isLoadingMaxButton = featureFlags.swapToolSimulateFee && !isNil(account?.address) && @@ -341,7 +326,16 @@ export const SwapTool: FunctionComponent = observer( !Boolean(swapState.quote) || isSwapToolLoading || Boolean(swapState.error) || - Boolean(swapState.networkFeeError))); + Boolean( + swapState.networkFeeError && + /** + * We can increase spend limit from the review order modal + * so the decision to disable the button should be made there + */ + !isOverspendErrorMessage({ + message: swapState.networkFeeError.message, + }) + ))); const showTokenSelectRecommendedTokens = isNil(forceSwapInPoolId); @@ -626,16 +620,6 @@ export const SwapTool: FunctionComponent = observer(
- {!isNil(warningText) && ( -
- {warningText} -
- )} {swapButton ?? (
+ {show1CT && showOneClickTradingSettings && ( +
+ setShowOneClickTradingSettings(false)} + onClose={() => setShowOneClickTradingSettings(false)} + transaction1CTParams={transaction1CTParams} + setTransaction1CTParams={setTransaction1CTParams} + standalone={false} + />
-
- {orderType === "limit" && tab !== "swap" && ( -
-
-
- {(tab === "buy" && !isBeyondOppositePrice) || - (tab === "sell" && isBeyondOppositePrice) ? ( - - +
+
{title}
+ +
+
+ {orderType === "limit" && tab !== "swap" && ( +
+
+
+ {(tab === "buy" && !isBeyondOppositePrice) || + (tab === "sell" && isBeyondOppositePrice) ? ( + + + + ) : ( + - - ) : ( - + )} +
+ + {t("limitOrders.priceReaches", { + denom: baseDenom ?? "", + price: limitPriceFiat + ? formatPretty( + limitPriceFiat, + getPriceExtendedFormatOptions( + limitPriceFiat.toDec() + ) + ) + : "", + })} + + {percentAdjusted && ( +
+
+ {!percentAdjusted.isZero() && ( + + )} +
+ + {formatPretty(percentAdjusted.mul(new Dec(100)).abs(), { + maxDecimals: 3, + })} + % + +
)}
- - {t("limitOrders.priceReaches", { - denom: baseDenom ?? "", - price: limitPriceFiat - ? formatPretty( - limitPriceFiat, - getPriceExtendedFormatOptions(limitPriceFiat.toDec()) - ) - : "", - })} - - {percentAdjusted && ( -
-
- {!percentAdjusted.isZero() && ( - - )} -
- - {formatPretty(percentAdjusted.mul(new Dec(100)).abs(), { - maxDecimals: 3, - })} - % - -
- )}
-
- )} -
-
-
- {fromAsset && ( - {`${fromAsset.coinDenom} - )} -
-

- {tab === "buy" - ? t("limitOrders.pay") - : t("limitOrders.sell")} -

- {inAmountToken && ( - - {formatPretty(inAmountToken)} - +
+
+
+ {fromAsset && ( + {`${fromAsset.coinDenom} )} +
+

+ {tab === "buy" + ? t("limitOrders.pay") + : t("limitOrders.sell")} +

+ {inAmountToken && ( + + {formatPretty(inAmountToken)} + + )} +
-
-
- {formatFiatPrice( - inAmountFiat ?? new PricePretty(DEFAULT_VS_CURRENCY, 0) - )} -
-
-
-
-
- +
+ {formatFiatPrice( + inAmountFiat ?? new PricePretty(DEFAULT_VS_CURRENCY, 0) + )}
-
-
-
- {toAsset && ( - {`${toAsset.coinDenom} - )} -
-

- {tab === "sell" - ? t("limitOrders.receive") - : t("portfolio.buy")} -

- - {expectedOutput && ( - <> - {formatPretty(expectedOutput.toDec(), { - minimumSignificantDigits: 6, - maximumSignificantDigits: 6, - maxDecimals: 10, - notation: "standard", - })}{" "} - {toAsset?.coinDenom} - - )} - +
+
+
+ +
-
-

- {outputDifference && ( - {`${ - outputDifference.toDec().isPositive() ? "-" : "+" - }${new RatePretty(outputDifference.toDec().abs())}`} +

+
+ {toAsset && ( + {`${toAsset.coinDenom} )} - - {formatFiatPrice( - expectedOutputFiat ?? - new PricePretty(DEFAULT_VS_CURRENCY, 0) +
+

+ {tab === "sell" + ? t("limitOrders.receive") + : t("portfolio.buy")} +

+ + {expectedOutput && ( + <> + {formatPretty(expectedOutput.toDec(), { + minimumSignificantDigits: 6, + maximumSignificantDigits: 6, + maxDecimals: 10, + notation: "standard", + })}{" "} + {toAsset?.coinDenom} + + )} + +
+
+
+

+ {outputDifference && ( + {`${ + outputDifference.toDec().isPositive() ? "-" : "+" + }${new RatePretty( + outputDifference.toDec().abs() + )}`} )} - -

+ + {formatFiatPrice( + expectedOutputFiat ?? + new PricePretty(DEFAULT_VS_CURRENCY, 0) + )} + +

+
-
-
-
- - {tab === "buy" - ? t("limitOrders.aboveMarket.title") - : t("limitOrders.belowMarket.title")} - - } - body={ - - {tab === "buy" - ? t("limitOrders.aboveMarket.description") - : t("limitOrders.belowMarket.description")} - - } - > -
- {isBeyondOppositePrice && ( - - )} - {orderType === "limit" - ? t("limitOrders.limit") - : t("limitOrders.market")} -
- - } - /> - {slippageConfig && orderType === "market" && ( -
+
+
+ + {tab === "buy" + ? t("limitOrders.aboveMarket.title") + : t("limitOrders.belowMarket.title")} + + } + body={ + + {tab === "buy" + ? t("limitOrders.aboveMarket.description") + : t("limitOrders.belowMarket.description")} + + } + > +
+ {isBeyondOppositePrice && ( + + )} + {orderType === "limit" + ? t("limitOrders.limit") + : t("limitOrders.market")} +
+ + } + /> + + {show1CT && is1CTEnabled && ( -
- setShowOneClickTradingSettings(true)} + remainingSpendLimit={remaining1CTSpendLimit} + wouldExceedSpendLimit={wouldExceedSpendLimit} + /> + } + /> + )} + {slippageConfig && orderType === "market" && ( +
+ +
{ - slippageConfig?.setIsManualSlippage(true); - setIsEditingSlippage(true); - }} - onBlur={() => { - if ( - isManualSlippageTooHigh && - +manualSlippage > 50 - ) { - handleManualSlippageChange( - (+manualSlippage).toString().split("")[0] - ); + > + { - handleManualSlippageChange(e.target.value); - - logEvent([ - EventName.Swap.slippageToleranceSet, + className="sm:caption w-fit bg-transparent px-0" + inputClassName={classNames( + "!bg-transparent focus:text-center text-right placeholder:text-wosmongton-300 transition-all focus-visible:outline-none", { - fromToken: fromAsset?.coinDenom, - toToken: toAsset?.coinDenom, - isOnHome: true, - percentage: - slippageConfig?.slippage.toString(), - page, - }, - ]); - }} + "text-rust-400 placeholder:text-rust-400": + isManualSlippageTooHigh, + } + )} + value={manualSlippage} + onFocus={() => { + slippageConfig?.setIsManualSlippage(true); + setIsEditingSlippage(true); + }} + onBlur={() => { + if ( + isManualSlippageTooHigh && + +manualSlippage > 50 + ) { + handleManualSlippageChange( + (+manualSlippage).toString().split("")[0] + ); + } + setIsEditingSlippage(false); + }} + onChange={(e) => { + handleManualSlippageChange(e.target.value); + + logEvent([ + EventName.Swap.slippageToleranceSet, + { + fromToken: fromAsset?.coinDenom, + toToken: toAsset?.coinDenom, + isOnHome: true, + percentage: + slippageConfig?.slippage.toString(), + page, + }, + ]); + }} + /> + {manualSlippage !== "" && ( + + % + + )} +
+
+ } + /> + {isManualSlippageTooHigh && ( +
+ +
+ + {t( + "limitOrders.errors.tradeMayResultInLossOfValue" + )} + + + {t("limitOrders.lowerSlippageToleranceRecommended")} + +
+
+ )} + {isManualSlippageTooLow && ( +
+ + - {manualSlippage !== "" && ( - - % - - )} + +
+ + {t("limitOrders.errors.tradeMayNotExecuted")} + + + {t("limitOrders.tryHigherSlippage")} +
+ )} +
+ )} + {orderType === "market" && ( +
+ )} + {orderType === "market" ? ( + + {amountWithSlippage && + fiatAmountWithSlippage && + toAsset && ( + + {formatPretty(amountWithSlippage, { + maxDecimals: 6, + })}{" "} + {quoteType === "out-given-in" + ? toAsset.coinDenom + : fromAsset?.coinDenom} + + )}{" "} + {fiatAmountWithSlippage && ( + + (~ + {formatPretty(fiatAmountWithSlippage, { + ...getPriceExtendedFormatOptions( + fiatAmountWithSlippage.toDec() + ), + })} + ) + + )} + } /> - {isManualSlippageTooHigh && ( -
- -
- - {t("limitOrders.errors.tradeMayResultInLossOfValue")} - - - {t("limitOrders.lowerSlippageToleranceRecommended")} - -
-
- )} - {isManualSlippageTooLow && ( -
- - - -
- - {t("limitOrders.errors.tradeMayNotExecuted")} - - - {t("limitOrders.tryHigherSlippage")} - -
-
- )} -
- )} - {orderType === "market" && ( -
- )} - {orderType === "market" ? ( + ) : ( + + {t("transfer.free")} + + } + /> + )} - {amountWithSlippage && - fiatAmountWithSlippage && - toAsset && ( - - {formatPretty(amountWithSlippage, { - maxDecimals: 6, - })}{" "} - {quoteType === "out-given-in" - ? toAsset.coinDenom - : fromAsset?.coinDenom} - - )}{" "} - {fiatAmountWithSlippage && ( - - (~ - {formatPretty(fiatAmountWithSlippage, { - ...getPriceExtendedFormatOptions( - fiatAmountWithSlippage.toDec() - ), - })} - ) - - )} - + // Do not show skeleton unless there has been no estimation/error yet + !!gasAmount || !!gasError ? ( + GasEstimation + ) : ( + + ) } /> - ) : ( - - {t("transfer.free")} +
+ {isBeyondOppositePrice && orderType === "limit" && ( +
+ +
+ + {tab === "buy" + ? t("limitOrders.aboveMarket.title") + : t("limitOrders.belowMarket.title")} + + + {tab === "buy" + ? t("limitOrders.aboveMarket.description") + : t("limitOrders.belowMarket.description")} +
+
+ )} + {show1CT && !is1CTEnabled && ( + + setTransaction1CTParams((prev) => { + if (!prev) return; + + return { + ...prev, + isOneClickEnabled: !prev.isOneClickEnabled, + }; + }) } + onParamsChange={() => setShowOneClickTradingSettings(true)} /> )} - - ) - } - /> + {!diffGteSlippage && ( +
+ +
+ )}
- {isBeyondOppositePrice && orderType === "limit" && ( -
- -
- - {tab === "buy" - ? t("limitOrders.aboveMarket.title") - : t("limitOrders.belowMarket.title")} - - - {tab === "buy" - ? t("limitOrders.aboveMarket.description") - : t("limitOrders.belowMarket.description")} - +
+ {diffGteSlippage && ( +
+
+
+
-
- )} - {!diffGteSlippage && ( -
+ + {t("limitOrders.quoteUpdated")} +
- )} -
+
+ )}
- {diffGteSlippage && ( -
-
-
- -
- - {t("limitOrders.quoteUpdated")} - - + )} + + ); +} + +const OneClickTradingPanel = ({ + t, + transactionParams, + onClick, + onParamsChange, +}: { + t: MultiLanguageT; + transactionParams: OneClickTradingTransactionParams | undefined; + onClick: () => void; + onParamsChange: () => void; +}) => { + return ( + <> +
+
+ 1ct rounded rectangle icon +
+
+

+ {t("oneClickTrading.reviewOrder.enableTitle")} +

+ +

+ {t("oneClickTrading.reviewOrder.enableDescription")} +

+
+ +
+
+ {transactionParams?.isOneClickEnabled && ( +

+ {t("oneClickTrading.reviewOrder.paramsDescription", { + sessionLength: t( + `oneClickTrading.sessionPeriods.${ + transactionParams?.sessionPeriod.end ?? "1hour" + }` + ), + spendLimit: + transactionParams?.spendLimit.toString() ?? + t("oneClickTrading.reviewOrder.defaultSpendLimit"), + })} + {" · "} + + {t("oneClickTrading.reviewOrder.change")} + +

)}
- + ); -} +}; + +const OneClickTradingActiveSessionParamsEdit = ({ + onClick, + changes = [], + transactionParams, + remainingSpendLimit, + wouldExceedSpendLimit, +}: { + remainingSpendLimit?: string; + changes: OneClickTradingParamsChanges; + transactionParams: OneClickTradingTransactionParams | undefined; + onClick: () => void; + wouldExceedSpendLimit?: boolean; +}) => { + const { t } = useTranslation(); + + return wouldExceedSpendLimit ? ( + <> +
+ + {t("oneClickTrading.reviewOrder.edit")} + + + + {t("oneClickTrading.reviewOrder.exceeded")} + + +
+ + ) : ( + + + {changes.includes("spendLimit") ? ( + + {transactionParams?.spendLimit.toString()} + + ) : ( + {remainingSpendLimit} + )} + {" / "} + {changes.includes("sessionPeriod") ? ( + + {transactionParams?.sessionPeriod.end} + + ) : ( + + )} + + + ); +}; diff --git a/packages/web/public/images/1ct-rounded-rectangle.svg b/packages/web/public/images/1ct-rounded-rectangle.svg new file mode 100644 index 0000000000..475eb0481d --- /dev/null +++ b/packages/web/public/images/1ct-rounded-rectangle.svg @@ -0,0 +1,5 @@ + + + + diff --git a/packages/web/utils/date.ts b/packages/web/utils/date.ts index 92e3542b0d..c0885f39ac 100644 --- a/packages/web/utils/date.ts +++ b/packages/web/utils/date.ts @@ -1,6 +1,9 @@ import dayjs from "dayjs"; -export function humanizeTime(date: dayjs.Dayjs): { +export function humanizeTime( + date: dayjs.Dayjs, + useShortTimeUnits = false +): { value: number | string; unitTranslationKey: string; } { @@ -10,7 +13,13 @@ export function humanizeTime(date: dayjs.Dayjs): { return { value: Math.max(secondsDiff, 0), unitTranslationKey: - secondsDiff === 1 ? "timeUnits.second" : "timeUnits.seconds", + secondsDiff === 1 + ? useShortTimeUnits + ? "timeUnitsShort.second" + : "timeUnits.second" + : useShortTimeUnits + ? "timeUnitsShort.seconds" + : "timeUnits.seconds", }; } @@ -19,7 +28,13 @@ export function humanizeTime(date: dayjs.Dayjs): { return { value: minutesDiff, unitTranslationKey: - minutesDiff === 1 ? "timeUnits.minute" : "timeUnits.minutes", + minutesDiff === 1 + ? useShortTimeUnits + ? "timeUnitsShort.minute" + : "timeUnits.minute" + : useShortTimeUnits + ? "timeUnitsShort.minutes" + : "timeUnits.minutes", }; } @@ -28,7 +43,13 @@ export function humanizeTime(date: dayjs.Dayjs): { return { value: hoursDiff, unitTranslationKey: - hoursDiff === 1 ? "timeUnits.hour" : "timeUnits.hours", + hoursDiff === 1 + ? useShortTimeUnits + ? "timeUnitsShort.hour" + : "timeUnits.hour" + : useShortTimeUnits + ? "timeUnitsShort.hours" + : "timeUnits.hours", }; } @@ -36,7 +57,14 @@ export function humanizeTime(date: dayjs.Dayjs): { if (daysDiff < 30) { return { value: daysDiff, - unitTranslationKey: daysDiff === 1 ? "timeUnits.day" : "timeUnits.days", + unitTranslationKey: + daysDiff === 1 + ? useShortTimeUnits + ? "timeUnitsShort.day" + : "timeUnits.day" + : useShortTimeUnits + ? "timeUnitsShort.days" + : "timeUnits.days", }; } diff --git a/packages/web/utils/trpc.ts b/packages/web/utils/trpc.ts index 0262453604..77b1146a0e 100644 --- a/packages/web/utils/trpc.ts +++ b/packages/web/utils/trpc.ts @@ -121,8 +121,9 @@ export const api = createTRPCNext({ links: [ loggerLink({ enabled: (opts) => - process.env.NODE_ENV === "development" || - (opts.direction === "down" && opts.result instanceof Error), + process.env.NEXT_PUBLIC_TRPC_LOGS !== "off" && + (process.env.NODE_ENV === "development" || + (opts.direction === "down" && opts.result instanceof Error)), }), /** * Split calls to the node server and the edge server.