diff --git a/packages/trpc/src/one-click-trading.ts b/packages/trpc/src/one-click-trading.ts index 53280cc455..b7d164ab8a 100644 --- a/packages/trpc/src/one-click-trading.ts +++ b/packages/trpc/src/one-click-trading.ts @@ -33,7 +33,7 @@ export const oneClickTradingRouter = createTRPCRouter({ spendLimitTokenDecimals: usdcAsset.coinDecimals, networkFeeLimit: OneClickTradingMaxGasLimit, sessionPeriod: { - end: "1hour" as const, + end: "7days" as const, }, }; } diff --git a/packages/types/src/one-click-trading-types.ts b/packages/types/src/one-click-trading-types.ts index 53741f9e4d..8e92e59816 100644 --- a/packages/types/src/one-click-trading-types.ts +++ b/packages/types/src/one-click-trading-types.ts @@ -18,12 +18,10 @@ export interface OneClickTradingTimeLimit { } export type OneClickTradingHumanizedSessionPeriod = - | "5min" - | "10min" - | "30min" | "1hour" - | "3hours" - | "12hours"; + | "1day" + | "7days" + | "30days"; export interface OneClickTradingTransactionParams { isOneClickEnabled: boolean; diff --git a/packages/utils/src/__tests__/date.spec.ts b/packages/utils/src/__tests__/date.spec.ts new file mode 100644 index 0000000000..3f3bc3a11c --- /dev/null +++ b/packages/utils/src/__tests__/date.spec.ts @@ -0,0 +1,114 @@ +import { safeTimeout } from "../date"; + +describe("safeTimeout", () => { + jest.useFakeTimers(); // Use fake timers for controlled testing + jest.spyOn(global, "setTimeout"); // Spy on the setTimeout function + + afterEach(() => { + jest.clearAllTimers(); // Clear timers after each test + jest.clearAllMocks(); + }); + + test("calls setTimeout directly for time within safe range", () => { + const callback = jest.fn(); + const timeInRange = 5000; // 5 seconds + + safeTimeout(callback, timeInRange); + + // Fast-forward time + jest.advanceTimersByTime(timeInRange); + + // Ensure the callback is called once + expect(callback).toHaveBeenCalledTimes(1); + expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), timeInRange); + }); + + test("splits large timeout into smaller intervals", () => { + const callback = jest.fn(); + const largeTimeout = 2591160000; // 29 days, 23 hours, 46 minutes + const MAX_TIMEOUT = 2 ** 31 - 1; // JavaScript's maximum timeout (~24.8 days) + + safeTimeout(callback, largeTimeout); + + // Fast-forward the first interval + jest.advanceTimersByTime(MAX_TIMEOUT); + + // Ensure another setTimeout was scheduled for the remainder + expect(setTimeout).toHaveBeenCalledTimes(2); + + // Fast-forward the remaining time + jest.advanceTimersByTime(largeTimeout - MAX_TIMEOUT); + + // Ensure the callback is called once after both intervals + expect(callback).toHaveBeenCalledTimes(1); + }); + + test("handles exactly the maximum timeout limit", () => { + const callback = jest.fn(); + const exactMaxTimeout = 2 ** 31 - 1; + + safeTimeout(callback, exactMaxTimeout); + + // Fast-forward time + jest.advanceTimersByTime(exactMaxTimeout); + + // Ensure the callback is called once + expect(callback).toHaveBeenCalledTimes(1); + expect(setTimeout).toHaveBeenCalledWith( + expect.any(Function), + exactMaxTimeout + ); + }); + + test("does not call the callback prematurely", () => { + const callback = jest.fn(); + const largeTimeout = 2591160000; // 29 days, 23 hours, 46 minutes + + safeTimeout(callback, largeTimeout); + + // Fast-forward time but do not complete all intervals + jest.advanceTimersByTime(2 ** 31 - 2); // Just shy of the max timeout + + // Callback should not be called yet + expect(callback).not.toHaveBeenCalled(); + }); + + test("clears the timeout before it completes", () => { + const callback = jest.fn(); // Mock callback function + const largeTimeout = 2591160000; // ~30 days + + // Set up the safe timeout + const { clear } = safeTimeout(callback, largeTimeout); + + // Fast-forward time by 5 seconds and then clear the timeout + jest.advanceTimersByTime(5000); // Advance 5 seconds + clear(); // Clear the timeout early + + // Advance time further to ensure the callback does not execute + jest.advanceTimersByTime(largeTimeout); + + // Verify that the callback was never called + expect(callback).not.toHaveBeenCalled(); + }); + + test("clears the second timer in a split timeout chain", () => { + const callback = jest.fn(); // Mock callback function + const largeTimeout = 2591160000; // ~30 days + const MAX_TIMEOUT = 2 ** 31 - 1; // JavaScript's maximum timeout (~24.8 days) + + // Set up the timeout + const { clear } = safeTimeout(callback, largeTimeout); + + // Advance time to the end of the first timer but before the second timer finishes + jest.advanceTimersByTime(MAX_TIMEOUT); + + // Clear the timeout during the second interval + clear(); + + // Advance time to simulate the remainder of the second timer + jest.advanceTimersByTime(largeTimeout - MAX_TIMEOUT); + + // Verify that the callback was not called + expect(callback).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/utils/src/date.ts b/packages/utils/src/date.ts index 42c75c1133..97e9ed026a 100644 --- a/packages/utils/src/date.ts +++ b/packages/utils/src/date.ts @@ -19,3 +19,38 @@ export function unixNanoSecondsToSeconds( ): number { return Number(unixNanoSeconds) / 1000000000; } + +/** + * A safe version of setTimeout that splits large timeouts into smaller intervals. + * This is necessary because the maximum timeout for setTimeout is 24.8 days. + * @param callback - The function to call after the timeout. + * @param milliseconds - The number of milliseconds to wait before calling the callback. + */ +export function safeTimeout(callback: () => void, milliseconds: number) { + const MAX_TIMEOUT = 2 ** 31 - 1; // Maximum safe timeout + let timeoutId: NodeJS.Timeout | null = null; // Track the current timeout + let cleared = false; // Track if the timeout has been cleared + + const clear = () => { + cleared = true; // Mark as cleared + if (timeoutId) { + clearTimeout(timeoutId); // Clear the current timeout + } + }; + + const runTimeout = (remainingTime: number) => { + if (cleared) return; // Do nothing if cleared + + if (remainingTime <= MAX_TIMEOUT) { + timeoutId = setTimeout(callback, remainingTime); + } else { + timeoutId = setTimeout(() => { + runTimeout(remainingTime - MAX_TIMEOUT); // Recurse with the remaining time + }, MAX_TIMEOUT); + } + }; + + runTimeout(milliseconds); + + return { clear }; +} diff --git a/packages/web/components/one-click-trading/screens/session-period-screen.tsx b/packages/web/components/one-click-trading/screens/session-period-screen.tsx index efd59a251c..84bd8f4c28 100644 --- a/packages/web/components/one-click-trading/screens/session-period-screen.tsx +++ b/packages/web/components/one-click-trading/screens/session-period-screen.tsx @@ -10,29 +10,26 @@ import { } from "~/components/screen-manager"; import { useOneClickTradingSession, useTranslation } from "~/hooks"; +export const oneClickTradingTimeMappings = { + "1hour": "oneClickTrading.sessionPeriods.1hour", + "1day": "oneClickTrading.sessionPeriods.1day", + "7days": "oneClickTrading.sessionPeriods.7days", + "30days": "oneClickTrading.sessionPeriods.30days", +}; + export function getSessionPeriodTranslationKey( input: OneClickTradingHumanizedSessionPeriod ) { - const timeMappings = { - "5min": "oneClickTrading.sessionPeriods.5min", - "10min": "oneClickTrading.sessionPeriods.10min", - "30min": "oneClickTrading.sessionPeriods.30min", - "1hour": "oneClickTrading.sessionPeriods.1hour", - "3hours": "oneClickTrading.sessionPeriods.3hours", - "12hours": "oneClickTrading.sessionPeriods.12hours", - }; - const mapped = timeMappings[input]; + const mapped = oneClickTradingTimeMappings[input]; if (!mapped) throw new Error(`No mapping for ${input}`); return mapped; } const SessionPeriods: OneClickTradingHumanizedSessionPeriod[] = [ - "5min", - "10min", - "30min", "1hour", - "3hours", - "12hours", + "1day", + "7days", + "30days", ]; interface SessionPeriodScreenProps extends OneClickTradingBaseScreenProps {} diff --git a/packages/web/hooks/mutations/one-click-trading/use-create-one-click-trading-session.tsx b/packages/web/hooks/mutations/one-click-trading/use-create-one-click-trading-session.tsx index acf1a68b49..2438415920 100644 --- a/packages/web/hooks/mutations/one-click-trading/use-create-one-click-trading-session.tsx +++ b/packages/web/hooks/mutations/one-click-trading/use-create-one-click-trading-session.tsx @@ -295,36 +295,27 @@ export async function makeCreate1CTSessionMessage({ let sessionPeriod: OneClickTradingTimeLimit; switch (transaction1CTParams.sessionPeriod.end) { - case "5min": - sessionPeriod = { - end: unixSecondsToNanoSeconds(dayjs().add(5, "minute").unix()), - }; - break; - case "10min": - sessionPeriod = { - end: unixSecondsToNanoSeconds(dayjs().add(10, "minute").unix()), - }; - break; - case "30min": + case "1hour": sessionPeriod = { - end: unixSecondsToNanoSeconds(dayjs().add(30, "minute").unix()), + end: unixSecondsToNanoSeconds(dayjs().add(1, "hour").unix()), }; break; - case "1hour": + case "1day": sessionPeriod = { - end: unixSecondsToNanoSeconds(dayjs().add(1, "hour").unix()), + end: unixSecondsToNanoSeconds(dayjs().add(1, "day").unix()), }; break; - case "3hours": + case "7days": sessionPeriod = { - end: unixSecondsToNanoSeconds(dayjs().add(3, "hours").unix()), + end: unixSecondsToNanoSeconds(dayjs().add(7, "day").unix()), }; break; - case "12hours": + case "30days": sessionPeriod = { - end: unixSecondsToNanoSeconds(dayjs().add(12, "hours").unix()), + end: unixSecondsToNanoSeconds(dayjs().add(30, "day").unix()), }; break; + default: throw new Error( `Unsupported time limit: ${transaction1CTParams.sessionPeriod.end}` diff --git a/packages/web/hooks/one-click-trading/__tests__/use-one-click-trading-params.spec.ts b/packages/web/hooks/one-click-trading/__tests__/use-one-click-trading-params.spec.ts index 164f9693ae..194f2035df 100644 --- a/packages/web/hooks/one-click-trading/__tests__/use-one-click-trading-params.spec.ts +++ b/packages/web/hooks/one-click-trading/__tests__/use-one-click-trading-params.spec.ts @@ -4,7 +4,7 @@ import { OneClickTradingHumanizedSessionPeriod, OneClickTradingTransactionParams, } from "@osmosis-labs/types"; -import { Dec, PricePretty } from "@osmosis-labs/unit"; +import { Dec, DecUtils, PricePretty } from "@osmosis-labs/unit"; import { OneClickTradingMaxGasLimit, unixSecondsToNanoSeconds, @@ -147,7 +147,7 @@ describe("useOneClickTradingParams", () => { act(() => { result.current.setTransaction1CTParams({ ...result.current.transaction1CTParams!, - sessionPeriod: { end: "12hours" }, + sessionPeriod: { end: "30days" }, }); }); @@ -195,7 +195,7 @@ describe("useOneClickTradingParams", () => { result.current.setTransaction1CTParams({ ...result.current.transaction1CTParams!, networkFeeLimit: "200000", - sessionPeriod: { end: "12hours" }, + sessionPeriod: { end: "30days" }, }); }); @@ -219,7 +219,7 @@ describe("useOneClickTradingParams", () => { ...defaultOneClickTradingInfo, networkFeeLimit: "200000", humanizedSessionPeriod: - "12hours" as OneClickTradingHumanizedSessionPeriod, + "1hour" as OneClickTradingHumanizedSessionPeriod, }; const { result } = renderHook(() => @@ -233,7 +233,7 @@ describe("useOneClickTradingParams", () => { result.current.setTransaction1CTParams({ ...result.current.transaction1CTParams!, networkFeeLimit: "300000", - sessionPeriod: { end: "3hours" }, + sessionPeriod: { end: "7days" }, }); }); @@ -259,3 +259,162 @@ describe("useOneClickTradingParams", () => { }); }); }); + +describe("getParametersFromOneClickTradingInfo", () => { + it("should set isOneClickEnabled based on defaultIsOneClickEnabled", () => { + const params = getParametersFromOneClickTradingInfo({ + oneClickTradingInfo: { + networkFeeLimit: "50000", + humanizedSessionPeriod: "1hour", + spendLimit: { amount: "1000", decimals: 2 }, + allowedMessages: [], + authenticatorId: "test-id", + publicKey: "test-key", + sessionKey: "test-session", + userOsmoAddress: "test-address", + hasSeenExpiryToast: false, + sessionPeriod: { + end: unixSecondsToNanoSeconds(dayjs().add(1, "hour").unix()), + }, + sessionStartedAtUnix: Date.now(), + }, + defaultIsOneClickEnabled: true, + }); + + expect(params.isOneClickEnabled).toBe(true); + }); + + it("should use OneClickTradingMaxGasLimit when networkFeeLimit is not a string", () => { + const params = getParametersFromOneClickTradingInfo({ + oneClickTradingInfo: { + // @ts-expect-error - this is to test the networkFeeLimit not being a string + networkFeeLimit: 50000, + humanizedSessionPeriod: "1hour", + spendLimit: { amount: "1000", decimals: 2 }, + allowedMessages: [], + authenticatorId: "test-id", + publicKey: "test-key", + sessionKey: "test-session", + userOsmoAddress: "test-address", + hasSeenExpiryToast: false, + sessionPeriod: { + end: unixSecondsToNanoSeconds(dayjs().add(1, "hour").unix()), + }, + sessionStartedAtUnix: Date.now(), + }, + defaultIsOneClickEnabled: false, + }); + + expect(params.networkFeeLimit).toBe(OneClickTradingMaxGasLimit); + }); + + it("should use the provided networkFeeLimit when it is a string", () => { + const customFeeLimit = "75000"; + const params = getParametersFromOneClickTradingInfo({ + oneClickTradingInfo: { + networkFeeLimit: customFeeLimit, + humanizedSessionPeriod: "1hour", + spendLimit: { amount: "1000", decimals: 2 }, + allowedMessages: [], + authenticatorId: "test-id", + publicKey: "test-key", + sessionKey: "test-session", + userOsmoAddress: "test-address", + hasSeenExpiryToast: false, + sessionPeriod: { + end: unixSecondsToNanoSeconds(dayjs().add(1, "hour").unix()), + }, + sessionStartedAtUnix: Date.now(), + }, + defaultIsOneClickEnabled: false, + }); + + expect(params.networkFeeLimit).toBe(customFeeLimit); + }); + + it("should map old humanizedSessionPeriod values to '7days'", () => { + const oldPeriods = ["5min", "10min", "30min", "3hours", "12hours"] as const; + + oldPeriods.forEach((period) => { + const params = getParametersFromOneClickTradingInfo({ + oneClickTradingInfo: { + networkFeeLimit: "50000", + // @ts-expect-error - this is to test the old period mapping + humanizedSessionPeriod: period, + spendLimit: { amount: "1000", decimals: 2 }, + allowedMessages: [], + authenticatorId: "test-id", + publicKey: "test-key", + sessionKey: "test-session", + userOsmoAddress: "test-address", + hasSeenExpiryToast: false, + sessionPeriod: { + end: unixSecondsToNanoSeconds(dayjs().add(1, "hour").unix()), + }, + sessionStartedAtUnix: Date.now(), + }, + defaultIsOneClickEnabled: true, + }); + + expect(params.sessionPeriod.end).toBe("7days"); + }); + }); + + it("should retain humanizedSessionPeriod values that are not old", () => { + const newPeriods = ["1day", "7days", "30days"] as const; + + newPeriods.forEach((period) => { + const params = getParametersFromOneClickTradingInfo({ + oneClickTradingInfo: { + networkFeeLimit: "50000", + humanizedSessionPeriod: period, + spendLimit: { amount: "1000", decimals: 2 }, + allowedMessages: [], + authenticatorId: "test-id", + publicKey: "test-key", + sessionKey: "test-session", + userOsmoAddress: "test-address", + hasSeenExpiryToast: false, + sessionPeriod: { + end: unixSecondsToNanoSeconds(dayjs().add(1, "hour").unix()), + }, + sessionStartedAtUnix: Date.now(), + }, + defaultIsOneClickEnabled: true, + }); + + expect(params.sessionPeriod.end).toBe(period); + }); + }); + + it("should correctly calculate spendLimit", () => { + const spendLimit = { amount: "1000", decimals: 2 }; + const expectedSpendLimitValue = new Dec("1000").quo( + DecUtils.getTenExponentN(2) + ); + + const params = getParametersFromOneClickTradingInfo({ + oneClickTradingInfo: { + networkFeeLimit: "50000", + humanizedSessionPeriod: "1hour", + spendLimit: spendLimit, + allowedMessages: [], + authenticatorId: "test-id", + publicKey: "test-key", + sessionKey: "test-session", + userOsmoAddress: "test-address", + hasSeenExpiryToast: false, + sessionPeriod: { + end: unixSecondsToNanoSeconds(dayjs().add(1, "hour").unix()), + }, + sessionStartedAtUnix: Date.now(), + }, + defaultIsOneClickEnabled: true, + }); + + expect(params.spendLimit.toDec().toString()).toBe( + expectedSpendLimitValue.toString() + ); + expect(params.spendLimit.fiatCurrency).toBe(DEFAULT_VS_CURRENCY); + }); +}); diff --git a/packages/web/hooks/one-click-trading/use-one-click-trading-params.ts b/packages/web/hooks/one-click-trading/use-one-click-trading-params.ts index 6c1dc15968..2bb26d9197 100644 --- a/packages/web/hooks/one-click-trading/use-one-click-trading-params.ts +++ b/packages/web/hooks/one-click-trading/use-one-click-trading-params.ts @@ -21,6 +21,14 @@ export function getParametersFromOneClickTradingInfo({ oneClickTradingInfo: OneClickTradingInfo; defaultIsOneClickEnabled: boolean; }): OneClickTradingTransactionParams { + const OldHumanizedSessionPeriods = [ + "5min", + "10min", + "30min", + "3hours", + "12hours", + ] as const; + return { isOneClickEnabled: defaultIsOneClickEnabled, networkFeeLimit: @@ -28,7 +36,11 @@ export function getParametersFromOneClickTradingInfo({ ? OneClickTradingMaxGasLimit : oneClickTradingInfo.networkFeeLimit, sessionPeriod: { - end: oneClickTradingInfo.humanizedSessionPeriod, + end: OldHumanizedSessionPeriods.includes( + oneClickTradingInfo.humanizedSessionPeriod as (typeof OldHumanizedSessionPeriods)[number] + ) // If the session period is one of the old ones, map it to "1hour" + ? "7days" + : oneClickTradingInfo.humanizedSessionPeriod, }, spendLimit: new PricePretty( DEFAULT_VS_CURRENCY, diff --git a/packages/web/hooks/one-click-trading/use-one-click-trading-session.ts b/packages/web/hooks/one-click-trading/use-one-click-trading-session.ts index ee170f4f7e..bdbd865ee9 100644 --- a/packages/web/hooks/one-click-trading/use-one-click-trading-session.ts +++ b/packages/web/hooks/one-click-trading/use-one-click-trading-session.ts @@ -1,5 +1,5 @@ import type { OneClickTradingInfo } from "@osmosis-labs/stores"; -import { unixNanoSecondsToSeconds } from "@osmosis-labs/utils"; +import { safeTimeout, unixNanoSecondsToSeconds } from "@osmosis-labs/utils"; import dayjs from "dayjs"; import { useCallback, useEffect, useState } from "react"; import { useAsync } from "react-use"; @@ -68,16 +68,16 @@ export const useOneClickTradingSession = ({ const sessionEndDate = dayjs.unix( unixNanoSecondsToSeconds(value.info.sessionPeriod.end) ); - const timeRemaining = sessionEndDate.unix() - dayjs().unix(); + const timeRemainingSeconds = sessionEndDate.unix() - dayjs().unix(); - const timeoutId = setTimeout(() => { + const { clear } = safeTimeout(() => { if (!value?.info) return; setIsExpired(true); onExpire?.({ oneClickTradingInfo: value.info }); - }, timeRemaining * 1000); + }, timeRemainingSeconds * 1000); - return () => clearTimeout(timeoutId); + return () => clear(); }, [isExpired, t, value?.info, onExpire]); const getTimeRemaining = useCallback(() => { diff --git a/packages/web/hooks/one-click-trading/use-one-click-trading-swap-review.ts b/packages/web/hooks/one-click-trading/use-one-click-trading-swap-review.ts index 643bfaed91..9a525575ed 100644 --- a/packages/web/hooks/one-click-trading/use-one-click-trading-swap-review.ts +++ b/packages/web/hooks/one-click-trading/use-one-click-trading-swap-review.ts @@ -3,7 +3,7 @@ import { makeRemoveAuthenticatorMsg } from "@osmosis-labs/tx"; import { OneClickTradingTransactionParams } from "@osmosis-labs/types"; import { Dec, PricePretty } from "@osmosis-labs/unit"; import { useCallback, useEffect, useMemo } from "react"; -import { useAsync } from "react-use"; +import { useAsync, useLocalStorage } from "react-use"; import { create } from "zustand"; import { useShallow } from "zustand/react/shallow"; @@ -21,7 +21,6 @@ const use1CTSwapReviewStore = create<{ transaction1CTParams?: OneClickTradingTransactionParams; spendLimitTokenDecimals?: number; changes?: OneClickTradingParamsChanges; - initialTransactionParams?: OneClickTradingTransactionParams; setTransaction1CTParams: ( transaction1CTParams: OneClickTradingTransactionParams | undefined ) => void; @@ -29,21 +28,15 @@ const use1CTSwapReviewStore = create<{ spendLimitTokenDecimals: number | undefined ) => void; setChanges: (changes: OneClickTradingParamsChanges | undefined) => void; - setInitialTransactionParams: ( - initialTransactionParams: OneClickTradingTransactionParams | undefined - ) => void; }>((set) => ({ spendLimitTokenDecimals: undefined, transaction1CTParams: undefined, changes: undefined, - initialTransactionParams: undefined, setTransaction1CTParams: (transaction1CTParams) => set({ transaction1CTParams }), setSpendLimitTokenDecimals: (spendLimitTokenDecimals) => set({ spendLimitTokenDecimals }), setChanges: (changes) => set({ changes }), - setInitialTransactionParams: (initialTransactionParams) => - set({ initialTransactionParams }), })); export function useOneClickTradingSwapReview({ @@ -51,6 +44,9 @@ export function useOneClickTradingSwapReview({ }: { isModalOpen: boolean; }) { + const [previousIsOneClickEnabled, setPreviousIsOneClickEnabled] = + useLocalStorage("previous-one-click-enabled", true); + const { isOneClickTradingEnabled: isEnabled, isOneClickTradingExpired: isExpired, @@ -59,7 +55,6 @@ export function useOneClickTradingSwapReview({ } = useOneClickTradingSession(); const { - initialTransaction1CTParams: initialTransactionParams, transaction1CTParams: transactionParams, setTransaction1CTParams: setTransactionParams, spendLimitTokenDecimals, @@ -68,7 +63,7 @@ export function useOneClickTradingSwapReview({ setChanges, } = useOneClickTradingParams({ oneClickTradingInfo, - defaultIsOneClickEnabled: isEnabled ?? false, + defaultIsOneClickEnabled: previousIsOneClickEnabled, }); const { wouldExceedSpendLimit, remainingSpendLimit } = @@ -96,14 +91,6 @@ export function useOneClickTradingSwapReview({ } }, [isModalOpen, spendLimitTokenDecimals]); - useEffect(() => { - if (isModalOpen) { - use1CTSwapReviewStore - .getState() - .setInitialTransactionParams(initialTransactionParams); - } - }, [isModalOpen, initialTransactionParams]); - useEffect(() => { if (isModalOpen) { use1CTSwapReviewStore.getState().setChanges(changes); @@ -117,7 +104,6 @@ export function useOneClickTradingSwapReview({ state.setTransaction1CTParams(undefined); state.setSpendLimitTokenDecimals(undefined); state.setChanges(undefined); - state.setInitialTransactionParams(undefined); } }, [isModalOpen, resetParams]); @@ -132,6 +118,7 @@ export function useOneClickTradingSwapReview({ remainingSpendLimit, setTransactionParams, resetParams, + setPreviousIsOneClickEnabled, }; } @@ -141,19 +128,14 @@ export function use1CTSwapReviewMessages() { const { accountStore } = useStore(); const account = accountStore.getWallet(accountStore.osmosisChainId); - const { - transaction1CTParams, - spendLimitTokenDecimals, - changes, - initialTransactionParams, - } = use1CTSwapReviewStore( - useShallow((state) => ({ - transaction1CTParams: state.transaction1CTParams, - spendLimitTokenDecimals: state.spendLimitTokenDecimals, - changes: state.changes, - initialTransactionParams: state.initialTransactionParams, - })) - ); + const { transaction1CTParams, spendLimitTokenDecimals, changes } = + use1CTSwapReviewStore( + useShallow((state) => ({ + transaction1CTParams: state.transaction1CTParams, + spendLimitTokenDecimals: state.spendLimitTokenDecimals, + changes: state.changes, + })) + ); const { oneClickTradingInfo, isOneClickTradingEnabled, isLoadingInfo } = useOneClickTradingSession(); @@ -182,25 +164,10 @@ export function use1CTSwapReviewMessages() { ); const shouldSend1CTTx = useMemo(() => { - // Turn on or off: The session status have changed either turned on or off explicitly - if ( - transaction1CTParams?.isOneClickEnabled !== - initialTransactionParams?.isOneClickEnabled - ) { - return true; - } - - // Modify: The session was already on, wasn't turned off and the params have changed - if ( - transaction1CTParams?.isOneClickEnabled && - initialTransactionParams?.isOneClickEnabled && - (changes ?? [])?.length > 0 - ) { - return true; - } - + if (isOneClickTradingEnabled && (changes ?? []).length === 0) return false; + if (transaction1CTParams?.isOneClickEnabled) return true; return false; - }, [transaction1CTParams, initialTransactionParams, changes]); + }, [transaction1CTParams, changes, isOneClickTradingEnabled]); const { value: oneClickMessages, loading: isLoadingOneClickMessages } = useAsync(async () => { diff --git a/packages/web/localizations/de.json b/packages/web/localizations/de.json index 1990711c97..7de8bc7782 100644 --- a/packages/web/localizations/de.json +++ b/packages/web/localizations/de.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5 Minuten", - "10min": "10 Minuten", - "30min": "30 Minuten", "1hour": "1 Stunde", - "3hours": "3 Stunden", - "12hours": "12 Stunden" + "1day": "1 Tag", + "7days": "7 Tage", + "30days": "30 Tage" }, "settings": { "header": "1-Klick-Handel", @@ -1104,7 +1102,7 @@ "hour": "Std", "hours": "Std", "day": "Tag", - "days": "Tag" + "days": "Tage" }, "transactions": { "noRecent": "Keine aktuellen Transfers", diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index 4b261a8723..e82f34fa9e 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5 minutes", - "10min": "10 minutes", - "30min": "30 minutes", "1hour": "1 hour", - "3hours": "3 hours", - "12hours": "12 hours" + "1day": "1 day", + "7days": "7 days", + "30days": "30 days" }, "settings": { "header": "1-Click Trading", @@ -1104,7 +1102,7 @@ "hour": "hr", "hours": "hr", "day": "day", - "days": "day" + "days": "days" }, "transactions": { "noRecent": "No recent transactions", diff --git a/packages/web/localizations/es.json b/packages/web/localizations/es.json index f51578fdc3..463237c081 100644 --- a/packages/web/localizations/es.json +++ b/packages/web/localizations/es.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5 minutos", - "10min": "10 minutos", - "30min": "30 minutos", "1hour": "1 hora", - "3hours": "3 horas", - "12hours": "12 horas" + "1day": "1 día", + "7days": "7 días", + "30days": "30 días" }, "settings": { "header": "Comercio con 1 clic", @@ -1104,7 +1102,7 @@ "hour": "hora", "hours": "hora", "day": "día", - "days": "día" + "days": "días" }, "transactions": { "noRecent": "Sin transferencias recientes", diff --git a/packages/web/localizations/fa.json b/packages/web/localizations/fa.json index 5f03d94a06..c8863ce971 100644 --- a/packages/web/localizations/fa.json +++ b/packages/web/localizations/fa.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5 دقیقه", - "10min": "10 دقیقه", - "30min": "30 دقیقه", "1hour": "1 ساعت", - "3hours": "3 ساعت", - "12hours": "12 ساعت" + "1day": "1 روز", + "7days": "7 روز", + "30days": "30 روز" }, "settings": { "header": "1- روی تجارت کلیک کنید", diff --git a/packages/web/localizations/fr.json b/packages/web/localizations/fr.json index 2acc02b7ac..b83af190f2 100644 --- a/packages/web/localizations/fr.json +++ b/packages/web/localizations/fr.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5 minutes", - "10min": "10 minutes", - "30min": "30 minutes", "1hour": "1 heure", - "3hours": "3 heures", - "12hours": "12 heures" + "1day": "1 jour", + "7days": "7 jours", + "30days": "30 jours" }, "settings": { "header": "Trading en 1 clic", @@ -1104,7 +1102,7 @@ "hour": "heure", "hours": "heure", "day": "jour", - "days": "jour" + "days": "jours" }, "transactions": { "noRecent": "Aucun transfert récent", diff --git a/packages/web/localizations/gu.json b/packages/web/localizations/gu.json index d5f79970e8..2dcb5d1346 100644 --- a/packages/web/localizations/gu.json +++ b/packages/web/localizations/gu.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5 મિનિટ", - "10min": "10 મિનિટ", - "30min": "30 મિનિટ", "1hour": "1 કલાક", - "3hours": "3 કલાક", - "12hours": "12 કલાક" + "1day": "1 દિવસ", + "7days": "7 દિવસ", + "30days": "30 દિવસ" }, "settings": { "header": "1-ક્લિક કરો ટ્રેડિંગ", @@ -1104,7 +1102,7 @@ "hour": "કલાક", "hours": "કલાક", "day": "દિવસ", - "days": "દિવસ" + "days": "દિવસો" }, "transactions": { "noRecent": "કોઈ તાજેતરના ટ્રાન્સફર નથી", diff --git a/packages/web/localizations/hi.json b/packages/web/localizations/hi.json index 238015367a..79d85cf6bd 100644 --- a/packages/web/localizations/hi.json +++ b/packages/web/localizations/hi.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5 मिनट", - "10min": "10 मिनटों", - "30min": "30 मिनट", "1hour": "1 घंटा", - "3hours": "3 घंटे", - "12hours": "12 घंटे" + "1day": "1 दिन", + "7days": "7 दिन", + "30days": "30 दिन" }, "settings": { "header": "1-ट्रेडिंग पर क्लिक करें", diff --git a/packages/web/localizations/ja.json b/packages/web/localizations/ja.json index 46af580a25..3962007d2f 100644 --- a/packages/web/localizations/ja.json +++ b/packages/web/localizations/ja.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5分", - "10min": "10分", - "30min": "30分", "1hour": "1時間", - "3hours": "3時間", - "12hours": "12時間" + "1day": "1日", + "7days": "7日間", + "30days": "30日間" }, "settings": { "header": "1クリック取引", diff --git a/packages/web/localizations/ko.json b/packages/web/localizations/ko.json index dada7b9697..f2a53be22d 100644 --- a/packages/web/localizations/ko.json +++ b/packages/web/localizations/ko.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5분", - "10min": "10분", - "30min": "30분", "1hour": "1시간", - "3hours": "3시간", - "12hours": "12시간" + "1day": "1일", + "7days": "7일", + "30days": "30일" }, "settings": { "header": "1-클릭 거래", @@ -1104,7 +1102,7 @@ "hour": "시간", "hours": "시간", "day": "낮", - "days": "낮" + "days": "날" }, "transactions": { "noRecent": "최근 이적 없음", diff --git a/packages/web/localizations/pl.json b/packages/web/localizations/pl.json index 5f9cc1dae1..ecdd1bfd5a 100644 --- a/packages/web/localizations/pl.json +++ b/packages/web/localizations/pl.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5 minut", - "10min": "10 minut", - "30min": "30 minut", "1hour": "1 godzina", - "3hours": "3 godziny", - "12hours": "12 godzin" + "1day": "1 dzień", + "7days": "7 dni", + "30days": "30 dni" }, "settings": { "header": "Handel jednym kliknięciem", @@ -1104,7 +1102,7 @@ "hour": "godz.", "hours": "godz.", "day": "dzień", - "days": "dzień" + "days": "dni" }, "transactions": { "noRecent": "Brak ostatnich transferów", diff --git a/packages/web/localizations/pt-br.json b/packages/web/localizations/pt-br.json index 6dfeb21360..d3c8f34b94 100644 --- a/packages/web/localizations/pt-br.json +++ b/packages/web/localizations/pt-br.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5 minutos", - "10min": "10 minutos", - "30min": "30 minutos", "1hour": "1 hora", - "3hours": "3 horas", - "12hours": "12 horas" + "1day": "1 dia", + "7days": "7 dias", + "30days": "30 dias" }, "settings": { "header": "Negociação em 1 clique", @@ -1104,7 +1102,7 @@ "hour": "hora", "hours": "hora", "day": "dia", - "days": "dia" + "days": "dias" }, "transactions": { "noRecent": "Nenhuma transferência recente", diff --git a/packages/web/localizations/ro.json b/packages/web/localizations/ro.json index 34601e55f3..f30052567d 100644 --- a/packages/web/localizations/ro.json +++ b/packages/web/localizations/ro.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5 minute", - "10min": "10 minute", - "30min": "30 de minute", "1hour": "1 oră", - "3hours": "3 ore", - "12hours": "12 ore" + "1day": "1 zi", + "7days": "7 zile", + "30days": "30 de zile" }, "settings": { "header": "1-Click Trading", @@ -1104,7 +1102,7 @@ "hour": "hr", "hours": "hr", "day": "zi", - "days": "zi" + "days": "zile" }, "transactions": { "noRecent": "Fără transferuri recente", diff --git a/packages/web/localizations/ru.json b/packages/web/localizations/ru.json index 6674b7156a..46cfddb2e3 100644 --- a/packages/web/localizations/ru.json +++ b/packages/web/localizations/ru.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5 минут", - "10min": "10 минут", - "30min": "30 минут", "1hour": "1 час", - "3hours": "3 часа", - "12hours": "12 часов" + "1day": "1 день", + "7days": "7 дней", + "30days": "30 дней" }, "settings": { "header": "Торговля в 1 клик", @@ -1104,7 +1102,7 @@ "hour": "час", "hours": "час", "day": "день", - "days": "день" + "days": "дней" }, "transactions": { "noRecent": "Нет недавних переводов", diff --git a/packages/web/localizations/tr.json b/packages/web/localizations/tr.json index 39179a17db..3d68a242a5 100644 --- a/packages/web/localizations/tr.json +++ b/packages/web/localizations/tr.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5 dakika", - "10min": "10 dakika", - "30min": "30 dakika", "1hour": "1 saat", - "3hours": "3 saat", - "12hours": "12 saat" + "1day": "1 gün", + "7days": "7 gün", + "30days": "30 gün" }, "settings": { "header": "Tek Tıklamayla Ticaret", @@ -1104,7 +1102,7 @@ "hour": "saat", "hours": "saat", "day": "gün", - "days": "gün" + "days": "günler" }, "transactions": { "noRecent": "Yakın zamanda transfer yok", diff --git a/packages/web/localizations/zh-cn.json b/packages/web/localizations/zh-cn.json index b7a4a7483c..d900572c35 100644 --- a/packages/web/localizations/zh-cn.json +++ b/packages/web/localizations/zh-cn.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5 分钟", - "10min": "10 分钟", - "30min": "30 分钟", "1hour": "1 小时", - "3hours": "3 小时", - "12hours": "12 小时" + "1day": "1天", + "7days": "7 天", + "30days": "30 天" }, "settings": { "header": "一键交易", diff --git a/packages/web/localizations/zh-hk.json b/packages/web/localizations/zh-hk.json index e32ab0acd4..e646b585df 100644 --- a/packages/web/localizations/zh-hk.json +++ b/packages/web/localizations/zh-hk.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5分鐘", - "10min": "10分鐘", - "30min": "30分鐘", "1hour": "1小時", - "3hours": "3小時", - "12hours": "12小時" + "1day": "1天", + "7days": "7天", + "30days": "30天" }, "settings": { "header": "一鍵交易", diff --git a/packages/web/localizations/zh-tw.json b/packages/web/localizations/zh-tw.json index 09532f6902..48e02280ef 100644 --- a/packages/web/localizations/zh-tw.json +++ b/packages/web/localizations/zh-tw.json @@ -398,12 +398,10 @@ } }, "sessionPeriods": { - "5min": "5分鐘", - "10min": "10分鐘", - "30min": "30分鐘", "1hour": "1小時", - "3hours": "3小時", - "12hours": "12小時" + "1day": "1天", + "7days": "7天", + "30days": "30天" }, "settings": { "header": "一鍵交易", diff --git a/packages/web/modals/review-order.tsx b/packages/web/modals/review-order.tsx index e37bdc5f9b..d484e347a6 100644 --- a/packages/web/modals/review-order.tsx +++ b/packages/web/modals/review-order.tsx @@ -19,6 +19,7 @@ import { Icon } from "~/components/assets"; import { Button } from "~/components/buttons"; import { OneClickTradingRemainingTime } from "~/components/one-click-trading/one-click-remaining-time"; import { OneClickTradingSettings } from "~/components/one-click-trading/one-click-trading-settings"; +import { oneClickTradingTimeMappings } from "~/components/one-click-trading/screens/session-period-screen"; import { GenericDisclaimer } from "~/components/tooltip/generic-disclaimer"; import { Button as UIButton } from "~/components/ui/button"; import { RecapRow } from "~/components/ui/recap-row"; @@ -129,6 +130,7 @@ export function ReviewOrder({ wouldExceedSpendLimit: wouldExceedSpendLimit1CT, setTransactionParams: setTransaction1CTParams, resetParams: reset1CTParams, + setPreviousIsOneClickEnabled, } = useOneClickTradingSwapReview({ isModalOpen: isOpen }); const wouldExceedSpendLimit = useMemo(() => { @@ -749,6 +751,8 @@ export function ReviewOrder({ setTransaction1CTParams((prev) => { if (!prev) return; + setPreviousIsOneClickEnabled(!prev.isOneClickEnabled); + return { ...prev, isOneClickEnabled: !prev.isOneClickEnabled, @@ -924,9 +928,12 @@ const OneClickTradingActiveSessionParamsEdit = ({ {remainingSpendLimit} )} {" / "} - {changes.includes("sessionPeriod") ? ( + {changes.includes("sessionPeriod") && + transactionParams?.sessionPeriod.end ? ( - {transactionParams?.sessionPeriod.end} + {t( + oneClickTradingTimeMappings[transactionParams.sessionPeriod.end] + )} ) : ( diff --git a/packages/web/utils/date.ts b/packages/web/utils/date.ts index af4f375a5b..936a6bc5d6 100644 --- a/packages/web/utils/date.ts +++ b/packages/web/utils/date.ts @@ -73,6 +73,7 @@ export function humanizeTime( const daysDiff = date.diff(dayjs(), "days"); if (daysDiff < 30) { + const hours = date.diff(dayjs(), "hours") % 24; return [ { value: daysDiff, @@ -85,6 +86,17 @@ export function humanizeTime( ? "timeUnitsShort.days" : "timeUnits.days", }, + { + value: hours, + unitTranslationKey: + hours === 1 + ? useShortTimeUnits + ? "timeUnitsShort.hour" + : "timeUnits.hour" + : useShortTimeUnits + ? "timeUnitsShort.hours" + : "timeUnits.hours", + }, ]; }