Skip to content

Latest commit

 

History

History
264 lines (230 loc) · 9.09 KB

readme.md

File metadata and controls

264 lines (230 loc) · 9.09 KB

本节作者:@愚指导

这一讲会完成交易相关逻辑,包括交易的询价、指定输入交易、指定输出交易等。


显示可用的交易对

首先,我们需要获取到当前可以用的交易对,并展示到下拉选择中供用户选择。我们需要使用 useReadPoolManagerGetPairs 调用 PoolManagerGetPairs 方法。

参考代码如下,我们设置了 tokenstokenAtokenB 三个状态:

// 用户可以选择的代币
const [tokens, setTokens] = useState<Token[]>([]);
// 用户选择的两个代币
const [tokenA, setTokenA] = useState<Token>();
const [tokenB, setTokenB] = useState<Token>();
// 获取所有的交易对
const { data: pairs = [] } = useReadPoolManagerGetPairs({
  address: getContractAddress("PoolManager"),
});

useEffect(() => {
  const options: Token[] = uniq(
    pairs.map((pair) => [pair.token0, pair.token1]).flat()
  ).map(getTokenInfo);
  setTokens(options);
  setTokenA(options[0]);
  setTokenB(options[1]);
}, [pairs]);

return (
  <>
    <TokenSelect value={tokenA} onChange={setTokenA} options={tokens} />
    <TokenSelect value={tokenB} onChange={setTokenB} options={tokens} />
  </>
);

以上只是核心代码,你可以参考它尝试自己来完成课程,代码中需要用到 getTokenInfo 方法,它维护在 demo/utils/common.ts 中,维护了 Token 的信息,在课程中,我们写死了相关信息,实际开发中,你应该有一个服务来维护各种 Token 的图标,名称等信息,提供接口给前端调用。

显示余额

我们需要显示用户的余额,这里我们需要使用 useReadErc20BalanceOf Hook 来获取用户的余额。我们新增一个组件 Balance.tsx,用于显示余额:

import React from "react";
import type { Token } from "@ant-design/web3";
import { CryptoPrice } from "@ant-design/web3";
import { useReadErc20BalanceOf } from "@/utils/contracts";
import { useAccount } from "wagmi";
import useTokenAddress from "@/hooks/useTokenAddress";

interface Props {
  token?: Token;
}

export default function Balance(props: Props) {
  const { address } = useAccount();
  const tokenAddress = useTokenAddress(props.token);
  const { data: balance } = useReadErc20BalanceOf({
    address: tokenAddress,
    args: [address as `0x${string}`],
    query: {
      enabled: !!tokenAddress,
      // 每 3 秒刷新一次
      refetchInterval: 3000,
    },
  });

  return balance === undefined ? (
    "-"
  ) : (
    <CryptoPrice
      value={balance}
      symbol={props.token?.symbol}
      decimals={props.token?.decimal}
      fixed={2}
    />
  );
}

组件接收一个 token 参数,它的类型是 Token,是 Ant Design Web3 中定义的一个类型,定义包含了该 Token 的信息,我们使用 Ant Design Web3 的 CryptoPrice 组件来显示余额。

另外我们通过设置了 refetchInterval3000 来自动更新余额。

报价

在发起交易之前,我们需要模拟调用报价接口,获取价格相关信息。因为该接口需要在用户输入金额时频繁调用,并且在调用后需要设置其它组件状态,不方便直接使用 Hooks 实现,所以我们封装了一个方法 updateAmountBWithAmountA

const updateAmountBWithAmountA = async (value: number) => {
  if (
    !publicClient ||
    !tokenAddressA ||
    !tokenAddressB ||
    isNaN(value) ||
    value === 0
  ) {
    return;
  }
  if (tokenAddressA === tokenAddressB) {
    message.error("Please select different tokens");
    return;
  }
  try {
    const newAmountB = await publicClient.simulateContract({
      address: getContractAddress("SwapRouter"),
      abi: swapRouterAbi,
      functionName: "quoteExactInput",
      args: [
        {
          tokenIn: tokenAddressA,
          tokenOut: tokenAddressB,
          indexPath: swapIndexPath,
          amountIn: parseAmountToBigInt(value, tokenA),
          sqrtPriceLimitX96,
        },
      ],
    });
    setAmountB(parseBigIntToAmount(newAmountB.result, tokenB));
    setIsExactInput(true);
  } catch (e: any) {
    message.error(e.message);
  }
};

我们在 TokenA 金额发生变化的时候获取报价,修改对应的 TokenB:

useEffect(() => {
  // 当用户输入发生变化时,重新请求报价接口计算价格
  if (isExactInput) {
    updateAmountBWithAmountA(amountA);
  } else {
    updateAmountAWithAmountB(amountB);
  }
}, [isExactInput, tokenAddressA, tokenAddressB, amountA, amountB]);

同理,我们也会有 updateAmountAWithAmountB 方法,它是在 TokenB 金额发生变化的时候获取报价,修改对应的 TokenA,这种情况则是指定输出来交易。

我们需要新增多个相关状态,这里就不具体展开了,完成的代码你可以在 demo/pages/wtfswap/index.tsx 中查看。

另外我们还需要设置交易的限价,也就是设置一个滑点,在课程中我们就简化实现(设置默认的 10000 个 tick 的价格偏差):

export const computeSqrtPriceLimitX96 = (
  pools: {
    pool: `0x${string}`;
    token0: `0x${string}`;
    token1: `0x${string}`;
    index: number;
    fee: number;
    feeProtocol: number;
    tickLower: number;
    tickUpper: number;
    tick: number;
    sqrtPriceX96: bigint;
  }[],
  zeroForOne: boolean
): bigint => {
  if (zeroForOne) {
    // 如果是 token0 交换 token1,那么交易完成后价格 token0 变多,价格下降下限
    // 先找到交易池的最小 tick
    const minTick =
      minBy(pools, (pool) => pool.tick)?.tick ?? TickMath.MIN_TICK;
    // 价格限制为最小 tick - 10000,避免价格过低,在实际项目中应该按照用户设置的滑点来调整
    const limitTick = Math.max(minTick - 10000, TickMath.MIN_TICK);
    return BigInt(TickMath.getSqrtRatioAtTick(limitTick).toString());
  } else {
    // 反之,设置一个最大的价格
    // 先找到交易池的最大 tick
    const maxTick =
      maxBy(pools, (pool) => pool.tick)?.tick ?? TickMath.MAX_TICK;
    // 价格限制为最大 tick + 10000,避免价格过高,在实际项目中应该按照用户设置的滑点来调整
    const limitTick = Math.min(maxTick + 10000, TickMath.MAX_TICK);
    return BigInt(TickMath.getSqrtRatioAtTick(limitTick).toString());
  }
};

computeSqrtPriceLimitX96 方法在 utils/common.ts 中,它会根据当前的交易池的 tick 信息,计算出一个价格限制,避免价格过高或过低。

交易

当获取报价成功后我们需要发起交易,和注入流动性类似,交易也需要操作用户金额,所以也需要使用 useWriteErc20Approve 进行授权。核心代码如下:

<Button
  type="primary"
  size="large"
  block
  className={styles.swapBtn}
  disabled={!tokenAddressA || !tokenAddressB || !amountA || !amountB}
  loading={loading}
  onClick={async () => {
    setLoading(true);
    try {
      if (isExactInput) {
        const swapParams = {
          tokenIn: tokenAddressA!,
          tokenOut: tokenAddressB!,
          amountIn: parseAmountToBigInt(amountA, tokenA),
          amountOutMinimum: parseAmountToBigInt(amountB, tokenB),
          recipient: account?.address as `0x${string}`,
          deadline: BigInt(Math.floor(Date.now() / 1000) + 1000),
          sqrtPriceLimitX96,
          indexPath: swapIndexPath,
        };
        console.log("swapParams", swapParams);
        await writeApprove({
          address: tokenAddressA!,
          args: [getContractAddress("SwapRouter"), swapParams.amountIn],
        });
        await writeExactInput({
          address: getContractAddress("SwapRouter"),
          args: [swapParams],
        });
      } else {
        const swapParams = {
          tokenIn: tokenAddressA!,
          tokenOut: tokenAddressB!,
          amountOut: parseAmountToBigInt(amountB, tokenB),
          amountInMaximum: parseAmountToBigInt(Math.ceil(amountA), tokenA),
          recipient: account?.address as `0x${string}`,
          deadline: BigInt(Math.floor(Date.now() / 1000) + 1000),
          sqrtPriceLimitX96,
          indexPath: swapIndexPath,
        };
        console.log("swapParams", swapParams);
        await writeApprove({
          address: tokenAddressA!,
          args: [getContractAddress("SwapRouter"), swapParams.amountInMaximum],
        });
        await writeExactOutput({
          address: getContractAddress("SwapRouter"),
          args: [swapParams],
        });
      }
      message.success("Swap success");
      setAmountA(NaN);
      setAmountB(NaN);
    } catch (e: any) {
      message.error(e.message);
    } finally {
      setLoading(false);
    }
  }}
>
  Swap
</Button>

具体实现的时候需要考虑较多的细节,包括选择哪些交易池更便宜,如何处理交易失败,如何更好的显示部分成交信息等,课程中只是做了一个简单的实现。

最后的效果如下:

swap

至此,我们的所有基础代码就开发完成了,后续我们会继续讲解如何部署和优化合约。