这篇文章介绍了如何在Uniswap V3上执行代币交换的步骤,重点在于通过Ethers.js与Uniswap的智能合约进行交互,提供了从设置节点到执行交换的详细指导,包括代码示例和图示,适合开发者快速上手,并了解Uniswap V3架构的核心概念。
info
Uniswap v4 已正式发布!查看我们的技术指南 - 如何在 Uniswap V4 上创建 Hooks,了解新功能以及如何使用它。
随着 Uniswap 不断改善用户体验,无论是前端还是通过智能合约,开发者需要跟上最新版本的更新。在2021年3月,Uniswap 发布了其新的 v3 架构,包括对智能合约和前端的更新。本指南将帮助开发者克服学习新 v3 架构的挑战,并向你展示如何使用 Ethers.js 程序化地交换代币。
让我们开始吧!
exactInputSingle
函数在 Sepolia 测试网执行一次池子交换 (ETH -> USDC)依赖项 | 版本 |
---|---|
node | ^18.18 |
dotenv | ^16.4.5 |
ethers | ^6.13.0 |
如果你已经对 Uniswap 熟悉,那么你可能会理解 Uniswap V2 的工作原理。快速回顾一下;Uniswap v2 使用常数乘积公式 (xy=k) 计算一对的交换价格。这些对中的流动性来自 LP(流动性提供者)的存款,涵盖了一对的整个价格范围。这使得流动性管理变得更简单,但资本效率较低,大部分流动性很少被使用。
而在 v3 中,引入了集中流动性,流动性提供者可以选择他们希望提供流动性的价格范围,从而实现更高的资本效率,更好地赚钱。这在资本投资和费用/收入回报上具有显著的效率提升;然而,这可能导致对流动性提供者的头寸进行更积极的管理,研究显示,暂时性损失可能更大 (来源)。
现在,让我们看看 Uniswap V3 协议的两个主要代码库:
Core 合约是 Uniswap V3 的低级操作。它们负责管理流动性池、执行交换并包括:
Periphery 合约是与核心合约交互的更高级合约,提供辅助功能和更简单的交互。这组合约包括:
接下来,让我们深入代码。在开始之前,我们需要处理一些前提条件,比如获取对 RPC 的访问和在 Sepolia 测试网为测试钱包充值 ETH。如果你已经拥有这两项内容,请 跳过 这些前提条件部分。
要与以太坊区块链通信,你需要获取对节点的访问。虽然我们可以运行自己的节点,但在 QuickNode,我们使启动区块链节点变得快捷而简单。你可以在 这里 注册一个账户。一旦启动节点,获取 HTTP URL。它应如下所示:
为了在链上交换代币,你需要 ETH 来支付 gas 费。由于我们使用的是 Sepolia 测试网,我们可以从 多链 QuickNode 水龙头 获取一些测试 ETH。
导航到 多链 QuickNode 水龙头 并连接你的钱包(例如:MetaMask、Coinbase Wallet)或粘贴你的钱包地址以获取测试 ETH。请注意,使用 EVM 水龙头时,以太坊主网要求的主网余额是 0.001 ETH。你还可以通过推特获取或使用你的 QuickNode 账户登录以获得额外奖励!
导航到 qn-guides-examples 仓库并克隆它:
git clone git@github.com:quiknode-labs/qn-guide-examples.git
接下来,导航到 defi/uniswap-v3-swaps 仓库:
cd qn-guide-examples/defi/uniswap-v3-swaps
然后,安装所需的依赖:
npm i
将 .env.example
重命名为 .env.local
并相应地填写环境变量。RPC_URL
的值应为你在上一步通过你的 QuickNode 控制面板创建的 QuickNode 节点端点,而 PRIVATE_KEY
的值应通过你的钱包安全设置获取(请查看这篇 文章 来了解如何操作)。确保保存文件。
在我们执行代码之前,让我们分析一下它的设计。
我们将按部分介绍代码。你可以在 index.js
中找到完整版本 这里。
首先,导入像 ethers、dotenv 和在创建合约实例时需要的 ABI 文件等依赖项。
import { ethers } from 'ethers'
import FACTORY_ABI from './abis/factory.json' assert { type: 'json' };
import QUOTER_ABI from './abis/quoter.json' assert { type: 'json' };
import SWAP_ROUTER_ABI from './abis/swaprouter.json' assert { type: 'json' };
import POOL_ABI from './abis/pool.json' assert { type: 'json' };
import TOKEN_IN_ABI from './abis/weth.json' assert { type: 'json' };
import 'dotenv/config'
虽然本指南涵盖了 Sepolia,你可以根据需要将 部署地址 更改为其他兼容 EVM 的链。
以下代码设置常量变量,这些变量将定义在 Sepolia 上开发的合约地址,并创建 ethers.Contract
实例。
// 部署地址
const POOL_FACTORY_CONTRACT_ADDRESS = '0x0227628f3F023bb0B980b67D528571c95c6DaC1c'
const QUOTER_CONTRACT_ADDRESS = '0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3'
const SWAP_ROUTER_CONTRACT_ADDRESS = '0x3bFA4769FB09eefC5a80d6E87c3B9C650f7Ae48E'
// 提供者、合约与签名器实例
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL)
const factoryContract = new ethers.Contract(POOL_FACTORY_CONTRACT_ADDRESS, FACTORY_ABI, provider);
const quoterContract = new ethers.Contract(QUOTER_CONTRACT_ADDRESS, QUOTER_ABI, provider)
const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider)
// 代币配置
const WETH = {
chainId: 11155111,
address: '0xfff9976782d46cc05630d1f6ebab18b2324d6b14',
decimals: 18,
symbol: 'WETH',
name: 'Wrapped Ether',
isToken: true,
isNative: true,
wrapped: true
}
const USDC = {
chainId: 11155111,
address: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
decimals: 6,
symbol: 'USDC',
name: 'USD//C',
isToken: true,
isNative: true,
wrapped: false
}
无论我们交换哪个代币,我们都需要授予 SwapRouter
合约批准,以便为我们花费代币。
下面的函数需要一个 token address
、ABI
、amount
(批准的数量)和 signer
对象。它使用 Contract.method.populateTransaction
函数在 ethers 中创建一个批准交易负载,然后使用 Signer.sendTransaction
发送交易并输出相关交易详情。
async function approveToken(tokenAddress, tokenABI, amount, wallet) {
try {
const tokenContract = new ethers.Contract(tokenAddress, tokenABI, wallet);
const approveTransaction = await tokenContract.approve.populateTransaction(
SWAP_ROUTER_CONTRACT_ADDRESS,
ethers.parseEther(amount.toString())
);
const transactionResponse = await wallet.sendTransaction(approveTransaction);
console.log(`-------------------------------`)
console.log(`正在发送批准交易...`)
console.log(`-------------------------------`)
console.log(`交易已发送: ${transactionResponse.hash}`)
console.log(`-------------------------------`)
const receipt = await transactionResponse.wait();
console.log(`批准交易已确认! https://sepolia.etherscan.io/txn/${receipt.hash}`);
} catch (error) {
console.error("在代币批准过程中发生错误:", error);
throw new Error("代币批准失败");
}
}
批准代币后下一步是获取池信息,即我们试图交换的代币对(例如:ETH/USDC)。
首先,我们在 Factory 合约上调用 getPool
函数,该函数将根据 tokenIn
、tokenOut
和 fee
返回一个池地址。获取到池地址后,我们可以创建一个 ethers 合约实例并初始化池元数据变量,以便稍后使用。
变量定义的快速总结:
async function getPoolInfo(factoryContract, tokenIn, tokenOut) {
const poolAddress = await factoryContract.getPool(tokenIn.address, tokenOut.address, 3000);
if (!poolAddress) {
throw new Error("获取池地址失败");
}
const poolContract = new ethers.Contract(poolAddress, POOL_ABI, provider);
const [token0, token1, fee] = await Promise.all([\
poolContract.token0(),\
poolContract.token1(),\
poolContract.fee(),\
]);
return { poolContract, token0, token1, fee };
}
Uniswap 实现了一个 Quoter 合约来获取交易报价。我们将使用此合约确定我们的交易期望输出量而不实际执行它。quoteExactInputSingle
方法是报价合约中可用的四种方法之一。它为单个池中的交换提供了输出金额的报价,给定你希望交换的输入金额。
我们可以利用 ethers 提供的 callStatic
方法向以太坊节点提交状态更改事务,但要求节点在本地模拟状态更改而不是执行它。该方法的目的是不打算在链上调用。
async function quoteAndLogSwap(quoterContract, fee, signer, amountIn) {
const quotedAmountOut = await quoterContract.quoteExactInputSingle.staticCall({
tokenIn: WETH.address,
tokenOut: USDC.address,
fee: fee,
recipient: signer.address,
deadline: Math.floor(new Date().getTime() / 1000 + 60 * 10),
amountIn: amountIn,
sqrtPriceLimitX96: 0,
});
console.log(`-------------------------------`)
console.log(`代币交换将导致: ${ethers.formatUnits(quotedAmountOut[0].toString(), USDC.decimals)} ${USDC.symbol} 交换 ${ethers.formatEther(amountIn)} ${WETH.symbol}`);
const amountOut = ethers.formatUnits(quotedAmountOut[0], USDC.decimals)
return amountOut;
}
现在我们有了报价,我们可以开始创建交换交易。prepareSwapParams
函数旨在设置代币交换所需的参数。该函数接受 pool contract
实例、输入和输出代币、一个 signer
、输入代币的数量(例如 amountIn)和预期的最小输出代币数量(例如 amountOut)。
async function prepareSwapParams(poolContract, signer, amountIn, amountOut) {
return {
tokenIn: WETH.address,
tokenOut: USDC.address,
fee: await poolContract.fee(),
recipient: signer.address,
amountIn: amountIn,
amountOutMinimum: amountOut,
sqrtPriceLimitX96: 0,
};
}
async function executeSwap(swapRouter, params, signer) {
const transaction = await swapRouter.exactInputSingle.populateTransaction(params);
const receipt = await signer.sendTransaction(transaction);
console.log(`-------------------------------`)
console.log(`收据: https://sepolia.etherscan.io/tx/${receipt.hash}`);
console.log(`-------------------------------`)
}
executeSwap
函数使用 exactInputSingle
方法填充包含给定参数的交易。它使用签名者发送交易,负责交易的签名和发送。在交易发送后,它在 Etherscan 上记录收据 URL。
为了将所有函数串联在一起,我们创建一个 main
函数,该函数接受输入金额(以 Ether 格式),例如 .001。
然后,它按顺序调用我们覆盖的所有步骤:
async function main(swapAmount) {
const inputAmount = swapAmount
const amountIn = ethers.parseUnits(inputAmount.toString(), 18);
try {
await approveToken(WETH.address, TOKEN_IN_ABI, amountIn, signer)
const { poolContract, token0, token1, fee } = await getPoolInfo(factoryContract, WETH, USDC);
console.log(`-------------------------------`)
console.log(`正在获取报价: ${WETH.symbol} 到 ${USDC.symbol}`);
console.log(`-------------------------------`)
console.log(`交换金额: ${ethers.formatEther(amountIn)}`);
const quotedAmountOut = await quoteAndLogSwap(quoterContract, fee, signer, amountIn);
const params = await prepareSwapParams(poolContract, signer, amountIn, quotedAmountOut[0].toString());
const swapRouter = new ethers.Contract(SWAP_ROUTER_CONTRACT_ADDRESS, SWAP_ROUTER_ABI, signer);
await executeSwap(swapRouter, params, signer);
} catch (error) {
console.error("发生错误:", error.message);
}
}
main(0.0001) // 根据需要更改金额
在运行执行命令之前,请确保你的提供者详情(例如:RPC_URL)和私钥已定义。接下来,运行命令:
node index.js
你将看到类似这样的输出:
-------------------------------
正在发送批准交易...
-------------------------------
交易已发送: 0x797429192800abec107ebfec73040bf88fd3c3ddabc2c0a66607393e14315ecd
-------------------------------
批准交易已确认! https://sepolia.etherscan.io/txn/0x797429192800abec107ebfec73040bf88fd3c3ddabc2c0a66607393e14315ecd
-------------------------------
正在获取报价: WETH 到 USDC
-------------------------------
交换金额: 0.0001
-------------------------------
代币交换将导致: 0.070107 USDC 交换 0.0001 WETH
-------------------------------
收据: https://sepolia.etherscan.io/tx/0xa508a0956e18ad23688be2b746c2a74bad4b04aad963c052ddcdc1202b3d9e8d
-------------------------------
通过查看区块浏览器验证交换。
就这样;你现在知道如何使用 exactInputSingle
函数在 Uniswap V3 上执行单一交换。
订阅我们的 时事通讯,获取更多关于以太坊的文章和指南。如果你有任何反馈,随时通过 Twitter 联系我们。你也可以在我们充满才华的开发者的 Discord 社区服务器上与我们交谈 :)
告诉我们 如果你有任何反馈或对新主题的请求。我们很乐意听到你的意见。
- 原文链接: quicknode.com/guides/def...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!