准备工作
以UniSwap v2 版本为例,部署到自己的私链上。
首先,下载并安装好Node.js,然后执行命令npm i yarn -g
全局安装yarn。
然后,下载需要的两个合约
git clone https://github.com/Uniswap/v2-core.git
git clone https://github.com/Uniswap/v2-periphery.git
最后,下载需要的前端项目。由于前端v1,v2,v3版本都在同一个项目,所以我们需要切换到最新的v2版本的标签v2.6.5
。由于后续需要修改这个前端项目使之能在私链部署,所以为了方便管理直接以v2.6.5
创建一个新的分支master
,执行如下命令
git clone https://github.com/Uniswap/interface.git
cd interface && git checkout -b master v2.6.5
由于需要multicall 执行批量查询工具,我们在目录下面放上 v2-core/contracts/Multicall.sol
合约。合约内容点击上面的链接获取。为了方便,我直接贴在下面
// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0;
pragma experimental ABIEncoderV2;
/// @title Multicall - Aggregate results from multiple read-only function calls
/// @author Michael Elliot <mike@makerdao.com>
/// @author Joshua Levine <joshua@makerdao.com>
/// @author Nick Johnson <arachnid@notdot.net>
contract Multicall {
struct Call {
address target;
bytes callData;
}
function aggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes[] memory returnData) {
blockNumber = block.number;
returnData = new bytes[](calls.length);
for(uint256 i = 0; i < calls.length; i++) {
(bool success, bytes memory ret) = calls[i].target.call(calls[i].callData);
require(success);
returnData[i] = ret;
}
}
// Helper functions
function getEthBalance(address addr) public view returns (uint256 balance) {
balance = addr.balance;
}
function getBlockHash(uint256 blockNumber) public view returns (bytes32 blockHash) {
blockHash = blockhash(blockNumber);
}
function getLastBlockHash() public view returns (bytes32 blockHash) {
blockHash = blockhash(block.number - 1);
}
function getCurrentBlockTimestamp() public view returns (uint256 timestamp) {
timestamp = block.timestamp;
}
function getCurrentBlockDifficulty() public view returns (uint256 difficulty) {
difficulty = block.difficulty;
}
function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) {
gaslimit = block.gaslimit;
}
function getCurrentBlockCoinbase() public view returns (address coinbase) {
coinbase = block.coinbase;
}
}
由于后续演示兑换需要两个ERC20合约,在目录下面放上 v2-core/contracts/MyToken.sol
合约内容如下所示:
pragma solidity >=0.5.0;
contract ERC20 {
function totalSupply() public view returns (uint supply); // 总供应量
function balanceOf( address who ) public view returns (uint value); // 指定账号余额
function allowance( address owner, address spender ) public view returns (uint _allowance); // 限额
function transfer( address to, uint value) public returns (bool ok); // 转账
function transferFrom( address from, address to, uint value) public returns (bool ok); // 从他人处转账
function approve( address spender, uint value ) public returns (bool ok); // 设置允许量值
event Transfer( address indexed from, address indexed to, uint value); // 转账事件
event Approval( address indexed owner, address indexed spender, uint value); // 允许事件
}
contract MyToken is ERC20 {
string public name; // 代币名称
string public symbol; // 代币符号
uint8 public decimals; // 小数点位
address _cfo;
mapping (address => uint256) _balances;
uint256 _supply;
mapping (address => mapping (address => uint256)) _approvals;
constructor (string memory n, string memory s, uint8 d, uint256 supply) public {
name = n;
symbol = s;
decimals = d;
_cfo = msg.sender;
_supply = supply * 10 ** uint256(d);
_balances[_cfo] = _supply;
}
modifier onlyCFO() {
require(msg.sender == _cfo);
_;
}
function totalSupply() public view returns (uint256) {
return _supply;
}
function balanceOf(address src) public view returns (uint256) {
return _balances[src];
}
function allowance(address src, address guy) public view returns (uint256) {
return _approvals[src][guy];
}
function transfer(address dst, uint wad) public returns (bool) {
assert(_balances[msg.sender] >= wad);
_balances[msg.sender] = _balances[msg.sender] - wad;
_balances[dst] = _balances[dst] + wad;
emit Transfer(msg.sender, dst, wad);
return true;
}
function transferFrom(address src, address dst, uint wad) public returns (bool) {
assert(_balances[src] >= wad);
assert(_approvals[src][msg.sender] >= wad);
_approvals[src][msg.sender] = _approvals[src][msg.sender] - wad;
_balances[src] = _balances[src] - wad;
_balances[dst] = _balances[dst] + wad;
emit Transfer(src, dst, wad);
return true;
}
function approve(address guy, uint256 wad) public returns (bool) {
_approvals[msg.sender][guy] = wad;
emit Approval(msg.sender, guy, wad);
return true;
}
}
编译合约
执行命令cd v2-core && yarn && yarn compile
编译v2-core里面的合约。在执行安装的过程中,有可能出现一些安装错误比如error D:\Code\swap\v2-core\node_modules\sha3: Command failed.
。这个不要紧,可以忽略。如果yarn出现这些错误不会进行yarn compile
。那么你重新执行以下编译命令即可。编译完成之后,会在合约目录下面生成一个build目录,里面有很多JSON文件,那么就证明编译成功了。
执行命令cd v2-periphery && yarn && yarn compile
编译v2-periphery里面的合约。
修改 init code hash
执行编译之后,还需要修改 v2-periphery/contracts/libraries/UniswapV2Library.sol
里面的 init code hash。系统默认为 96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f
。我们要使用编译后的文件v2-core/build/UniswapV2Pair.json
里面的evm.bytecode.object
字段计算哈希。打开网站http://eth.lucq.fun/#/hash
,点击keeack256
同时勾选输入数据16进制转uint8
,将evm.bytecode.object
内容复制到输入框,将下面计算出来的哈希替换掉 v2-periphery/contracts/libraries/UniswapV2Library.sol
中的 96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f
字符串。如果计算出来的哈希值跟系统的不一样,那么需要重新编译一下合约!
部署合约
我们需要部署如下几个合约
- WETH9 用于将原生的ETH包裹为WETH
- UniswapV2Pair 配对合约
- UniswapV2Factory 工厂合约
- UniswapV2Router01 路由合约
- UniswapV2Router02 路由合约
- multicall 批量查询工具。
- MyToken ERC20合约,为了后续演示兑换,这里会使用不同的入参部署2遍合约。
为了部署方便,在v2-core与v2-periphery同级目录创建一个deploy目录,用于执行合约的部署。
mkdir deploy && cd deploy && npm init -f && npm i web3 && touch index.js
部署脚本index.js
内容如下所示:
const Web3 = require('web3')
const WETH9 = require('../v2-periphery/build/WETH9.json')
const UniswapV2Pair = require('../v2-core/build/UniswapV2Pair.json')
const UniswapV2Factory = require('../v2-core/build/UniswapV2Factory.json')
const UniswapV2Router01 = require('../v2-periphery/build/UniswapV2Router01.json')
const UniswapV2Router02 = require('../v2-periphery/build/UniswapV2Router02.json')
const Multicall = require('../v2-core/build/Multicall.json')
const MyToken = require('../v2-core/build/MyToken.json')
const endpoint = 'http://node.lucq.fun';
const hexPrivateKey = '0xf78a036930ce63791ea6ea20072986d8c3f16a6811f6a2583b0787c45086f769';
async function sendTransaction(web3, chainId, account, data, nonce, gasPrice, to, value) {
const message = {
from: account.address,
gas: 5000000,
gasPrice: gasPrice,
data: data.startsWith('0x') ? data : '0x' + data,
nonce: nonce,
chainId: chainId
}
if (to) {
message.to = to
}
if (value) {
message.value = value
}
const transaction = await account.signTransaction(message)
return web3.eth.sendSignedTransaction(transaction.rawTransaction)
}
(async () => {
const options = { timeout: 1000 * 30 }
const web3 = new Web3(new Web3.providers.HttpProvider(endpoint, options))
const account = web3.eth.accounts.privateKeyToAccount(hexPrivateKey)
const chainId = await web3.eth.getChainId()
const gasPrice = await web3.eth.getGasPrice()
let nonce = await web3.eth.getTransactionCount(account.address)
// deploy WETH contract
let weth = null
{
const contract = new web3.eth.Contract(WETH9.abi)
const data = contract.deploy({ data: WETH9.bytecode }).encodeABI()
const receipt = await sendTransaction(web3, chainId, account, data, nonce, gasPrice)
console.info('WETH:', weth = receipt.contractAddress)
nonce = nonce + 1
}
// deploy UniswapV2Factory contract
let factory = null
{
const contract = new web3.eth.Contract(UniswapV2Factory.abi)
const options = { data: UniswapV2Factory.bytecode, arguments: [account.address] }
const data = contract.deploy(options).encodeABI()
const receipt = await sendTransaction(web3, chainId, account, data, nonce, gasPrice)
console.info('UniswapV2Factory:', factory = receipt.contractAddress)
nonce = nonce + 1
}
// deploy UniswapV2Router01 contract
{
const contract = new web3.eth.Contract(UniswapV2Router01.abi)
const options = { data: UniswapV2Router01.bytecode, arguments: [factory, weth] }
const data = contract.deploy(options).encodeABI()
const receipt = await sendTransaction(web3, chainId, account, data, nonce, gasPrice)
console.info('UniswapV2Router01:', receipt.contractAddress)
nonce = nonce + 1
}
// deploy UniswapV2Router02 contract
{
const contract = new web3.eth.Contract(UniswapV2Router02.abi)
const options = { data: UniswapV2Router02.bytecode, arguments: [factory, weth] }
const data = contract.deploy(options).encodeABI()
const receipt = await sendTransaction(web3, chainId, account, data, nonce, gasPrice)
console.info('UniswapV2Router02:', receipt.contractAddress)
nonce = nonce + 1
}
// deploy Multicall contract
{
const contract = new web3.eth.Contract(Multicall.abi)
const options = { data: Multicall.bytecode, arguments: [] }
const data = contract.deploy(options).encodeABI()
const receipt = await sendTransaction(web3, chainId, account, data, nonce, gasPrice)
console.info('Multicall:', receipt.contractAddress)
nonce = nonce + 1
}
// deploy MyToken contract
{
const contract = new web3.eth.Contract(MyToken.abi)
const options = { data: MyToken.bytecode, arguments: ["Probe", "Pro", 18, "1000000000000000000000000"] }
const data = contract.deploy(options).encodeABI()
const receipt = await sendTransaction(web3, chainId, account, data, nonce, gasPrice)
console.info('MyToken Probe:', receipt.contractAddress)
nonce = nonce + 1
}
// deploy MyToken contract
{
let contract = new web3.eth.Contract(MyToken.abi)
const options = { data: MyToken.bytecode, arguments: ["Tether USD", "USDT", 18, "1000000000000000000000000"] }
let data = contract.deploy(options).encodeABI()
const receipt = await sendTransaction(web3, chainId, account, data, nonce, gasPrice)
console.info('MyToken USDT:', receipt.contractAddress)
nonce = nonce + 1
// transfer probe to 0x1111102dd32160b064f2a512cdef74bfdb6a9f96
// 0x1111102dd32160b064f2a512cdef74bfdb6a9f96 privatekey is 95e06fa1a8411d7f6693f486f0f450b122c58feadbcee43fbd02e13da59395d5
const to = "0x1111102dd32160b064f2a512cdef74bfdb6a9f96"
contract.options.address = receipt.contractAddress
data = contract.methods["transfer"].apply(contract.methods, [to, "10000000000000000000"]).encodeABI();
await sendTransaction(web3, chainId, account, data, nonce, gasPrice, receipt.contractAddress)
nonce = nonce + 1
}
// deposit ETH to WETH contract
{
const contract = new web3.eth.Contract(WETH9.abi, weth)
const value = "1000000000000000000000"
const data = contract.methods["deposit"].apply(contract.methods, []).encodeABI();
await sendTransaction(web3, chainId, account, data, nonce, gasPrice, weth, value)
nonce = nonce + 1
}
// calc code hash
{
let data = UniswapV2Pair.bytecode
if (!data.startsWith('0x')) data = '0x' + data
const hash = web3.utils.keccak256(data)
console.info('INIT_CODE_HASH:', hash)
if (hash != "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f") {
console.warn("计算出来的init code hash为 " + hash + " ,与系统默认的 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f 不一致,请确保你已经更新合约v2-periphery/contracts/libraries/UniswapV2Library.sol里面的init code hash并重新编译过合约了。")
}
}
})()
在deploy执行命令node index.js
部署合约,如果部署成功,你将得到如下输出:
WETH: 0x546bc6E008689577C69C42b9C1f6b4C923f59B5d
UniswapV2Factory: 0xDad56A6B5eed8567Fc4395d05b59D15077c2c888
UniswapV2Router01: 0x33Add53fb1CDeF4A10BeE7249b66a685200DDd2f
UniswapV2Router02: 0xb4936c57f5b6B5a247aD6089C56064DF98fFf470
Multicall: 0x6e355de3fbc1290c5A72D9A9400A0304E2b8a756
MyToken Probe: 0x4BD9051a87E8d731E452eD84D22AA6E33b608E25
MyToken Tether USD: 0x67a2de7C64F04C1c8E894674acB2A2F99710bDDE
INIT_CODE_HASH: 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f
保留这些信息,后续你将会使用到。
部署前端
执行命令cd interface && yarn
安装前端所需要额依赖。由于前端的合约都是使用部署在以太坊上面的合约,我们需要将我们部署的合约地址替换掉以太坊上面的合约地址。需要替换的地方如下所,注意:以下均都是对interface目录下面的文件进行的修改:
- src/constants/index.ts 文件中 ROUTER_ADDRESS 的值替换为 UniswapV2Router02
- src/state/swap/hooks.ts 文件中 BAD_RECIPIENT_ADDRESSES 数组的值修改为 [UniswapV2Factory, UniswapV2Router01, UniswapV2Router02]
- node_modules/@uniswap/sdk/dist/sdk.cjs.development.js 和 node_modules/@uniswap/sdk/dist/sdk.esm.js 文件中 FACTORY_ADDRESS 为 UniswapV2Factory
- node_modules/@uniswap/sdk/dist/sdk.cjs.development.js 和 node_modules/@uniswap/sdk/dist/sdk.esm.js 文件中 INIT_CODE_HASH 为 INIT_CODE_HASH
由于我们是部署在私链上,我们需要将私链的信息配置好。假设我的私链名称为PRIVNET
,ChainId
是 1024,需要替换的地方或者新增的有这些方面:
- node_modules/@uniswap/sdk/dist/constants.d.ts 中ChainId添加 PRIVNET = 1024
- node_modules/@uniswap/sdk/dist/sdk.cjs.development.js 中的大概第22行函数 function (ChainId),需要添加
ChainId[ChainId["PRIVNET"] = 1024] = "PRIVNET";
- node_modules/@uniswap/sdk/dist/sdk.esm.js 中的大概第18行函数 function (ChainId),需要添加
ChainId[ChainId["PRIVNET"] = 1024] = "PRIVNET";
- node_modules/@uniswap/sdk/dist/sdk.cjs.development.js 中全局变量大概第422行 WETH,添加
_WETH[exports.ChainId.PRIVNET] = /*#__PURE__*/new Token(exports.ChainId.PRIVNET, '0x546bc6E008689577C69C42b9C1f6b4C923f59B5d', 18, 'WETH', 'Wrapped Ether')
- node_modules/@uniswap/sdk/dist/sdk.esm.js 中全局变量大概第422行 WETH,添加
_WETH[ChainId.PRIVNET] = /*#__PURE__*/new Token(ChainId.PRIVNET, '0x546bc6E008689577C69C42b9C1f6b4C923f59B5d', 18, 'WETH', 'Wrapped Ether')
- /node_modules/@uniswap/sdk/dist/entities/token.d.ts 中 WETH,添加 1024: Token;
- src/components/Header/index.tsx 文件,在全局对象 NETWORK_LABELS 里面添加 [ChainId.PRIVNET]: 'Privnet'
- src/connectors/index.ts 文件,在 supportedChainIds 数组里面添加你自己的chainId 1024
- src/constants/index.ts 文件,在全局对象 WETH_ONLY 里面添加 [ChainId.PRIVNET]: [WETH[ChainId.PRIVNET]]
- src/constants/multicall/index.ts 文件,在全局对象 MULTICALL_NETWORKS 里面添加 [ChainId.PRIVNET]: '0x6e355de3fbc1290c5A72D9A9400A0304E2b8a756',里面的地址是之前我们部署的 Multicall 合约地址。
- src/constants/v1/index.ts 文件,在全局对象 V1_FACTORY_ADDRESSES 里面添加 [ChainId.PRIVNET]: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',地址是随便填的。因为用不到。但是不填会报错。
- src/state/lists/hooks.ts 文件,在全局对象 EMPTY_LIST 里面添加 [ChainId.PRIVNET]: {}
- src/utils/index.ts 文件,在全局对象 ETHERSCAN_PREFIXES 里面添加 1024: ''。其中 1024 是私链的 ChainId
添加代币列表
经过上面的修改,前端成功的运行起来了,但是在添加流动性进入Manage Lists
页面的时候,是系统默认的代币列表。当然,这个页面你也可以添加自己token列表。这个列表在文件 interface/src/constants/lists.ts
中的 DEFAULT_LIST_OF_LISTS 常量数组。将数组中的列表中返回自己的列表即可。列表内容参照如下所示。我将上面部署出来的代币写成了一个JSON对象,并在我自己的服务器上提供了一个http地址返回。JSON对象内容如下:
{
"name": "MyPrivateChainTokens",
"logoURI": "https://umaproject.org/assets/images/UMA_square_red_logo_circle.png",
"keywords": ["private"],
"timestamp": "2021-12-08T01:40:34.305Z",
"tokens": [
{
"chainId": 1024,
"address": "0x546bc6E008689577C69C42b9C1f6b4C923f59B5d",
"name": "WETH",
"symbol": "WETH",
"decimals": 18
},
{
"chainId": 1024,
"address": "0x4BD9051a87E8d731E452eD84D22AA6E33b608E25",
"name": "Probe",
"symbol": "Pro",
"decimals": 18
},
{
"chainId": 1024,
"address": "0x67a2de7C64F04C1c8E894674acB2A2F99710bDDE",
"name": "USDT",
"symbol": "USDT",
"decimals": 18
}
],
"version": { "major": 1, "minor": 0, "patch": 0 }
}
相应的,lists.ts内容改为如下:
// the Uniswap Default token list lives here
export const DEFAULT_TOKEN_LIST_URL = 'https://b.lucq.fun/api/noteShare/?id=24&json=true'
export const DEFAULT_LIST_OF_LISTS: string[] = [
DEFAULT_TOKEN_LIST_URL,
]
如果你没有自己的服务器部署自己的tokens列表。那么你可以在我的网站https://b.lucq.fun
使用test账号登陆进去,在备忘页面将JSON内容作为一个备忘添加到我系统。然后查看该备忘内容,点击复制分享按钮,将复制到的地址在后面添加&json=true
即可做为你自己的tokens列表https地址。
当然,如果你不想这么麻烦,那么可以在Select a token
页面,将你的ERC20地址输进去一个一个添加也是可以的。
修改原生代币名称
如果我们需要将原生代币名称由 ETH 改为其它的,例如 PRO,还需要作出以下修改。
- node_modules/@uniswap/sdk/dist/sdk.cjs.development.js 中 Currency.ETHE 将值改为 new Currency(18, 'PRO', 'PRO Token');
- node_modules/@uniswap/sdk/dist/sdk.esm.js 中 Currency.ETHER,将值改为 new Currency(18, 'PRO', 'PRO Token');
- src/components/Header/index.tsx 中将文本 ETH 修改为 PRO
- src/components/SearchModal/CommonBases.tsx 中将文本 修改为 PRO
一个例子
根据上面的步骤,我已经基于我的私链chainId:1024,RPC: http://node.lucq.fun 部署了相应的uniswap。访问地址为:http://uniswap.lucq.fun/ 。在这个环境上,我已经创建了如下2个配对:
- WETH 与 USDT 的配对,大概 1 WETH == 100 USDT
- WETH 与 Pro 的配对,大概 1 WETH == 1000 Pro
有一个私钥为 95e06fa1a8411d7f6693f486f0f450b122c58feadbcee43fbd02e13da59395d5 的地址我已经内置了大量 USDT。你可以测试用USDT兑换Pro,或者用USDT兑换WETH,然后调用WETH合约将WETH提取出来换成原生的ETH等操作。
在测试的过程中,发现读取tokens list有点问题,你可以利用地址直接将Token添加进去,三个ERC20合约相对应的地址见上面部署里面的合约地址信息。