Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(permit): allowance warning #3184

Merged
merged 23 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6216f3a
refactor: remove unnecessary toString()
alfetopito Oct 6, 2023
52ca3b5
chore: fix typo
alfetopito Oct 6, 2023
d7b4a96
fix: transform BigNumber instances into hex strings for compatibility…
alfetopito Oct 6, 2023
8732312
feat: add fullAppData to local order instance
alfetopito Oct 6, 2023
ab3e5c5
feat: add decodeAppData to appData module
alfetopito Oct 6, 2023
5dc302a
feat: add getAppDataHooks to appData module
alfetopito Oct 6, 2023
1573e5c
feat: add useCheckHasValidPendingPermit to permit module
alfetopito Oct 6, 2023
ae952f0
feat: add permit checking to OrderRow allowance warning
alfetopito Oct 6, 2023
a6f3f1c
refactor: rename getParsedOrderFromItem to getParsedOrderFromTableItem
alfetopito Oct 9, 2023
161e637
refactor: change isParsed order to be a bit more semantic
alfetopito Oct 9, 2023
e34bc56
feat: add ordersPermitStatusAtom
alfetopito Oct 9, 2023
92f7eef
feat: add PendingPermitUpdater
alfetopito Oct 9, 2023
4aeb8bf
feat: add useGetOrdersPermitStatus
alfetopito Oct 9, 2023
243abce
feat: pass down ordersPermitStatus
alfetopito Oct 9, 2023
7e8f905
feat: add PendingPermitUpdater to OrdersTableWidget
alfetopito Oct 9, 2023
8fa8ddf
chore: remove unused export
alfetopito Oct 9, 2023
aed5c0e
refactor: sort exports
alfetopito Oct 9, 2023
3263718
fix: add back export. I removed the wrong one 🤦
alfetopito Oct 9, 2023
a432bf6
refactor: extract useGetOrdersToCheckPendingPermit
alfetopito Oct 10, 2023
21c3f16
chore: debug statements
alfetopito Oct 10, 2023
2ae222a
fix: force atoms to load so stored value is respected
alfetopito Oct 10, 2023
113788f
refactor: use the handy atomWithPartialUpdate
alfetopito Oct 11, 2023
f874fc6
refactor: move useGetOrdersToCheckPendingPermit to its own file
alfetopito Oct 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, there is a key of the changes

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
Loading