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 a29fc5d18f..81bf3508f4 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 @@ -32,7 +32,7 @@ import { useOneClickTradingSession, useTranslation, } from "~/hooks"; -import { formatSpendLimit } from "~/hooks/one-click-trading/use-one-click-trading-session-manager"; +import { formatSpendLimit } from "~/hooks/one-click-trading/use-one-click-trading-swap-review"; import { useEstimateTxFees } from "~/hooks/use-estimate-tx-fees"; import { ModalBase, ModalCloseButton } from "~/modals"; import { useStore } from "~/stores"; diff --git a/packages/web/components/one-click-trading/profile-one-click-trading-settings.tsx b/packages/web/components/one-click-trading/profile-one-click-trading-settings.tsx index 3cbe9362a8..65fc7e684f 100644 --- a/packages/web/components/one-click-trading/profile-one-click-trading-settings.tsx +++ b/packages/web/components/one-click-trading/profile-one-click-trading-settings.tsx @@ -72,7 +72,6 @@ export const ProfileOneClickTradingSettings = ({ { spendLimitTokenDecimals, transaction1CTParams, - walletRepo: accountStore.getWalletRepo(chainStore.osmosis.chainId), /** * If the user has an existing session, remove it and add the new one. */ diff --git a/packages/web/components/place-limit-tool/index.tsx b/packages/web/components/place-limit-tool/index.tsx index e9deafb737..f70eb366bb 100644 --- a/packages/web/components/place-limit-tool/index.tsx +++ b/packages/web/components/place-limit-tool/index.tsx @@ -876,7 +876,9 @@ export const PlaceLimitTool: FunctionComponent = observer( }} amountWithSlippage={amountWithSlippage} fiatAmountWithSlippage={fiatAmountWithSlippage} - isConfirmationDisabled={isSendingTx} + isConfirmationDisabled={ + isSendingTx || swapState.isLoadingOneClickMessages + } isOpen={reviewOpen} onClose={() => setReviewOpen(false)} expectedOutput={swapState.expectedTokenAmountOut} diff --git a/packages/web/components/swap-tool/index.tsx b/packages/web/components/swap-tool/index.tsx index b36c07ba7d..7cf86eb419 100644 --- a/packages/web/components/swap-tool/index.tsx +++ b/packages/web/components/swap-tool/index.tsx @@ -293,7 +293,8 @@ export const SwapTool: FunctionComponent = observer( const isSwapToolLoading = isWalletLoading || swapState.isQuoteLoading || - swapState.isLoadingNetworkFee; + swapState.isLoadingNetworkFee || + swapState.isLoadingOneClickMessages; let buttonText: string; if (swapState.error) { diff --git a/packages/web/hooks/limit-orders/use-place-limit.ts b/packages/web/hooks/limit-orders/use-place-limit.ts index aab455e406..1b8dbacf36 100644 --- a/packages/web/hooks/limit-orders/use-place-limit.ts +++ b/packages/web/hooks/limit-orders/use-place-limit.ts @@ -14,7 +14,10 @@ import { isValidNumericalRawInput, useAmountInput, } from "~/hooks/input/use-amount-input"; +import { useTranslation } from "~/hooks/language"; import { useOrderbook } from "~/hooks/limit-orders/use-orderbook"; +import { onAdd1CTSession } from "~/hooks/mutations/one-click-trading"; +import { use1CTSwapReviewMessages } from "~/hooks/one-click-trading"; import { mulPrice } from "~/hooks/queries/assets/use-coin-fiat-value"; import { usePrice } from "~/hooks/queries/assets/use-price"; import { useAmplitudeAnalytics } from "~/hooks/use-amplitude-analytics"; @@ -67,6 +70,8 @@ export const usePlaceLimit = ({ maxSlippage, quoteType = "out-given-in", }: UsePlaceLimitParams) => { + const apiUtils = api.useUtils(); + const { t } = useTranslation(); const { logEvent } = useAmplitudeAnalytics(); const { accountStore } = useStore(); const { @@ -279,6 +284,15 @@ export const usePlaceLimit = ({ placeLimitMsg, ]); + const { oneClickMessages, isLoadingOneClickMessages, shouldSend1CTTx } = + use1CTSwapReviewMessages(); + + const limitMessages = useMemo(() => { + return encodedMsg && !isMarket + ? [encodedMsg, ...(oneClickMessages?.msgs ?? [])] + : []; + }, [encodedMsg, isMarket, oneClickMessages?.msgs]); + const placeLimit = useCallback(async () => { const quantity = paymentTokenValue?.toCoin().amount ?? "0"; if (quantity === "0") { @@ -335,7 +349,7 @@ export const usePlaceLimit = ({ } } - if (!placeLimitMsg) return; + if (!limitMessages) return; const paymentDenom = paymentTokenValue?.toCoin().denom ?? ""; @@ -360,16 +374,38 @@ export const usePlaceLimit = ({ try { logEvent([EventName.LimitOrder.placeOrderStarted, baseEvent]); - await account?.cosmwasm.sendExecuteContractMsg( + await accountStore.signAndBroadcast( + accountStore.osmosisChainId, "executeWasm", - orderbookContractAddress, - placeLimitMsg!, - [ - { - amount: quantity, - denom: paymentDenom, - }, - ] + limitMessages, + "", + undefined, + undefined, + (tx) => { + if (!tx.code) { + if ( + shouldSend1CTTx && + oneClickMessages && + oneClickMessages.type === "create-1ct-session" + ) { + onAdd1CTSession({ + privateKey: oneClickMessages.key, + tx, + userOsmoAddress: account?.address ?? "", + fallbackGetAuthenticatorId: + apiUtils.local.oneClickTrading.getSessionAuthenticator.fetch, + accountStore, + allowedMessages: oneClickMessages.allowedMessages, + sessionPeriod: oneClickMessages.sessionPeriod, + spendLimitTokenDecimals: + oneClickMessages.spendLimitTokenDecimals, + transaction1CTParams: oneClickMessages.transaction1CTParams, + allowedAmount: oneClickMessages.allowedAmount, + t, + }); + } + } + } ); logEvent([EventName.LimitOrder.placeOrderCompleted, baseEvent]); } catch (error) { @@ -385,19 +421,23 @@ export const usePlaceLimit = ({ ]); } }, [ - orderbookContractAddress, - account, - orderDirection, paymentTokenValue, isMarket, - marketState, + limitMessages, paymentFiatValue, - baseAsset, - quoteAsset, - logEvent, + orderDirection, + baseAsset?.coinDenom, + quoteAsset?.coinDenom, page, feeUsdValue, - placeLimitMsg, + marketState, + logEvent, + accountStore, + shouldSend1CTTx, + oneClickMessages, + account?.address, + apiUtils.local.oneClickTrading.getSessionAuthenticator.fetch, + t, ]); const { data, isLoading: isBalancesLoading } = @@ -585,7 +625,7 @@ export const usePlaceLimit = ({ error: limitGasError, } = useEstimateTxFees({ chainId: accountStore.osmosisChainId, - messages: encodedMsg && !isMarket ? [encodedMsg] : [], + messages: limitMessages, enabled: shouldEstimateLimitGas, }); @@ -642,6 +682,7 @@ export const usePlaceLimit = ({ reset, error, feeUsdValue, + isLoadingOneClickMessages, gas: { gasAmountFiat, isLoading: isGasLoading, 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 e8d1719c56..bbc7f974a1 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 @@ -1,8 +1,11 @@ import { toBase64 } from "@cosmjs/encoding"; -import { WalletRepo } from "@cosmos-kit/core"; import { PrivKeySecp256k1 } from "@keplr-wallet/crypto"; import { Dec, DecUtils } from "@keplr-wallet/unit"; import { DeliverTxResponse } from "@osmosis-labs/stores"; +import { + makeAddAuthenticatorMsg, + makeRemoveAuthenticatorMsg, +} from "@osmosis-labs/tx"; import { AuthenticatorType, AvailableOneClickTradingMessages, @@ -18,7 +21,6 @@ import { } from "@osmosis-labs/utils"; import { useMutation, UseMutationOptions } from "@tanstack/react-query"; import dayjs from "dayjs"; -import { useLocalStorage } from "react-use"; import { displayToast, ToastType } from "~/components/alert"; import { OneClickFloatingBannerDoNotShowKey } from "~/components/one-click-trading/one-click-trading-toast"; @@ -169,6 +171,211 @@ export async function getAuthenticatorIdFromTx({ return authenticatorId; } +export async function onAdd1CTSession({ + privateKey, + tx, + userOsmoAddress, + fallbackGetAuthenticatorId, + accountStore, + allowedMessages, + sessionPeriod, + spendLimitTokenDecimals, + transaction1CTParams, + allowedAmount, + t, +}: { + privateKey: PrivKeySecp256k1; + tx: DeliverTxResponse; + userOsmoAddress: string; + fallbackGetAuthenticatorId: Parameters< + typeof getAuthenticatorIdFromTx + >[0]["fallbackGetAuthenticatorId"]; + accountStore: ReturnType["accountStore"]; + allowedMessages: AvailableOneClickTradingMessages[]; + sessionPeriod: OneClickTradingTimeLimit; + spendLimitTokenDecimals: number; + transaction1CTParams: OneClickTradingTransactionParams; + allowedAmount: string; + t: ReturnType["t"]; +}) { + const publicKey = toBase64(privateKey.getPubKey().toBytes()); + + const authenticatorId = await getAuthenticatorIdFromTx({ + events: tx.events, + userOsmoAddress, + fallbackGetAuthenticatorId, + publicKey, + }); + + accountStore.setOneClickTradingInfo({ + authenticatorId, + publicKey, + sessionKey: toBase64(privateKey.toBytes()), + allowedMessages, + sessionPeriod, + sessionStartedAtUnix: dayjs().unix(), + networkFeeLimit: transaction1CTParams.networkFeeLimit, + spendLimit: { + amount: allowedAmount, + decimals: spendLimitTokenDecimals, + }, + hasSeenExpiryToast: false, + humanizedSessionPeriod: transaction1CTParams.sessionPeriod.end, + userOsmoAddress, + }); + + localStorage.setItem(OneClickFloatingBannerDoNotShowKey, "true"); + accountStore.setShouldUseOneClickTrading({ nextValue: true }); + + const sessionEndDate = dayjs.unix( + unixNanoSecondsToSeconds(sessionPeriod.end) + ); + const humanizedTime = humanizeTime(sessionEndDate); + displayToast( + { + titleTranslationKey: "oneClickTrading.toast.oneClickTradingActive", + captionElement: ( +

+ {humanizedTime.value} {t(humanizedTime.unitTranslationKey)}{" "} + {t("remaining")} +

+ ), + }, + ToastType.ONE_CLICK_TRADING + ); +} + +export async function makeCreate1CTSessionMessage({ + transaction1CTParams, + spendLimitTokenDecimals, + additionalAuthenticatorsToRemove, + userOsmoAddress, + apiUtils, +}: { + transaction1CTParams: OneClickTradingTransactionParams; + spendLimitTokenDecimals: number; + additionalAuthenticatorsToRemove?: bigint[]; + userOsmoAddress: string; + apiUtils: ReturnType; +}) { + let authenticators: ParsedAuthenticator[]; + try { + ({ authenticators } = + await apiUtils.local.oneClickTrading.getAuthenticators.fetch({ + userOsmoAddress, + })); + } catch (error) { + throw new CreateOneClickSessionError( + "Failed to fetch account public key and authenticators." + ); + } + + const key = PrivKeySecp256k1.generateRandomKey(); + const allowedAmount = transaction1CTParams.spendLimit + .toDec() + .mul(DecUtils.getTenExponentN(spendLimitTokenDecimals)) + .truncate() + .toString(); + const allowedMessages: AvailableOneClickTradingMessages[] = [ + "/osmosis.poolmanager.v1beta1.MsgSwapExactAmountIn", + "/osmosis.poolmanager.v1beta1.MsgSplitRouteSwapExactAmountIn", + "/osmosis.poolmanager.v1beta1.MsgSwapExactAmountOut", + "/osmosis.poolmanager.v1beta1.MsgSplitRouteSwapExactAmountOut", + "/osmosis.concentratedliquidity.v1beta1.MsgWithdrawPosition", + "/osmosis.valsetpref.v1beta1.MsgSetValidatorSetPreference", + ]; + + 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": + sessionPeriod = { + end: unixSecondsToNanoSeconds(dayjs().add(30, "minute").unix()), + }; + break; + case "1hour": + sessionPeriod = { + end: unixSecondsToNanoSeconds(dayjs().add(1, "hour").unix()), + }; + break; + case "3hours": + sessionPeriod = { + end: unixSecondsToNanoSeconds(dayjs().add(3, "hours").unix()), + }; + break; + case "12hours": + sessionPeriod = { + end: unixSecondsToNanoSeconds(dayjs().add(12, "hours").unix()), + }; + break; + default: + throw new Error( + `Unsupported time limit: ${transaction1CTParams.sessionPeriod.end}` + ); + } + + const oneClickTradingAuthenticator = getOneClickTradingSessionAuthenticator({ + key, + allowedAmount, + allowedMessages, + sessionPeriod, + }); + + const authenticatorToRemoveId = + authenticators.length === 15 + ? authenticators + .filter((authenticator) => + isAuthenticatorOneClickTradingSession({ authenticator }) + ) + .reduce((min, authenticator) => { + if (isNil(min)) return authenticator.id; + return new Dec(authenticator.id).lt(new Dec(min)) + ? authenticator.id + : min; + }, null as string | null) + : undefined; + + const authenticatorsToRemove = authenticatorToRemoveId + ? [BigInt(authenticatorToRemoveId)] + : []; + + if (additionalAuthenticatorsToRemove) { + authenticatorsToRemove.push(...additionalAuthenticatorsToRemove); + } + + const addAuthenticatorMsg = makeAddAuthenticatorMsg({ + authenticatorType: oneClickTradingAuthenticator.authenticatorType, + data: oneClickTradingAuthenticator.data, + sender: userOsmoAddress, + }); + + const removeAuthenticatorMsgs = authenticatorsToRemove.map((id) => + makeRemoveAuthenticatorMsg({ + id, + sender: userOsmoAddress, + }) + ); + + return { + msgs: await Promise.all([...removeAuthenticatorMsgs, addAuthenticatorMsg]), + allowedMessages, + allowedAmount, + sessionPeriod, + key, + transaction1CTParams, + spendLimitTokenDecimals, + }; +} + export const useCreateOneClickTradingSession = ({ onBroadcasted, queryOptions, @@ -178,7 +385,6 @@ export const useCreateOneClickTradingSession = ({ unknown, unknown, { - walletRepo: WalletRepo; spendLimitTokenDecimals: number | undefined; transaction1CTParams: OneClickTradingTransactionParams | undefined; additionalAuthenticatorsToRemove?: bigint[]; @@ -187,24 +393,22 @@ export const useCreateOneClickTradingSession = ({ >; } = {}) => { const { accountStore } = useStore(); - const account = accountStore.getWallet(accountStore.osmosisChainId); const { logEvent } = useAmplitudeAnalytics(); const apiUtils = api.useUtils(); - const [, setDoNotShowFloatingBannerAgain] = useLocalStorage( - OneClickFloatingBannerDoNotShowKey, - false - ); const { t } = useTranslation(); return useMutation( async ({ - walletRepo, transaction1CTParams, spendLimitTokenDecimals, additionalAuthenticatorsToRemove, }) => { - if (!account?.osmosis) { + const userOsmoAddress = accountStore.getWallet( + accountStore.osmosisChainId + )?.address; + + if (!userOsmoAddress) { throw new CreateOneClickSessionError("Osmosis account not found"); } @@ -214,188 +418,60 @@ export const useCreateOneClickTradingSession = ({ ); } - if (!walletRepo.current) { - throw new CreateOneClickSessionError( - "walletRepo.current is not defined." - ); - } - if (!spendLimitTokenDecimals) { throw new CreateOneClickSessionError( "Spend limit token decimals are not defined." ); } - let authenticators: ParsedAuthenticator[]; - try { - ({ authenticators } = - await apiUtils.local.oneClickTrading.getAuthenticators.fetch({ - userOsmoAddress: walletRepo.current.address!, - })); - } catch (error) { - throw new CreateOneClickSessionError( - "Failed to fetch account public key and authenticators." - ); - } - - const key = PrivKeySecp256k1.generateRandomKey(); - const allowedAmount = transaction1CTParams.spendLimit - .toDec() - .mul(DecUtils.getTenExponentN(spendLimitTokenDecimals)) - .truncate() - .toString(); - const allowedMessages: AvailableOneClickTradingMessages[] = [ - "/osmosis.poolmanager.v1beta1.MsgSwapExactAmountIn", - "/osmosis.poolmanager.v1beta1.MsgSplitRouteSwapExactAmountIn", - "/osmosis.poolmanager.v1beta1.MsgSwapExactAmountOut", - "/osmosis.poolmanager.v1beta1.MsgSplitRouteSwapExactAmountOut", - "/osmosis.concentratedliquidity.v1beta1.MsgWithdrawPosition", - "/osmosis.valsetpref.v1beta1.MsgSetValidatorSetPreference", - ]; - - 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": - sessionPeriod = { - end: unixSecondsToNanoSeconds(dayjs().add(30, "minute").unix()), - }; - break; - case "1hour": - sessionPeriod = { - end: unixSecondsToNanoSeconds(dayjs().add(1, "hour").unix()), - }; - break; - case "3hours": - sessionPeriod = { - end: unixSecondsToNanoSeconds(dayjs().add(3, "hours").unix()), - }; - break; - case "12hours": - sessionPeriod = { - end: unixSecondsToNanoSeconds(dayjs().add(12, "hours").unix()), - }; - break; - default: - throw new Error( - `Unsupported time limit: ${transaction1CTParams.sessionPeriod.end}` - ); - } - - const oneClickTradingAuthenticator = - getOneClickTradingSessionAuthenticator({ - key, - allowedAmount, - allowedMessages, - sessionPeriod, + const { msgs, allowedMessages, allowedAmount, sessionPeriod, key } = + await makeCreate1CTSessionMessage({ + transaction1CTParams, + spendLimitTokenDecimals, + additionalAuthenticatorsToRemove, + userOsmoAddress, + apiUtils, }); - /** - * If the user has 15 authenticators, remove the oldest AllOf - * which is the previous OneClickTrading session - */ - const authenticatorToRemoveId = - authenticators.length === 15 - ? authenticators - .filter((authenticator) => - isAuthenticatorOneClickTradingSession({ authenticator }) - ) - /** - * Find the oldest 1-Click Trading Session by comparing the id. - * The smallest id is the oldest authenticator. - */ - .reduce((min, authenticator) => { - if (isNil(min)) return authenticator.id; - return new Dec(authenticator.id).lt(new Dec(min)) - ? authenticator.id - : min; - }, null as string | null) - : undefined; - - const authenticatorsToRemove = authenticatorToRemoveId - ? [BigInt(authenticatorToRemoveId)] - : []; - - if (additionalAuthenticatorsToRemove) { - authenticatorsToRemove.push(...additionalAuthenticatorsToRemove); - } - const tx = await new Promise((resolve, reject) => { - account.osmosis - .sendAddOrRemoveAuthenticatorsMsg({ - addAuthenticators: [oneClickTradingAuthenticator], - removeAuthenticators: authenticatorsToRemove, - memo: "", - - onBroadcasted, - onFulfill: (tx) => { - if (tx.code === 0) { - resolve(tx); - } else { - reject(new Error("Transaction failed")); - } - }, - }) + accountStore + .signAndBroadcast( + accountStore.osmosisChainId, + "addOrRemoveAuthenticators", + msgs, + "", + undefined, + undefined, + { + onBroadcasted, + onFulfill: (tx) => { + if (tx.code === 0) { + resolve(tx); + } else { + reject(new Error("Transaction failed")); + } + }, + } + ) .catch((error) => { reject(error); }); }); - const publicKey = toBase64(key.getPubKey().toBytes()); - - const authenticatorId = await getAuthenticatorIdFromTx({ - events: tx.events, - userOsmoAddress: walletRepo.current.address!, + onAdd1CTSession({ + privateKey: key, + tx, + userOsmoAddress, fallbackGetAuthenticatorId: apiUtils.local.oneClickTrading.getSessionAuthenticator.fetch, - publicKey, - }); - - accountStore.setOneClickTradingInfo({ - authenticatorId, - publicKey, - sessionKey: toBase64(key.toBytes()), + accountStore, allowedMessages, sessionPeriod, - sessionStartedAtUnix: dayjs().unix(), - networkFeeLimit: transaction1CTParams.networkFeeLimit, - spendLimit: { - amount: allowedAmount, - decimals: spendLimitTokenDecimals, - }, - hasSeenExpiryToast: false, - humanizedSessionPeriod: transaction1CTParams.sessionPeriod.end, - userOsmoAddress: walletRepo.current!.address!, + spendLimitTokenDecimals, + transaction1CTParams, + allowedAmount, + t, }); - - setDoNotShowFloatingBannerAgain(true); - accountStore.setShouldUseOneClickTrading({ nextValue: true }); - - const sessionEndDate = dayjs.unix( - unixNanoSecondsToSeconds(sessionPeriod.end) - ); - const humanizedTime = humanizeTime(sessionEndDate); - displayToast( - { - titleTranslationKey: "oneClickTrading.toast.oneClickTradingActive", - captionElement: ( -

- {humanizedTime.value} {t(humanizedTime.unitTranslationKey)}{" "} - {t("remaining")} -

- ), - }, - ToastType.ONE_CLICK_TRADING - ); }, { ...queryOptions, diff --git a/packages/web/hooks/mutations/one-click-trading/use-remove-one-click-trading-session.ts b/packages/web/hooks/mutations/one-click-trading/use-remove-one-click-trading-session.ts index 785ff814ae..a0a03538c2 100644 --- a/packages/web/hooks/mutations/one-click-trading/use-remove-one-click-trading-session.ts +++ b/packages/web/hooks/mutations/one-click-trading/use-remove-one-click-trading-session.ts @@ -1,4 +1,5 @@ import { DeliverTxResponse } from "@osmosis-labs/stores"; +import { makeRemoveAuthenticatorMsg } from "@osmosis-labs/tx"; import { useMutation, UseMutationOptions } from "@tanstack/react-query"; import { displayToast, ToastType } from "~/components/alert"; @@ -19,32 +20,42 @@ export const useRemoveOneClickTradingSession = ({ queryOptions?: UseRemoveOneClickTradingMutationOptions; } = {}) => { const { accountStore } = useStore(); - const account = accountStore.getWallet(accountStore.osmosisChainId); const { logEvent } = useAmplitudeAnalytics(); return useMutation( async ({ authenticatorId }) => { - if (!account?.osmosis) { - throw new Error("Osmosis account not found"); + const userOsmoAddress = accountStore.getWallet( + accountStore.osmosisChainId + )?.address; + + if (!userOsmoAddress) { + throw new Error("User Osmo address not found"); } + const msg = await makeRemoveAuthenticatorMsg({ + id: BigInt(authenticatorId), + sender: userOsmoAddress, + }); + await new Promise((resolve, reject) => { - account.osmosis - .sendAddOrRemoveAuthenticatorsMsg({ - addAuthenticators: [], - removeAuthenticators: [BigInt(authenticatorId)], - memo: "", - onFulfill: (tx) => { - if (tx.code === 0) { - resolve(tx); - } else { - reject(new Error("Transaction failed")); - } - }, - signOptions: { - preferNoSetFee: true, - }, - }) + accountStore + .signAndBroadcast( + accountStore.osmosisChainId, + "addOrRemoveAuthenticators", + [msg], + "", + undefined, + { preferNoSetFee: true }, + { + onFulfill: (tx) => { + if (tx.code === 0) { + resolve(tx); + } else { + reject(new Error("Transaction failed")); + } + }, + } + ) .catch((error) => { reject(error); }); diff --git a/packages/web/hooks/one-click-trading/index.ts b/packages/web/hooks/one-click-trading/index.ts index cfcdaf671a..22c01f1047 100644 --- a/packages/web/hooks/one-click-trading/index.ts +++ b/packages/web/hooks/one-click-trading/index.ts @@ -1,3 +1,3 @@ export * from "./use-one-click-trading-params"; export * from "./use-one-click-trading-session"; -export * from "./use-one-click-trading-session-manager"; +export * from "./use-one-click-trading-swap-review"; diff --git a/packages/web/hooks/one-click-trading/use-one-click-trading-session-manager.ts b/packages/web/hooks/one-click-trading/use-one-click-trading-session-manager.ts deleted file mode 100644 index c6b0db29bf..0000000000 --- a/packages/web/hooks/one-click-trading/use-one-click-trading-session-manager.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { Dec, PricePretty } from "@keplr-wallet/unit"; -import { OneClickTradingInfo } from "@osmosis-labs/stores"; -import { OneClickTradingTransactionParams } from "@osmosis-labs/types"; -import { useCallback, useEffect, useMemo, useRef } from "react"; - -import { displayErrorRemovingSessionToast } from "~/components/alert/one-click-trading-toasts"; -import { isRejectedTxErrorMessage } from "~/components/alert/prettify"; -import { EventName } from "~/config/analytics-events"; -import { useCreateOneClickTradingSession } from "~/hooks/mutations/one-click-trading"; -import { useRemoveOneClickTradingSession } from "~/hooks/mutations/one-click-trading/use-remove-one-click-trading-session"; -import { useOneClickTradingParams } from "~/hooks/one-click-trading/use-one-click-trading-params"; -import { useOneClickTradingSession } from "~/hooks/one-click-trading/use-one-click-trading-session"; -import { useAmplitudeAnalytics } from "~/hooks/use-amplitude-analytics"; -import { useStore } from "~/stores"; -import { trimPlaceholderZeros } from "~/utils/number"; -import { api } from "~/utils/trpc"; - -export function useOneClickTradingSessionManager({ - onCommit, -}: { - onCommit: () => void; -}) { - const { logEvent } = useAmplitudeAnalytics(); - - const { accountStore, chainStore } = useStore(); - - const { - isOneClickTradingEnabled: isEnabled, - isOneClickTradingExpired: isExpired, - oneClickTradingInfo: info, - isLoadingInfo, - } = useOneClickTradingSession(); - - const { - initialTransaction1CTParams: initialTransactionParams, - transaction1CTParams: transactionParams, - setTransaction1CTParams: setTransactionParams, - spendLimitTokenDecimals, - reset: resetParams, - changes, - setChanges, - } = useOneClickTradingParams({ - oneClickTradingInfo: info, - defaultIsOneClickEnabled: isEnabled ?? false, - }); - - const shouldSend1CTTx = useMemo(() => { - // Turn on or off: The session status have changed either turned on or off explicitly - if ( - transactionParams?.isOneClickEnabled !== - initialTransactionParams?.isOneClickEnabled - ) { - return true; - } - - // Modify: The session was already on, wasn't turned off and the params have changed - if ( - transactionParams?.isOneClickEnabled && - initialTransactionParams?.isOneClickEnabled && - changes.length > 0 - ) { - return true; - } - - return false; - }, [transactionParams, initialTransactionParams, changes]); - - const { - wouldExceedSpendLimit, - remainingSpendLimit, - sessionAuthenticator, - isLoading: isLoadingRemainingSpendLimit, - } = useOneClickRemainingSpendLimit({ - enabled: isEnabled, - transactionParams, - oneClickTradingInfo: info, - }); - - const isLoading = isLoadingInfo || isLoadingRemainingSpendLimit; - - const createSession = useCreateOneClickTradingSession(); - const removeSession = useRemoveOneClickTradingSession(); - - const onCommitRef = useRef(onCommit); - const isEnabledRef = useRef(isEnabled); - useEffect(() => { - onCommitRef.current = onCommit; - isEnabledRef.current = isEnabled; - }, [onCommit, isEnabled]); - - const startSession = useCallback(() => { - if (!transactionParams) return; - - const rollbackCreateSession = () => { - setTransactionParams({ - ...(initialTransactionParams ?? transactionParams), - isOneClickEnabled: false, - }); - }; - - createSession.mutate( - { - spendLimitTokenDecimals, - transaction1CTParams: transactionParams, - walletRepo: accountStore.getWalletRepo(chainStore.osmosis.chainId), - /** - * If the user has an existing session, remove it and add the new one. - */ - additionalAuthenticatorsToRemove: sessionAuthenticator - ? [BigInt(sessionAuthenticator.id)] - : undefined, - }, - { - onSuccess: async () => { - // Wait for isEnabled to be updated before committing - await new Promise((resolve, reject) => { - let retries = 0; - const maxRetries = 10; // 1 second - const checkIsEnabled = () => { - if (isEnabledRef.current) { - resolve(); - } else if (retries >= maxRetries) { - reject( - new Error( - "Timed out waiting for one-click trading to be enabled" - ) - ); - } else { - retries++; - setTimeout(checkIsEnabled, 100); - } - }; - checkIsEnabled(); - }); - onCommitRef.current(); - logEvent([EventName.OneClickTrading.enableOneClickTrading]); - }, - onError: () => { - rollbackCreateSession(); - onCommitRef.current(); - }, - } - ); - }, [ - transactionParams, - createSession, - spendLimitTokenDecimals, - accountStore, - chainStore.osmosis.chainId, - sessionAuthenticator, - setTransactionParams, - initialTransactionParams, - logEvent, - ]); - - const stopSession = useCallback(() => { - if (!transactionParams) return; - - const rollbackRemoveSession = () => { - setTransactionParams({ - ...(initialTransactionParams ?? transactionParams), - isOneClickEnabled: true, - }); - }; - - if (!info) { - displayErrorRemovingSessionToast(); - rollbackRemoveSession(); - throw new Error("oneClickTradingInfo is undefined"); - } - - removeSession.mutate( - { - authenticatorId: info?.authenticatorId, - }, - { - onError: (e) => { - const error = e as Error; - rollbackRemoveSession(); - if (!isRejectedTxErrorMessage({ message: error?.message })) { - displayErrorRemovingSessionToast(); - } - onCommitRef.current(); - }, - onSettled: () => { - onCommitRef.current(); - }, - } - ); - }, [ - info, - initialTransactionParams, - removeSession, - setTransactionParams, - transactionParams, - ]); - - const commitSessionChange = useCallback(() => { - if (!shouldSend1CTTx) { - onCommitRef.current(); - return; - } - - if (!transactionParams) return; - - if (transactionParams.isOneClickEnabled) { - startSession(); - } else { - stopSession(); - } - }, [shouldSend1CTTx, transactionParams, startSession, stopSession]); - - return { - isEnabled, - isExpired, - isLoading, - commitSessionChangeIsLoading: - createSession.isLoading || removeSession.isLoading, - changes, - setChanges, - transactionParams, - wouldExceedSpendLimit, - remainingSpendLimit, - setTransactionParams, - commitSessionChange, - resetParams, - }; -} - -export function useOneClickRemainingSpendLimit({ - enabled = true, - transactionParams, - oneClickTradingInfo, -}: { - enabled?: boolean; - transactionParams?: OneClickTradingTransactionParams; - oneClickTradingInfo?: OneClickTradingInfo; -}) { - const { accountStore, chainStore } = useStore(); - const account = accountStore.getWallet(chainStore.osmosis.chainId); - - const shouldFetchExistingSessionAuthenticator = - !!account?.address && !!oneClickTradingInfo; - - const { data: sessionAuthenticator, isLoading } = - api.local.oneClickTrading.getSessionAuthenticator.useQuery( - { - userOsmoAddress: account?.address ?? "", - publicKey: oneClickTradingInfo?.publicKey ?? "", - }, - { - enabled: enabled && shouldFetchExistingSessionAuthenticator, - cacheTime: 15_000, // 15 seconds - staleTime: 15_000, // 15 seconds - retry: false, - } - ); - - const { data: amountSpentData } = - api.local.oneClickTrading.getAmountSpent.useQuery( - { - authenticatorId: oneClickTradingInfo?.authenticatorId ?? "", - userOsmoAddress: oneClickTradingInfo?.userOsmoAddress ?? "", - }, - { - enabled: enabled && !!oneClickTradingInfo, - } - ); - - const remainingSpendLimit = useMemo( - () => - transactionParams?.spendLimit && amountSpentData?.amountSpent - ? formatSpendLimit( - transactionParams.spendLimit.sub(amountSpentData.amountSpent) - ) - : undefined, - [transactionParams, amountSpentData] - ); - - const wouldExceedSpendLimit = useCallback( - ({ - wantToSpend, - maybeWouldSpendTotal, - }: { - wantToSpend: Dec; - maybeWouldSpendTotal?: Dec; - }) => { - if (wantToSpend.isZero()) return false; - - const spendLimit = transactionParams?.spendLimit?.toDec() ?? new Dec(0); - const amountSpent = amountSpentData?.amountSpent?.toDec() ?? new Dec(0); - /** - * If we have simulation results then we use the exact amount that would be spent - * if not we provide a fallback by adding already spent amount and the next spending - * (the fallback ignores the fact that for some tokens, the value is not included in the spend limit) - */ - const wouldSpend = maybeWouldSpendTotal ?? amountSpent.add(wantToSpend); - - return wouldSpend.gt(spendLimit); - }, - [amountSpentData, transactionParams] - ); - - return { - amountSpent: amountSpentData?.amountSpent, - remainingSpendLimit, - wouldExceedSpendLimit, - sessionAuthenticator, - isLoading, - }; -} - -export function formatSpendLimit(price: PricePretty | undefined) { - return `${price?.symbol}${trimPlaceholderZeros( - price?.toDec().toString(2) ?? "" - )}`; -} 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 new file mode 100644 index 0000000000..42dd3f75ee --- /dev/null +++ b/packages/web/hooks/one-click-trading/use-one-click-trading-swap-review.ts @@ -0,0 +1,325 @@ +import { Dec, PricePretty } from "@keplr-wallet/unit"; +import { OneClickTradingInfo } from "@osmosis-labs/stores"; +import { makeRemoveAuthenticatorMsg } from "@osmosis-labs/tx"; +import { OneClickTradingTransactionParams } from "@osmosis-labs/types"; +import { useCallback, useEffect, useMemo } from "react"; +import { useAsync } from "react-use"; +import { create } from "zustand"; +import { useShallow } from "zustand/react/shallow"; + +import { makeCreate1CTSessionMessage } from "~/hooks/mutations/one-click-trading"; +import { + OneClickTradingParamsChanges, + useOneClickTradingParams, +} from "~/hooks/one-click-trading/use-one-click-trading-params"; +import { useOneClickTradingSession } from "~/hooks/one-click-trading/use-one-click-trading-session"; +import { useStore } from "~/stores"; +import { trimPlaceholderZeros } from "~/utils/number"; +import { api } from "~/utils/trpc"; + +const use1CTSwapReviewStore = create<{ + transaction1CTParams?: OneClickTradingTransactionParams; + spendLimitTokenDecimals?: number; + changes?: OneClickTradingParamsChanges; + initialTransactionParams?: OneClickTradingTransactionParams; + setTransaction1CTParams: ( + transaction1CTParams: OneClickTradingTransactionParams | undefined + ) => void; + setSpendLimitTokenDecimals: ( + 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({ + isModalOpen, +}: { + isModalOpen: boolean; +}) { + const { + isOneClickTradingEnabled: isEnabled, + isOneClickTradingExpired: isExpired, + oneClickTradingInfo, + isLoadingInfo, + } = useOneClickTradingSession(); + + const { + initialTransaction1CTParams: initialTransactionParams, + transaction1CTParams: transactionParams, + setTransaction1CTParams: setTransactionParams, + spendLimitTokenDecimals, + reset: resetParams, + changes, + setChanges, + } = useOneClickTradingParams({ + oneClickTradingInfo, + defaultIsOneClickEnabled: isEnabled ?? false, + }); + + const { wouldExceedSpendLimit, remainingSpendLimit } = + useOneClickRemainingSpendLimit({ + enabled: isEnabled, + transactionParams, + oneClickTradingInfo, + }); + + const isLoading = isLoadingInfo; + + useEffect(() => { + if (isModalOpen) { + use1CTSwapReviewStore + .getState() + .setTransaction1CTParams(transactionParams); + } + }, [transactionParams, isModalOpen]); + + useEffect(() => { + if (isModalOpen) { + use1CTSwapReviewStore + .getState() + .setSpendLimitTokenDecimals(spendLimitTokenDecimals); + } + }, [isModalOpen, spendLimitTokenDecimals]); + + useEffect(() => { + if (isModalOpen) { + use1CTSwapReviewStore + .getState() + .setInitialTransactionParams(initialTransactionParams); + } + }, [isModalOpen, initialTransactionParams]); + + useEffect(() => { + if (isModalOpen) { + use1CTSwapReviewStore.getState().setChanges(changes); + } + }, [isModalOpen, changes]); + + useEffect(() => { + if (!isModalOpen) { + const state = use1CTSwapReviewStore.getState(); + resetParams(); + state.setTransaction1CTParams(undefined); + state.setSpendLimitTokenDecimals(undefined); + state.setChanges(undefined); + state.setInitialTransactionParams(undefined); + } + }); + + return { + isEnabled, + isExpired, + isLoading, + changes, + setChanges, + transactionParams, + wouldExceedSpendLimit, + remainingSpendLimit, + setTransactionParams, + resetParams, + }; +} + +export function use1CTSwapReviewMessages() { + const apiUtils = api.useUtils(); + + 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 { oneClickTradingInfo, isOneClickTradingEnabled, isLoadingInfo } = + useOneClickTradingSession(); + + const shouldFetchExistingSessionAuthenticator = + !!account?.address && !!oneClickTradingInfo; + + const { + data: sessionAuthenticator, + isLoading: isLoadingSessionAuthenticator, + } = api.local.oneClickTrading.getSessionAuthenticator.useQuery( + { + userOsmoAddress: account?.address ?? "", + publicKey: oneClickTradingInfo?.publicKey ?? "", + }, + { + enabled: + isOneClickTradingEnabled && shouldFetchExistingSessionAuthenticator, + cacheTime: 15_000, // 15 seconds + staleTime: 15_000, // 15 seconds + retry: false, + } + ); + + 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; + } + + return false; + }, [transaction1CTParams, initialTransactionParams, changes]); + + const { value: oneClickMessages, loading: isLoadingOneClickMessages } = + useAsync(async () => { + if ( + !transaction1CTParams || + !spendLimitTokenDecimals || + !account?.address || + !shouldSend1CTTx + ) + return undefined; + + if (transaction1CTParams.isOneClickEnabled) { + const result = await makeCreate1CTSessionMessage({ + apiUtils, + transaction1CTParams, + spendLimitTokenDecimals, + userOsmoAddress: account?.address ?? "", + /** + * If the user has an existing session, remove it and add the new one. + */ + additionalAuthenticatorsToRemove: sessionAuthenticator + ? [BigInt(sessionAuthenticator.id)] + : undefined, + }); + return { type: "create-1ct-session" as const, ...result }; + } + + return { + type: "remove-1ct-session" as const, + msgs: [ + await makeRemoveAuthenticatorMsg({ + id: BigInt(oneClickTradingInfo!.authenticatorId), + sender: account.address, + }), + ], + }; + }, [ + account?.address, + apiUtils, + oneClickTradingInfo, + sessionAuthenticator, + shouldSend1CTTx, + spendLimitTokenDecimals, + transaction1CTParams, + ]); + + return { + oneClickMessages, + shouldSend1CTTx, + isLoadingOneClickMessages: shouldSend1CTTx + ? isLoadingOneClickMessages || + (shouldFetchExistingSessionAuthenticator + ? isLoadingSessionAuthenticator + : false) || + isLoadingInfo + : false, + }; +} + +function useOneClickRemainingSpendLimit({ + enabled = true, + transactionParams, + oneClickTradingInfo, +}: { + enabled?: boolean; + transactionParams?: OneClickTradingTransactionParams; + oneClickTradingInfo?: OneClickTradingInfo; +}) { + const { data: amountSpentData } = + api.local.oneClickTrading.getAmountSpent.useQuery( + { + authenticatorId: oneClickTradingInfo?.authenticatorId ?? "", + userOsmoAddress: oneClickTradingInfo?.userOsmoAddress ?? "", + }, + { + enabled: enabled && !!oneClickTradingInfo, + } + ); + + const remainingSpendLimit = useMemo( + () => + transactionParams?.spendLimit && amountSpentData?.amountSpent + ? formatSpendLimit( + transactionParams.spendLimit.sub(amountSpentData.amountSpent) + ) + : undefined, + [transactionParams, amountSpentData] + ); + + const wouldExceedSpendLimit = useCallback( + ({ + wantToSpend, + maybeWouldSpendTotal, + }: { + wantToSpend: Dec; + maybeWouldSpendTotal?: Dec; + }) => { + if (wantToSpend.isZero()) return false; + + const spendLimit = transactionParams?.spendLimit?.toDec() ?? new Dec(0); + const amountSpent = amountSpentData?.amountSpent?.toDec() ?? new Dec(0); + /** + * If we have simulation results then we use the exact amount that would be spent + * if not we provide a fallback by adding already spent amount and the next spending + * (the fallback ignores the fact that for some tokens, the value is not included in the spend limit) + */ + const wouldSpend = maybeWouldSpendTotal ?? amountSpent.add(wantToSpend); + + return wouldSpend.gt(spendLimit); + }, + [amountSpentData, transactionParams] + ); + + return { + amountSpent: amountSpentData?.amountSpent, + remainingSpendLimit, + wouldExceedSpendLimit, + }; +} + +export function formatSpendLimit(price: PricePretty | undefined) { + return `${price?.symbol}${trimPlaceholderZeros( + price?.toDec().toString(2) ?? "" + )}`; +} diff --git a/packages/web/hooks/use-swap.tsx b/packages/web/hooks/use-swap.tsx index be2714e38f..e83028f725 100644 --- a/packages/web/hooks/use-swap.tsx +++ b/packages/web/hooks/use-swap.tsx @@ -17,7 +17,6 @@ import { getSwapMessages, getSwapTxParameters, QuoteDirection, - SwapTxRouteInGivenOut, SwapTxRouteOutGivenIn, } from "@osmosis-labs/tx"; import { Currency, MinimalAsset } from "@osmosis-labs/types"; @@ -29,7 +28,7 @@ import { } from "@osmosis-labs/utils"; import { createTRPCReact } from "@trpc/react-query"; import { parseAsString, useQueryState } from "nuqs"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "react-toastify"; import { useAsync } from "react-use"; @@ -47,7 +46,11 @@ import { getTokenOutFiatValue, } from "~/hooks/fiat-getters"; import { useTranslation } from "~/hooks/language"; -import { useOneClickTradingSession } from "~/hooks/one-click-trading"; +import { onAdd1CTSession } from "~/hooks/mutations/one-click-trading"; +import { + use1CTSwapReviewMessages, + useOneClickTradingSession, +} from "~/hooks/one-click-trading"; import { mulPrice } from "~/hooks/queries/assets/use-coin-fiat-value"; import { useDeepMemo } from "~/hooks/use-deep-memo"; import { useEstimateTxFees } from "~/hooks/use-estimate-tx-fees"; @@ -109,6 +112,7 @@ export function useSwap( quoteType = "out-given-in", }: SwapOptions = { maxSlippage: undefined } ) { + const apiUtils = api.useUtils(); const { chainStore, accountStore } = useStore(); const account = accountStore.getWallet(chainStore.osmosis.chainId); const featureFlags = useFeatureFlags(); @@ -324,13 +328,20 @@ export function useSwap( outAmountInput.debouncedInAmount !== null && Boolean(outAmountInput.amount); + const { oneClickMessages, isLoadingOneClickMessages, shouldSend1CTTx } = + use1CTSwapReviewMessages(); + + const messages = useMemo(() => { + return [...(quote?.messages ?? []), ...(oneClickMessages?.msgs ?? [])]; + }, [quote?.messages, oneClickMessages?.msgs]); + const { data: networkFee, error: networkFeeError, isLoading: isLoadingNetworkFee_, } = useEstimateTxFees({ chainId: chainStore.osmosis.chainId, - messages: quote?.messages, + messages, enabled: true, signOptions: { useOneClickTrading: isOneClickTradingEnabled, @@ -379,44 +390,11 @@ export function useSwap( return balance.toDec().lt(amountWithSlippage); }, [inAmountInput.balance, inAmountInput.amount, maxSlippage, quoteType]); - /** - * Create refs so we can wait for them to finish in the sendTradeTokenInTx function - * This is a safety measure to prevent signed swaps from failing due to stale gas amount - * e.g. after enabling 1CT session the gas estimation needs to be updated before sending the swap - */ - const isLoadingNetworkFeeRef = useRef(isLoadingNetworkFee); - const networkFeeErrorRef = useRef(networkFeeError); - useEffect(() => { - networkFeeErrorRef.current = networkFeeError; - isLoadingNetworkFeeRef.current = isLoadingNetworkFee; - }, [isLoadingNetworkFee, networkFeeError]); - /** Send trade token in transaction. */ const sendTradeTokenInTx = useCallback( () => new Promise<"multiroute" | "multihop" | "exact-in">( async (resolve, reject) => { - // Wait for network fee to load, retry up to 10 times - let retries = 0; - while (isLoadingNetworkFeeRef.current && retries < 10) { - if (networkFeeErrorRef.current) { - return reject(new Error(networkFeeErrorRef.current.message)); - } - await new Promise((resolve) => setTimeout(resolve, 100)); - retries++; - } - - // After retries, check if still loading - if (isLoadingNetworkFeeRef.current) { - return reject( - new Error("Network fee is still loading after 1 second") - ); - } - - if (networkFeeErrorRef.current) { - return reject(new Error(networkFeeErrorRef.current.message)); - } - if (!maxSlippage) return reject(new Error("Max slippage is not defined.")); if (!inAmountInput.amount || !outAmountInput.amount) @@ -461,10 +439,10 @@ export function useSwap( // // This occurs because 1CT transactions require more gas, which makes // our current quote's gas estimation incorrect and low. This results in out of gas error. - const messageCanBeSignedWithOneClickTrading = !isNil(quote?.messages) + const messageCanBeSignedWithOneClickTrading = !isNil(messages) ? isOneClickTradingEnabled && (await accountStore.shouldBeSignedWithOneClickTrading({ - messages: quote.messages, + messages, })) : false; @@ -538,122 +516,86 @@ export function useSwap( : {}), }; - /** - * Send messages to account - */ - if (quoteType === "out-given-in") { - const { routes, tokenIn, tokenOutMinAmount } = txParams; - const typedRoutes = routes as SwapTxRouteOutGivenIn[]; - if (routes.length === 1) { - const { pools } = typedRoutes[0]; - account.osmosis - .sendSwapExactAmountInMsg( - pools, - tokenIn!, - tokenOutMinAmount!, - undefined, - signOptions, - ({ code }) => { - if (code) - reject( - new Error("Failed to send swap exact amount in message") - ); - else resolve(pools.length === 1 ? "exact-in" : "multihop"); - } - ) - .catch(reject); - } else if (routes.length > 1) { - account.osmosis - .sendSplitRouteSwapExactAmountInMsg( - typedRoutes, - tokenIn!, - tokenOutMinAmount!, - undefined, - signOptions, - ({ code }) => { - if (code) - reject( - new Error( - "Failed to send split route swap exact amount in message" - ) - ); - else resolve("multiroute"); - } - ) - .catch(reject); - } else { - // should not be possible because button should be disabled - reject(new Error("No routes given")); - } - } else { - const { routes, tokenOut, tokenInMaxAmount } = txParams; - const typedRoutes = routes as SwapTxRouteInGivenOut[]; - if (routes.length === 1) { - const { pools } = typedRoutes[0]; - account.osmosis - .sendSwapExactAmountOutMsg( - pools, - { - coinMinimalDenom: tokenOut!.coinMinimalDenom, - amount: tokenOut!.amount, - }, - tokenInMaxAmount!, - undefined, - signOptions, - ({ code }) => { - if (code) - reject( - new Error("Failed to send swap exact amount in message") - ); - else resolve(pools.length === 1 ? "exact-in" : "multihop"); - } - ) - .catch(reject); - } else if (routes.length > 1) { - account.osmosis - .sendSplitRouteSwapExactAmountOutMsg( - typedRoutes, - tokenOut!, - tokenInMaxAmount!, - undefined, - signOptions, - ({ code }) => { - if (code) - reject( - new Error( - "Failed to send split route swap exact amount in message" - ) - ); - else resolve("multiroute"); + const { routes } = txParams; + const typedRoutes = routes as SwapTxRouteOutGivenIn[]; + accountStore + .signAndBroadcast( + chainStore.osmosis.chainId, + routes.length === 1 + ? "swapExactAmountIn" + : "splitRouteSwapExactAmountIn", + messages, + undefined, + signOptions?.fee, + signOptions, + (tx) => { + const { code } = tx; + if (code) { + reject( + new Error("Failed to send swap exact amount in message") + ); + } else { + if ( + shouldSend1CTTx && + oneClickMessages && + oneClickMessages.type === "create-1ct-session" + ) { + onAdd1CTSession({ + privateKey: oneClickMessages.key, + tx, + userOsmoAddress: account?.address ?? "", + fallbackGetAuthenticatorId: + apiUtils.local.oneClickTrading.getSessionAuthenticator + .fetch, + accountStore, + allowedMessages: oneClickMessages.allowedMessages, + sessionPeriod: oneClickMessages.sessionPeriod, + spendLimitTokenDecimals: + oneClickMessages.spendLimitTokenDecimals, + transaction1CTParams: + oneClickMessages.transaction1CTParams, + allowedAmount: oneClickMessages.allowedAmount, + t, + }); } - ) - .catch(reject); - } else { - // should not be possible because button should be disabled - reject(new Error("No routes given")); - } - } + + resolve( + routes.length === 1 + ? typedRoutes[0].pools.length === 1 + ? "exact-in" + : "multihop" + : "multiroute" + ); + } + } + ) + .catch(reject); } ).finally(() => { inAmountInput.reset(); outAmountInput.reset(); }), [ - maxSlippage, - inAmountInput, account, - quote, - isOneClickTradingEnabled, accountStore, - hasOverSpendLimitError, - networkFeeError, + apiUtils.local.oneClickTrading.getSessionAuthenticator.fetch, + chainStore.osmosis.chainId, featureFlags.swapToolSimulateFee, + hasOverSpendLimitError, + inAmountInput, + isOneClickTradingEnabled, + maxSlippage, + messages, networkFee, + networkFeeError, + oneClickMessages, + outAmountInput, + quote, + quoteType, + shouldSend1CTTx, swapAssets.fromAsset, swapAssets.toAsset, t, - quoteType, - outAmountInput, ] ); @@ -782,6 +724,7 @@ export function useSwap( networkFee, isLoadingNetworkFee: inAmountInput.isLoadingCurrentBalanceNetworkFee || isLoadingNetworkFee, + isLoadingOneClickMessages, networkFeeError, error: precedentError, spotPriceQuote, @@ -1098,6 +1041,8 @@ function useSwapAmountInput({ const { isOneClickTradingEnabled } = useOneClickTradingSession(); + const { oneClickMessages } = use1CTSwapReviewMessages(); + const networkFeeQueryEnabled = !isQuoteForCurrentBalanceLoading && balanceQuoteQueryEnabled && @@ -1108,7 +1053,10 @@ function useSwapAmountInput({ error: currentBalanceNetworkFeeError, } = useEstimateTxFees({ chainId: chainStore.osmosis.chainId, - messages: quoteForCurrentBalance?.messages, + messages: [ + ...(quoteForCurrentBalance?.messages ?? []), + ...(oneClickMessages?.msgs ?? []), + ], enabled: networkFeeQueryEnabled, signOptions: { useOneClickTrading: isOneClickTradingEnabled, diff --git a/packages/web/modals/profile.tsx b/packages/web/modals/profile.tsx index 9bc59300cc..a2c2655e21 100644 --- a/packages/web/modals/profile.tsx +++ b/packages/web/modals/profile.tsx @@ -547,9 +547,6 @@ const OneClickTradingProfileSection: FunctionComponent<{ spendLimitTokenDecimals: oneClickTradingInfo.spendLimit.decimals, transaction1CTParams, - walletRepo: accountStore.getWalletRepo( - accountStore.osmosisChainId - ), /** * If the user has an existing session, remove it and add the new one. */ diff --git a/packages/web/modals/review-order.tsx b/packages/web/modals/review-order.tsx index 19df7ef69f..1ef8c4b221 100644 --- a/packages/web/modals/review-order.tsx +++ b/packages/web/modals/review-order.tsx @@ -7,7 +7,11 @@ import { } from "@keplr-wallet/unit"; import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; import { ObservableSlippageConfig } from "@osmosis-labs/stores"; -import { QuoteDirection } from "@osmosis-labs/tx"; +import { + type makeAddAuthenticatorMsg, + type makeRemoveAuthenticatorMsg, + QuoteDirection, +} from "@osmosis-labs/tx"; import { OneClickTradingTransactionParams } from "@osmosis-labs/types"; import classNames from "classnames"; import Image from "next/image"; @@ -31,7 +35,7 @@ import { OneClickTradingParamsChanges, useAmplitudeAnalytics, useFeatureFlags, - useOneClickTradingSessionManager, + useOneClickTradingSwapReview, useTranslation, useWindowSize, } from "~/hooks"; @@ -49,7 +53,14 @@ import { interface ReviewOrderProps { isOpen: boolean; onClose: () => void; - confirmAction: () => void; + confirmAction: (params: { + create1CTSessionMsg: + | Awaited> + | undefined; + remove1CTSessionMsg: + | Awaited> + | undefined; + }) => void; isConfirmationDisabled: boolean; slippageConfig?: ObservableSlippageConfig; amountWithSlippage?: IntPretty; @@ -128,15 +139,8 @@ export function ReviewOrder({ remainingSpendLimit: remaining1CTSpendLimit, wouldExceedSpendLimit: wouldExceedSpendLimit1CT, setTransactionParams: setTransaction1CTParams, - commitSessionChange: commit1CTSessionChange, resetParams: reset1CTParams, - commitSessionChangeIsLoading: commit1CTSessionChangeIsLoading, - } = useOneClickTradingSessionManager({ - onCommit: () => { - confirmAction(); - onClose(); - }, - }); + } = useOneClickTradingSwapReview({ isModalOpen: isOpen }); const wouldExceedSpendLimit = useMemo(() => { return wouldExceedSpendLimit1CT({ @@ -762,12 +766,13 @@ export function ReviewOrder({