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

Connect add liquidity hook with pool data #34

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion apps/deposit-pool/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<Head>
<link rel="manifest" href="/manifest.json" />
</Head>
<body className="flex h-full flex-col font-sans font-normal bg-background text-foreground">
<body className="flex h-full flex-col font-sans font-normal bg-transparent text-foreground">
<IFrameContextProvider>{children}</IFrameContextProvider>
</body>
</html>
Expand Down
79 changes: 47 additions & 32 deletions apps/deposit-pool/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,30 @@

import { Button, Form } from "@bleu/ui";

import { useCallback, useMemo } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm, useFormContext, useWatch } from "react-hook-form";
import { depositSchema, DepositSchemaType } from "#/utils/schema";
import {
BalancesPreview,
type IMinimalPool,
IPool,
PoolsDropdownMenu,
Spinner,
useIFrameContext,
} from "@bleu/cow-hooks-ui";
import { ALL_SUPPORTED_CHAIN_IDS } from "@cowprotocol/cow-sdk";
import { zodResolver } from "@hookform/resolvers/zod";
import { useCallback, useMemo } from "react";
import { useForm, useWatch } from "react-hook-form";
import { useUserPoolBalance } from "#/hooks/useUserPoolBalance";
import { useUserPools } from "#/hooks/useUserPools";
import { depositSchema } from "#/utils/schema";

const PREVIEW_LABELS = ["Pool Balance", "Deposit"];
import { useTokenPools } from "#/hooks/useTokenPools";
import { PoolItemInfo } from "#/components/PoolItemInfo";
import { TokenAmountInputs } from "#/components/TokenAmountInputs";
import { Address } from "viem";

export default function Page() {
const { context } = useIFrameContext();
const { data: pools } = useUserPools(context?.chainId, context?.account);
const { data: pools, isLoading: isLoadingPools } = useTokenPools(
context?.chainId,
context?.orderParams?.buyTokenAddress as Address
);

const form = useForm<typeof depositSchema._type>({
const form = useForm<DepositSchemaType>({
resolver: zodResolver(depositSchema),
defaultValues: {
poolId: "",
Expand All @@ -33,56 +36,68 @@ export default function Page() {
setValue,
control,
formState: { isSubmitting, isSubmitSuccessful },
} = form;
} = useFormContext<DepositSchemaType>();

const { poolId } = useWatch({ control });

const selectedPool = useMemo(
() => pools?.find((pool) => pool.id === poolId),
[pools, poolId],
[pools, poolId]
);

const onSubmit = useMemo(() => form.handleSubmit(() => {}), [form]);
const onSubmitCallback = useCallback(async (data: DepositSchemaType) => {
console.log(data);
}, []);

const { data: poolBalances, isLoading } = useUserPoolBalance({
poolId,
user: context?.account,
chainId: context?.chainId,
});
const onSubmit = useMemo(
() => form.handleSubmit(onSubmitCallback),
[form, onSubmitCallback]
);

if (!context) return null;

if (!context.account) {
return <span className="mt-10 text-center">Connect your wallet</span>;
return <span className="mt-10 text-center">Connect your wallet first</span>;
}

if (!ALL_SUPPORTED_CHAIN_IDS.includes(context.chainId)) {
return <span className="mt-10 text-center">Unsupported chain</span>;
}

if (isLoadingPools) {
return (
<div className="text-center mt-10 p-2">
<Spinner size="xl" />
</div>
);
}

if (!context?.orderParams?.buyTokenAddress) {
return (
<div className="w-full text-center mt-10 p-2">
<span>Please specify your swap order before proceeding</span>
</div>
);
}

yvesfracari marked this conversation as resolved.
Show resolved Hide resolved
return (
<Form
{...form}
onSubmit={onSubmit}
className="w-full flex flex-col gap-1 py-1 px-4"
>
<PoolsDropdownMenu
onSelect={(pool: IMinimalPool) => setValue("poolId", pool.id)}
selectedPool={selectedPool}
onSelect={(pool: IPool) => setValue("poolId", pool.id)}
PoolItemInfo={PoolItemInfo}
pools={pools || []}
selectedPool={selectedPool}
/>
{poolId && (
{selectedPool && (
<div className="size-full flex flex-col gap-2">
<BalancesPreview
labels={PREVIEW_LABELS}
balancesList={
poolBalances ? [poolBalances, poolBalances] : undefined
}
isLoading={isLoading}
/>
<TokenAmountInputs pool={selectedPool} />
<Button
type="submit"
className="my-2"
className="my-2 rounded-xl text-lg min-h-[58px]"
loading={isSubmitting || isSubmitSuccessful}
loadingText="Creating hook..."
>
Expand Down
15 changes: 15 additions & 0 deletions apps/deposit-pool/src/components/PoolItemInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IPool } from "@bleu/cow-hooks-ui";
import { formatNumber } from "@bleu/ui";

export function PoolItemInfo({ pool }: { pool: IPool }) {
const aprSumPct =
pool.dynamicData.aprItems.reduce((acc, { apr }) => acc + apr, 0) * 100;

return (
<i className="text-xs">
TVL: ${formatNumber(pool.dynamicData.totalLiquidity, 2)} - Volume (24h): $
{formatNumber(pool.dynamicData.totalLiquidity, 2)} - APR:{" "}
{formatNumber(aprSumPct, 2)}%
</i>
);
}
188 changes: 188 additions & 0 deletions apps/deposit-pool/src/components/TokenAmountInputs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { usePoolBalance } from "#/hooks/usePoolBalance";
import {
IBalance,
IPool,
Spinner,
TokenLogoWithWeight,
useIFrameContext,
} from "@bleu/cow-hooks-ui";
import Image from "next/image";
import { formatNumber, Input, Label } from "@bleu/ui";
import { useCallback, useEffect, useMemo, useState } from "react";
import { calculateProportionalTokenAmounts, getTokenPrice } from "#/utils/math";
import { useFormContext, useWatch } from "react-hook-form";
import { DepositSchemaType } from "#/utils/schema";
import { Address, formatUnits } from "viem";

export function TokenAmountInputs({ pool }: { pool: IPool | undefined }) {
const { context } = useIFrameContext();
const { control, setValue } = useFormContext<DepositSchemaType>();

const { data: poolBalances, isLoading: isBalanceLoading } = usePoolBalance({
poolId: pool?.id,
chainId: context?.chainId,
});

const tokenPrices = useMemo(
() => poolBalances?.map((poolBalance) => getTokenPrice(poolBalance)),
[poolBalances]
);

const amounts = useWatch({ control, name: "amounts" });

const totalUsd = useMemo(() => {
if (!poolBalances || !tokenPrices || !amounts) return 0;

return poolBalances.reduce((acc, poolBalance, index) => {
const amount = Number(amounts?.[poolBalance.token.address.toLowerCase()]);
const tokenPrice = tokenPrices[index];

console.log({ amount, tokenPrice });

if (amount && tokenPrice) {
return acc + amount * tokenPrice;
}

return acc;
}, 0);
}, [poolBalances, tokenPrices, amounts]);

const updateTokenAmounts = useCallback(
(amount: number, address: Address) => {
if (!poolBalances || !tokenPrices || !pool) return;

const proportionalAmounts = calculateProportionalTokenAmounts({
pool: pool,
poolBalances: poolBalances,
tokenAddress: address,
tokenAmount: amount,
});

proportionalAmounts.tokenAmounts.forEach((tokenAmount) => {
const tokenAmountAddress = tokenAmount.address.toLowerCase();
if (address.toLowerCase() === tokenAmountAddress) return;

const tokenAmountKey = `amounts.${tokenAmountAddress}` as const;
const calculatedAmount = Number(
formatUnits(tokenAmount.rawAmount, tokenAmount.decimals)
);
setValue(tokenAmountKey, calculatedAmount);
});

setValue(`referenceTokenAddress`, address);
},
[poolBalances, tokenPrices, pool, setValue]
);

if (!context) return null;

if (isBalanceLoading) return <Spinner size="xl" />;

if (!poolBalances || !poolBalances.length)
return (
<span className="mt-10 text-center">Error loading pool balances</span>
);

return (
<div className="flex flex-col gap-2">
<Label className="block text-sm">Add liquidity</Label>
<div className="flex flex-col gap-2 bg-muted text-muted-foreground rounded-xl p-2">
{poolBalances.map((poolBalance, index) => (
<TokenAmountInput
key={poolBalance.token.address}
poolBalance={poolBalance}
tokenPrice={tokenPrices?.[index]}
updateTokenAmounts={updateTokenAmounts}
/>
))}
</div>
<div className="flex flex-row justify-between border border-1 border-muted px-5 py-2 mb-3 rounded-xl text-md">
<span>Total</span>
<span className="text-right">
${totalUsd >= 0 ? formatNumber(totalUsd, 2) : "0"}
</span>
</div>
<div className="flex flex-row justify-between bg-muted text-muted-foreground items-center rounded-xl px-5 py-2 text-sm gap-5">
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
width={100}
height={100}
viewBox="0 0 32 32"
className={`w-full h-full fill-muted-foreground/50`}
>
{/* Copied from cowswap assets: https://github.com/cowprotocol/cowswap/blob/4b89ecbf661e6c30193586c704e23c78b2bfc22b/libs/assets/src/cow-swap/alert-circle.svg */}
<path d="M16 0C7.168 0 0 7.168 0 16s7.168 16 16 16 16-7.168 16-16S24.832 0 16 0Zm1.6 24h-3.2v-9.6h3.2V24Zm0-12.8h-3.2V8h3.2v3.2Z" />
</svg>
</div>
Once you add the hook, any changes to the swap won't automatically
update it. Review and adjust before swapping.
</div>
</div>
);
}

export function TokenAmountInput({
poolBalance,
tokenPrice,
updateTokenAmounts,
}: {
poolBalance: IBalance;
tokenPrice?: number;
updateTokenAmounts: (amount: number, address: Address) => void;
}) {
const { register, control } = useFormContext<DepositSchemaType>();

const amount = useWatch({
control,
name: `amounts.${poolBalance.token.address.toLowerCase()}`,
});

const amountUsd = useMemo(() => {
if (!amount || !tokenPrice) return 0;

return amount * tokenPrice;
}, [amount, tokenPrice]);

const onChange = useCallback(
(amount: number) => {
if (updateTokenAmounts) {
updateTokenAmounts(amount, poolBalance.token.address as Address);
}
},
[updateTokenAmounts, poolBalance.token.address]
);

return (
<div className="flex flex-row justify-between items-center px-3">
<TokenLogoWithWeight
token={poolBalance.token}
weight={poolBalance.weight}
className="text-lg"
/>
<div className="flex flex-col gap-1 text-right">
<Input
className="bg-transparent border-transparent text-md text-right placeholder:text-foreground/50 px-0"
type="number"
placeholder="0.0"
{...register(`amounts.${poolBalance.token.address.toLowerCase()}`, {
onChange: (e) => {
onChange(Number(e.target.value));
},
})}
onKeyDown={(e) => {
if (
["Enter", "-", "e", "E", "+", "ArrowUp", "ArrowDown"].includes(
e.key
)
)
e.preventDefault();
}}
/>
<i className="text-xs text-right font-light">
${amountUsd && amountUsd >= 0 ? formatNumber(amountUsd, 2) : "0"}
</i>
</div>
</div>
);
}
Loading
Loading