From 232bae9c77970549eada5d390a9110fc21cf3962 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari <55461956+yvesfracari@users.noreply.github.com> Date: Mon, 15 Jul 2024 10:45:06 -0300 Subject: [PATCH] Pedro/cow 311 copy minor cow feedbacks pt 2 (#55) * fix ens on receiver * fix decimals on order details tooltips * fix order details blinking and change to table row clickable button * add order cancelation on order details * add schema errors on submit button --- .../[safeAddress]/[orderId]/page.tsx | 27 ++---- src/components/HistoryOrdersTab.tsx | 38 +++------ src/components/OpenOrdersTab.tsx | 49 +++++------ src/components/OrderDetails.tsx | 85 +++++++++++++++---- src/components/SwapCardSubmitButton.tsx | 19 ++++- src/lib/schema.ts | 45 +++++----- 6 files changed, 154 insertions(+), 109 deletions(-) diff --git a/src/app/[chainId]/[safeAddress]/[orderId]/page.tsx b/src/app/[chainId]/[safeAddress]/[orderId]/page.tsx index 6e33129..ed7bdfc 100644 --- a/src/app/[chainId]/[safeAddress]/[orderId]/page.tsx +++ b/src/app/[chainId]/[safeAddress]/[orderId]/page.tsx @@ -1,8 +1,6 @@ -import { notFound } from "next/navigation"; import { Address } from "viem"; import { OrderDetails } from "#/components/OrderDetails"; -import { getProcessedStopLossOrder } from "#/lib/orderFetcher"; import { ChainId } from "#/lib/publicClients"; export default async function OrderPage({ @@ -14,22 +12,11 @@ export default async function OrderPage({ orderId: string; }; }) { - const defaultOrder = await getProcessedStopLossOrder({ - chainId: params.chainId, - orderId: params.orderId, - address: params.safeAddress, - }); - - if (defaultOrder) { - return ( - - ); - } - - notFound(); + return ( + + ); } diff --git a/src/components/HistoryOrdersTab.tsx b/src/components/HistoryOrdersTab.tsx index b15fcfe..e02e4df 100644 --- a/src/components/HistoryOrdersTab.tsx +++ b/src/components/HistoryOrdersTab.tsx @@ -1,7 +1,6 @@ "use client"; import { - Button, epochToDate, formatNumber, Spinner, @@ -12,15 +11,14 @@ import { TableHeader, TableRow, } from "@bleu/ui"; -import { OpenInNewWindowIcon } from "@radix-ui/react-icons"; import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk"; import { formatUnits } from "viem"; import { useOrder } from "#/contexts/ordersContext"; import { StopLossOrderType } from "#/lib/types"; -import { LinkComponent } from "./Link"; import { StatusBadge } from "./StatusBadge"; +import { useRouter } from "next/navigation"; export function HistoryOrdersTab() { const { historyOrders, isLoading } = useOrder(); @@ -28,16 +26,11 @@ export function HistoryOrdersTab() { return ( - - Created - Order - Trigger price - Filled - Status - - Details - - + Created + Order + Trigger price + Filled + Status {historyOrders.length ? ( @@ -46,7 +39,7 @@ export function HistoryOrdersTab() { }) ) : ( - + {isLoading ? ( ) : ( @@ -64,6 +57,7 @@ export function HistoryOrdersTab() { export function HistoryOrderRow({ order }: { order: StopLossOrderType }) { const { safe } = useSafeAppsSDK(); + const router = useRouter(); if (!order.stopLossData) { return null; @@ -94,7 +88,12 @@ export function HistoryOrderRow({ order }: { order: StopLossOrderType }) { ).toLocaleString(); return ( - + + router.push(`/${safe.chainId}/${safe.safeAddress}/${order?.id}`) + } + > {orderDateTime}
@@ -112,15 +111,6 @@ export function HistoryOrderRow({ order }: { order: StopLossOrderType }) { - - - - - ); } diff --git a/src/components/OpenOrdersTab.tsx b/src/components/OpenOrdersTab.tsx index 6079d9f..7ba8d61 100644 --- a/src/components/OpenOrdersTab.tsx +++ b/src/components/OpenOrdersTab.tsx @@ -12,7 +12,6 @@ import { TableHeader, TableRow, } from "@bleu/ui"; -import { OpenInNewWindowIcon } from "@radix-ui/react-icons"; import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk"; import { useState } from "react"; import { formatUnits } from "viem"; @@ -25,6 +24,7 @@ import { IToken, StopLossOrderType } from "#/lib/types"; import { LinkComponent } from "./Link"; import { Spinner } from "./Spinner"; import { StatusBadge } from "./StatusBadge"; +import { useRouter } from "next/navigation"; export function OpenOrdersTab() { const { @@ -53,20 +53,15 @@ export function OpenOrdersTab() {
- - - Select - - Created - Order - Trigger price - Current market price - Filled - Status - - Details - {" "} - + + Select + + Created + Order + Trigger price + Current market price + Filled + Status {openOrders.length ? ( @@ -123,6 +118,7 @@ export function OpenOrderRow({ onSelect: (selected: boolean) => void; }) { const { safe } = useSafeAppsSDK(); + const router = useRouter(); const { useTokenPairPrice } = useTokens(); @@ -160,8 +156,18 @@ export function OpenOrderRow({ ).toLocaleString(); return ( - - + + router.push(`/${safe.chainId}/${safe.safeAddress}/${order?.id}`) + } + > + { + e.stopPropagation(); + }} + className="cursor-normal" + > { onSelect(checked as boolean); @@ -190,15 +196,6 @@ export function OpenOrderRow({ - - - - - ); } diff --git a/src/components/OrderDetails.tsx b/src/components/OrderDetails.tsx index 41d7044..273077e 100644 --- a/src/components/OrderDetails.tsx +++ b/src/components/OrderDetails.tsx @@ -1,6 +1,7 @@ "use client"; import { + Button, ClickToCopy, cn, epochToDate, @@ -8,7 +9,7 @@ import { formatNumber, Separator, } from "@bleu/ui"; -import { ArrowLeftIcon, CopyIcon } from "@radix-ui/react-icons"; +import { ArrowLeftIcon, CopyIcon, ReloadIcon } from "@radix-ui/react-icons"; import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk"; import Link from "next/link"; import useSWR from "swr"; @@ -23,21 +24,21 @@ import { getProcessedStopLossOrder } from "#/lib/orderFetcher"; import { ChainId } from "#/lib/publicClients"; import { formatTimeDelta } from "#/lib/timeDelta"; import { TOOLTIP_DESCRIPTIONS } from "#/lib/tooltipDescriptions"; -import { StopLossOrderTypeWithCowOrders } from "#/lib/types"; import { buildBlockExplorerTokenURL, buildBlockExplorerTxUrl, buildOrderCowExplorerUrl, truncateAddress, } from "#/utils"; +import { Spinner } from "./Spinner"; +import { useOrder } from "#/contexts/ordersContext"; +import { OrderCancelArgs, TRANSACTION_TYPES } from "#/lib/transactionFactory"; export function OrderDetails({ - defaultOrder, orderId, chainId, address, }: { - defaultOrder: StopLossOrderTypeWithCowOrders; orderId: string; address: Address; chainId: ChainId; @@ -50,18 +51,35 @@ export function OrderDetails({ address, }); }; - const { data: order } = useSWR(["orderDetails"], orderFetcher, { - fallbackData: defaultOrder, - }); + const { + data: order, + isValidating, + isLoading, + mutate, + } = useSWR(["orderDetails"], orderFetcher); + + const { + txManager: { writeContract, isPonderUpdating }, + } = useOrder(); + + const isUpdating = isLoading || isPonderUpdating || isValidating; + + if (isUpdating && !order) { + return ; + } + + if (!order) { + return null; + } const orderDateTime = formatDateTime( - epochToDate(Number(order?.blockTimestamp)), + epochToDate(Number(order?.blockTimestamp)) ); const orderWaitTime = formatTimeDelta( - order?.stopLossData?.validityBucketSeconds as number, + order?.stopLossData?.validityBucketSeconds as number ); const maxOracleUpdateTime = formatTimeDelta( - order?.stopLossData?.maxTimeSinceLastOracleUpdate as number, + order?.stopLossData?.maxTimeSinceLastOracleUpdate as number ); const amountIn = @@ -82,6 +100,15 @@ export function OrderDetails({ const orderSurplus = ((executionPrice - limitPrice) / limitPrice) * 100; const priceUnit = `${order?.stopLossData?.tokenOut.symbol} per ${order?.stopLossData?.tokenIn.symbol}`; + const onCancelOrder = () => { + if (!order) return; + const deleteTxArgs = { + type: TRANSACTION_TYPES.ORDER_CANCEL, + hash: order.hash, + } as OrderCancelArgs; + writeContract([deleteTxArgs]); + }; + return (
@@ -92,8 +119,26 @@ export function OrderDetails({ > -

Order Details

-
+
+

Order Details

+ {isUpdating ? ( + + ) : ( + + )} +
+
{formatNumber(amountIn, 4)}{" "} - + {formatNumber(amountOut, 4)}{" "} - +
{formatNumber(strikePrice, 4)}{" "} - {priceUnit} + {priceUnit}
{formatNumber(limitPrice, 4)}{" "} - {priceUnit} + {priceUnit}
{(order?.filledPct || 0) > 0 && ( @@ -318,7 +369,7 @@ export function OrderDetails({ (); const { tokenBuyOracle, tokenSellOracle, advancedSettings, isLoading } = @@ -50,6 +50,7 @@ export function SwapCardSubmitButton() { tokenSellOracle, tokenBuyOracle, advancedSettings, + errors, }); return ( @@ -77,6 +78,7 @@ function getButtonState({ tokenSellOracle, tokenBuyOracle, advancedSettings, + errors, }: { draftOrders: DraftOrder[]; tokenBuy?: IToken; @@ -89,6 +91,7 @@ function getButtonState({ tokenSellOracle?: Address; tokenBuyOracle?: Address; advancedSettings: AdvancedSwapSettings; + errors: FieldErrors; }): { disabled: boolean; text: string; @@ -151,6 +154,18 @@ function getButtonState({ text: "Error quoting tokens, make sure that CoW supports them.", }; } + if (Object.values(errors).length) { + const errorList = Object.values(errors); + const firstErrorMessage = errorList[0]; + const firstErrorKey = Object.keys(errors).find( + // @ts-ignore + (key) => errors[key] === firstErrorMessage + ); + return { + disabled: false, + text: `Error on ${firstErrorKey}: ${firstErrorMessage}. Click to try again`, + }; + } return { disabled: false, text: "Review Stop Loss order", diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 4e7fbcd..25fe5d3 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -17,21 +17,26 @@ const basicTokenSchema = z.object({ symbol: z.string(), }); -const ensSchema = z - .string() - .min(1) - .refine((value) => value.includes(".eth"), { - message: "Provided address is invalid", - }) - .transform(async (value) => { - const publicClient = publicClientsFromIds[1]; - return (await publicClient.getEnsAddress({ - name: normalize(value), - })) as Address; - }) - .refine((value) => isAddress(value), { - message: "Provided address is invalid", - }); +const generateEnsSchema = (chainId: number) => { + if (chainId === 1) { + return z + .string() + .min(1) + .refine((value) => value.includes(".eth"), { + message: "Provided address is invalid", + }) + .transform(async (value) => { + const publicClient = publicClientsFromIds[1]; + return (await publicClient.getEnsAddress({ + name: normalize(value), + })) as Address; + }) + .refine((value) => isAddress(value), { + message: "Provided address is invalid", + }); + } + return basicAddressSchema; +}; const generateOracleSchema = ({ chainId }: { chainId: ChainId }) => { const publicClient = publicClientsFromIds[chainId]; @@ -48,7 +53,7 @@ const generateOracleSchema = ({ chainId }: { chainId: ChainId }) => { }, { message: "Address does not conform to Oracle interface", - }, + } ); }; @@ -70,7 +75,7 @@ export const generateSwapSchema = (chainId: ChainId) => { path: ["tokenBuy"], message: "Tokens sell and buy must be different", - }, + } ) .superRefine((data, ctx) => { const amountDecimals = data.isSellOrder @@ -109,7 +114,7 @@ export const generateAdvancedSettingsSchema = (chainId: ChainId) => generateOracleSchema({ chainId }), z.literal(""), ]), - receiver: z.union([basicAddressSchema, ensSchema]), + receiver: z.union([basicAddressSchema, generateEnsSchema(chainId)]), partiallyFillable: z.coerce.boolean(), }) .refine( @@ -122,7 +127,7 @@ export const generateAdvancedSettingsSchema = (chainId: ChainId) => { message: "If one oracle is set, both must be set", path: ["tokenSellOracle"], - }, + } ) .refine( (data) => { @@ -134,5 +139,5 @@ export const generateAdvancedSettingsSchema = (chainId: ChainId) => { message: "If one oracle is set, both must be set", path: ["tokenBuyOracle"], - }, + } );