用户界面

用户界面 #

在这个 milestone 中,我们增加了移除池子流动性和收集累计费用的功能。因此,我们需要在用户界面上增加这些改变,允许用户移除流动性。

获取 Position #

为了让用户能够选择移除流动性的数量,我们首先需要从池子中获取用户的 position。为了简单起见,我们可以在管理合约中增加一个辅助函数,返回用户在某个特定池子中的 position:

function getPosition(GetPositionParams calldata params)
    public
    view
    returns (
        uint128 liquidity,
        uint256 feeGrowthInside0LastX128,
        uint256 feeGrowthInside1LastX128,
        uint128 tokensOwed0,
        uint128 tokensOwed1
    )
{
    IUniswapV3Pool pool = getPool(params.tokenA, params.tokenB, params.fee);

    (
        liquidity,
        feeGrowthInside0LastX128,
        feeGrowthInside1LastX128,
        tokensOwed0,
        tokensOwed1
    ) = pool.positions(
        keccak256(
            abi.encodePacked(
                params.owner,
                params.lowerTick,
                params.upperTick
            )
        )
    );
}

这让我们不需要在前端计算池子地址和 position 信息了。

接下来,在用户输入一个 position 区间后,我们获取对应的 position:

const getAvailableLiquidity = debounce((amount, isLower) => {
  const lowerTick = priceToTick(isLower ? amount : lowerPrice);
  const upperTick = priceToTick(isLower ? upperPrice : amount);

  const params = {
    tokenA: token0.address,
    tokenB: token1.address,
    fee: fee,
    owner: account,
    lowerTick: nearestUsableTick(lowerTick, feeToSpacing[fee]),
    upperTick: nearestUsableTick(upperTick, feeToSpacing[fee]),
  }

  manager.getPosition(params)
    .then(position => setAvailableAmount(position.liquidity.toString()))
    .catch(err => console.error(err));
}, 500);

获取池子地址 #

由于我们需要调用池子的 burncollect 函数,我们还是需要在前端计算池子地址。回忆一下池子合约地址是通过 CREATE2 计算出来的,需要盐值和合约代码的哈希。幸运的是,Ethers.js 有一个 getCreate2Address 函数,允许我们在 JavaScript 中计算 CREATE2

const sortTokens = (tokenA, tokenB) => {
  return tokenA.toLowerCase() < tokenB.toLowerCase ? [tokenA, tokenB] : [tokenB, tokenA];
}

const computePoolAddress = (factory, tokenA, tokenB, fee) => {
  [tokenA, tokenB] = sortTokens(tokenA, tokenB);

  return ethers.utils.getCreate2Address(
    factory,
    ethers.utils.keccak256(
      ethers.utils.solidityPack(
        ['address', 'address', 'uint24'],
        [tokenA, tokenB, fee]
      )),
    poolCodeHash
  );
}

然而,池子的代码哈希必须被硬编码,因为我们不希望把代码储存到前端然后再计算哈希。我们可以使用 Forge 来获取对应哈希:

$ forge inspect UniswapV3Pool bytecode| xargs cast keccak 
0x...

然后把结果存储在 js 里的一个常数中:

const poolCodeHash = "0x9dc805423bd1664a6a73b31955de538c338bac1f5c61beb8f4635be5032076a2";

移除流动性 #

在获取了流动性数量和池子地址之后,我们调用 burn

const removeLiquidity = (e) => {
  e.preventDefault();

  if (!token0 || !token1) {
    return;
  }

  setLoading(true);

  const lowerTick = nearestUsableTick(priceToTick(lowerPrice), feeToSpacing[fee]);
  const upperTick = nearestUsableTick(priceToTick(upperPrice), feeToSpacing[fee]);

  pool.burn(lowerTick, upperTick, amount)
    .then(tx => tx.wait())
    .then(receipt => {
      if (!receipt.events[0] || receipt.events[0].event !== "Burn") {
        throw Error("Missing Burn event after burning!");
      }

      const amount0Burned = receipt.events[0].args.amount0;
      const amount1Burned = receipt.events[0].args.amount1;

      return pool.collect(account, lowerTick, upperTick, amount0Burned, amount1Burned)
    })
    .then(tx => tx.wait())
    .then(() => toggle())
    .catch(err => console.error(err));
}

如果燃烧成功,我们立刻调用 collect,来收集对应的累积费用。