Road to Web3 第九周 创建一个Swap Dapp

原文地址 :https://docs.alchemy.com/docs/how-to-build-a-token-swap-dapp-with-0x-api

请大家关注我的推特(twitter.com/SoullessL)和Link3(link3.to/caishen),获取最新的Alchemy小白教程。教程汇总链接(jayjiang.gitbook.io/web3book/alchemy-road-to-web3)。

准备工作

进入 https://github.com/0xProject/swap-demo-tutorial ,点击fork,把代码复制到我们自己的Github。

然后会转跳到我们自己的Github地址,选择 swap-demo-tutorial-part-9 进入。

进入以后,我们复制这个链接地址。然后登录 https://gitpod.io/workspaces,点击 “New workspace”,然后把我们刚才拿到的地址复制进去。复制进去以后,点击下面那块灰色区域,进行加载编辑项目。

进入项目以后,我们在Terminal里面输入下面的命令,切换我们的工作目录。

cd swap-demo-tutorial-part-9

然后我们找到 Extensions菜单点击,在搜索框里输入Live Server,记得是图中红框里的那个,然后点击Install安装。

然后我们在Terminal里面输入下面的代码安装browserify。

npm install -g browserify

然后在Termianl里面分别输入(不要一起输入)下面的每一行代码,分别安装 qs,bignumber,web3

npm i qs
npm i bignumber
npm i web3

找到index.html文件夹,如图所示的地方,添加“Select A Token” 文字。

找到index.js文件,用以下的内容替换文件的所有内容

const qs = require('qs');
const Web3 = require('web3');
const { default: BigNumber } = require('bignumber.js');

let currentTrade = {};
let currentSelectSide;
let tokens;

async function init() {
    await listAvailableTokens();
}

async function listAvailableTokens() {
    console.log("initializing");
    // let response = await fetch('https://tokens.coingecko.com/uniswap/all.json');
    // let tokenListJSON = await response.json();
    let response='{"name":"CoinGecko","logoURI":"https://www.coingecko.com/assets/thumbnail-007177f3eca19695592f0b8b0eabbdae282b54154e1be912285c9034ea6cbaf2.png","keywords":["defi"],"timestamp":"2022-08-17T04:08:12.925+00:00","tokens":[{"chainId":56,"address":"0x55d398326f99059fF775485246999027B3197955","name":"busd","symbol":"busd","decimals":18,"logoURI":"https://assets.coingecko.com/coins/images/9956/thumb/4943.png?1636636734"},{"chainId":56,"address":"0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c","name":"bnb","symbol":"bnb","decimals":18,"logoURI":"https://assets.coingecko.com/coins/images/9956/thumb/4943.png?1636636734"}],"version":{"major":975,"minor":1,"patch":0}}';
    let tokenListJSON = JSON.parse(response);
    console.log("Listing available tokens: ", tokenListJSON);
    tokens = tokenListJSON.tokens;
    console.log("tokens: ", tokens);

    let parent = document.getElementById("token_list");
    for(const i in tokens) {
        let div = document.createElement("div");
        div.className = "token_row";

        let html = 
            `<img class="token_list_img" src="${tokens[i].logoURI}">
                <span class="token_list_text">${tokens[i].symbol}</span>`;
        div.innerHTML = html;
        div.onclick = () => {
            selectToken(tokens[i]);
        }
        parent.appendChild(div);       
    }
}

function selectToken(token) {
    closeModal();
    currentTrade[currentSelectSide] = token;
    console.log("currentTrade: ", currentTrade);
    renderInterface();
}

function renderInterface() {
    if(currentTrade.from) {
        document.getElementById("from_token_img").src = currentTrade.from.logoURI;
        document.getElementById("from_token_text").innerHTML = currentTrade.from.symbol;
    }
    if(currentTrade.to) {
        document.getElementById("to_token_img").src = currentTrade.to.logoURI;
        document.getElementById("to_token_text").innerHTML = currentTrade.to.symbol;
    }
}

async function connect() {
    if (typeof window.ethereum !== "undefined") {
        try {
            console.log("Connecting");
            await ethereum.request({ method: "eth_requestAccounts" });
        } catch (error) {
            console.log(error);
        }
        document.getElementById("login_button").innerHTML = "Connected";
        document.getElementById("swap_button").disabled = false;
    } else {
        document.getElementById("login_button").innerHTML = 
            "Please install Metamask";
    }
}

async function getPrice() {
    console.log("Getting Price");

    if(!currentTrade.from || !currentTrade.to || !document.getElementById("from_amount").value) return;
    let amount = Number(document.getElementById("from_amount").value * 10 ** currentTrade.from.decimals);

    const params = {
        sellToken: currentTrade.from.address,
        buyToken: currentTrade.to.address,
        sellAmount: amount,
    }

    // Fetch the swap price
    const response = await fetch(`https://bsc.api.0x.org/swap/v1/price?${qs.stringify(params)}`);

    swapPriceJSON = await response.json();
    console.log("Price: ", swapPriceJSON);

    document.getElementById("to_amount").value = swapPriceJSON.buyAmount / (10 ** currentTrade.to.decimals);
    document.getElementById("gas_estimate").innerHTML = swapPriceJSON.estimatedGas;
}

async function getQuote(account) {
    console.log("Getting Quote");

    if(!currentTrade.from || !currentTrade.to || !document.getElementById("from_amount").value) return;
    let amount = Number(document.getElementById("from_amount").value * 10 ** currentTrade.from.decimals);

    const params = {
        sellToken: currentTrade.from.symbol,
        buyToken: currentTrade.to.symbol,
        sellAmount: amount,
        takerAddress: account,
        slippagePercentage: 0.05
    }; 

    // Fetch the swap price
    const response = await fetch(`https://bsc.api.0x.org/swap/v1/quote?${qs.stringify(params)}`);

    swapQuoteJSON = await response.json();
    console.log("Quote: ", swapQuoteJSON);

    // document.getElementById("to_amount").value = swapQuoteJSON.price;
    document.getElementById("gas_estimate").innerHTML = swapQuoteJSON.estimatedGas;

    return swapQuoteJSON;
}

async function trySwap() {

    let accounts = await ethereum.request({ method: "eth_accounts" });
    let takerAddress = accounts[0];

    console.log("takerAddress:", takerAddress);

    const swapQuoteJSON = await getQuote(takerAddress);

    // Set Token Allowance
    // Interact with ERC20TokenContract
    const web3 = new Web3(Web3.givenProvider);
    const fromTokenAddress = currentTrade.from.address;
    const erc20abi = [{ "inputs": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "symbol", "type": "string" }, { "internalType": "uint256", "name": "max_supply", "type": "uint256" } ], "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } ], "name": "Approval", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } ], "name": "Transfer", "type": "event" }, { "inputs": [ { "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "address", "name": "spender", "type": "address" } ], "name": "allowance", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "approve", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "account", "type": "address" } ], "name": "balanceOf", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "burn", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "account", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "burnFrom", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "decimals", "outputs": [ { "internalType": "uint8", "name": "", "type": "uint8" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "subtractedValue", "type": "uint256" } ], "name": "decreaseAllowance", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "addedValue", "type": "uint256" } ], "name": "increaseAllowance", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "name", "outputs": [ { "internalType": "string", "name": "", "type": "string" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "symbol", "outputs": [ { "internalType": "string", "name": "", "type": "string" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "totalSupply", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "recipient", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "transfer", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "sender", "type": "address" }, { "internalType": "address", "name": "recipient", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "transferFrom", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }]
    console.log("trying swap"); 

    const ERC20TokenContract = new web3.eth.Contract(erc20abi, fromTokenAddress);
    console.log("setup ERC20TokenContract: ", ERC20TokenContract);

    const maxApproval = new BigNumber(2).pow(256).minus(1);
    console.log("approval amount: ", maxApproval);

    const tx = await ERC20TokenContract.methods
        .approve(swapQuoteJSON.allowanceTarget, maxApproval)
        .send({ from: takerAddress })
        .then((tx) => {
            console.log("tx: ", tx)
        });

    const receipt = await web3.eth.sendTransaction(swapQuoteJSON);
    console.log("receipt: ", receipt);
}

init();

function openModal(side) {
    currentSelectSide = side;
    document.getElementById("token_modal").style.display = "block";
}

function closeModal() {
    document.getElementById("token_modal").style.display = "none";
}

document.getElementById("login_button").onclick = connect;
document.getElementById("from_token_select").onclick = () => {
     openModal("from");
};
document.getElementById("to_token_select").onclick = () => {
    openModal("to");
};
document.getElementById("modal_close").onclick = closeModal;
document.getElementById("from_amount").onblur = getPrice;
document.getElementById("swap_button").onclick = trySwap;

然后我们在Terminal里面输入下面的代码,之后我们会看到创建了一个新的 bundle.js文件。

browserify index.js --standalone bundle -o bundle.js

然后我们鼠标放到index.html页面,右键点击“Open with Live Server”,过一会就会进入到我们做的swap页面,选择Token的时候应该只有bnb和busd两个token选择,则说明代码没问题了。

然后我们把Metamask切换到币安链,然后点击Sign in with MetaMask登录。选择用BNB交换BUSD,这里需要在你的钱包里有一些币安币,用的是正式网,不是测试网。

然后我们点击swap,会出来一个授权,让我们授权程序可以交换BNB,我们点击确定,等待主网确定。等确定完,会自动在Metamask弹出交易窗口,然后我们点击确定,就可以成功把0.001bnb交换成Busd了。

然后我们在Metamask里面找到这个交易记录,把交易记录的地址复制出来,到时候填到提交表单里。

交易成功以后,为了安全,我们可以去(https://bscscan.com/tokenapprovalchecker),找到我们刚才授权的应用,取消授权。

然后我们在 swap-demo-tutorial-part-9 文件夹下面,新建一个 .gitignore的文件,添加内容 node_modules,这样我们就不用上传无用的node_modules文件夹了。

然后我们在Source Control 里填写备注信息,点击Commit按钮右边的小三角,点击 Commit&Push 把代码提交到我们的Github。

提交表单

链接:https://alchemyapi.typeform.com/roadtoweek9

表单最后填写,项目的Github地址(https://github.com/你的Github名字/swap-demo-tutorial/tree/main/swap-demo-tutorial-part-9)和你在前面复制的交易记录(https://bscscan.com/tx/你的交易记录tx)。

Subscribe to SoullessL
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.