Skip to content

Commit

Permalink
feat(permit): allowance warning (#3184)
Browse files Browse the repository at this point in the history
* refactor: remove unnecessary toString()

* chore: fix typo

* fix: transform BigNumber instances into hex strings for compatibility with 1inch lib

* feat: add fullAppData to local order instance

* feat: add decodeAppData to appData module

* feat: add getAppDataHooks to appData module

* feat: add useCheckHasValidPendingPermit to permit module

* feat: add permit checking to OrderRow allowance warning

* refactor: rename getParsedOrderFromItem to getParsedOrderFromTableItem

* refactor: change isParsed order to be a bit more semantic

* feat: add ordersPermitStatusAtom

* feat: add PendingPermitUpdater

* feat: add useGetOrdersPermitStatus

* feat: pass down ordersPermitStatus

* feat: add PendingPermitUpdater to OrdersTableWidget

* chore: remove unused export

* refactor: sort exports

* fix: add back export. I removed the wrong one 🤦

* refactor: extract useGetOrdersToCheckPendingPermit

* chore: debug statements

* fix: force atoms to load so stored value is respected

* refactor: use the handy atomWithPartialUpdate

* refactor: move useGetOrdersToCheckPendingPermit to its own file
  • Loading branch information
alfetopito authored Oct 11, 2023
1 parent e902e36 commit f4700d9
Show file tree
Hide file tree
Showing 26 changed files with 366 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions apps/cowswap-frontend/src/legacy/state/orders/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export interface BaseOrder extends Omit<OrderCreation, 'signingScheme'> {

// 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
Expand Down
6 changes: 4 additions & 2 deletions apps/cowswap-frontend/src/legacy/utils/trade.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -156,6 +156,7 @@ export function mapUnsignedOrderToOrder({ unsignedOrder, additionalParams }: Map
sellAmountBeforeFee,
orderCreationHash,
quoteId,
appData: { fullAppData },
} = additionalParams
const status = _getOrderStatus(allowsOffchainSigning, isOnChain)

Expand All @@ -170,6 +171,7 @@ export function mapUnsignedOrderToOrder({ unsignedOrder, additionalParams }: Map
outputToken: buyToken,
quoteId,
class: additionalParams.class,
fullAppData,

// Status
status,
Expand Down
1 change: 1 addition & 0 deletions apps/cowswap-frontend/src/modules/appData/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
24 changes: 24 additions & 0 deletions apps/cowswap-frontend/src/modules/appData/utils/decodeAppData.ts
Original file line number Diff line number Diff line change
@@ -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<string>): 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
}
}
21 changes: 21 additions & 0 deletions apps/cowswap-frontend/src/modules/appData/utils/getAppDataHooks.ts
Original file line number Diff line number Diff line change
@@ -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<AnyAppDataDocVersion | string>): 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
}
Original file line number Diff line number Diff line change
@@ -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])
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ 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[]
history: OrderTableItem[]
}

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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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])

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -148,8 +151,11 @@ export function OrdersTableWidget({

useValidatePageUrlParams(orders.length, currentTabId, currentPageNumber)

const ordersToCheckPendingPermit = useGetOrdersToCheckPendingPermit(ordersList, chainId, balancesAndAllowances)

return (
<>
<PendingPermitUpdater orders={ordersToCheckPendingPermit} />
<ContentWrapper>
<OrdersTableContainer
chainId={chainId}
Expand All @@ -168,6 +174,7 @@ export function OrdersTableWidget({
allowsOffchainSigning={allowsOffchainSigning}
orderType={orderType}
pendingActivities={pendingActivity}
ordersPermitStatus={ordersPermitStatus}
>
{isOpenOrdersTab && orders.length && <MultipleCancellationMenu pendingOrders={tableItemsToOrders(orders)} />}
</OrdersTableContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export interface OrderRowProps {
orderParams: OrderParams
onClick: () => void
orderActions: OrderActions
hasValidPendingPermit?: boolean | undefined
children?: JSX.Element
}

Expand All @@ -164,6 +165,7 @@ export function OrderRow({
prices,
spotPrice,
children,
hasValidPendingPermit,
}: OrderRowProps) {
const { buyAmount, rateInfoParams, hasEnoughAllowance, hasEnoughBalance, chainId } = orderParams
const { creationTime, expirationTime, status } = order
Expand All @@ -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)
Expand Down Expand Up @@ -357,7 +359,7 @@ export function OrderRow({
{hasEnoughBalance === false && (
<BalanceWarning symbol={inputTokenSymbol} isScheduled={isOrderScheduled} />
)}
{hasEnoughAllowance === false && (
{hasEnoughAllowance === false && hasValidPendingPermit === false && (
<AllowanceWarning
approve={() => orderActions.approveOrderToken(order.inputToken)}
symbol={inputTokenSymbol}
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
Expand All @@ -40,7 +40,7 @@ import { getOrderParams } from './utils/getOrderParams'

import { buildOrdersTableUrl } from '../../utils/buildOrdersTableUrl'
import {
getParsedOrderFromItem,
getParsedOrderFromTableItem,
isParsedOrder,
OrderTableItem,
tableItemsToOrders,
Expand Down Expand Up @@ -206,6 +206,7 @@ export interface OrdersTableProps {
balancesAndAllowances: BalancesAndAllowances
getSpotPrice: (params: SpotPricesKeyParams) => Price<Currency, Currency> | null
orderActions: OrderActions
ordersPermitStatus: OrdersPermitStatus
}

export function OrdersTable({
Expand All @@ -219,6 +220,7 @@ export function OrdersTable({
getSpotPrice,
orderActions,
currentPageNumber,
ordersPermitStatus,
}: OrdersTableProps) {
const location = useLocation()
const [isRateInverted, setIsRateInverted] = useState(false)
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -400,7 +402,7 @@ export function OrdersTable({

<Rows>
{ordersPage.map((item) => {
const { inputToken, outputToken } = getParsedOrderFromItem(item)
const { inputToken, outputToken } = getParsedOrderFromTableItem(item)
const spotPrice = getSpotPrice({
chainId: chainId as SupportedChainId,
sellTokenAddress: inputToken.address,
Expand All @@ -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 (
<OrderRow
key={order.id}
Expand All @@ -419,10 +425,11 @@ export function OrdersTable({
order={order}
spotPrice={spotPrice}
prices={pendingOrdersPrices[order.id]}
orderParams={getOrderParams(chainId, balancesAndAllowances, order)}
orderParams={orderParams}
isRateInverted={isRateInverted}
orderActions={orderActions}
onClick={() => orderActions.selectReceiptOrder(order)}
hasValidPendingPermit={hasValidPendingPermit}
/>
)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,6 @@ export default (
getSpotPrice={() => null}
orderActions={orderActions}
orderType={TabOrderTypes.LIMIT}
ordersPermitStatus={{}}
/>
)
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export function OrdersTableContainer({
children,
orderType,
pendingActivities,
ordersPermitStatus,
}: OrdersProps) {
const content = () => {
if (!isWalletConnected) {
Expand Down Expand Up @@ -233,6 +234,7 @@ export function OrdersTableContainer({
balancesAndAllowances={balancesAndAllowances}
getSpotPrice={getSpotPrice}
orderActions={orderActions}
ordersPermitStatus={ordersPermitStatus}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit f4700d9

Please sign in to comment.