系列导航
| 顺序 | 文章 | 定位 |
|---|---|---|
| 1 | 📍当前:Web3 链上转账完整流程(小白版) | 零代码:建立整体认识 + 智能合约在项目里的意义 |
| 2 | MyToken 代币合约 + MiniSwap 兑换合约(AMM / 恒定乘积 / DeFi),以及智能合约实际作用总结 | |
| 3 | 网页如何连接 MetaMask / TronLink,发起转账和兑换 | |
| 4 | 后端如何监听链上事件、可靠地抄进数据库,并实时推送给页面 |
本系列围绕一个学习项目(Mini DApp)展开。它做了三件事:自己发行一种叫 MTK 的代币;让用户之间转账(ETH / TRX / USDT / MTK);让用户在链上把一种资产兑换成另一种(MiniSwap 兑换合约)。项目同时跑在三条链上:本地链 anvil、以太坊测试网 Sepolia、波场测试网 Nile。
本篇不含需要读懂的代码,只帮你搭好“整体地图”,并回答一个核心问题:智能合约在这个项目里到底有什么用? 后面三篇再分别钻进合约、前端、后端。
读完本篇你将学会
- 看懂 Web3 里最常见的几个词:钱包、地址、区块链、智能合约、燃料费
- 分清“原生币”“稳定币”“代币”,知道 ETH / TRX / USDT / MTK 各属于哪一类
- 知道一个 Web3 App 由哪三部分组成、各自负责什么
- 说清一笔链上转账从准备账户到出现在列表里的完整 4 步
- ★ 讲清智能合约在本项目中的三个作用:发行代币、链上转账、链上兑换
前置知识:无需任何区块链基础。懂一点前端或后端开发会更好理解,但不是必须。
一、先认识 5 个词
这一章解决的问题:看 Web3 文章时满屏陌生词,先把最核心的 5 个弄明白。
| 词 | 一句话解释 | 生活类比 |
|---|---|---|
| 钱包(MetaMask / TronLink) | 浏览器插件,保管你的私钥,替你“签字”。以太坊系用 MetaMask,波场用 TronLink | 你的印章 |
| 地址(Address) | 你的账号。以太坊地址是 0x 开头的一串字符;波场地址是 T 开头的一串字符 | 银行卡号 |
| 区块链(Blockchain) | 一本所有人都能查、没人能偷改的公共账本 | 公开的总账本 |
| 智能合约(Smart Contract) | 部署在链上的一段程序,规定“谁有多少币、怎样才能转、怎样兑换”,本项目用 Solidity 语言编写 | 银行的规章制度 + 金库 |
| 燃料费(Gas) | 在链上做任何“写操作”都要付的手续费 | 办业务的手续费 |
再补 4 个小词:**私钥(Private Key)**是只有你知道的一串密码,谁拿到它谁就能动你的资产;**签名(Signature)**是用私钥给操作“盖章”,证明是本人发起;**交易(Transaction)**是向区块链提交的一次写操作,比如转账、兑换、部署合约;**区块(Block)**是链把一段时间内的交易打包成的“一页账”,一页页串起来就是“链”。
记住一句话:钱在链上,数据库里只是“抄本”。
二、原生币 vs 代币:ETH、TRX、USDT、MTK 各是哪一种?
这一章解决的问题:BTC、ETH、USDT、MTK 都叫“币”,它们到底有什么区别?
Web3 代币泛指在区块链上发行的数字资产。它不只能当“钱”用,还可以代表投票治理权、某项服务的使用权,或某件独一无二的资产(如 NFT,非同质化代币,每一个都独一无二)。日常交流中,大家把它们统称为“代币”或“加密货币”;但从技术上看,可以细分为三类:
| 类别 | 是什么 | 余额记在哪里 | 例子 |
|---|---|---|---|
| 原生币(Coin) | 区块链“自带”的币,每条链只有一个,用来支付手续费、维护网络安全 | 直接记在账户上 | BTC、ETH、BNB、SOL、TRX、XRP、ZEC、DOGE、HYPE |
| 代币(Token) | 通过智能合约在别人的链上发行,自己没有独立的链,“寄生”在已有的链上 | 记在发行它的合约里 | USDT、USDC、LINK、OKB、本项目的 MTK |
| 测试币 | 测试网的原生币,没有任何真实价值,供开发者免费领取、测试用 | 直接记在账户上 | Sepolia ETH、Nile TRX |
测试网(Testnet):和真实网络(主网,Mainnet)规则一样、但币不值钱的“练习场”,专门给开发者试错用。
主流币种对比:
| 名称 | 类别 | 所在的链 | 主要用途 | 真实价值 |
|---|---|---|---|---|
| BTC(比特币) | 原生币 | Bitcoin | 区块链的鼻祖,价值存储(“数字黄金”)、点对点支付 | 有 |
| ETH(以太币) | 原生币 | Ethereum | 支付 Gas、运行智能合约,以太坊生态的“硬通货” | 有 |
| BNB(币安币) | 原生币 | BNB Chain | 支付 Gas;同时是币安交易所的平台币(可抵扣交易手续费) | 有 |
| SOL(索拉纳币) | 原生币 | Solana | 支付手续费、运行智能合约,以速度快、费用低著称 | 有 |
| TRX(波场币) | 原生币 | TRON | 支付手续费;质押 TRX 可获得“带宽 / 能量”来抵扣手续费 | 有 |
| XRP(瑞波币) | 原生币 | XRP Ledger | 跨境结算与支付 | 有 |
| ZEC(大零币) | 原生币(隐私币) | Zcash | 支持“隐私交易”:用零知识证明隐藏转账双方和金额 | 有 |
| DOGE(狗狗币) | 原生币(Meme 币) | Dogecoin | 源于网络玩笑的币,主要用于小额支付、打赏 | 有 |
| HYPE(Hyperliquid 币) | 原生币 | Hyperliquid | Hyperliquid 链的原生币:支付 Gas、质押,与链上合约交易所生态绑定 | 有 |
| USDT(泰达币) | 代币(稳定币) | 同时发行在 Ethereum(ERC-20)、TRON(TRC-20)等多条链上 | 由 Tether 公司发行,锚定 1 美元,常用于避险和交易计价 | 有(约 1 美元) |
| USDC(美元币) | 代币(稳定币) | 同时发行在 Ethereum、Solana、Base 等多条链上 | 由 Circle 公司发行,锚定 1 美元,定期公布储备审计 | 有(约 1 美元) |
| LINK(Chainlink 币) | 代币 | 以 ERC-20 形式发行在 Ethereum 上,也可跨链到其他链 | Chainlink 预言机网络的代币:支付节点把链外数据(如价格)送上链的费用、质押 | 有 |
| OKB(欧易平台币) | 代币(平台币) | 以 ERC-20 形式发行在 Ethereum 上;同时是 OKX 旗下 X Layer 链的 Gas 币 | OKX 交易所的平台币:抵扣手续费、参与平台活动 | 有 |
| Sepolia ETH(Sepolia 测试币) | 测试币 | Ethereum Sepolia 测试网 | 开发者测试;本项目在 Sepolia 上部署合约、转账、兑换都用它付手续费 | 无(0 元) |
| Nile TRX(Nile 测试币) | 测试币 | TRON Nile 测试网 | 本项目在 Nile 上付手续费、参与兑换 | 无(0 元) |
| 测试 USDT(本项目用到的) | 代币(测试稳定币) | Sepolia(Aave 测试 USDT)、Nile(TRON 官方测试 USDT) | 模拟稳定币;不是我们部署的,是第三方合约 | 无 |
| MTK(MyToken,本项目) | 代币 | anvil / Sepolia / Nile | 学习演示:本项目用智能合约自己发行的代币 | 无 |
表中几个词:**质押(Staking)**指把币锁定一段时间来换取收益或权益;零知识证明是一种“不泄露内容也能证明它成立”的密码学技术;**预言机(Oracle)**负责把链外数据送上链。看不懂也不影响读本系列。
看懂这张表,就能理解本项目里的几个现象:
- MTK 和 USDT 是同一类东西:都是用智能合约发行的代币。以太坊上的 USDT 和本项目的 MyToken 一样遵循 ERC-20 标准(以太坊上代币的“统一接口规范”,规定了查余额、转账等功能该叫什么名字、怎么用,钱包因此能认出所有 ERC-20 代币);波场上对应的标准叫 TRC-20,接口和 ERC-20 基本一致。
- 手续费只能用原生币付:所以在 Sepolia 上转 MTK / USDT 要付 SepoliaETH,在 Nile 上转 MTK / USDT 要付 TRX。
- 按用途还能再细分:稳定币(USDT、USDC)、隐私币(ZEC)、Meme 币(DOGE)、预言机代币(LINK)、平台币(BNB、OKB)。“原生币 / 代币”说的是技术上怎么发行,“稳定币 / 平台币”说的是拿来做什么,两种分法可以同时成立,比如 USDC 既是代币,也是稳定币。本项目页面上就按“原生币 → 稳定币 → 代币”给资产排序。
- 同一个币可能有多重身份:BNB 既是 BNB Chain 的原生币,又是币安的平台币;OKB 在以太坊上是 ERC-20 代币,在 X Layer 上又被用作 Gas。判断“原生币还是代币”,要看在哪条链上。
- 代币要“导入”才看得到:原生币余额在账户上,钱包直接就能显示;代币余额在合约里,钱包得知道合约地址才能去查。
- 测试币虽然叫 ETH / TRX,但本身没有价值:它在测试网上的作用和主网上完全一样(付 Gas),但不能兑换成真钱。平时优先去水龙头免费领取;水龙头领不到时(不少水龙头要求账户在主网上有余额,或限制每天的领取量),也可以在闲鱼等平台花少量钱买一点应急。只按“够用”的量买,千万不要当成投资,也不要相信任何“测试币会升值”的说法。
小结:原生币是链自带的、记在账户上;代币是合约发行的、记在合约里。USDT 和 MTK 都是代币,转它们要用原生币付手续费。
三、这个 App 由三部分组成
这一章解决的问题:一个能转账、能兑换的 Web3 App,背后到底有哪些“零件”?
| 部分 | 在哪里运行 | 负责什么 | 详细文档 |
|---|---|---|---|
| 智能合约 | 区块链上 | MyToken:记账(谁有多少 MTK)、执行转账;MiniSwap:保管资金池、按公式执行兑换 | |
| 前端页面 | 浏览器 | 让用户查余额、转账、兑换,并交给钱包(MetaMask / TronLink)签名 | |
| 后端 + 数据库 | 服务器 | 把链上的转账、兑换记录“抄”到数据库,配上昵称给页面展示,并实时推送(SSE)给页面 |
它们之间的关系可以画成下面这张图:
flowchart LR
U[用户] --> FE[前端页面<br/>Vue + ethers / tronWeb]
FE -- 请求签名 --> W[钱包<br/>MetaMask / TronLink]
W -- 发送交易:转账 / 兑换 --> C[链上智能合约<br/>MyToken / MiniSwap / USDT]
FE -- 查余额、查报价(免费) --> C
C -- Transfer / Swap 事件 --> BE[后端 listener]
BE --> DB[(MySQL 数据库)]
BE -- SSE 实时推送 --> FE
FE -- 查记录 /api --> BE
- 转账 / 兑换(写):页面 → 钱包签名 → 区块链上的合约改账本
- 看记录(读):区块链 → 后端监听并抄写 → 数据库 → 通过 SSE 推送提醒页面刷新列表
- 查余额、查报价(读):页面直接问区块链(免费,不用签名)
SSE(Server-Sent Events,服务器推送事件):浏览器和服务器保持一条长连接,服务器有新数据就主动“推”给页面。本项目后端入库后通过
/api/events推送tx(新转账)、swap(新兑换)等事件;推送断了,页面会自动退回每 15 秒轮询一次。
和传统 Web 开发最大的不同:账本不在自己的数据库里,而在链上的合约里。后端停了照样能转账、兑换,数据库只是方便查询的副本。
四、三条链:先在本地练,再上两个测试网
这一章解决的问题:开发时在哪条链上跑?为什么要三条?
- anvil 是在自己电脑上运行的“迷你区块链”,用来开发调试;
- Sepolia 是以太坊的公共测试网,全世界共享,用来做以太坊生态的最终验证;
- Nile 是波场(TRON)的公共测试网。波场是 USDT 转账量最大的链之一,它的地址格式、钱包、交易结构都和以太坊不同,接入它能让你看到“同一套业务在两种生态上”的差异。
| 对比 | 本地链 anvil | 测试链 Sepolia | 测试链 Nile(TRON) |
|---|---|---|---|
| chainId | 31337 | 11155111 | 3448148188 |
| 在哪里 | 自己的电脑 | 全世界共享的公共测试网 | 全世界共享的公共测试网 |
| 用什么钱包 | MetaMask | MetaMask | TronLink |
| 地址长什么样 | 0x 开头 | 0x 开头 | T 开头(Base58 格式) |
| 原生币 / 测试币怎么来 | 启动就送,每个账户 10000 ETH | 去“水龙头”网站免费领或闲鱼购买 | 去 Nile 水龙头领取测试 TRX |
| 谁能看到 | 只有自己 | 任何人,可在 sepolia.etherscan.io 查询 | 任何人,可在 nile.tronscan.org 查询 |
| 转账后多久出现在列表 | 几秒 | 约半分钟(要等 2 个区块确认) | 约 1 分钟(要等 19 个区块确认,3 秒一个块) |
| 用途 | 开发调试,随时可以重来 | 模拟真实以太坊网络,做最终验证 | 模拟真实波场网络,体验 TRC-20 与 TronLink |
**水龙头(Faucet)**是免费发放测试币的网站,像“免费试用装”;区块浏览器(如 sepolia.etherscan.io、nile.tronscan.org)是查链上数据的网站,输入地址或交易就能看到详情。
三条链的账本完全独立:同一个地址在不同链上的余额互不相干。
每条链上有哪些资产、能做什么(本项目当前真实状态):
| 链 | 原生币 | 稳定币(第三方合约) | 自发代币 | 页面上能转账的资产 | 兑换合约 MiniSwap |
|---|---|---|---|---|---|
| 本地链 anvil | ETH | 无 | MTK 0xdc64…f6c9 | MTK(ETH 只用来付 Gas) | 未部署 |
| Sepolia 测试网 | ETH | USDT(Aave 测试 USDT,6 位小数) | MTK 0xcbee…2447 | ETH / USDT / MTK | 已部署 0x6569…eafb |
| Nile 测试网 | TRX | USDT(TRON 官方测试 USDT,TRC-20,6 位小数) | MTK 0xd666…11b1 | TRX / USDT / MTK | 已部署 0xbbfa…16d3 |
注:TRON 地址在链上其实就是“0x41 前缀 + 20 字节”,再做 Base58 编码就成了 T 开头的样子。本项目的后端和数据库统一存 0x 形式,前端只在调用 TronLink 和展示时转成 T 开头,所以表里 Nile 的合约地址也写成 0x 形式。
五、完整流程:4 步走
这一章解决的问题:一笔 MTK 转账从零开始,到底要经历哪几步?(兑换的流程在第六章讲,它的骨架和转账一模一样。)
第 1 步:准备账户
- 在 MetaMask(或 TronLink)里创建账户。创建账户不上链、不花钱,只是用助记词(Mnemonic,一组 12 个左右的英文单词,是所有账户私钥的“总钥匙”)算出一把新钥匙。
- 本项目的账户分工:
| 用途 | 本地链 anvil | 测试链 Sepolia | 测试链 Nile |
|---|---|---|---|
| 部署者(拿到全部 MTK) | Anvil-A | 部署者和持币人 | 部署者(TronLink 账户) |
| 收款 / 转账 | Anvil-B、C、D | 收款人1、收款人2 | 其他 TronLink 账户 |
- 测试网上还要准备一点原生币当手续费:Sepolia 准备 SepoliaETH,Nile 准备测试 TRX。
- 页面上每条链独立连接账户:在“本地链 / Sepolia”页签下连接 MetaMask,在“Nile”页签下连接 TronLink,互不影响。
第 2 步:发行 MTK(部署合约)
- 合约代码里写好了:名字 MyToken、符号 MTK、18 位小数、总量 100 万、部署时全部给部署者。
- 部署(Deploy)就是发一笔特殊交易,把合约代码放到链上。部署完会得到一个合约地址:
- 本地链:
0xdc64…f6c9 - Sepolia:
0xcbee…2447 - Nile:
0xd666…11b1(0x 形式)
- 本地链:
- 合约一旦部署就永远不能修改。想改名字只能重新部署一个新合约。
- 本项目实测,部署在 Sepolia 上大约花 0.001 SepoliaETH。
- Nile 的部署方式不同:同一份合约用兼容 TRON 的编译配置(
FOUNDRY_PROFILE=tron)编译,再通过 TRON 的 HTTP API 部署(make deploy-nile),因为以太坊的部署工具 forge script 发不了 TRON 格式的交易。
第 3 步:转账
以 Sepolia 上“收款人2 转 500 MTK 给收款人1”为例:
- 打开页面,选择 Sepolia,连接 MetaMask,选中收款人2。
- 选择资产 MTK,填入收款人1 的地址,点“查询余额”。查询是免费的,不弹钱包。
- 填金额 500,点“转账”。
- MetaMask 弹窗,显示手续费约 0.0006 SepoliaETH,点确认。
- 交易被打包进区块(网络把交易正式写进账本的一页),合约把收款人2 的余额减 500、收款人1 的余额加 500,并发出一条 Transfer 事件(Event,合约在链上留下的一条“公告”:谁转给谁、多少)。到这里转账已经完成。
- 页面上的进度条依次亮起:签名并广播 → 打包上链 → 后端已入库。
转 ETH / TRX(原生币)时,第 5 步不经过任何合约,也没有 Transfer 事件;转 USDT 时调用的是别人部署的 USDT 合约。这三种转账的区别,第六章会专门讲。
第 4 步:在列表里看到记录
- 后端一直在“监听”:代币合约的 Transfer 事件、MiniSwap 的 Swap 事件,以及逐块扫描原生币转账。一有新记录,就把“谁转给谁、多少钱”写进数据库。
- 写入后,后端通过 SSE 把新记录推送给页面,页面立即刷新,显示成“迷糊的水獭#7322 → 勇敢的鹦鹉#3717,500 MTK”。
- 昵称是后端随机起的,链上只有地址,没有昵称。
小结:准备账户 → 部署合约 → 签名转账(合约改账本)→ 后端抄进数据库并推送给页面。第 3 步完成时钱就已经到账,第 4 步只是为了方便展示。
六、★ 智能合约在这个项目里的意义
这一章解决的问题:前面一直在说“合约”,它在本项目里到底解决了什么问题?换成一个普通后端 + 数据库不行吗?
先给结论:智能合约让“规则”本身跑在链上——余额怎么记、钱怎么转、价格怎么算,都由一段谁也改不了的公开代码决定,任何人(包括项目方自己)都没法绕过它。在本项目里,这体现为三件事:
┌─────────────────────┐ ┌──────────────────────────┐ ┌──────────────────────────┐
│ ① 自己发行代币 │ │ ② 用户之间链上转账 │ │ ③ 资产之间链上兑换 │
│ MyToken 合约 → MTK │ │ ETH/TRX:链自带,不用合约 │ │ MiniSwap 合约:资金池 │
│ 名称/符号/精度/总量 │ │ USDT:调第三方合约 │ │ 按公式自动报价、自动成交 │
│ 都由代码定死 │ │ MTK:调我们自己的合约 │ │ Sepolia、Nile 各一个 │
└─────────────────────┘ └──────────────────────────┘ └──────────────────────────┘
6.1 意义一:可以自己发行代币(MTK)
在传统互联网里,“发行积分”要自己建表、自己记账,用户只能相信你不会乱改。在链上,发行一种代币只需要写一个遵循 ERC-20 标准的合约并部署上去——本项目的 contracts/src/MyToken.sol 就是这样一个合约,它继承了 OpenZeppelin(经过审计的开源合约库)的 ERC20 实现,自己只写了十几行。
发行时能自定义的东西:
| 可以自定义的 | 本项目的取值 | 说明 |
|---|---|---|
| 合约名 | MyToken | Solidity 里 contract MyToken 的名字,只是代码层面的名字,用户一般看不到 |
代币名称 name | "MyToken" | 钱包、区块浏览器里显示的全称 |
代币符号 symbol | "MTK" | 钱包里显示的简称,和 ETH、USDT 并列 |
精度 decimals | 18 | 链上只存整数,“1 MTK”在链上存的是 1 × 10¹⁸。USDT 是 6 位、TRX 也是 6 位,ETH 是 18 位 |
| 总量 | 1,000,000 MTK | 部署时一次性全部铸造(mint)给部署者,之后没有增发函数,总量永远不变 |
小细节:MyToken 的
name/symbol写成了直接返回常量的pure函数,而不是存进合约存储,这样部署时能省下约 4 万 Gas。具体原因见第 2 篇。
那代币图标呢?——链上没有图标。 这是新手最常见的误解。ERC-20 标准里只有 name、symbol、decimals 这类字段,没有“图标”字段,合约里也存不了图片。你在钱包、交易所里看到的 USDT 绿色图标,都是在链下配置的:
| 图标来源 | 怎么做 | 谁能看到 |
|---|---|---|
钱包 wallet_watchAsset(EIP-747) | DApp 调用钱包的这个方法,请求“添加代币”,参数里带上图标的 URL | 该用户自己的钱包 |
| 区块浏览器 | 项目方在 Etherscan / TronScan 上提交代币信息(Logo、官网等),审核通过后显示 | 所有访问浏览器的人 |
| Token List | 把代币信息(地址、符号、精度、logoURI)登记到公开的代币列表(如 Uniswap Token List) | 使用该列表的钱包 / DApp |
本项目目前没有实现 wallet_watchAsset,所以 MetaMask 里要手动“导入代币”(填合约地址)才能看到 MTK,而且没有图标。如果想加,大致是下面这样(示意代码,项目中并不存在):
// 示意:请求 MetaMask 把 MTK 加入资产列表(EIP-747),image 是链下托管的图标地址
await window.ethereum.request({
method: "wallet_watchAsset",
params: {
type: "ERC20",
options: {
address: "0xcbee6a188946136b460c833b238eb5665e1f2447", // Sepolia 上的 MTK
symbol: "MTK",
decimals: 18,
image: "https://example.com/mtk.png", // 示意地址,需要自己托管图标
},
},
});
6.2 意义二:可以实现链上用户之间的转账
页面上的“转账”按钮看起来都一样,但 ETH / TRX、USDT、MTK 三种资产在链上的实现方式完全不同:
| 资产 | 交易发给谁(to) | 交易里带的是什么 | 经过合约吗 | 余额记在哪 | 有 Transfer 事件吗 |
|---|---|---|---|---|---|
| ETH / TRX(原生币) | 直接发给收款人地址 | value(附带的原生币金额),没有调用数据 | 不经过,链本身完成转账 | 账户上 | 没有 |
| USDT(稳定币) | 第三方的 USDT 合约地址 | 调用数据:transfer(收款人, 金额) | 经过别人部署的合约 | USDT 合约的账本里 | 有 |
| MTK(自发代币) | 我们自己的 MyToken 合约地址 | 调用数据:transfer(收款人, 金额) | 经过我们自己的合约 | MyToken 合约的账本里 | 有 |
几点值得细品:
- 原生币转账不需要合约:它是链的“出厂功能”。代价是链上不会留下事件,所以后端只能逐个区块扫描交易来发现 ETH / TRX 转账(而且只记录在页面上登记过的地址,否则整条链的转账都会被抄进来)。
- USDT 和 MTK 的调用方式一模一样:因为它们都遵循 ERC-20 / TRC-20 标准,前端用同一套代码就能转,只是合约地址不同。区别在于“规则归谁管”:USDT 的合约是 Tether 或测试网方部署的,我们无法控制;MTK 的合约是我们写的,规则我们在部署前说了算(部署后连我们也改不了)。
- 转代币时,交易的
to是合约地址,不是收款人。“转账给 B”的实际含义是:请合约把账本上 A 名下的一部分记到 B 名下。B 全程不需要签名,也不需要在线。 - 第三方合约可能“不守规矩”:实测 Nile 上的测试 USDT 调用
transfer成功时却返回 false。这就是为什么 MiniSwap 不看返回值,而是看“余额差值”来判断钱有没有到账(第 2 篇细讲)。
6.3 意义三:可以实现链上资产之间的兑换
有了多种资产,下一个自然的需求是“拿 ETH 换点 USDT”。在中心化交易所,这要靠交易所的撮合引擎和数据库;在链上,本项目用一个自己写的兑换合约 MiniSwap(contracts/src/MiniSwap.sol,迷你版 Uniswap V2)来完成。
用最简单的比喻理解:MiniSwap 是一台“自动售货机”。
- 机器里预先放好了几对“货”:比如一格装着 0.5 ETH 和 5000 MTK,这一格叫一个资金池(Pool),放货的过程叫提供流动性(本项目只有部署者 owner 能放货)。
- 你投进去一种资产,机器按一个固定公式算出该吐给你多少另一种资产,并当场吐出来。没有人工、没有撮合、没有对手方——你的交易对手就是这个池子。
- 你投进去的越多,池子里你要的那种货就越少,机器就会越“惜售”,给出的单价越差。这个机制叫 AMM(Automated Market Maker,自动做市商),它的公式是“两种资产数量的乘积保持不变(x · y = k)”,每笔收 0.3% 手续费,手续费留在池子里。公式细节、滑点保护、防重入等放在
第 2 篇展开,这里只看效果。
每条链可兑换的组合:
| 链 | 可兑换的资产 | 池子(初始放入的货) | 初始价格 |
|---|---|---|---|
| 本地链 anvil | —(未部署兑换合约,本地只演示转账) | — | — |
| Sepolia 测试网 | ETH / USDT / MTK 两两互换(3 个池子) | ETH-USDT:0.5 ETH / 500 USDT;ETH-MTK:0.5 ETH / 5000 MTK;USDT-MTK:500 USDT / 5000 MTK | 1 ETH = 1000 USDT = 10000 MTK |
| Nile 测试网 | TRX / USDT / MTK 两两互换(3 个池子) | TRX-USDT:100 TRX / 30 USDT;TRX-MTK:100 TRX / 300 MTK;USDT-MTK:30 USDT / 300 MTK | 1 TRX = 0.3 USDT = 3 MTK |
真实兑换记录(来自页面“我的兑换记录”和后端数据库):
Sepolia:
| 时间 | 卖出 | 买入 |
|---|---|---|
| 11:17:24 | 6 USDT(部署账户) | 0.005911 ETH |
| 11:27:48 | 500 MTK | 0.04533 ETH |
| 11:29:12 | 1000 MTK | 83.124895 USDT |
| 11:37:36 | 0.01 ETH | 118.016236 MTK |
| 11:39:00 | 0.2 ETH | 145.491046 USDT |
| 11:44:00 | 100 USDT | 0.150367 ETH |
| 11:45:48 | 10 USDT | 140.144514 MTK |
Nile:
| 时间 | 卖出 | 买入 |
|---|---|---|
| 12:10:09 | 10 TRX | 27.198326 MTK |
| 12:11:45 | 10 TRX | 2.719832 USDT |
| 12:15:12 | 1 USDT | 3.878394 TRX |
| 12:16:36 | 1 USDT | 9.64932 MTK |
| 12:19:27 | 1000 MTK | 86.367818 TRX |
| 12:20:57 | 1000 MTK | 24.008221 USDT |
从这些记录里能直接读出 AMM 的几个特点:
- 小单接近标价,大单明显吃亏。Sepolia 上用 500 MTK 换 ETH:按初始价 1 MTK = 0.0001 ETH,理想应得 0.05 ETH,实际得到 0.04533 ETH。差出来的部分一小半是 0.3% 手续费,大半是因为 500 MTK 相对池子(5000 MTK)不算小,把价格“推”动了——页面上显示的价格影响约 9.06%。
- 价格随交易实时变化。这笔之后池子变成约 0.4547 ETH / 5500 MTK,MTK 变“多”了、变便宜了;紧接着有人用 0.01 ETH 买 MTK,得到 118.016 MTK,比初始价(0.01 ETH = 100 MTK)更划算。
- 浅池 + 大单 = 巨大价格影响。Nile 的 TRX-MTK 池只有 100 TRX / 300 MTK,先有人用 10 TRX 买走约 27.2 MTK,池子变为 110 TRX / 272.8 MTK;之后有人一次卖出 1000 MTK(是池中 MTK 的 3.7 倍),只换到 86.37 TRX,而按初始价应值约 333 TRX,MTK 的价格随之暴跌约 94%。但池子永远不会被买空——公式决定了剩下的 TRX 只会越来越少、却到不了 0。
- 手续费留在池子里。Sepolia 的 ETH-MTK 池经过上面两笔交易后,x · y 从 2500 变成约 2500.8,多出来的就是手续费。
真实的 DeFi(去中心化金融)里,多个池子之间价格失衡时,会有“套利者”低买高卖,把价格重新拉回一致。本项目是学习用的测试网,没人来搬砖,所以你能在记录里看到价格偏离得很厉害。
兑换的流程骨架和转账完全一样:
选择“卖出 / 买入”资产和数量 → 页面调用合约 getAmountOut 免费报价(附价格影响、最少收到)
→ (卖代币时)先 approve 授权合约动用你的代币 → 钱包签名 swap 交易
→ 合约收钱、按公式算、转给你、emit Swap 事件 → 后端监听 Swap 事件入库(swaps 表)
→ SSE 推送 → “我的兑换记录”出现这一条
- 卖代币(USDT / MTK)要先授权(approve):合约不能主动从你账户里拿代币,你得先告诉代币合约“允许 MiniSwap 动用我的 N 个币”。页面把它做成了“授权并兑换”一个按钮,钱包会弹两次确认。
- 卖原生币(ETH / TRX)不用授权:直接把原生币随交易一起付给合约即可。
- 滑点保护:交易从签名到上链有时间差,期间别人的交易可能改变价格。页面让你选 0.5% / 1% / 3% 的滑点容忍,合约收到的“最少收到”低于这个值就整笔回滚,防止被坑。
七、数据库里有什么
这一章解决的问题:既然钱在链上,后端数据库到底存了什么?
| 表 | 存什么 | 例子 |
|---|---|---|
| users | 地址和昵称的对应关系 | 0x455b… = 迷糊的水獭#7322 |
| user_networks | 用户在哪条链上登记过(连接过钱包) | 勇敢的鹦鹉#3717 在 Sepolia 登记 |
| transactions | 每一笔转账(从链上抄来的,带 network 和合约地址) | 收款人2 → 收款人1,500 MTK |
| swaps | 每一笔兑换(从 Swap 事件抄来的) | 卖出 500 MTK,买入 0.04533 ETH |
| sync_states | 后端在每条链、每个合约上抄到了第几个区块 | Sepolia 已抄到第 11786xxx 块 |
删光数据库也不会丢钱:余额在链上。重启后端,它会从链上把记录重新抄回来(原生币转账例外:原生币扫描首次开启时从当前区块开始,不补历史)。
八、常见疑问
转 MTK 为什么要付 SepoliaETH(或 TRX)? 手续费只能用这条链的原生币付,而且由发起交易的人付。收款的人不用付,也不用提前做任何准备。
“导入代币”是干什么的?为什么 MTK 没有图标?
导入是告诉钱包“去这个合约查一下我有多少 MTK”,它只影响钱包里能不能看到余额,不影响收款。图标不在链上,需要通过 wallet_watchAsset、区块浏览器或 Token List 在链下配置,本项目暂未实现(见 6.1)。
为什么测试网上的转账要等一会儿才出现在列表? 后端要等这笔交易之后再出若干个区块才入库,这叫等待“确认数”:Sepolia 等 2 个块(约 24 秒),Nile 等 19 个块(约 1 分钟)。原因是公共链上偶尔会发生区块重组(Reorg):刚出的几个区块可能被撤销、换成另一个版本,多等几块能避免记录作废。本地链 anvil 不会重组,所以几秒就能看到。
兑换时为什么钱包弹了两次? 卖出的是代币(USDT / MTK)时,第一次是 approve 授权,第二次才是真正的 swap。卖出 ETH / TRX 只弹一次。
兑换价格是谁定的? 没有人定。价格由池子里两种资产的数量按公式自动算出,每笔交易都会改变池子,从而改变下一笔的价格。
MetaMask 里的账户为什么删不掉? 账户是用助记词按顺序算出来的,删了还能再算出来,所以只能隐藏。
MTK 总量会变吗? 不会。转账、兑换都只是在账户(或池子)之间挪数字。所有人的余额加上池子里的 MTK,永远是 100 万 MTK。
九、最该记住的 6 句话
- 余额存在链上(原生币在账户上,代币在合约里),不在钱包里,也不在数据库里。
- 查余额、查报价免费;转账、兑换要签名、付手续费,手续费由发起方用原生币付。
- 合约部署后不能修改,合约地址由“部署者 + 第几笔交易”算出来。
- 代币转账真正完成的标志:合约改了账本,并发出一条 Transfer 事件;兑换则是发出 Swap 事件。
- 智能合约在本项目里做了三件事:发行 MTK、执行代币转账、按公式自动兑换;原生币转账是链自带的,不需要合约。
- 后端只是抄写员:监听事件、抄进数据库、推送给页面展示。
十、技术栈与工具清单(参与项目前必读)
这一章要解决的问题:想亲手把项目跑起来,需要装哪些工具?每个工具在哪一环起作用?
只想了解概念的读者可以先跳过,准备动手运行项目时再回来看。表格里的名词会在第 2~4 篇中逐一讲解。
以下版本号是本项目安装时拉取到的最新版本。
智能合约层
| 名称 | 版本 | 作用 / 本项目用途 | Web2 类比 | 链接 |
|---|---|---|---|---|
| Solidity | 0.8.37 | 编写 MyToken、MiniSwap 合约的语言;编译目标 EVM 版本为 osaka(见 foundry.toml),部署到 TRON 时用 FOUNDRY_PROFILE=tron(paris) | Go / Java 语言本身 | |
| Foundry(forge / cast / anvil) | 1.8.3 | forge:编译、测试、部署脚本、Gas 报告;cast:命令行调用链;anvil:本地链 | go build + go test + curl + 本地数据库 | |
| forge-std | 1.16.2 | Foundry 测试标准库(Test、vm.prank、vm.expectEmit) | testify | |
| OpenZeppelin Contracts | 5.7.0 | 经过审计的 ERC20 实现(MyToken 继承自它)、ReentrancyGuard 防重入(MiniSwap 使用) | 官方标准库 / Spring Security | |
| MiniSwap(本项目自写) | — | 迷你版 Uniswap V2 兑换合约:多个资金池、x·y=k 定价、0.3% 手续费、滑点与过期保护 | 一个“自动报价的撮合服务” | contracts/src/MiniSwap.sol |
forge-std 和 OpenZeppelin 以 git 子模块的形式放在
contracts/lib/下,版本锁定在contracts/foundry.lock。make install会依次执行git submodule update --init --recursive和forge install。
后端(Go)
| 名称 | 版本 | 作用 / 本项目用途 | 链接 |
|---|---|---|---|
| Go | 1.27 | 后端编程语言(1.24 及以上才支持 go tool air 这种 tool 依赖写法) | |
| Gin | 1.12.0 | HTTP 路由,提供 /api/* 接口(含 SSE 推送 /api/events) | |
| gin-contrib/cors | 1.7.9 | 跨域中间件。开发环境中前端(5173 端口)访问后端(8080 端口)时需要 | |
| GORM | 1.31.2 | ORM:模型定义、AutoMigrate、一对多关联、Preload、OnConflict | |
| GORM MySQL Driver | 1.6.0 | GORM 连接 MySQL 的驱动 | |
| go-ethereum(geth) | 1.17.6 | ethclient 通过 HTTP 查询链上数据(FilterLogs 查询事件、BlockNumber、HeaderByNumber),通过 WebSocket 订阅事件(SubscribeFilterLogs);crypto.Keccak256Hash 计算事件签名。Nile 的 JSON-RPC 兼容以太坊格式,同一套 listener 复用 | |
| shopspring/decimal | 1.4.0 | 精确的十进制运算:把最小单位换算成可读金额(amountDisplay),避免 float64 丢精度 | |
| godotenv | 1.5.1 | 加载 backend/.env | |
| Air | 1.67.4 | 热重载,保存 .go 文件后自动编译并重启。已通过 go get -tool 写入 go.mod,用 go tool air 运行 | |
| deploy-nile(本项目自写) | — | backend/cmd/deploy-nile:通过 TRON HTTP API 把 MyToken / MiniSwap 部署到 Nile(forge script 发不了 TRON 格式的交易) | — |
前端
| 名称 | 版本 | 作用 / 本项目用途 | 链接 |
|---|---|---|---|
| Node.js | 24 | 运行 pnpm、Vite、vue-tsc 的 JavaScript 运行时(本机实测 v24.14.0;Docker 构建使用 node:24-alpine) | |
| TypeScript | 6.0.3 | 前端编程语言,所有代码都是 .ts 或 <script setup lang="ts">,没有 .js 文件 | |
| pnpm | 11.x | 包管理器,锁文件是 pnpm-lock.yaml(本机实测 11.12.0;Docker 构建中通过 corepack enable 启用) | |
| Vite | 8.3 | 开发服务器与构建工具;配置了路径别名 /@/ → src/ | |
| @vitejs/plugin-vue | 6.0.9 | Vite 的 Vue 插件,让 Vite 能编译 .vue 单文件组件 | |
| Vue 3 | 3.5 | UI 框架 | |
| vue-tsc | 3.3 | 对 .vue 文件做类型检查,执行 pnpm build 时会先运行它 | |
| @vue/tsconfig | 0.9.1 | Vue 官方的 TypeScript 基础配置,tsconfig.app.json 继承自它 | |
| @types/node | 26.6.2 | Node.js 的类型声明,供 vite.config.ts(用到 node:url)做类型检查 | |
| TDesign Vue Next | 1.20.8 | 腾讯出品的 Vue 3 组件库:Form、Input、Button、Table、Tag、RadioGroup、Steps、Message | |
| Ethers.js | 6.17 | 与 EVM 链交互:BrowserProvider 连接钱包、Contract 调用合约、parseUnits / formatUnits 换算金额 | |
| MetaMask | — | 以太坊钱包:保管私钥并签名,通过 EIP-1193 暴露给页面;页面通过 EIP-6963(多钱包发现协议)自动发现已安装的以太坊钱包 | |
| TronLink | — | 波场钱包:注入 window.tron(TIP-1193,接口与 EIP-1193 一致),Nile 上的签名都由它完成 | |
| tronWeb | —(由 TronLink 注入,不是 npm 依赖) | 与 TRON 链交互:查 TRX 余额、构造 / 签名 / 广播 TRX 转账、调用 TRC-20 与 MiniSwap 合约 |
TypeScript 固定在 6.x 的原因:TypeScript 7 是用原生代码重写的版本,不再提供 vue-tsc 依赖的 JS 编译器 API,实测
vue-tsc会直接报错ERR_PACKAGE_PATH_NOT_EXPORTED。等 vue-tsc 适配 TS 7 后再升级。
链服务
| 名称 | 作用 / 本项目用途 | 链接 |
|---|---|---|
| Sepolia + Etherscan | 以太坊公共测试链;在区块浏览器里可以查看交易和 Event Log | |
| Infura(本项目使用)/ Alchemy | 托管的 RPC 节点服务。后端用 HTTPS 地址查询(SEPOLIA_RPC_URL),用 WSS 地址订阅事件(SEPOLIA_WS_URL);forge / cast 部署时也用它 | |
| publicnode 公共 RPC | 免费公共节点,只在“向 MetaMask 添加 Sepolia 网络”时作为参数传入(前端 VITE_SEPOLIA_PUBLIC_RPC) | |
| Sepolia 水龙头(Faucet) | 免费领取 SepoliaETH,用于支付部署、转账、兑换的 Gas。Infura、Alchemy、Google Cloud 等都有提供,不少要求账户在主网上有少量余额 | |
| Aave 测试 USDT | Sepolia 上没有 Tether 官方的 USDT,本项目使用 Aave 测试网的 USDT(6 位精度),可在 Aave 测试网水龙头领取 | 合约 0xaa8e23fb1079ea71e0a56f48a2aa51851d8433d0 |
| TRON Nile + TronScan | 波场公共测试链;在 nile.tronscan.org 查看交易、合约 | |
| TronGrid | 波场官方节点服务。后端通过它兼容以太坊的 /jsonrpc 端点轮询 Nile(不支持 WebSocket);部署工具走它的 HTTP API | |
| TRON 官方测试 USDT | Nile 上的 TRC-20 USDT(TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf,6 位精度),不是我们部署的 |
数据库、开发工具与部署
| 名称 | 作用 / 本项目用途 | 链接 |
|---|---|---|
| MySQL 8+ | 存储 users、user_networks、transactions、swaps、sync_states 五张表(Docker Compose / K8s 中使用 mysql:8.4 镜像) | |
| Git | 管理代码;合约依赖以 git 子模块形式安装 | |
| jq | export-abi.sh 用它从 JSON 中提取 ABI 和合约地址;Makefile 的 seed-* 用它读取部署者地址 | |
| Make | 把常用命令统一成 make xxx,make help 可查看全部命令 | GNU Make |
| Docker / Docker Compose | 构建镜像,一键启动 mysql、backend、frontend。用到的基础镜像:golang:1.27(编译后端)、gcr.io/distroless/static-debian12(运行后端,不含 shell,攻击面小)、node:24-alpine(构建前端)、nginx:alpine、mysql:8.4 | |
| nginx | 托管前端静态文件,并把 /api 反向代理到后端(对 /api/events 单独关闭缓冲,保证 SSE 实时推送) | |
| Kubernetes(kubectl) | 容器编排,部署清单在 deploy/k8s/ |
一次转账 / 兑换中各工具的出场顺序:
- 以太坊(anvil / Sepolia):MetaMask 签名 → ethers 编码并发送 → anvil / Infura 节点 → Solidity 合约执行(MyToken / USDT / MiniSwap)→ go-ethereum 通过 WebSocket 收到事件推送、再用 eth_getLogs 查询日志 → GORM 写入 MySQL → Gin 通过 SSE 推送 → decimal 换算金额 → TDesign 表格展示
- 波场(Nile):TronLink 签名 → tronWeb 构造并广播 → TronGrid 节点 → 合约在 TVM(波场虚拟机,兼容 EVM)中执行 → go-ethereum 通过 TronGrid
/jsonrpc轮询日志 → 之后与以太坊相同
十一、一张表总结:智能合约的实际作用
| 场景 | 谁来执行规则 | 本项目里的合约 | 链上留下什么 | 没有合约会怎样 |
|---|---|---|---|---|
| 发行代币 | 合约代码(部署后谁都改不了) | MyToken(MTK) | 总量、余额、Transfer 事件 | 只能自己在数据库记积分,用户只能相信你 |
| 原生币转账 | 链本身 | 不需要合约 | 交易本身(无事件) | —(链的出厂功能) |
| 代币转账 | 代币合约的 transfer | 第三方 USDT / 我们的 MyToken | 账本变化 + Transfer 事件 | 代币根本不存在 |
| 资产兑换 | 兑换合约按 x·y=k 公式自动成交 | MiniSwap(Sepolia、Nile) | 池子储备变化 + Swap 事件 | 需要中心化交易所托管资金、撮合订单 |
一句话:合约 = 跑在链上、公开透明、谁也改不了的业务规则。在本项目里,它负责“发币、记账、兑换”;前端只负责“提请求”,后端只负责“抄记录”。更完整的总结(包括合约能做什么、不能做什么)见第 2 篇的“智能合约实际作用总结”。
下一篇预告
现在你已经有了整张地图。下一篇 智能合约(contracts)详解 会打开两个合约的源码:MyToken 里“余额怎么记、转账怎么校验、Transfer 事件怎么发出”;MiniSwap 里“资金池怎么存、x·y=k 公式怎么算、滑点和重入怎么防”,并用本篇的真实兑换记录逐笔验算,最后给出智能合约实际作用的完整总结。
后续 3 篇分别适合谁读(建议按顺序,只关心某一端也可以直接跳读):
- 第 2 篇 智能合约:想知道“链上的规则是怎么写出来的”、对 Solidity、AMM / DeFi 感兴趣的读者
- 第 3 篇 前端:前端开发者,想知道网页如何连接 MetaMask / TronLink,发起一笔真实的链上转账和兑换
- 第 4 篇 后端:后端开发者,想知道如何把三条链上的转账、兑换记录可靠地同步到自己的数据库,并实时推送给页面