();
const { getOrderDataByOrderId } = useBuilder();
const recipeData = getOrderDataByOrderId(data.orderId);
- if (!recipeData) return null;
- const sellTokenWalletAmount = Number(
- formatUnits(
- BigInt(fetchBalance(data.tokenSell.address)),
- data.tokenSell.decimals
- )
- );
- const [sellAmount, buyAmount] = calculateAmounts(recipeData);
+
+ useEffect(() => {
+ if (!recipeData) return;
+ const newSellTokenWalletAmount = Number(
+ formatUnits(
+ BigInt(fetchBalance(data.tokenSell.address)),
+ data.tokenSell.decimals
+ )
+ );
+ setSellTokenWalletAmount(newSellTokenWalletAmount);
+ const [newSellAmount, newBuyAmount] = calculateAmounts(recipeData);
+ setSellAmount(newSellAmount);
+ setBuyAmount(newBuyAmount);
+ }, [recipeData, data, fetchBalance]);
+
+ if (!sellAmount || !buyAmount) return null;
+
const sellAmountWithSymbol = `${formatNumber(sellAmount, 2, "decimal", "compact", 0.01)} ${data.tokenSell.symbol}`;
const buyAmountWithSymbol = `${formatNumber(buyAmount, 2, "decimal", "compact", 0.01)} ${data.tokenBuy.symbol}`;
return (
@@ -34,10 +47,13 @@ export function SwapNode({
Swap
- {sellTokenWalletAmount < sellAmount && (
-
+ {(sellTokenWalletAmount || 0) < sellAmount && (
+
)}
-
{" "}
+
{data.isSellOrder
? `Sell ${sellAmountWithSymbol} for at least ${buyAmountWithSymbol}`
diff --git a/src/lib/fetchTokenInfo.ts b/src/lib/fetchTokenInfo.ts
new file mode 100644
index 0000000..5282c66
--- /dev/null
+++ b/src/lib/fetchTokenInfo.ts
@@ -0,0 +1,27 @@
+import { Address, erc20Abi, isAddress } from "viem";
+import { ChainId, publicClientsFromIds } from "./publicClients";
+import { IToken } from "./types";
+
+export async function fetchTokenInfo(
+ tokenAddress: Address,
+ chainId: ChainId
+): Promise {
+ const publicClient = publicClientsFromIds[chainId];
+ const [symbol, decimals] = await Promise.all([
+ publicClient.readContract({
+ address: tokenAddress,
+ abi: erc20Abi,
+ functionName: "symbol",
+ }),
+ publicClient.readContract({
+ address: tokenAddress,
+ abi: erc20Abi,
+ functionName: "decimals",
+ }),
+ ]);
+ return {
+ address: tokenAddress as Address,
+ decimals,
+ symbol,
+ };
+}
diff --git a/src/lib/schema.ts b/src/lib/schema.ts
index 77d2662..9fae97d 100644
--- a/src/lib/schema.ts
+++ b/src/lib/schema.ts
@@ -1,5 +1,7 @@
import { Address, isAddress } from "viem";
import { z } from "zod";
+import { normalize } from "viem/ens";
+
import { IToken, TIME_OPTIONS } from "./types";
import { ChainId, publicClientsFromIds } from "./publicClients";
import { fetchCowQuote } from "./cowApi/fetchCowQuote";
@@ -20,6 +22,22 @@ 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 generateOracleSchema = ({ chainId }: { chainId: ChainId }) => {
const publicClient = publicClientsFromIds[chainId];
return basicAddressSchema.refine(
@@ -51,27 +69,17 @@ export const stopLossConditionSchema = z
message: "Tokens sell and buy must be different",
});
-export const swapSchema = z
- .object({
- tokenSell: basicTokenSchema,
- tokenBuy: basicTokenSchema,
- amount: z.coerce.number().positive(),
- allowedSlippage: z.coerce.number().positive(),
- receiver: basicAddressSchema,
- isPartiallyFillable: z.coerce.boolean(),
- validFrom: z.coerce.string(),
- isSellOrder: z.coerce.boolean(),
- validityBucketTime: z.nativeEnum(TIME_OPTIONS),
- })
- .refine(
- (data) => {
- return data.tokenSell.address != data.tokenBuy.address;
- },
- {
- path: ["tokenBuy"],
- message: "Tokens sell and buy must be different",
- }
- );
+export const swapSchema = z.object({
+ tokenSell: basicTokenSchema,
+ tokenBuy: basicTokenSchema,
+ amount: z.coerce.number().positive(),
+ allowedSlippage: z.coerce.number().positive(),
+ receiver: z.union([basicAddressSchema, ensSchema]),
+ isPartiallyFillable: z.coerce.boolean(),
+ validFrom: z.coerce.string(),
+ isSellOrder: z.coerce.boolean(),
+ validityBucketTime: z.nativeEnum(TIME_OPTIONS),
+});
export const generateStopLossRecipeSchema = ({
chainId,
@@ -83,7 +91,7 @@ export const generateStopLossRecipeSchema = ({
tokenSell: basicTokenSchema,
tokenBuy: basicTokenSchema,
amount: z.coerce.number().positive(),
- allowedSlippage: z.coerce.number().positive(),
+ allowedSlippage: z.coerce.number().nonnegative().max(100),
receiver: basicAddressSchema,
isPartiallyFillable: z.coerce.boolean(),
validFrom: z.coerce.string(),
@@ -94,6 +102,15 @@ export const generateStopLossRecipeSchema = ({
tokenBuyOracle: basicAddressSchema,
maxTimeSinceLastOracleUpdate: z.nativeEnum(TIME_OPTIONS),
})
+ .refine(
+ (data) => {
+ return data.tokenSell.address != data.tokenBuy.address;
+ },
+ {
+ path: ["tokenBuy"],
+ message: "Tokens sell and buy must be different",
+ }
+ )
.superRefine((data, ctx) => {
const oracleRouter = new CHAINS_ORACLE_ROUTER_FACTORY[chainId as ChainId](
{
@@ -138,7 +155,7 @@ export const generateStopLossRecipeSchema = ({
if (res.errorType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
- message: capitalize(res.description),
+ message: `${res.errorType}: ${capitalize(res.description)}`,
});
}
});
diff --git a/src/lib/timeDelta.ts b/src/lib/timeDelta.ts
new file mode 100644
index 0000000..0c41e92
--- /dev/null
+++ b/src/lib/timeDelta.ts
@@ -0,0 +1,30 @@
+export function formatTimeDelta(totalSeconds: number): string {
+ const secondsPerMinute = 60;
+ const secondsPerHour = secondsPerMinute * 60;
+ const secondsPerDay = secondsPerHour * 24;
+ const secondsPerYear = secondsPerDay * 365;
+
+ let remainingSeconds = totalSeconds;
+
+ const years = Math.floor(remainingSeconds / secondsPerYear);
+ remainingSeconds %= secondsPerYear;
+
+ const days = Math.floor(remainingSeconds / secondsPerDay);
+ remainingSeconds %= secondsPerDay;
+
+ const hours = Math.floor(remainingSeconds / secondsPerHour);
+ remainingSeconds %= secondsPerHour;
+
+ const minutes = Math.floor(remainingSeconds / secondsPerMinute);
+ remainingSeconds %= secondsPerMinute;
+
+ const parts: string[] = [];
+ if (years > 0) parts.push(`${years} year${years > 1 ? "s" : ""}`);
+ if (days > 0) parts.push(`${days} day${days > 1 ? "s" : ""}`);
+ if (hours > 0) parts.push(`${hours} hour${hours > 1 ? "s" : ""}`);
+ if (minutes > 0) parts.push(`${minutes} minute${minutes > 1 ? "s" : ""}`);
+ if (remainingSeconds > 0)
+ parts.push(`${remainingSeconds} second${remainingSeconds > 1 ? "s" : ""}`);
+
+ return parts.join(", ");
+}