各位观众,大家好!今天咱们来聊聊一个听起来高大上,但实际上也没那么神秘的话题:JavaScript 里的区块链和智能合约。别怕,咱们不搞理论轰炸,直接上手撸代码,保证你听完能自己写个简单的智能合约交互界面。
开场白:区块链?智能合约?JavaScript?这是什么组合?
想象一下,区块链就像一个公开透明的账本,每个人都可以查看,但没人能随意篡改。智能合约呢,就像写在这个账本上的自动执行的协议,一旦条件满足,它就会自动运行。而 JavaScript,就是我们用来和这个账本,以及上面的智能合约“对话”的语言。
第一章:准备工作:搭建你的开发环境
要想和区块链玩耍,首先得有个 playground。
- Node.js 和 npm (或 yarn): 这俩是 JavaScript 的基石,没有它们,寸步难行。去 Node.js 官网下载安装包吧,npm 会一起安装的。Yarn 是一个可选的包管理器,比 npm 快一点,看个人喜好。
- Ganache: 这是一个本地的区块链模拟器,可以让你在电脑上模拟一个真实的区块链环境,不用花真金白银在测试网上折腾。下载安装后,启动它,你会看到 10 个预先分配好 ETH 的账户,这就是你的“测试币”。
-
Truffle: 这是一个开发、测试和部署智能合约的框架。它能帮你编译 Solidity 代码,部署到区块链上,还能帮你写测试用例。用 npm 安装:
npm install -g truffle
- MetaMask: 这是一个浏览器插件,相当于一个区块链钱包,可以让你在浏览器里管理你的账户,和区块链应用交互。安装好后,记得导入 Ganache 里的一个账户,这样你才能用测试币。
第二章:智能合约:用 Solidity 编写一个简单的合约
Solidity 是以太坊上编写智能合约的主要语言,语法有点像 JavaScript。
-
创建 Truffle 项目:
mkdir my-dapp cd my-dapp truffle init
这会创建一个包含 contracts、migrations 和 test 目录的文件夹。
-
编写合约: 在
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
函数用来获取这个数字。 -
编写迁移文件: 在
migrations
目录下创建一个文件,命名为2_deploy_contracts.js
,写入以下代码:const SimpleStorage = artifacts.require("SimpleStorage"); module.exports = function (deployer) { deployer.deploy(SimpleStorage); };
这个文件告诉 Truffle 如何部署你的合约。
-
编译合约:
truffle compile
这会把你的 Solidity 代码编译成以太坊虚拟机可以执行的字节码。
-
部署合约: 确保 Ganache 正在运行,然后运行:
truffle migrate
这会把你的合约部署到 Ganache 模拟的区块链上。
第三章:JavaScript 接口:与智能合约交互
现在合约已经部署好了,接下来就是用 JavaScript 和它交互了。
-
安装 web3.js: web3.js 是一个 JavaScript 库,可以让你和以太坊区块链交互。
npm install web3
-
编写 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 代码可以监听这个事件,并更新页面。
-
运行代码: 用浏览器打开
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 和它交互:
-
创建 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 代币。
-
安装 OpenZeppelin Contracts:
npm install @openzeppelin/contracts
-
编写迁移文件: 在
migrations
目录下创建一个文件,命名为3_deploy_token.js
,写入以下代码:const MyToken = artifacts.require("MyToken"); module.exports = function (deployer) { deployer.deploy(MyToken, 1000000000); // 初始发行 10 亿个代币 };
-
编译和部署合约:
truffle compile truffle migrate
-
编写 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>
这段代码添加了一个转账功能,可以让你把代币转给其他账户。
-
运行代码: 用浏览器打开
index.html
,你应该能看到你的代币余额,可以输入一个接收地址和转账数量,然后点击 "Transfer" 按钮,就可以把代币转给其他账户了。
好了,今天的讲座就到这里。希望你能通过这篇文章,对 JavaScript 和区块链有一个更深入的了解。 记住,区块链的世界充满机遇,勇敢地去探索吧! 祝你编码愉快!