diff --git a/apps/cowswap-frontend/src/common/updaters/orders/GpOrdersUpdater.ts b/apps/cowswap-frontend/src/common/updaters/orders/GpOrdersUpdater.ts index 88f51d14ca..c2cc7923c4 100644 --- a/apps/cowswap-frontend/src/common/updaters/orders/GpOrdersUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/orders/GpOrdersUpdater.ts @@ -11,7 +11,7 @@ import { Order, OrderStatus } from 'legacy/state/orders/actions' import { useAddOrUpdateOrders, useClearOrdersStorage } from 'legacy/state/orders/hooks' import { classifyOrder, OrderTransitionStatus } from 'legacy/state/orders/utils' -import { useTokensForOrdersList, getTokensListFromOrders } from 'modules/orders' +import { getTokensListFromOrders, useTokensForOrdersList } from 'modules/orders' import { apiOrdersAtom } from 'modules/orders/state/apiOrdersAtom' import { useGpOrders } from 'api/gnosisProtocol/hooks' @@ -80,6 +80,7 @@ function _transformGpOrderToStoreOrder( summary: '', status, receiver: receiver || '', + fullAppData: order.fullAppData, apiAdditionalInfo: order, isCancelling: apiStatus === 'pending' && order.invalidated, // already cancelled in the API, not yet in the UI // EthFlow related diff --git a/apps/cowswap-frontend/src/legacy/state/orders/actions.ts b/apps/cowswap-frontend/src/legacy/state/orders/actions.ts index a08dd6ecd6..162055f7c8 100644 --- a/apps/cowswap-frontend/src/legacy/state/orders/actions.ts +++ b/apps/cowswap-frontend/src/legacy/state/orders/actions.ts @@ -59,6 +59,10 @@ export interface BaseOrder extends Omit { // Additional information from the order available in the API apiAdditionalInfo?: OrderInfoApi + // De-normalizing it as this is known at order placement time as `appData`, + // but when returned from the api is replaced with the `appDataHash` + // See this order response for example https://barn.api.cow.fi/goerli/api/v1/orders/0xc170856a42f38ba07a7af3ea8f299ea724ec0aa22445eb741cbad7f9dd4fcda05b0abe214ab7875562adee331deff0fe1912fe4265269bb1 + fullAppData?: EnrichedOrder['fullAppData'] // Wallet specific presignGnosisSafeTxHash?: string // Gnosis Safe tx diff --git a/apps/cowswap-frontend/src/legacy/utils/trade.ts b/apps/cowswap-frontend/src/legacy/utils/trade.ts index 55d856f7cc..9ab6869232 100644 --- a/apps/cowswap-frontend/src/legacy/utils/trade.ts +++ b/apps/cowswap-frontend/src/legacy/utils/trade.ts @@ -1,5 +1,5 @@ -import { RADIX_DECIMAL, NATIVE_CURRENCY_BUY_ADDRESS } from '@cowprotocol/common-const' -import { isAddress, shortenAddress, formatTokenAmount, formatSymbol } from '@cowprotocol/common-utils' +import { NATIVE_CURRENCY_BUY_ADDRESS, RADIX_DECIMAL } from '@cowprotocol/common-const' +import { formatSymbol, formatTokenAmount, isAddress, shortenAddress } from '@cowprotocol/common-utils' import { EcdsaSigningScheme, OrderClass, @@ -156,6 +156,7 @@ export function mapUnsignedOrderToOrder({ unsignedOrder, additionalParams }: Map sellAmountBeforeFee, orderCreationHash, quoteId, + appData: { fullAppData }, } = additionalParams const status = _getOrderStatus(allowsOffchainSigning, isOnChain) @@ -170,6 +171,7 @@ export function mapUnsignedOrderToOrder({ unsignedOrder, additionalParams }: Map outputToken: buyToken, quoteId, class: additionalParams.class, + fullAppData, // Status status, diff --git a/apps/cowswap-frontend/src/modules/appData/index.ts b/apps/cowswap-frontend/src/modules/appData/index.ts index 96a003b233..770057cc7d 100644 --- a/apps/cowswap-frontend/src/modules/appData/index.ts +++ b/apps/cowswap-frontend/src/modules/appData/index.ts @@ -3,4 +3,5 @@ export * from './updater/AppDataUpdater' export { useAppData, useUploadAppData } from './hooks' export { updateHooksOnAppData, buildAppData } from './utils/buildAppData' export { buildAppDataHooks } from './utils/buildAppDataHooks' +export * from './utils/getAppDataHooks' export type { AppDataInfo, UploadAppDataParams } from './types' diff --git a/apps/cowswap-frontend/src/modules/appData/utils/decodeAppData.ts b/apps/cowswap-frontend/src/modules/appData/utils/decodeAppData.ts new file mode 100644 index 0000000000..b9aba4974c --- /dev/null +++ b/apps/cowswap-frontend/src/modules/appData/utils/decodeAppData.ts @@ -0,0 +1,24 @@ +import { AnyAppDataDocVersion } from '@cowprotocol/app-data' + +import { Nullish } from 'types' + +/** + * Decode appData from a string to a AnyAppDataDocVersion instance + * Keep in mind it can be a valid JSON but not necessarily a valid AppDataDoc + * + * Returns undefined if the given appData is not a valid JSON + */ +export function decodeAppData(appData: Nullish): AnyAppDataDocVersion | undefined { + if (!appData) { + return undefined + } + + try { + // TODO: returned value can be a valid JSON but not necessarily a valid AppDataDoc + return JSON.parse(appData) + } catch (e) { + console.info(`[decodeAppData] given appData is not a valid JSON`, appData) + + return undefined + } +} diff --git a/apps/cowswap-frontend/src/modules/appData/utils/getAppDataHooks.ts b/apps/cowswap-frontend/src/modules/appData/utils/getAppDataHooks.ts new file mode 100644 index 0000000000..7aa825789e --- /dev/null +++ b/apps/cowswap-frontend/src/modules/appData/utils/getAppDataHooks.ts @@ -0,0 +1,21 @@ +import { AnyAppDataDocVersion } from '@cowprotocol/app-data' + +import { Nullish } from 'types' + +import { decodeAppData } from './decodeAppData' + +import { AppDataHooks } from '../types' + +/** + * Get hooks from fullAppData, which can be JSON stringified or the instance + * + * Returns undefined if the fullAppData is falsy or if there are no hooks + */ +export function getAppDataHooks(fullAppData: Nullish): AppDataHooks | undefined { + const decodedAppData = typeof fullAppData === 'string' ? decodeAppData(fullAppData) : fullAppData + + if (!decodedAppData || !('hooks' in decodedAppData.metadata)) return undefined + + // TODO: this requires app-data v0.9.0. Might not work for newer versions... + return decodedAppData.metadata.hooks as AppDataHooks +} diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useGetOrdersToCheckPendingPermit.ts b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useGetOrdersToCheckPendingPermit.ts new file mode 100644 index 0000000000..444c72662b --- /dev/null +++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useGetOrdersToCheckPendingPermit.ts @@ -0,0 +1,34 @@ +import { useMemo } from 'react' + +import { SupportedChainId } from '@cowprotocol/cow-sdk' + +import { BalancesAndAllowances } from 'modules/tokens' + +import { ParsedOrder } from 'utils/orderUtils/parseOrder' + +import { OrdersTableList } from './useOrdersTableList' + +import { getOrderParams } from '../../../pure/OrdersTableContainer/utils/getOrderParams' +import { isParsedOrder } from '../../../utils/orderTableGroupUtils' + +export function useGetOrdersToCheckPendingPermit( + ordersList: OrdersTableList, + chainId: SupportedChainId, + balancesAndAllowances: BalancesAndAllowances +) { + return useMemo(() => { + // Pick only the pending orders + return ordersList.pending.reduce((acc: ParsedOrder[], item) => { + // Only do it for regular orders (not TWAP) + if (isParsedOrder(item)) { + const { hasEnoughAllowance } = getOrderParams(chainId, balancesAndAllowances, item) + + // Only if the order has not enough allowance + if (hasEnoughAllowance === false) { + acc.push(item) + } + } + return acc + }, []) + }, [balancesAndAllowances, chainId, ordersList.pending]) +} diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useOrdersTableList.ts b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useOrdersTableList.ts index d42302c4f1..186e89fe5e 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useOrdersTableList.ts +++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useOrdersTableList.ts @@ -3,7 +3,7 @@ import { useMemo } from 'react' import { Order, PENDING_STATES } from 'legacy/state/orders/actions' import { groupOrdersTable } from '../../../utils/groupOrdersTable' -import { getParsedOrderFromItem, isParsedOrder, OrderTableItem } from '../../../utils/orderTableGroupUtils' +import { getParsedOrderFromTableItem, isParsedOrder, OrderTableItem } from '../../../utils/orderTableGroupUtils' export interface OrdersTableList { pending: OrderTableItem[] @@ -11,8 +11,8 @@ export interface OrdersTableList { } const ordersSorter = (a: OrderTableItem, b: OrderTableItem) => { - const aCreationTime = getParsedOrderFromItem(a).creationTime - const bCreationTime = getParsedOrderFromItem(b).creationTime + const aCreationTime = getParsedOrderFromTableItem(a).creationTime + const bCreationTime = getParsedOrderFromTableItem(b).creationTime return bCreationTime.getTime() - aCreationTime.getTime() } diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx index 19e4afc5c7..d98cc04619 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx @@ -17,20 +17,22 @@ import { OrdersReceiptModal } from 'modules/ordersTable/containers/OrdersReceipt import { useSelectReceiptOrder } from 'modules/ordersTable/containers/OrdersReceiptModal/hooks' import { OrderActions } from 'modules/ordersTable/pure/OrdersTableContainer/types' import { buildOrdersTableUrl, parseOrdersTableUrl } from 'modules/ordersTable/utils/buildOrdersTableUrl' +import { PendingPermitUpdater, useGetOrdersPermitStatus } from 'modules/permit' import { useBalancesAndAllowances } from 'modules/tokens' import { useCancelOrder } from 'common/hooks/useCancelOrder' +import { useCategorizeRecentActivity } from 'common/hooks/useCategorizeRecentActivity' import { ordersToCancelAtom, updateOrdersToCancelAtom } from 'common/hooks/useMultipleOrdersCancellation/state' import { CancellableOrder } from 'common/utils/isOrderCancellable' import { ParsedOrder } from 'utils/orderUtils/parseOrder' +import { useGetOrdersToCheckPendingPermit } from './hooks/useGetOrdersToCheckPendingPermit' import { OrdersTableList, useOrdersTableList } from './hooks/useOrdersTableList' import { useOrdersTableTokenApprove } from './hooks/useOrdersTableTokenApprove' import { useValidatePageUrlParams } from './hooks/useValidatePageUrlParams' -import { useCategorizeRecentActivity } from '../../../../common/hooks/useCategorizeRecentActivity' import { OrdersTableContainer, TabOrderTypes } from '../../pure/OrdersTableContainer' -import { getParsedOrderFromItem, OrderTableItem, tableItemsToOrders } from '../../utils/orderTableGroupUtils' +import { getParsedOrderFromTableItem, OrderTableItem, tableItemsToOrders } from '../../utils/orderTableGroupUtils' function getOrdersListByIndex(ordersList: OrdersTableList, id: string): OrderTableItem[] { return id === OPEN_TAB.id ? ordersList.pending : ordersList.history @@ -73,6 +75,7 @@ export function OrdersTableWidget({ const getSpotPrice = useGetSpotPrice() const selectReceiptOrder = useSelectReceiptOrder() const isSafeViaWc = useIsSafeViaWc() + const ordersPermitStatus = useGetOrdersPermitStatus() const spender = useMemo(() => (chainId ? GP_VAULT_RELAYER[chainId] : undefined), [chainId]) @@ -101,7 +104,7 @@ export function OrdersTableWidget({ const tokens = useMemo(() => { const pendingOrders = isOpenOrdersTab ? ordersList.pending : [] - return pendingOrders.map((item) => getParsedOrderFromItem(item).inputToken) + return pendingOrders.map((item) => getParsedOrderFromTableItem(item).inputToken) }, [isOpenOrdersTab, ordersList.pending]) // Get effective balance @@ -148,8 +151,11 @@ export function OrdersTableWidget({ useValidatePageUrlParams(orders.length, currentTabId, currentPageNumber) + const ordersToCheckPendingPermit = useGetOrdersToCheckPendingPermit(ordersList, chainId, balancesAndAllowances) + return ( <> + {isOpenOrdersTab && orders.length && } diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx index 21f8a63541..e71f7ff25d 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx @@ -148,6 +148,7 @@ export interface OrderRowProps { orderParams: OrderParams onClick: () => void orderActions: OrderActions + hasValidPendingPermit?: boolean | undefined children?: JSX.Element } @@ -164,6 +165,7 @@ export function OrderRow({ prices, spotPrice, children, + hasValidPendingPermit, }: OrderRowProps) { const { buyAmount, rateInfoParams, hasEnoughAllowance, hasEnoughBalance, chainId } = orderParams const { creationTime, expirationTime, status } = order @@ -174,7 +176,7 @@ export function OrderRow({ const showCancellationModal = orderActions.getShowCancellationModal(order) const withWarning = - (hasEnoughBalance === false || hasEnoughAllowance === false) && + (hasEnoughBalance === false || (hasEnoughAllowance === false && hasValidPendingPermit === false)) && // show the warning only for pending and scheduled orders (status === OrderStatus.PENDING || status === OrderStatus.SCHEDULED) const theme = useContext(ThemeContext) @@ -357,7 +359,7 @@ export function OrderRow({ {hasEnoughBalance === false && ( )} - {hasEnoughAllowance === false && ( + {hasEnoughAllowance === false && hasValidPendingPermit === false && ( orderActions.approveOrderToken(order.inputToken)} symbol={inputTokenSymbol} diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTable.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTable.tsx index 7fa718815c..b9d87eadb5 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTable.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTable.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState, useEffect, useMemo, useRef } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import iconOrderExecution from '@cowprotocol/assets/cow-swap/orderExecution.svg' import { SupportedChainId } from '@cowprotocol/cow-sdk' @@ -11,19 +11,19 @@ import SVG from 'react-inlinesvg' import { useLocation } from 'react-router-dom' import styled from 'styled-components/macro' -import { QuestionWrapper } from 'legacy/components/QuestionHelper' -import QuestionHelper from 'legacy/components/QuestionHelper' +import QuestionHelper, { QuestionWrapper } from 'legacy/components/QuestionHelper' import { PendingOrdersPrices } from 'modules/orders/state/pendingOrdersPricesAtom' import { SpotPricesKeyParams } from 'modules/orders/state/spotPricesAtom' import { ORDERS_TABLE_PAGE_SIZE } from 'modules/ordersTable/const/tabs' import { + CheckboxCheckmark, TableHeader, TableRowCheckbox, TableRowCheckboxWrapper, - CheckboxCheckmark, } from 'modules/ordersTable/pure/OrdersTableContainer/styled' import { OrderActions } from 'modules/ordersTable/pure/OrdersTableContainer/types' +import { OrdersPermitStatus } from 'modules/permit' import { BalancesAndAllowances } from 'modules/tokens' import { ordersTableFeatures } from 'common/constants/featureFlags' @@ -40,7 +40,7 @@ import { getOrderParams } from './utils/getOrderParams' import { buildOrdersTableUrl } from '../../utils/buildOrdersTableUrl' import { - getParsedOrderFromItem, + getParsedOrderFromTableItem, isParsedOrder, OrderTableItem, tableItemsToOrders, @@ -206,6 +206,7 @@ export interface OrdersTableProps { balancesAndAllowances: BalancesAndAllowances getSpotPrice: (params: SpotPricesKeyParams) => Price | null orderActions: OrderActions + ordersPermitStatus: OrdersPermitStatus } export function OrdersTable({ @@ -219,6 +220,7 @@ export function OrdersTable({ getSpotPrice, orderActions, currentPageNumber, + ordersPermitStatus, }: OrdersTableProps) { const location = useLocation() const [isRateInverted, setIsRateInverted] = useState(false) @@ -261,14 +263,14 @@ export function OrdersTable({ }, [showOrdersExplainerBanner]) const cancellableOrders = useMemo( - () => ordersPage.filter((item) => isOrderOffChainCancellable(getParsedOrderFromItem(item))), + () => ordersPage.filter((item) => isOrderOffChainCancellable(getParsedOrderFromTableItem(item))), [ordersPage] ) const allOrdersSelected = useMemo(() => { if (!cancellableOrders.length) return false - return cancellableOrders.every((item) => selectedOrdersMap[getParsedOrderFromItem(item).id]) + return cancellableOrders.every((item) => selectedOrdersMap[getParsedOrderFromTableItem(item).id]) }, [cancellableOrders, selectedOrdersMap]) const getPageUrl = useCallback((index: number) => buildOrdersTableUrl(location, { pageNumber: index }), [location]) @@ -400,7 +402,7 @@ export function OrdersTable({ {ordersPage.map((item) => { - const { inputToken, outputToken } = getParsedOrderFromItem(item) + const { inputToken, outputToken } = getParsedOrderFromTableItem(item) const spotPrice = getSpotPrice({ chainId: chainId as SupportedChainId, sellTokenAddress: inputToken.address, @@ -410,6 +412,10 @@ export function OrdersTable({ if (isParsedOrder(item)) { const order = item + const orderParams = getOrderParams(chainId, balancesAndAllowances, order) + + const hasValidPendingPermit = ordersPermitStatus[order.id] + return ( orderActions.selectReceiptOrder(order)} + hasValidPendingPermit={hasValidPendingPermit} /> ) } else { diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.cosmos.tsx index 106ec79be7..0288e07a25 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.cosmos.tsx @@ -69,5 +69,6 @@ export default ( getSpotPrice={() => null} orderActions={orderActions} orderType={TabOrderTypes.LIMIT} + ordersPermitStatus={{}} /> ) diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.tsx index 3572e28bbf..dcde2ae431 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.tsx @@ -167,6 +167,7 @@ export function OrdersTableContainer({ children, orderType, pendingActivities, + ordersPermitStatus, }: OrdersProps) { const content = () => { if (!isWalletConnected) { @@ -233,6 +234,7 @@ export function OrdersTableContainer({ balancesAndAllowances={balancesAndAllowances} getSpotPrice={getSpotPrice} orderActions={orderActions} + ordersPermitStatus={ordersPermitStatus} /> ) } diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.ts b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.ts index c7e0077ed9..afda0bf5fd 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.ts +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.ts @@ -23,8 +23,8 @@ export function getOrderParams( balancesAndAllowances: BalancesAndAllowances, order: ParsedOrder ): OrderParams { - const sellAmount = CurrencyAmount.fromRawAmount(order.inputToken, order.sellAmount.toString()) - const buyAmount = CurrencyAmount.fromRawAmount(order.outputToken, order.buyAmount.toString()) + const sellAmount = CurrencyAmount.fromRawAmount(order.inputToken, order.sellAmount) + const buyAmount = CurrencyAmount.fromRawAmount(order.outputToken, order.buyAmount) const rateInfoParams: RateInfoParams = { chainId, diff --git a/apps/cowswap-frontend/src/modules/ordersTable/utils/orderTableGroupUtils.ts b/apps/cowswap-frontend/src/modules/ordersTable/utils/orderTableGroupUtils.ts index 621ed72dd2..2f4db047ae 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/utils/orderTableGroupUtils.ts +++ b/apps/cowswap-frontend/src/modules/ordersTable/utils/orderTableGroupUtils.ts @@ -7,10 +7,11 @@ export interface OrderTableGroup { export type OrderTableItem = OrderTableGroup | ParsedOrder -export const isParsedOrder = (item: OrderTableItem): item is ParsedOrder => item.hasOwnProperty('creationTime') +export const isParsedOrder = (item: OrderTableItem): item is ParsedOrder => !('children' in item) -export const getParsedOrderFromItem = (item: OrderTableItem): ParsedOrder => (isParsedOrder(item) ? item : item.parent) +export const getParsedOrderFromTableItem = (item: OrderTableItem): ParsedOrder => + isParsedOrder(item) ? item : item.parent export function tableItemsToOrders(items: OrderTableItem[]): ParsedOrder[] { - return items.map(getParsedOrderFromItem) + return items.map(getParsedOrderFromTableItem) } diff --git a/apps/cowswap-frontend/src/modules/permit/const.ts b/apps/cowswap-frontend/src/modules/permit/const.ts index eeb9b673d0..516059f1f3 100644 --- a/apps/cowswap-frontend/src/modules/permit/const.ts +++ b/apps/cowswap-frontend/src/modules/permit/const.ts @@ -29,3 +29,5 @@ export const ORDER_TYPE_SUPPORTS_PERMIT: Record = { [TradeType.LIMIT_ORDER]: true, [TradeType.ADVANCED_ORDERS]: false, } + +export const PENDING_ORDER_PERMIT_CHECK_INTERVAL = ms`1min` diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useCheckHasValidPendingPermit.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useCheckHasValidPendingPermit.ts new file mode 100644 index 0000000000..6197865e15 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useCheckHasValidPendingPermit.ts @@ -0,0 +1,121 @@ +import { useCallback } from 'react' + +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { useWalletInfo } from '@cowprotocol/wallet' +import { Web3Provider } from '@ethersproject/providers' +import { useWeb3React } from '@web3-react/core' + +import { DAI_PERMIT_SELECTOR, Eip2612PermitUtils, EIP_2612_PERMIT_SELECTOR } from '@1inch/permit-signed-approvals-utils' + +import { getAppDataHooks } from 'modules/appData' + +import { ParsedOrder } from 'utils/orderUtils/parseOrder' + +import { CheckHasValidPendingPermit } from '../types' +import { getPermitUtilsInstance } from '../utils/getPermitUtilsInstance' + +export function useCheckHasValidPendingPermit(): CheckHasValidPendingPermit { + const { chainId } = useWalletInfo() + const { provider } = useWeb3React() + + return useCallback( + async (order: ParsedOrder): Promise => { + if (!provider) { + // Missing required params, we can't tell + return undefined + } + + return checkHasValidPendingPermit(order, provider, chainId) + }, + [chainId, provider] + ) +} + +async function checkHasValidPendingPermit( + order: ParsedOrder, + provider: Web3Provider, + chainId: SupportedChainId +): Promise { + const { fullAppData, partiallyFillable, executionData } = order + const preHooks = getAppDataHooks(fullAppData)?.pre + + if ( + // No hooks === no permit + !preHooks || + // Permit is only executed for partially fillable orders in the first execution + // Thus, if there is any amount executed, partiallyFillable permit is no longer valid + (partiallyFillable && executionData.filledAmount.gt('0')) + ) { + // These cases we know for sure permit isn't valid or there is no permit + return false + } + + const eip2162Utils = getPermitUtilsInstance(chainId, provider, order.owner) + + const tokenAddress = order.inputToken.address + const tokenName = order.inputToken.name || tokenAddress + + const checkedHooks = await Promise.all( + preHooks.map(({ callData }) => + checkIsSingleCallDataAValidPermit(order, chainId, eip2162Utils, tokenAddress, tokenName, callData) + ) + ) + + const validPermits = checkedHooks.filter((v) => v !== undefined) + + if (!validPermits.length) { + // No permits means no preHook permits, we can say that there is no valid permit + return false + } + + // Only when all permits are valid, then the order permits are still valid + return validPermits.every(Boolean) +} + +async function checkIsSingleCallDataAValidPermit( + order: ParsedOrder, + chainId: SupportedChainId, + eip2162Utils: Eip2612PermitUtils, + tokenAddress: string, + tokenName: string, + callData: string +): Promise { + const params = { chainId, tokenName, tokenAddress, callData } + + let recoverPermitOwnerPromise: Promise | undefined = undefined + + // If pre-hook doesn't start with either selector, it's not a permit + if (callData.startsWith(EIP_2612_PERMIT_SELECTOR)) { + recoverPermitOwnerPromise = eip2162Utils.recoverPermitOwnerFromCallData({ + ...params, + // I don't know why this was removed, ok? + // We added it back on buildPermitCallData.ts + // But it looks like this is needed 🤷 + // Check the test for this method https://github.com/1inch/permit-signed-approvals-utils/blob/master/src/eip-2612-permit.test.ts#L85-L106 + callData: callData.replace(EIP_2612_PERMIT_SELECTOR, '0x'), + }) + } else if (callData.startsWith(DAI_PERMIT_SELECTOR)) { + recoverPermitOwnerPromise = eip2162Utils.recoverDaiLikePermitOwnerFromCallData({ + ...params, + callData: callData.replace(DAI_PERMIT_SELECTOR, '0x'), + }) + } + + if (!recoverPermitOwnerPromise) { + // The callData doesn't match any known permit type + return undefined + } + + try { + const recoveredOwner = await recoverPermitOwnerPromise + + // Permit is valid when recovered owner matches order owner + return recoveredOwner.toLowerCase() === order.owner.toLowerCase() + } catch (e) { + console.debug( + `[checkHasValidPendingPermit] Failed to check permit validity for order ${order.id} with callData ${callData}`, + e + ) + return false + } +} diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts index 0505914230..82edb237c0 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts @@ -1,10 +1,15 @@ -import { useSetAtom } from 'jotai' +import { useAtomValue, useSetAtom } from 'jotai' import { useCallback } from 'react' import { useWalletInfo } from '@cowprotocol/wallet' import { useWeb3React } from '@web3-react/core' -import { getPermitCacheAtom, storePermitCacheAtom } from '../state/permitCacheAtom' +import { + getPermitCacheAtom, + staticPermitCacheAtom, + storePermitCacheAtom, + userPermitCacheAtom, +} from '../state/permitCacheAtom' import { GeneratePermitHook, GeneratePermitHookParams, PermitHookData } from '../types' import { generatePermitHook } from '../utils/generatePermitHook' import { getPermitUtilsInstance } from '../utils/getPermitUtilsInstance' @@ -15,6 +20,15 @@ import { getPermitUtilsInstance } from '../utils/getPermitUtilsInstance' export function useGeneratePermitHook(): GeneratePermitHook { const getCachedPermit = useSetAtom(getPermitCacheAtom) const storePermit = useSetAtom(storePermitCacheAtom) + + // Warming up stored atoms + // + // For some reason, atoms start always in the default state (`{}`) on load, + // even if localStorage contains data, wiping previously saved data. + // Here we force an individual read of each atom, which does populate them properly + useAtomValue(staticPermitCacheAtom) + useAtomValue(userPermitCacheAtom) + const { chainId } = useWalletInfo() const { provider } = useWeb3React() diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useOrdersPermitStatus.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useOrdersPermitStatus.ts new file mode 100644 index 0000000000..b61aba3797 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useOrdersPermitStatus.ts @@ -0,0 +1,7 @@ +import { useAtomValue } from 'jotai' + +import { ordersPermitStatusAtom } from '../state/ordersPermitStatusAtom' + +export function useGetOrdersPermitStatus() { + return useAtomValue(ordersPermitStatusAtom) +} diff --git a/apps/cowswap-frontend/src/modules/permit/index.ts b/apps/cowswap-frontend/src/modules/permit/index.ts index 5f70182cfc..9de2b7724e 100644 --- a/apps/cowswap-frontend/src/modules/permit/index.ts +++ b/apps/cowswap-frontend/src/modules/permit/index.ts @@ -1,5 +1,7 @@ export * from './hooks/useAccountAgnosticPermitHookData' -export * from './hooks/useIsTokenPermittable' export * from './hooks/useGeneratePermitHook' -export * from './utils/handlePermit' +export * from './hooks/useIsTokenPermittable' +export * from './hooks/useOrdersPermitStatus' export * from './types' +export * from './updaters/PendingPermitUpdater' +export * from './utils/handlePermit' diff --git a/apps/cowswap-frontend/src/modules/permit/state/ordersPermitStatusAtom.ts b/apps/cowswap-frontend/src/modules/permit/state/ordersPermitStatusAtom.ts new file mode 100644 index 0000000000..cf132cefc3 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/permit/state/ordersPermitStatusAtom.ts @@ -0,0 +1,9 @@ +import { atom } from 'jotai' + +import { atomWithPartialUpdate } from '@cowprotocol/common-utils' + +import { OrdersPermitStatus } from '../types' + +export const { atom: ordersPermitStatusAtom, updateAtom: updateOrdersPermitStatusAtom } = atomWithPartialUpdate( + atom({}) +) diff --git a/apps/cowswap-frontend/src/modules/permit/types.ts b/apps/cowswap-frontend/src/modules/permit/types.ts index 5993a6fa67..d1cc15571f 100644 --- a/apps/cowswap-frontend/src/modules/permit/types.ts +++ b/apps/cowswap-frontend/src/modules/permit/types.ts @@ -7,6 +7,8 @@ import { Eip2612PermitUtils } from '@1inch/permit-signed-approvals-utils' import { AppDataInfo } from 'modules/appData' +import { ParsedOrder } from 'utils/orderUtils/parseOrder' + export type PermitType = 'dai-like' | 'eip-2612' export type SupportedPermitInfo = { @@ -92,3 +94,7 @@ export type PermitCacheKeyParams = { export type StorePermitCacheParams = PermitCacheKeyParams & { hookData: PermitHookData } export type GetPermitCacheParams = PermitCacheKeyParams + +export type CheckHasValidPendingPermit = (order: ParsedOrder) => Promise + +export type OrdersPermitStatus = Record diff --git a/apps/cowswap-frontend/src/modules/permit/updaters/PendingPermitUpdater.ts b/apps/cowswap-frontend/src/modules/permit/updaters/PendingPermitUpdater.ts new file mode 100644 index 0000000000..b286652ed9 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/permit/updaters/PendingPermitUpdater.ts @@ -0,0 +1,39 @@ +import { useSetAtom } from 'jotai' +import { useEffect, useRef } from 'react' + +import { ParsedOrder } from 'utils/orderUtils/parseOrder' + +import { PENDING_ORDER_PERMIT_CHECK_INTERVAL } from '../const' +import { useCheckHasValidPendingPermit } from '../hooks/useCheckHasValidPendingPermit' +import { updateOrdersPermitStatusAtom } from '../state/ordersPermitStatusAtom' + +export type PendingPermitUpdaterProps = { + orders: ParsedOrder[] +} + +export function PendingPermitUpdater({ orders }: PendingPermitUpdaterProps): null { + const ordersRef = useRef(orders) + ordersRef.current = orders + + const checkHasValidPendingPermit = useCheckHasValidPendingPermit() + const updateOrdersPermitStatus = useSetAtom(updateOrdersPermitStatusAtom) + + useEffect(() => { + const checkOrders = () => { + console.debug(`UpdatePendingPermit: checking orders`, ordersRef.current.length) + ordersRef.current.forEach((order) => { + checkHasValidPendingPermit(order).then((status) => { + console.debug(`UpdatePendingPermit: checked order ${order.id} with status ${status}`) + updateOrdersPermitStatus({ [order.id]: status }) + }) + }) + } + + checkOrders() + const interval = setInterval(checkOrders, PENDING_ORDER_PERMIT_CHECK_INTERVAL) + + return () => clearInterval(interval) + }, [checkHasValidPendingPermit, updateOrdersPermitStatus]) + + return null +} diff --git a/apps/cowswap-frontend/src/modules/tokens/hooks/useBalancesAndAllowances.ts b/apps/cowswap-frontend/src/modules/tokens/hooks/useBalancesAndAllowances.ts index 38dcbd6df8..7d16e34069 100644 --- a/apps/cowswap-frontend/src/modules/tokens/hooks/useBalancesAndAllowances.ts +++ b/apps/cowswap-frontend/src/modules/tokens/hooks/useBalancesAndAllowances.ts @@ -5,7 +5,7 @@ import { BalancesAndAllowances, BalancesAndAllowancesParams } from '../types' /** * Return the balances and allowances of the tokens. * - * This hooks is different than the useOnchainBalancesAndAllowance one in the fact that the user might contain some + * This hook is different from the useOnchainBalancesAndAllowance one in the fact that the user might contain some * un-commited transaction that might affect the balances. */ export function useBalancesAndAllowances(params: BalancesAndAllowancesParams): BalancesAndAllowances { diff --git a/apps/cowswap-frontend/src/modules/wallet/utils/PermitProviderConnector.ts b/apps/cowswap-frontend/src/modules/wallet/utils/PermitProviderConnector.ts index 126cb31acb..8a8f6c47c0 100644 --- a/apps/cowswap-frontend/src/modules/wallet/utils/PermitProviderConnector.ts +++ b/apps/cowswap-frontend/src/modules/wallet/utils/PermitProviderConnector.ts @@ -1,6 +1,7 @@ import { getContract } from '@cowprotocol/common-utils' import { defaultAbiCoder, ParamType } from '@ethersproject/abi' import { TypedDataField } from '@ethersproject/abstract-signer' +import { BigNumber } from '@ethersproject/bignumber' import type { Web3Provider } from '@ethersproject/providers' import { Wallet } from '@ethersproject/wallet' @@ -42,6 +43,31 @@ export class PermitProviderConnector implements ProviderConnector { return defaultAbiCoder.decode([type], hex)[0] } decodeABIParameters(types: AbiInput[], hex: string): T { - return defaultAbiCoder.decode(types as unknown as (ParamType | string)[], hex) as T + const decodedValues = defaultAbiCoder.decode(types as unknown as (ParamType | string)[], hex) as T + + // Ethersjs decodes numbers as BigNumber instances + // However, 1inch utils do not deal with BigNumber instances, + // so we need this mess to convert them to hex strings, which 1inch understands + // TODO: Any way to make this typing mess any cleaner? + if (decodedValues && typeof decodedValues === 'object') { + const copy: Record = {} + + Object.keys(decodedValues).forEach((key) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const value = decodedValues[key] + if (BigNumber.isBigNumber(value)) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + copy[key] = value.toHexString() + } else { + copy[key] = value + } + }) + + return copy as T + } + + return decodedValues } } diff --git a/apps/cowswap-frontend/src/utils/orderUtils/parseOrder.ts b/apps/cowswap-frontend/src/utils/orderUtils/parseOrder.ts index fec8eae063..a18121cfe9 100644 --- a/apps/cowswap-frontend/src/utils/orderUtils/parseOrder.ts +++ b/apps/cowswap-frontend/src/utils/orderUtils/parseOrder.ts @@ -48,6 +48,7 @@ export interface ParsedOrder { creationTime: Date expirationTime: Date composableCowInfo?: ComposableCowInfo + fullAppData: Order['fullAppData'] executionData: ParsedOrderExecutionData } @@ -110,6 +111,7 @@ export const parseOrder = (order: Order): ParsedOrder => { receiver: order.receiver || undefined, creationTime, expirationTime, + fullAppData: order.fullAppData, executionData, } }