Skip to content

Commit

Permalink
Learning tanstack useMutation 🤯
Browse files Browse the repository at this point in the history
  • Loading branch information
MattPereira committed Aug 4, 2024
1 parent fe504fe commit f88b054
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 66 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

A frontend tool for creating and initializing various pool types on Balancer

### Notes

- Cannot create pools with same token pairs, weight, and swap fee
- except on testnets its allowed for convenience

## Requirements

To run the code locally, the following tools are required:
Expand Down
67 changes: 32 additions & 35 deletions packages/nextjs/app/cow/_components/ManagePoolCreation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { StepsDisplay } from "./StepsDisplay";
import { Address } from "viem";
import { useAccount } from "wagmi";
import { TransactionButton } from "~~/components/common/";
import { type ExistingPool, useNewPoolEvents, useReadPool, useWritePool } from "~~/hooks/cow/";
import { useReadToken, useWriteToken } from "~~/hooks/token";
import { type ExistingPool, useCreatePool, useNewPoolEvents, useReadPool, useWritePool } from "~~/hooks/cow/";
import { useApproveToken, useReadToken } from "~~/hooks/token";

type TokenInput = {
rawAmount: bigint;
Expand Down Expand Up @@ -34,9 +34,8 @@ export const ManagePoolCreation = ({
}: ManagePoolCreationProps) => {
const [currentStep, setCurrentStep] = useState(1);
const [userPoolAddress, setUserPoolAddress] = useState<Address>();

// TODO: refactor to using tanstack query
const [isCreatingPool, setIsCreatingPool] = useState(false);
const [isApproving, setIsApproving] = useState(false);
const [isBinding, setIsBinding] = useState(false);
const [isSettingFee, setIsSettingSwapFee] = useState(false);
const [isFinalizing, setIsFinalizing] = useState(false);
Expand All @@ -45,39 +44,30 @@ export const ManagePoolCreation = ({
const { data: pool, refetch: refetchPool } = useReadPool(userPoolAddress);
const { allowance: allowance1, refetchAllowance: refetchAllowance1 } = useReadToken(token1?.address, pool?.address);
const { allowance: allowance2, refetchAllowance: refetchAllowance2 } = useReadToken(token2?.address, pool?.address);
const { approve: approve1 } = useWriteToken(token1?.address, pool?.address);
const { approve: approve2 } = useWriteToken(token2?.address, pool?.address);
const { createPool, bind, setSwapFee, finalize } = useWritePool(pool?.address);
const { bind, setSwapFee, finalize } = useWritePool(pool?.address);

const { mutate: createPool, isPending: createPoolIsPending } = useCreatePool();
const { mutate: approve, isPending: approveIsPending } = useApproveToken();

useNewPoolEvents(connectedAddress, setUserPoolAddress); // listen for user's pool creation events

const validTokenAmounts = token1.rawAmount > 0n && token2.rawAmount > 0n;

const handleCreatePool = async () => {
try {
setIsCreatingPool(true);
const newPool = await createPool(name, symbol);
setUserPoolAddress(newPool);
setCurrentStep(2);
} catch (e) {
console.error("Error creating pool", e);
} finally {
setIsCreatingPool(false);
}
};

const handleApproveTokens = async () => {
try {
setIsApproving(true);
const token1Payload = { token: token1?.address, spender: pool?.address, rawAmount: token1.rawAmount };
const token2Payload = { token: token2?.address, spender: pool?.address, rawAmount: token2.rawAmount };
const txs = [];
if (token1.rawAmount > allowance1) txs.push(approve1(token1.rawAmount));
if (token2.rawAmount > allowance2) txs.push(approve2(token2.rawAmount));
if (token1.rawAmount > allowance1) txs.push(approve(token1Payload));
if (token2.rawAmount > allowance2) txs.push(approve(token2Payload));
await Promise.all(txs);
refetchAllowance1();
refetchAllowance2();
if (allowance1 >= token1.rawAmount && allowance2 >= token2.rawAmount) {
setCurrentStep(3);
}
} catch (e) {
console.error("Error approving tokens", e);
} finally {
setIsApproving(false);
}
};

Expand Down Expand Up @@ -122,11 +112,8 @@ export const ManagePoolCreation = ({

useEffect(() => {
// Creating the pool sets the name and symbol permanently
if (currentStep > 1) {
setIsFormDisabled(true);
} else {
setIsFormDisabled(false);
}
currentStep > 1 ? setIsFormDisabled(true) : setIsFormDisabled(false);

// If user has no pools or their most recent pool is already finalized
if (userPoolAddress || pool?.isFinalized) {
setCurrentStep(1);
Expand Down Expand Up @@ -174,9 +161,9 @@ export const ManagePoolCreation = ({
return (
<TransactionButton
title="Create Pool"
isPending={isCreatingPool}
isPending={createPoolIsPending}
isDisabled={
isCreatingPool ||
createPoolIsPending ||
// If user has not selected tokens or entered amounts
!token1.address ||
!token2.address ||
Expand All @@ -186,16 +173,26 @@ export const ManagePoolCreation = ({
name === "" ||
symbol === ""
}
onClick={handleCreatePool}
onClick={() =>
createPool(
{ name, symbol },
{
onSuccess: newPoolAddress => {
console.log("updating userPoolAddress from useMutations onSuccess!!", newPoolAddress);
setUserPoolAddress(newPoolAddress);
},
},
)
}
/>
);
case 2:
return (
<TransactionButton
title="Approve"
isPending={isApproving}
isPending={approveIsPending}
isDisabled={
isApproving ||
approveIsPending ||
// If user has not selected tokens or entered amounts
token1.address === undefined ||
token2.address === undefined ||
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/hooks/cow/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from "./useWritePool";
export * from "./useCheckIfPoolExists";
export * from "./types";
export * from "./useNewPoolEvents";
export * from "./useCreatePool";
6 changes: 5 additions & 1 deletion packages/nextjs/hooks/cow/useCheckIfPoolExists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export const useCheckIfPoolExists = (token1: Address | undefined, token2: Addres
return hasOnlyTwoTokens && has5050Weight && hasMaxSwapFee && includesToken1 && includesToken2;
});

// Don't prevent pool duplication on testnet since limited number of faucet tokens
if (chainName === "SEPOLIA") {
return { existingPool: undefined };
}

return { existingPool };
// return { existingPool: undefined };
};
37 changes: 37 additions & 0 deletions packages/nextjs/hooks/cow/useCreatePool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useMutation } from "@tanstack/react-query";
import { Address, parseEventLogs } from "viem";
import { usePublicClient } from "wagmi";
import { abis } from "~~/contracts/abis";
import { useScaffoldWriteContract } from "~~/hooks/scaffold-eth";

type CreatePoolPayload = {
name: string;
symbol: string;
};

export const useCreatePool = () => {
const { writeContractAsync: bCoWFactory } = useScaffoldWriteContract("BCoWFactory");
const publicClient = usePublicClient();

const createPool = async ({ name, symbol }: CreatePoolPayload): Promise<Address> => {
if (!publicClient) throw new Error("No public client");
const hash = await bCoWFactory({
functionName: "newBPool",
args: [name, symbol],
});
if (!hash) throw new Error("No pool creation transaction hash");
const txReceipt = await publicClient.getTransactionReceipt({ hash });
const logs = parseEventLogs({
abi: abis.CoW.BCoWFactory,
logs: txReceipt.logs,
});
const newPool = (logs[0].args as { caller: string; bPool: string }).bPool;
console.log("New pool address from txReceipt logs:", newPool);

return newPool;
};

return useMutation({
mutationFn: (payload: CreatePoolPayload) => createPool(payload),
});
};
3 changes: 2 additions & 1 deletion packages/nextjs/hooks/cow/useNewPoolEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ export const useNewPoolEvents = (
const { data: events, isLoading: isLoadingEvents } = useScaffoldEventHistory({
contractName: "BCoWFactory",
eventName: "LOG_NEW_POOL",
fromBlock: 6415186n,
fromBlock: 6437600n,
watch: true,
filters: { caller: connectedAddress },
});

// If event listener sees a new pool event from user
useScaffoldWatchContractEvent({
contractName: "BCoWFactory",
eventName: "LOG_NEW_POOL",
Expand Down
24 changes: 2 additions & 22 deletions packages/nextjs/hooks/cow/useWritePool.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Address, parseEventLogs } from "viem";
import { Address } from "viem";
import { usePublicClient, useWalletClient } from "wagmi";
import { abis } from "~~/contracts/abis";
import { useTransactor } from "~~/hooks/scaffold-eth";
import { useScaffoldWriteContract } from "~~/hooks/scaffold-eth";

const DENORMALIZED_WEIGHT = 1000000000000000000n; // bind 2 tokens with 1e18 weight for each to get a 50/50 pool

Expand All @@ -11,25 +10,6 @@ export const useWritePool = (pool: Address | undefined) => {
const { data: walletClient } = useWalletClient();
const publicClient = usePublicClient();
const writeTx = useTransactor(); // scaffold hook for tx status toast notifications
const { writeContractAsync: bCoWFactory } = useScaffoldWriteContract("BCoWFactory");

const createPool = async (name: string, symbol: string): Promise<Address> => {
if (!publicClient) throw new Error("No public client");
const hash = await bCoWFactory({
functionName: "newBPool",
args: [name, symbol],
});
if (!hash) throw new Error("No pool creation transaction hash");
const txReceipt = await publicClient.getTransactionReceipt({ hash });
const logs = parseEventLogs({
abi: abis.CoW.BCoWFactory,
logs: txReceipt.logs,
});
const newPool = (logs[0].args as { caller: string; bPool: string }).bPool;
console.log("New pool address from txReceipt logs:", newPool);

return newPool;
};

const setSwapFee = async (rawAmount: bigint) => {
if (!pool) throw new Error("Cannot set swap fee without pool address");
Expand Down Expand Up @@ -93,5 +73,5 @@ export const useWritePool = (pool: Address | undefined) => {
});
};

return { createPool, bind, setSwapFee, finalize };
return { bind, setSwapFee, finalize };
};
2 changes: 1 addition & 1 deletion packages/nextjs/hooks/token/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from "./useReadToken";
export * from "./useWriteToken";
export * from "./useApproveToken";
export * from "./useFetchTokenList";
export * from "./useFetchTokenPrices";
export * from "./types";
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { erc20Abi } from "@balancer/sdk";
import { useMutation } from "@tanstack/react-query";
import { Address } from "viem";
import { usePublicClient, useWalletClient } from "wagmi";
import { useTransactor } from "~~/hooks/scaffold-eth";

type UseWriteToken = {
approve: (amount: bigint) => Promise<void>;
type ApprovePayload = {
token: Address | undefined;
spender: Address | undefined;
rawAmount: bigint;
};

export const useWriteToken = (token: Address | undefined, spender: Address | undefined): UseWriteToken => {
export const useApproveToken = () => {
const { data: walletClient } = useWalletClient();
const publicClient = usePublicClient();
const writeTx = useTransactor(); // scaffold hook for tx status toast notifications

const approve = async (rawAmount: bigint) => {
const approve = async ({ token, spender, rawAmount }: ApprovePayload) => {
if (!token) throw new Error("Cannot approve token without token address");
if (!spender) throw new Error("Cannot approve token without spender address");
if (!walletClient) throw new Error("No wallet client found");
Expand All @@ -30,13 +33,15 @@ export const useWriteToken = (token: Address | undefined, spender: Address | und
await writeTx(() => walletClient.writeContract(approveSpenderOnToken), {
blockConfirmations: 1,
onBlockConfirmation: () => {
console.log("Approved contract to spend max amount of", token);
console.log("Approved contract to spend", rawAmount, " amount of", token);
},
});
} catch (e) {
console.error(e);
}
};

return { approve };
return useMutation({
mutationFn: (payload: ApprovePayload) => approve(payload),
});
};

0 comments on commit f88b054

Please sign in to comment.