9. 智能合约设计①
手把手编写智能合约,经典代码学到什么程度:看懂合约,不能当个大傻子,只会被淘汰
- 内容:
- 安装套件工具(
truffle和Ganache) - 学习
Solidity语法
- 安装套件工具(
08:40开始- 做合约挣得多,但是难入门,在公司慢慢申请转岗
- 合约审计网站,简单的开发区块链
- 不抢活,最后就会被替代
- 学习使用套件:
Truffle
- 提供一套本地模拟的区块链,非常直观,只用于学习使用
- 实际开发中是不用的
- 文档地址:
truffle、ganache、drizzletruffle:创建合约和部署合约,写合约 + 编译 + 部署(后端工具)npm install -g truffleGanache:本地区块链模拟器(测试网络)下载桌面端工具Drizzle:前端框架,帮你读取链上数据(前端工具)- 目前用前两个就够用了
- 它们三个加起来叫
Truffle Suite,以前是一整套开发以太坊Dapp的工具链。
ganache:/ɡəˈnæʃ/ACCOUNTSBLOCKSTRANSACTIONSCONTRACTSEVENTSLOGS
Ganache 工具
| 栏目 | 作用 |
|---|---|
| Accounts | 钱包、余额、私钥 |
| Blocks | 区块浏览器、挖矿 |
| Transactions | 链上所有交易记录 |
| Contracts | 已部署的合约 |
| Events | Solidity emit 的事件 |
| Logs | EVM 底层日志 |
学习关系
| 工具 | 作用 | 使用场景 |
|---|---|---|
| Remix IDE | 在线写、编译、部署、调试智能合约的网页 IDE | 入门学习、快速实验合约 |
| Ganache | 本地假链(本地区块链环境) | 本地测试部署合约,不花真币 |
| Hardhat | 专业的智能合约开发框架 | 正式开发、自动化测试、真实网络部署 |
学习流程
- 三阶段工具:
js
1. Remix —— 入门写合约(最简单)
↓
2. Ganache —— 本地跑假链(测试)
↓
3. Hardhat —— 专业框架(正式开发 + 部署到测试网/主网)- 真正
DApp流程:
js
用 Hardhat 写合约 & 自动化测试
↓
部署到 Hardhat 内置链(或 Ganache)
↓
测试没问题
↓
部署到测试网(Goerli / Sepolia)
↓
部署到主网编写智能合约
- Solidity:写智能合约的编程语言
- 合约编辑器:类似babel
- 位置:
00:52:00开始一步步写合约、部署 - 钱包:
- 不要:创建账户就无法删除了,不同的账户只是私钥不用,助记词是一样的
- 优选:添加钱包-导入账户(通过私钥),这种本地测试用的才能删除
- 添加网络:添加测试账户是没有钱的,需要添加测试网络才能有钱
Solidity
需要进行版本的强控制,不同版本的兼容性几乎没有,类似曾经的
Angular 1.0 和 2.0之间的差距一样行业本身就这样,愿用不用,不用滚;一堆投机搞钱的,主流层能跑通就行了
学习一个基本的
hello world并部署线上创建项目:
truffle init创建合约:
truffle create contract xxName,一键在contracts文件夹创建.sol文件部署脚本:
migrations文件夹下面编写.js部署脚本,命令行执行truffle deploy调用
events:
调用钱包的方法
- 使用
ethereum进行原生js调用 - 使用
ethers.js v6调用 - 使用
wagmi封装好的工具,进行调用
Solidity源码·Hello World
constructor:从前端角度叫变量,从web3角度叫世界状态,存储在所有人的电脑里面
Hello World
js
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "hardhat/console.sol";
contract Hello {
string public greeting;
constructor() {
greeting = "Hello World!";
console.log(888, greeting);
}
function setGreeting(string memory _greeting) public {
greeting = _greeting;
}
function say() public view returns (string memory) {
return greeting;
}
}使用服务启动页面
- 由于开发区块链浏览器,所以单纯的是用
html无法满足需求为此添加server.js
server.js
js
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = 8080;
// MIME 类型映射
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.json': 'application/json',
'.css': 'text/css',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml'
};
const server = http.createServer((req, res) => {
console.log(`请求: ${req.url}`);
// 路由处理:如果 URL 包含 /tx/、/block/、/address/,则返回 explorer.html
if (req.url.includes('/tx/') || req.url.includes('/block/') || req.url.includes('/address/')) {
const filePath = path.join(__dirname, 'explorer.html');
fs.readFile(filePath, (err, content) => {
if (err) {
res.writeHead(500);
res.end('服务器错误: ' + err.code);
} else {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(content, 'utf-8');
}
});
return;
}
// 默认处理静态文件
let filePath = path.join(__dirname, req.url === '/' ? 'index-json.html' : req.url);
const extname = path.extname(filePath);
const contentType = mimeTypes[extname] || 'application/octet-stream';
fs.readFile(filePath, (err, content) => {
if (err) {
if (err.code === 'ENOENT') {
res.writeHead(404);
res.end('文件未找到: ' + req.url);
} else {
res.writeHead(500);
res.end('服务器错误: ' + err.code);
}
} else {
res.writeHead(200, { 'Content-Type': contentType });
res.end(content, 'utf-8');
}
});
});
server.listen(PORT, '0.0.0.0', () => {
console.log(`服务器运行在 http://0.0.0.0:${PORT}/`);
console.log(`支持路由: /tx/..., /block/..., /address/...`);
});- 查看服务器进程:
ps aux | grep "node server.js" | grep -v grep - 停止服务器:
killall node,指定进程IDkill 69285 - 启动服务器:
node server.js
memory 是什么?
Solidity存储变量有三种地方:
| 关键字 | 存在哪 | 生命周期 | 成本 |
|---|---|---|---|
storage | 区块链永久存储 | 永久 | 贵 |
memory | 内存(临时) | 函数执行期间 | 便宜 |
calldata | 外部输入,不可修改 | 函数期间 | 最便宜 |
为什么要 view?
- 因为
EVM需要知道函数是否“修改状态”。 - 三种类型: | 修饰符 | 是否修改链状态 | 是否需要
Gas| 说明 | | --- | --- | --- | --- | |view| ❌不修改 | ❌不需要(读操作) | 只能读取,不允许写入 | |pure| ❌只计算 | ❌不需要 | 不读也不写 | | 无修饰符 | ✔️修改 | ✔️需要Gas| 写入链上状态 |
view 是告诉区块链:“我只看,不动。”
为什么要 returns(string memory)?
Solidity要非常明确地告诉EVM:- 返回值的类型(
string) - 返回值放在哪(
memory)
- 返回值的类型(
因为链上需要严格定义内存位置,不然没法运行。
JS 不需要声明类型,但 Solidity 必须声明:
| 部分 | 作用 |
|---|---|
| 状态变量(链上永久) | string public greeting |
| 构造函数(部署时执行) | 设置默认 "Hello World" |
写入函数(会消耗 Gas) | setGreeting |
读取函数(不消耗 Gas) | say() |
部署上线
- Remix:Ethereum IDE
Ethereum IDE= 以太坊智能合约编辑器 + 编译器 + 调试器 + 部署工具 的集合- 换句话说:它不只是一个代码编辑器,而是一整套专门为
Solidity/EVM开发准备的开发环境。
contracts文件夹新建一个Hello.sol文件,复制粘贴即可- 选中
Hello.sol文件,切换Solidity compiler模块 - 点击
Compile 5_Hello.sol按钮,进行编译- 编译完成会拿到
ABI二进制应用程序接口,和API一样,一个JSON数据:- 编译合约之后,编译器会生成一个
ABI(Application Binary Interface,应用二进制接口) 文件。 - 它通常是一个
JSON格式的数据,里面描述了合约对外暴露的函数、事件和参数类型,相当于“合约的使用说明书”,方便前端或其他程序按这个接口和链上的二进制合约交互。
- 编译合约之后,编译器会生成一个
- 编译完成会拿到
- 切换到
Deploy & run transactions(部署并运行交易)- 虚拟机:最有名的是
Remix VM (Cancun),但目前默认Remix VM (Prague),这块就不要改了 - 账户:默认第一个用户,谁部署合约,谁就是这个合约的
owner- 有很多貔貅盘,只允许
owner提取,其他人都只能充值
- 有很多貔貅盘,只允许
Gas:有四种单位,最小为Wei
- 虚拟机:最有名的是
- 操作按钮:点击黄色按钮
Deploy- 黄色按钮花费
Gas,蓝色按钮免费,红色按钮花费本金 contract address(合约地址):xxxxx- 形式上和默认的钱包地址没有任何区别
- 最大的区别:这里面的钱,只有
owner通过代码操作,没有私钥
- 黄色按钮花费
从零创建项目
创建-编写-编译-部署合约
前端调用
mkdir test && cd testtruffle initGanache工具-NEW WORKSPACE-ADD PROJECT(可以重命名)-引入项目的truffle-config.js文件-STARTGanache-ACCOUNTS-随便找个账户的私钥复制-Metamask-添加钱包-通过私钥导入账户- 注意:这里不要创建账户,创建的账户是无法删除的
- 开启
Metamask测试网络:- 位置:设置-高级
- 操作:勾选三个项(显示十六进制数据、在测试网络上显示转换、显示测试网络)
- 查看:所有热门网络-
Select network-Custom - 开启:所有热门网络-
Select network-Custom-添加自定义网络(URL/ID)都可以在Ganache顶部栏找到 Sepolia(以太坊官方测试链):这是唯一一个以太坊基金会维护的官方链
- 接下来整体就打通了
- 存在问题:由于没有测试没有汇率,导致账户额度为零
- 打开部署项目配置
- 把
truffle-config.js里面的networks下面代码打开注释就可以了- 注意:
port是7545
- 注意:
- 说明:用于部署时连接区块链网络(
Ganache、本地节点、测试链)
js
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 7545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
},- 把
truffle-config.js里面的compilers-solc下面代码打开注释就可以了 - 说明:控制
Solidity编译行为
js
settings: { // See the solidity docs for advice about optimization and evmVersion
optimizer: {
enabled: false,
runs: 200
},
evmVersion: "byzantium"
}- 创建合约:
truffle create contract TestContracttruffle create all xxx:创建整套合约,合约文件、测试文件、部署脚本
编写合约
js
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
contract InfoContract {
string name;
uint256 age;
event Instructor(string name, uint256 age);
function setInfo(string memory _name, uint256 _age) public {
name = _name;
age = _age;
// 触发事件
emit Instructor(name, age);
}
function sayHi() public view returns(string memory) {
return "Hello World!";
}
function getInfo() public view returns(string memory, uint256) {
return (name, age);
}
}- 编写部署脚本
migrations文件夹1_deploy_contracts.js
js
// 部署脚本
const InfoContract = artifacts.require("InfoContract");
module.exports = function (deployer) {
deployer.deploy(InfoContract);
};- 部署脚本:
truffle deploy,build文件夹生成对应的文件 - 前端调用:独立的
index.html页面(面试有的公司让手写)
前端 js 原生调用合约代码
前端js原生调用合约代码
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>调用钱包</title>
</head>
<body>
使用原生的JavaScript API连接钱包 获取钱包地址和余额
<button id="connect">连接钱包</button>
<p>钱包地址:<span id="address">未连接</span></p>
<p>余额:<span id="balance">-</span> ETH</p>
<script>
document.getElementById('connect').addEventListener('click', async () => {
// 检查是否安装了 MetaMask 插件
if (typeof window.ethereum === 'undefined') {
// if (provider?.isMetaMask) {
// console.log('MetaMask');
// }
console.log('请安装 MetaMask');
return;
}
try {
const accounts = await window.ethereum.request({
// 强行弹出钱包 请求用户授权连接钱包
method: 'eth_requestAccounts',
});
// 很多账户,默认获取第一个账户
const address = accounts[0];
console.log('Connected account:', address);
document.getElementById('address').textContent = address;
const balance = await window.ethereum.request({
// 查看&获取账户余额
method: 'eth_getBalance',
params: [address, 'latest'],
});
// balance 是十六进制字符串,需要先转换为十进制
// 这里的 balance 是字符串,如果使用 parseFloat 返回 0
const balanceInWei = BigInt(balance); // hex → BigInt
const balanceInEth = Number(balanceInWei) / 1e18; // 转浮点显示
document.getElementById('balance').textContent = balanceInEth; // 保留4位小数
} catch (error) {
console.error('连接钱包失败:', error);
}
});
</script>
</body>
</html>使用 ethers.js 调用
ethers.js调用钱包
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>调用钱包 - Ethers.js</title>
</head>
<body>
使用 Ethers.js 连接钱包 获取钱包地址和余额
<button id="connect">连接钱包</button>
<p>钱包地址:<span id="address">未连接</span></p>
<p>余额:<span id="balance">-</span> ETH</p>
<!-- 引入 ethers.js 库 -->
<script src="./ethers.umd.js"></script>
<script>
document.getElementById('connect').addEventListener('click', async () => {
// 检查是否安装了 MetaMask 插件
if (typeof window.ethereum === 'undefined') {
console.log('请安装 MetaMask');
alert('请安装 MetaMask 钱包插件');
return;
}
try {
// 使用 ethers.js v6 创建 BrowserProvider
const provider = new ethers.BrowserProvider(window.ethereum);
// 请求用户授权连接钱包
await provider.send("eth_requestAccounts", []);
// 获取签名者(当前连接的账户)
const signer = await provider.getSigner();
// 获取账户地址
const address = await signer.getAddress();
console.log('Connected account:', address);
document.getElementById('address').textContent = address;
// 获取账户余额
const balance = await provider.getBalance(address);
// 使用 ethers.js v6 的 formatEther 方法转换为以太币单位
const balanceInEth = ethers.formatEther(balance);
document.getElementById('balance').textContent = balanceInEth;
} catch (error) {
console.error('连接钱包失败:', error);
alert('连接钱包失败,请查看控制台');
}
});
</script>
</body>
</html>部署的 json 是什么
前端 = 读取 JSON → 解析 ABI → 使用 Web3/ethers 调用链上的合约
build/contracts/*.json文件,包含一个合约所有需要的元信息- 名字:合约构建产物(
contract artifact),简称Artifact文件。 - 包含了
ABI,但不是纯ABI文件,是合约完整构建信息 truffle deploy生成的json是合约Artifact(构建产物),包含ABI,但远不止ABI。Artifact(构建产物)是软件编译后生成的最终可用文件/成果物。
- 名字:合约构建产物(
| 文件/概念 | 作用 | 是否包含 ABI |
|---|---|---|
build/contracts/*.json | Truffle合约构建产物(全面信息) | ✔包含 |
ABI (.abi) | 只用于合约交互的接口描述 | ✔只有 ABI |
bytecode | EVM执行字节码 | ❌不包含 ABI |
那我是不是可以理解,前端调用.json文件就是调用整个合约呢?
- 前端调用
.json≠ 调用整个合约 - 更准确是:前端读取
.json中的ABI+ 合约地址,然后通过区块链节点调用链上的合约。 .json本身不是合约,它只是描述合约长什么样的说明书。- 你可以把
.json Artifact想象成:
| 比喻 | 用途 |
|---|---|
合约 .sol = 电器 | 实际功能(部署后才能用) |
ABI + 地址 = 遥控器 | 按钮对应函数,帮你远程调用 |
.json = 包含说明书+遥控器配置的使用手册 | 前端读取后才能正确控制电器 |
调用余额精度异常问题
- 前端调用钱包余额,出现了多种情况,原生的有三种:
第一种:parseFloat
- 问题:由于
balance是字符串类型,所以导致最终解析为0
js
const balanceInEth = parseFloat(balance) / 1e18;第二种:parseInt
- 解决第一种:字符串解析异常问题
- 问题:存在溢出与精度丢失问题,超过
2^53–1会精度丢失
js
const balanceInWei = parseInt(balance, 16); // 从十六进制转换为十进制
const balanceInEth = balanceInWei / 1e18; // 转换为以太币单位,18个0第三种:BigInt+Number
- 解决:精度丢失问题
js
const balanceInWei = BigInt(balance); // hex → BigInt
const balanceInEth = Number(balanceInWei) / 1e18; // 转浮点显示| 阶段 | 第一种 parseInt | 第二种 BigInt |
|---|---|---|
读取 0x52654f43a73934068 | 直接变 Number → 可能溢出 | 转为 BigInt → 无损储存 |
计算 balanceInWei | 可能已变成错误数 | 仍然保持超高精度 |
最后转 Number | 无意义(已错) | 此时才缩小值 → 减少溢出概率 |
parseInt 是先溢出再计算,BigInt 是先保证精度再缩小范围。
部署项目不属于打包项目
- 打包前端 = 把代码做成可运行的软件安装包
- 部署合约 = 打包 + 安装 + 上线给所有人使用
| 概念 | 前端打包(build) | 区块链部署(deploy) |
|---|---|---|
| 打包后是否自动发布? | ❌ build只是生成文件,不会上线 | ✔ 部署 = 编译+上线一步到位 |
| 是否可修改? | ✔ 可覆盖重新发布 | ❌ 上链后不可随便替换(除非升级合约) |
- 打包是“做出东西”
- 部署是“做出来 + 放到环境里跑”
项目架构说明
结论先说:一个项目 ≠ 一个合约
- 项目(
Project):包含很多内容:多个合约、脚本、测试、迁移、配置等 contracts文件夹里每个.sol文件,通常就是一个合约(也可能一个文件里写多个合约)- 一个系统(
DApp)通常由多个合约组成,而不是一个合约撑到底。
truffle 命令
常用命令
truffle init:初始化项目truffle create:创建合约、迁移脚本或测试文件的模板truffle build:打包项目,不会编译合约,编译合约是truffle compiletruffle deploy:部署项目
Truffle 开发全流程图(从 init → compile → migrate → test → debug → console)
全部命令
truffle build:执行构建流程(打包项目)truffle call:调用只读合约函数(不改链上状态)truffle compile:编译合约源码truffle config:设置Truffle的配置项truffle console:打开带合约对象的交互控制台truffle create:创建合约 / 迁移 / 测试文件模板truffle dashboard:用浏览器钱包(如MetaMask)签名开发交易truffle db:使用Truffle的区块链数据库功能truffle debug:调试链上的某笔交易truffle deploy:部署合约(等同于migrate)truffle develop:启动本地开发链并进入控制台truffle exec:在Truffle环境中执行JS脚本truffle help:查看所有命令或某个命令的帮助truffle init:初始化一个Truffle项目truffle migrate:执行迁移脚本,部署合约truffle networks:查看各网络上部署的合约地址信息truffle obtain:下载并缓存指定版本的编译器truffle opcode:打印合约编译后的EVM操作码truffle preserve:把数据保存到IPFS/Filecoin等去中心化存储truffle run:运行第三方扩展命令truffle test:运行测试(JS或Solidity)truffle unbox:下载并解压Truffle模板项目(Box)truffle version:显示Truffle版本信息truffle watch:监听文件变化并自动重新构建项目
Gas 和本金
一句话总结
Gas= 手续费(给矿工 / 验证者的费用)- 本金 = 你在合约里实际转出去的钱(业务逻辑的金额)
👉 两者都用 ETH 支付,但是它们不是同一种钱,也不是同一个用途。
Case 1:普通转账 1 ETH
- 你支付的有:
| 类型 | 数值 | 去向 |
|---|---|---|
| 本金 | 1 ETH | 对方账户 |
Gas | 0.0005 ETH(例子) | 矿工 |
你总共扣了 1.0005 ETH,但 1 ETH 是业务款,gas 是手续费。
Case 2:执行 ENS 设置头像(没有转钱)
- 你支付的有:
| 类型 | 数值 | 去向 |
|---|---|---|
| 本金 | 0 ETH | 无 |
Gas | 0.003 ETH(假设) | 矿工 |
- 你只花
gas,没有本金。
Case 3:Mint NFT,价格 0.08 ETH
- 你支付的有:
| 类型 | 数值 | 去向 |
|---|---|---|
| 本金 | 0.08 ETH | NFT 项目方 |
Gas | 0.002 ETH | 矿工 |
- 总扣除
0.082 ETH,但性质完全不同。
对比一下
- 都是
ETH - 但它们是“两个口袋”的钱,平台处理方式完全不同:
- 这里的两个口袋指的是进入的两个地方
- 一个是接收方,一个是手续费旷工
| 项目 | 本金 | Gas |
|---|---|---|
| 必须转出? | 不一定(看业务逻辑) | 必须(所有交易) |
| 谁收到? | 合约或对方用户 | 矿工/验证者 |
| 由什么决定? | 你调用的合约逻辑 | 网络繁忙程度、你设置的 gas price |
| 消耗方式 | 作为支付内容 | 只作为手续费销毁或奖励 |
佳哥笔记
Details
js
1.web2
架构组 性能优化 基建(monorepo yd-hooks yd-utils yd-libs wasm) AI工程组
cli -> 创建项13目
AI -> 前端代码 合适的文件夹里
基建 -> 引用
性能 -> sdk
更熟的人
设计稿 ??? 牛马 业务
2.web3
钱包封装、合约SDK
同样的链不同的钱包
不同链的不同的钱包
编译
上线版本
1.部署需要花钱 gas
2.执行构造函数需要还钱 gas
3.写变量需要花钱 gas
4.某个函数 执行时间+写变量 花很多钱 gas