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

Build calls with permit2 #74

Merged
merged 3 commits into from
Sep 4, 2024
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
4 changes: 2 additions & 2 deletions packages/nextjs/app/hooks/HookDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const HooksDetails = ({ hooks }: { hooks: HookInfo[] }) => {

<div className="text-xl">{hook.description}</div>

<div className="flex justify-between">
<div className="flex flex-col md:flex-row flex-wrap md:justify-between">
<div>Audited: {hook.audited}</div>
<div>Categories: {categories}</div>
<Link
Expand Down Expand Up @@ -51,7 +51,7 @@ export const HooksDetails = ({ hooks }: { hooks: HookInfo[] }) => {

<div className="hidden lg:flex text-center">
<Link
className="hover:underline flex gap-2 items-center text-nowrap overflow-hidden whitespace-nowrap"
className="hover:underline hover:text-accent flex gap-2 items-center text-nowrap overflow-hidden whitespace-nowrap"
target="_blank"
rel="noopener noreferrer"
href={hook.github}
Expand Down
6 changes: 2 additions & 4 deletions packages/nextjs/app/hooks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,15 @@ const Hooks: NextPage = async () => {
<div className="mb-7 w-full text-center">
<h1 className="text-3xl md:text-5xl font-bold mb-7 text-center">Pool Hooks</h1>
<div className="text-xl my-10">
Extend the functionality of liquidity pools with hooks contracts. Consider utilizing one of the examples below
or{" "}
Extend the functionality of liquidity pools with hooks contracts. Use one of our curated examples below or{" "}
<Link
target="_blank"
rel="noopener noreferrer"
href="https://balancer-hooks.vercel.app/submit-hook.html"
className="link"
>
submit your own creation
submit your own
</Link>
.
</div>
</div>
<div className="w-full flex flex-col gap-3">
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const Home: NextPage = () => {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10 w-full">
{TOOLS.map(item => (
<Link
className="relative bg-base-200 hover:scale-105 hover:bg-neutral text-2xl text-center p-8 rounded-3xl shadow-lg"
className="relative bg-base-200 hover:shadow-inner text-2xl text-center p-8 rounded-3xl shadow-lg"
key={item.href}
href={item.href}
passHref
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React, { useState } from "react";
import React, { useCallback, useState } from "react";
import { ResultsDisplay, TokenField, TransactionButton } from ".";
import { InputAmount, calculateProportionalAmounts } from "@balancer/sdk";
import { InputAmount, PERMIT2, calculateProportionalAmounts, erc20Abi } from "@balancer/sdk";
import { useQueryClient } from "@tanstack/react-query";
import debounce from "lodash.debounce";
import { formatUnits, parseUnits } from "viem";
import { useContractEvent } from "wagmi";
import { useContractEvent, useContractRead } from "wagmi";
import { Alert } from "~~/components/common/";
import abis from "~~/contracts/abis";
import { useAddLiquidity, useQueryAddLiquidity } from "~~/hooks/balancer/";
import { useAddLiquidity, useQueryAddLiquidity, useTargetFork } from "~~/hooks/balancer/";
import { PoolActionsProps, PoolOperationReceipt, TokenAmountDetails } from "~~/hooks/balancer/types";
import { useApproveTokens, useReadTokens } from "~~/hooks/token/";
import { useAllowancesOnTokens, useApproveOnToken } from "~~/hooks/token/";

/**
* 1. Query adding some amount of liquidity to the pool
Expand All @@ -30,21 +31,36 @@ export const AddLiquidityForm: React.FC<PoolActionsProps> = ({
const [tokenInputs, setTokenInputs] = useState<InputAmount[]>(initialTokenInputs);
const [addLiquidityReceipt, setAddLiquidityReceipt] = useState<PoolOperationReceipt>(null);
const [referenceAmount, setReferenceAmount] = useState<InputAmount>(); // only for the proportional add liquidity case
const [isCalculatingProportional, setIsCalculatingProportional] = useState(false);

const queryClient = useQueryClient();
const {
data: queryResponse,
isFetching: isQueryFetching,
error: queryError,
refetch: refetchQueryAddLiquidity,
} = useQueryAddLiquidity(pool, tokenInputs, referenceAmount);
const { sufficientAllowances, isApproving, approveTokens } = useApproveTokens(tokenInputs);
const { mutate: addLiquidity, isPending: isAddLiquidityPending, error: addLiquidityError } = useAddLiquidity();
const { refetchTokenAllowances } = useReadTokens(tokenInputs);
const queryClient = useQueryClient();
const { tokensToApprove, refetchTokenAllowances } = useAllowancesOnTokens(tokenInputs);
const {
mutate: addLiquidity,
isPending: isAddLiquidityPending,
error: addLiquidityError,
} = useAddLiquidity(tokenInputs);

// Delay update of token inputs so user has time to finish typing numbers longer than 1 digit
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedSetTokenInputs = useCallback(
debounce(updatedTokens => {
setTokenInputs(updatedTokens);
setIsCalculatingProportional(false);
}, 1000),
[],
);

const handleInputChange = (index: number, value: string) => {
queryClient.removeQueries({ queryKey: ["queryAddLiquidity"] });
setAddLiquidityReceipt(null);

const updatedTokens = tokenInputs.map((token, idx) => {
if (idx === index) {
return { ...token, rawAmount: parseUnits(value, token.decimals) };
Expand All @@ -64,10 +80,12 @@ export const AddLiquidityForm: React.FC<PoolActionsProps> = ({
})),
};

setIsCalculatingProportional(true);
const referenceAmount = updatedTokens[index];
const { bptAmount, tokenAmounts } = calculateProportionalAmounts(poolStateWithBalances, referenceAmount);
setReferenceAmount(bptAmount);
setTokenInputs(tokenAmounts);
const { tokenAmounts } = calculateProportionalAmounts(poolStateWithBalances, referenceAmount);
setReferenceAmount(referenceAmount);
setTokenInputs(updatedTokens);
debouncedSetTokenInputs(tokenAmounts);
} else {
setTokenInputs(updatedTokens);
}
Expand Down Expand Up @@ -105,7 +123,7 @@ export const AddLiquidityForm: React.FC<PoolActionsProps> = ({
},
});

const error = queryError || addLiquidityError;
const error: Error | null = queryError || addLiquidityError;
const isFormEmpty = tokenInputs.some(token => token.rawAmount === 0n);

return (
Expand All @@ -129,10 +147,10 @@ export const AddLiquidityForm: React.FC<PoolActionsProps> = ({
label="Query"
onClick={handleQueryAddLiquidity}
isDisabled={isQueryFetching}
isFormEmpty={isFormEmpty}
isFormEmpty={isFormEmpty || isCalculatingProportional}
/>
) : !sufficientAllowances ? (
<TransactionButton label="Approve" isDisabled={isApproving} onClick={approveTokens} />
) : tokensToApprove.length > 0 ? (
<ApproveButtons tokens={tokensToApprove} refetchTokenAllowances={refetchTokenAllowances} />
) : (
<TransactionButton label="Add Liquidity" isDisabled={isAddLiquidityPending} onClick={handleAddLiquidity} />
)}
Expand All @@ -159,7 +177,42 @@ export const AddLiquidityForm: React.FC<PoolActionsProps> = ({
/>
)}

{(error as Error) && <Alert type="error">{(error as Error).message}</Alert>}
{error && <Alert type="error">{error.message}</Alert>}
</section>
);
};

const ApproveButtons = ({
tokens,
refetchTokenAllowances,
}: {
tokens: InputAmount[];
refetchTokenAllowances: () => void;
}) => {
const { chainId } = useTargetFork();
const token = tokens[0];

const { data: symbol } = useContractRead({
address: token.address,
abi: erc20Abi,
functionName: "symbol",
});

const {
mutateAsync: approve,
isPending: isApprovePending,
error: approveError,
} = useApproveOnToken(token.address, PERMIT2[chainId]);

const handleApprove = async () => {
await approve();
refetchTokenAllowances();
};

return (
<div>
<TransactionButton label={`Approve ${symbol}`} isDisabled={isApprovePending} onClick={handleApprove} />
{approveError && <Alert type="error">{approveError.message}</Alert>}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useAccount } from "wagmi";
import { Alert } from "~~/components/common";
import { Pool, RefetchPool, TokenBalances } from "~~/hooks/balancer";
import { useAccountBalance, useScaffoldContractWrite } from "~~/hooks/scaffold-eth";
import { useReadTokens } from "~~/hooks/token";
import { useTokenBalancesOfUser } from "~~/hooks/token";

type Operation = "Swap" | "AddLiquidity" | "RemoveLiquidity";

Expand All @@ -14,13 +14,7 @@ type Operation = "Swap" | "AddLiquidity" | "RemoveLiquidity";
export const PoolOperations: React.FC<{ pool: Pool; refetchPool: RefetchPool }> = ({ pool, refetchPool }) => {
const [activeTab, setActiveTab] = useState<Operation>("Swap");

const tokens = pool.poolTokens.map(token => ({
address: token.address as `0x${string}`,
decimals: token.decimals,
rawAmount: 0n, // Quirky solution cus useReadTokens expects type InputAmount[] cus originally built for AddLiquidityForm :D
}));

const { tokenBalances, refetchTokenBalances } = useReadTokens(tokens);
const { tokenBalances, refetchTokenBalances } = useTokenBalancesOfUser(pool.poolTokens);

const tabs = {
Swap: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const RemoveLiquidityForm: React.FC<PoolActionsProps> = ({ pool, refetchP
},
});

const error = queryError || removeLiquidityError || approveError;
const error: Error | null = queryError || removeLiquidityError || approveError;
const isFormEmpty = bptInput.displayValue === "";
const isSufficientAllowance = allowance !== undefined && allowance >= bptInput.rawAmount;

Expand All @@ -112,7 +112,7 @@ export const RemoveLiquidityForm: React.FC<PoolActionsProps> = ({ pool, refetchP
{!queryResponse || removeLiquidityReceipt || isFormEmpty ? (
<TransactionButton label="Query" onClick={handleQuery} isDisabled={isQueryFetching} isFormEmpty={isFormEmpty} />
) : !isSufficientAllowance ? (
<TransactionButton label="Approve" isDisabled={isApprovePending} onClick={handleApprove} />
<TransactionButton label={`Approve ${pool.symbol}`} isDisabled={isApprovePending} onClick={handleApprove} />
) : (
<TransactionButton
label="Remove Liquidity"
Expand Down Expand Up @@ -141,7 +141,7 @@ export const RemoveLiquidityForm: React.FC<PoolActionsProps> = ({ pool, refetchP
/>
)}

{(error as Error) && <Alert type="error">{(error as Error).message}</Alert>}
{error && <Alert type="error">{error.message}</Alert>}
</section>
);
};
42 changes: 13 additions & 29 deletions packages/nextjs/app/pools/_components/operations/SwapForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useContractEvent } from "wagmi";
import { Alert } from "~~/components/common";
import { useQuerySwap, useSwap, useTargetFork } from "~~/hooks/balancer/";
import { PoolActionsProps, PoolOperationReceipt, SwapConfig } from "~~/hooks/balancer/types";
import { useAllowanceOnPermit2, useAllowanceOnToken, useApproveOnPermit2, useApproveOnToken } from "~~/hooks/token";
import { useAllowanceOnToken, useApproveOnToken } from "~~/hooks/token";

const initialSwapConfig = {
tokenIn: {
Expand Down Expand Up @@ -62,21 +62,15 @@ export const SwapForm: React.FC<PoolActionsProps> = ({ pool, refetchPool, tokenB
error: queryError,
refetch: refetchQuerySwap,
} = useQuerySwap(swapInput, setSwapConfig);
const { data: allowanceOnPermit2, refetch: refetchAllowanceOnPermit2 } = useAllowanceOnPermit2(tokenIn.address);
const { data: allowanceOnToken, refetch: refetchAllowanceOnToken } = useAllowanceOnToken(
tokenIn.address,
PERMIT2[chainId],
);
const {
mutateAsync: approveRouter,
isPending: isApproveRouterPending,
error: approveRouterError,
mutateAsync: approveOnToken,
isPending: isApprovePending,
error: approveError,
} = useApproveOnToken(tokenIn.address, PERMIT2[chainId]);
const {
mutateAsync: approvePermit2,
isPending: isApprovePermit2Pending,
error: approvePermit2Error,
} = useApproveOnPermit2(tokenIn.address);
const { mutate: swap, isPending: isSwapPending, error: swapError } = useSwap(swapInput);

const handleQuerySwap = async () => {
Expand All @@ -86,14 +80,8 @@ export const SwapForm: React.FC<PoolActionsProps> = ({ pool, refetchPool, tokenB
};

const handleApprove = async () => {
if (allowanceOnPermit2 && allowanceOnPermit2[0] < swapConfig.tokenIn.rawAmount) {
if (allowanceOnToken !== undefined && allowanceOnToken < swapConfig.tokenIn.rawAmount) {
await approveRouter();
refetchAllowanceOnToken();
}
await approvePermit2();
refetchAllowanceOnPermit2();
}
await approveOnToken();
refetchAllowanceOnToken();
};

const handleSwap = async () => {
Expand Down Expand Up @@ -125,10 +113,6 @@ export const SwapForm: React.FC<PoolActionsProps> = ({ pool, refetchPool, tokenB
}));
};

const sufficientAllowance = useMemo(() => {
return allowanceOnPermit2 && allowanceOnPermit2[0] >= swapConfig.tokenIn.rawAmount;
}, [allowanceOnPermit2, swapConfig.tokenIn.rawAmount]);

useContractEvent({
address: VAULT_V3[chainId],
abi: vaultV3Abi,
Expand All @@ -153,8 +137,12 @@ export const SwapForm: React.FC<PoolActionsProps> = ({ pool, refetchPool, tokenB
},
});

const sufficientAllowance = useMemo(() => {
return allowanceOnToken && allowanceOnToken >= swapConfig.tokenIn.rawAmount;
}, [allowanceOnToken, swapConfig.tokenIn.rawAmount]);

const isFormEmpty = swapConfig.tokenIn.amount === "" && swapConfig.tokenOut.amount === "";
const error = queryError || swapError || approveRouterError || approvePermit2Error;
const error: Error | null = queryError || swapError || approveError;

return (
<section className="flex flex-col gap-5">
Expand Down Expand Up @@ -189,16 +177,12 @@ export const SwapForm: React.FC<PoolActionsProps> = ({ pool, refetchPool, tokenB
isFormEmpty={isFormEmpty}
/>
) : !sufficientAllowance ? (
<TransactionButton
label="Approve"
isDisabled={isApprovePermit2Pending || isApproveRouterPending}
onClick={handleApprove}
/>
<TransactionButton label={`Approve ${tokenIn.symbol}`} isDisabled={isApprovePending} onClick={handleApprove} />
) : (
<TransactionButton label="Swap" isDisabled={isSwapPending} onClick={handleSwap} />
)}

{(error as Error) && <Alert type="error">{(error as Error).message} / </Alert>}
{error && <Alert type="error">{error.message} / </Alert>}

{queryResponse && (
<ResultsDisplay
Expand Down
12 changes: 11 additions & 1 deletion packages/nextjs/contracts/externalContracts.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
BALANCER_BATCH_ROUTER,
BALANCER_ROUTER,
PERMIT2,
VAULT_V3,
balancerBatchRouterAbi,
balancerRouterAbi,
vaultExtensionV3Abi, // balancerBatchRouterAbi // Batch Router not exported from balancer sdk?
permit2Abi,
vaultExtensionV3Abi,
} from "@balancer/sdk";
import { sepolia } from "viem/chains";
import scaffoldConfig from "~~/scaffold.config";
Expand Down Expand Up @@ -40,6 +42,10 @@ const externalContracts = {
address: BALANCER_BATCH_ROUTER[scaffoldConfig.targetFork.id],
abi: balancerBatchRouterAbi,
},
Permit2: {
address: PERMIT2[scaffoldConfig.targetFork.id],
abi: permit2Abi,
},
},
11155111: {
Vault: {
Expand All @@ -54,6 +60,10 @@ const externalContracts = {
address: BALANCER_BATCH_ROUTER[sepolia.id],
abi: balancerBatchRouterAbi,
},
Permit2: {
address: PERMIT2[sepolia.id],
abi: permit2Abi,
},
},
} as const;

Expand Down
Loading
Loading