JS `Blockchain` `Smart Contracts` (智能合约) 的 JavaScript 接口与工具

各位观众,大家好!今天咱们来聊聊一个听起来高大上,但实际上也没那么神秘的话题:JavaScript 里的区块链和智能合约。别怕,咱们不搞理论轰炸,直接上手撸代码,保证你听完能自己写个简单的智能合约交互界面。

开场白:区块链?智能合约?JavaScript?这是什么组合?

想象一下,区块链就像一个公开透明的账本,每个人都可以查看,但没人能随意篡改。智能合约呢,就像写在这个账本上的自动执行的协议,一旦条件满足,它就会自动运行。而 JavaScript,就是我们用来和这个账本,以及上面的智能合约“对话”的语言。

第一章:准备工作:搭建你的开发环境

要想和区块链玩耍,首先得有个 playground。

  1. Node.js 和 npm (或 yarn): 这俩是 JavaScript 的基石,没有它们,寸步难行。去 Node.js 官网下载安装包吧,npm 会一起安装的。Yarn 是一个可选的包管理器,比 npm 快一点,看个人喜好。
  2. Ganache: 这是一个本地的区块链模拟器,可以让你在电脑上模拟一个真实的区块链环境,不用花真金白银在测试网上折腾。下载安装后,启动它,你会看到 10 个预先分配好 ETH 的账户,这就是你的“测试币”。
  3. Truffle: 这是一个开发、测试和部署智能合约的框架。它能帮你编译 Solidity 代码,部署到区块链上,还能帮你写测试用例。用 npm 安装:

    npm install -g truffle
  4. MetaMask: 这是一个浏览器插件,相当于一个区块链钱包,可以让你在浏览器里管理你的账户,和区块链应用交互。安装好后,记得导入 Ganache 里的一个账户,这样你才能用测试币。

第二章:智能合约:用 Solidity 编写一个简单的合约

Solidity 是以太坊上编写智能合约的主要语言,语法有点像 JavaScript。

  1. 创建 Truffle 项目:

    mkdir my-dapp
    cd my-dapp
    truffle init

    这会创建一个包含 contracts、migrations 和 test 目录的文件夹。

  2. 编写合约:contracts 目录下创建一个文件,命名为 SimpleStorage.sol,写入以下代码:

    pragma solidity ^0.8.0;
    
    contract SimpleStorage {
        uint256 storedData;
    
        function set(uint256 x) public {
            storedData = x;
        }
    
        function get() public view returns (uint256) {
            return storedData;
        }
    }

    这个合约非常简单,它有一个状态变量 storedData,可以用来存储一个数字,还有一个 set 函数用来设置这个数字,一个 get 函数用来获取这个数字。

  3. 编写迁移文件:migrations 目录下创建一个文件,命名为 2_deploy_contracts.js,写入以下代码:

    const SimpleStorage = artifacts.require("SimpleStorage");
    
    module.exports = function (deployer) {
        deployer.deploy(SimpleStorage);
    };

    这个文件告诉 Truffle 如何部署你的合约。

  4. 编译合约:

    truffle compile

    这会把你的 Solidity 代码编译成以太坊虚拟机可以执行的字节码。

  5. 部署合约: 确保 Ganache 正在运行,然后运行:

    truffle migrate

    这会把你的合约部署到 Ganache 模拟的区块链上。

第三章:JavaScript 接口:与智能合约交互

现在合约已经部署好了,接下来就是用 JavaScript 和它交互了。

  1. 安装 web3.js: web3.js 是一个 JavaScript 库,可以让你和以太坊区块链交互。

    npm install web3
  2. 编写 JavaScript 代码: 创建一个 HTML 文件,比如 index.html,然后在里面写 JavaScript 代码:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Simple Storage</title>
    </head>
    <body>
        <h1>Simple Storage</h1>
        <label for="newValue">New Value:</label>
        <input type="number" id="newValue">
        <button id="setButton">Set Value</button>
        <p>Stored Value: <span id="storedValue"></span></p>
    
        <script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
        <script>
            window.addEventListener('load', async () => {
                // 连接到 MetaMask
                if (window.ethereum) {
                    window.web3 = new Web3(window.ethereum);
                    try {
                        // 请求用户授权
                        await window.ethereum.enable();
                    } catch (error) {
                        console.error("User denied account access");
                    }
                } else if (window.web3) {
                    window.web3 = new Web3(web3.currentProvider);
                } else {
                    console.log('Non-Ethereum browser detected. You should consider trying MetaMask!');
                }
    
                // 合约地址和 ABI (Application Binary Interface)
                const contractAddress = '你的合约地址'; // 替换成你部署的合约地址
                const contractABI = [
                    {
                        "inputs": [
                            {
                                "internalType": "uint256",
                                "name": "x",
                                "type": "uint256"
                            }
                        ],
                        "name": "set",
                        "outputs": [],
                        "stateMutability": "nonpayable",
                        "type": "function"
                    },
                    {
                        "inputs": [],
                        "name": "get",
                        "outputs": [
                            {
                                "internalType": "uint256",
                                "name": "",
                                "type": "uint256"
                            }
                        ],
                        "stateMutability": "view",
                        "type": "function"
                    }
                ]; // 替换成你合约的 ABI
    
                // 创建合约实例
                const simpleStorage = new web3.eth.Contract(contractABI, contractAddress);
    
                // 获取存储的值
                const getStoredValue = async () => {
                    const value = await simpleStorage.methods.get().call();
                    document.getElementById('storedValue').innerText = value;
                };
    
                // 设置存储的值
                document.getElementById('setButton').addEventListener('click', async () => {
                    const newValue = document.getElementById('newValue').value;
                    // 获取当前账户
                    const accounts = await web3.eth.getAccounts();
                    // 调用 set 函数
                    await simpleStorage.methods.set(newValue).send({ from: accounts[0] });
                    // 刷新显示
                    getStoredValue();
                });
    
                // 初始加载时获取值
                getStoredValue();
            });
        </script>
    </body>
    </html>

    代码解释:

    • 连接到 MetaMask: 这段代码首先检测浏览器是否安装了 MetaMask,如果安装了,就连接到 MetaMask。
    • 获取合约地址和 ABI: 你需要把 contractAddress 替换成你部署的合约地址,contractABI 替换成你合约的 ABI。 ABI 可以在 Truffle 编译后的 JSON 文件里找到 (在 build/contracts 目录下)。
    • 创建合约实例:web3.eth.Contract 创建一个合约实例,这样你就可以用 JavaScript 调用合约里的函数了。
    • 调用 get 函数: simpleStorage.methods.get().call() 会调用合约里的 get 函数,call() 表示这是一个只读操作,不会修改区块链状态,所以不需要花费 gas。
    • 调用 set 函数: simpleStorage.methods.set(newValue).send({ from: accounts[0] }) 会调用合约里的 set 函数,send() 表示这是一个修改区块链状态的操作,需要花费 gas。 from: accounts[0] 指定了调用这个函数的账户,也就是你的 MetaMask 里的账户。
    • 事件监听: 可以监听智能合约发出的事件,比如当 storedData 改变时,合约可以发出一个事件,你的 JavaScript 代码可以监听这个事件,并更新页面。
  3. 运行代码: 用浏览器打开 index.html,你应该能看到一个简单的界面,可以输入一个数字,然后点击 "Set Value" 按钮,就可以把这个数字存储到智能合约里了。

第四章:常用工具和库

除了 web3.js,还有一些其他常用的工具和库可以帮助你开发区块链应用:

工具/库 描述
ethers.js 另一个 JavaScript 库,功能和 web3.js 类似,但设计更简洁,更模块化。
Truffle Boxes 预先配置好的 Truffle 项目模板,包含了一些常用的库和组件,可以让你快速开始开发。
OpenZeppelin 一个智能合约安全库,包含了很多常用的智能合约组件,比如 ERC20 代币,访问控制等等。
Remix IDE 一个在线的 Solidity 集成开发环境,可以让你直接在浏览器里编写、编译和部署智能合约。
Infura 一个以太坊节点服务,可以让你不用自己运行一个以太坊节点,就可以和区块链交互。
The Graph 一个区块链数据索引服务,可以让你更方便地查询区块链上的数据。
Hardhat 另一个以太坊开发环境,和 Truffle 类似,但功能更强大,更灵活。
Chainlink 一个去中心化的预言机网络,可以让你把链下的数据引入到智能合约里。

第五章:安全注意事项

智能合约的安全至关重要,一旦合约出现漏洞,可能会导致严重的经济损失。以下是一些安全注意事项:

  • 使用 OpenZeppelin 等安全库: 这些库经过了广泛的测试和审计,可以避免很多常见的安全问题。
  • 进行代码审计: 请专业的安全审计公司对你的代码进行审计,找出潜在的漏洞。
  • 编写单元测试: 编写全面的单元测试,确保你的合约在各种情况下都能正常工作。
  • 小心重入攻击: 重入攻击是一种常见的智能合约攻击,攻击者可以通过递归调用合约的函数来窃取资金。
  • 限制 Gas 消耗: 确保你的合约的 Gas 消耗在一个合理的范围内,避免 Gas 耗尽攻击。
  • 使用静态分析工具: 使用静态分析工具可以自动检测代码中的安全问题。
  • 关注安全漏洞报告: 及时关注智能合约安全漏洞报告,并修复你的代码。

总结:JavaScript + 区块链 = 无限可能

JavaScript 和区块链的结合,为我们打开了一扇通往去中心化世界的大门。你可以用 JavaScript 开发各种各样的区块链应用,比如去中心化交易所、去中心化社交网络、去中心化游戏等等。 只要你敢想,没有什么是不可能的。

最后的彩蛋:一个更复杂的例子 (ERC20 代币)

咱们来写一个简单的 ERC20 代币合约,然后用 JavaScript 和它交互:

  1. 创建 ERC20 合约:contracts 目录下创建一个文件,命名为 MyToken.sol,写入以下代码:

    pragma solidity ^0.8.0;
    
    import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
    
    contract MyToken is ERC20 {
        constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
            _mint(msg.sender, initialSupply);
        }
    }

    这个合约继承了 OpenZeppelin 的 ERC20 合约,只需要几行代码就可以创建一个 ERC20 代币。

  2. 安装 OpenZeppelin Contracts:

    npm install @openzeppelin/contracts
  3. 编写迁移文件:migrations 目录下创建一个文件,命名为 3_deploy_token.js,写入以下代码:

    const MyToken = artifacts.require("MyToken");
    
    module.exports = function (deployer) {
        deployer.deploy(MyToken, 1000000000); // 初始发行 10 亿个代币
    };
  4. 编译和部署合约:

    truffle compile
    truffle migrate
  5. 编写 JavaScript 代码: 修改 index.html 里的 JavaScript 代码,添加转账功能:

    <!DOCTYPE html>
    <html>
    <head>
        <title>MyToken</title>
    </head>
    <body>
        <h1>MyToken</h1>
        <p>Your Balance: <span id="balance"></span></p>
        <label for="recipient">Recipient:</label>
        <input type="text" id="recipient">
        <label for="amount">Amount:</label>
        <input type="number" id="amount">
        <button id="transferButton">Transfer</button>
    
        <script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
        <script>
            window.addEventListener('load', async () => {
                // 连接到 MetaMask
                if (window.ethereum) {
                    window.web3 = new Web3(window.ethereum);
                    try {
                        // 请求用户授权
                        await window.ethereum.enable();
                    } catch (error) {
                        console.error("User denied account access");
                    }
                } else if (window.web3) {
                    window.web3 = new Web3(web3.currentProvider);
                } else {
                    console.log('Non-Ethereum browser detected. You should consider trying MetaMask!');
                }
    
                // 合约地址和 ABI
                const contractAddress = '你的合约地址'; // 替换成你部署的合约地址
                const contractABI = [
                    {
                        "inputs": [
                            {
                                "internalType": "uint256",
                                "name": "initialSupply",
                                "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": [],
                        "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": [
                            {
                                "internalType": "address",
                                "name": "to",
                                "type": "address"
                            },
                            {
                                "internalType": "uint256",
                                "name": "amount",
                                "type": "uint256"
                            }
                        ],
                        "name": "transfer",
                        "outputs": [
                            {
                                "internalType": "bool",
                                "name": "",
                                "type": "bool"
                            }
                        ],
                        "stateMutability": "nonpayable",
                        "type": "function"
                    },
                    {
                        "inputs": [
                            {
                                "internalType": "address",
                                "name": "from",
                                "type": "address"
                            },
                            {
                                "internalType": "address",
                                "name": "to",
                                "type": "address"
                            },
                            {
                                "internalType": "uint256",
                                "name": "amount",
                                "type": "uint256"
                            }
                        ],
                        "name": "transferFrom",
                        "outputs": [
                            {
                                "internalType": "bool",
                                "name": "",
                                "type": "bool"
                            }
                        ],
                        "stateMutability": "nonpayable",
                        "type": "function"
                    },
                    {
                        "stateMutability": "payable",
                        "type": "receive"
                    }
                ]; // 替换成你合约的 ABI
    
                // 创建合约实例
                const myToken = new web3.eth.Contract(contractABI, contractAddress);
    
                // 获取账户余额
                const getBalance = async () => {
                    const accounts = await web3.eth.getAccounts();
                    const balance = await myToken.methods.balanceOf(accounts[0]).call();
                    document.getElementById('balance').innerText = balance;
                };
    
                // 转账
                document.getElementById('transferButton').addEventListener('click', async () => {
                    const recipient = document.getElementById('recipient').value;
                    const amount = document.getElementById('amount').value;
                    const accounts = await web3.eth.getAccounts();
                    await myToken.methods.transfer(recipient, amount).send({ from: accounts[0] });
                    getBalance();
                });
    
                // 初始加载时获取余额
                getBalance();
            });
        </script>
    </body>
    </html>

    这段代码添加了一个转账功能,可以让你把代币转给其他账户。

  6. 运行代码: 用浏览器打开 index.html,你应该能看到你的代币余额,可以输入一个接收地址和转账数量,然后点击 "Transfer" 按钮,就可以把代币转给其他账户了。

好了,今天的讲座就到这里。希望你能通过这篇文章,对 JavaScript 和区块链有一个更深入的了解。 记住,区块链的世界充满机遇,勇敢地去探索吧! 祝你编码愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注