摘要:在 Remix VM 写带 mapping(address=>uint256) 与 Updated/Transfer 事件的 Ledger 合约:0.8.28 编译、VM 部署(构造 logs:1),再用本机 eth-tester + web3.py 核对 balanceOf 与事件;不需测试网 ETH,不碰主网。

上一课在 Remix 里跑通了最小合约;这一课只盯两样东西:mapping(address => uint256) 怎么存余额,以及写入时发出的 event 怎么在终端 / 回执里核对。演示全程用 Remix IDE 的 Remix VM 部署,再用本机 eth-tester + web3.py 8.0.0 等价跑一遍;不需要测试网 ETH,也绝不碰主网 / 真私钥。

合约结构入门见 Solidity 第一课:Remix VM 部署;回执里 Logs 字段含义见 以太坊交易回执怎么读;用 JS/Python 读链上状态可接着看 ethers.js 读合约 与 web3.py 读 ERC-20。

一、mapping 与 event:分别解决什么

mapping 是合约存储里的键值表:本例用地址当键、用 uint256 当余额。它不能像数组一样 length 遍历,所以链上「有没有改成功」通常靠两件事核对——调用只读 balanceOf,以及看交易回执里的事件日志。

构件 本例 核对时看什么
mapping(address => uint256) _balances(private) 只能通过 balanceOf / 自己写的 getter 读
event Updated who indexed + old/new 每次改余额打一条;构造函数也会打
event Transfer from/to indexed + amount transfer 时额外打,方便按地址筛
写入函数 setBalance / transfer 改存储必须发交易;VM 里是假 Gas
只读函数 balanceOf / owner eth_call,不改状态、不产生 logs

二、合约代码:先写再部署

在 Remix Playground 新建或粘贴下面的 Ledger.sol(本场实拍文件名仍显示为工作区里的 HelloWorld.sol,合约名是 Ledger,以编译器下拉为准):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title Ledger — mapping + event demo for Remix VM / eth-tester
contract Ledger {
    mapping(address => uint256) private _balances;
    address public owner;
    event Updated(address indexed who, uint256 oldBal, uint256 newBal);
    event Transfer(address indexed from, address indexed to, uint256 amount);

    constructor() {
        owner = msg.sender;
        _balances[msg.sender] = 1000;
        emit Updated(msg.sender, 0, 1000);
    }

    function balanceOf(address account) public view returns (uint256) {
        return _balances[account];
    }

    function setBalance(address account, uint256 newBal) public {
        require(msg.sender == owner, "only owner");
        uint256 old = _balances[account];
        _balances[account] = newBal;
        emit Updated(account, old, newBal);
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(_balances[msg.sender] >= amount, "insufficient");
        uint256 fromOld = _balances[msg.sender];
        uint256 toOld = _balances[to];
        _balances[msg.sender] = fromOld - amount;
        _balances[to] = toOld + amount;
        emit Transfer(msg.sender, to, amount);
        emit Updated(msg.sender, fromOld, fromOld - amount);
        emit Updated(to, toOld, toOld + amount);
        return true;
    }
}
Solidity mapping 与事件:怎么在浏览器核对-程序旅途
图1:app.remix.live 中文界面:编辑器展示 contract Ledger、mapping(address => uint256) private _balances、event Updated / Transfer、constructor 初始余额 1000、balanceOf / setBalance / transfer;Remix 2.6.5,2026-10-09 实拍

三、编译:0.8.28 对齐 pragma

左侧「Solidity 编译器」选 0.8.28(满足 ^0.8.20),编译成功后左侧图标绿勾,合约列表出现 Ledger (…sol):

Solidity mapping 与事件:怎么在浏览器核对-程序旅途
图2:Remix「SOLIDITY 编译器」:Compiler 0.8.28+commit.7893614a;编译按钮与合约下拉显示 Ledger;左侧编译图标绿勾;界面简体中文,2026-10-09

四、Remix VM 部署:构造函数就会打 Updated

「部署 & 发交易」环境选 Remix VM (Osaka),合约选 Ledger,点部署。本场实例地址形如 0xd91…39138;终端绿色勾,构造交易 logs: 1(正是 constructor 里那条 Updated(owner, 0, 1000)):

Solidity mapping 与事件:怎么在浏览器核对-程序旅途
图3:Remix「部署 & 发交易」:环境 Remix VM (Osaka);Account 余额约 99.999 ETH;已部署 Ledger;橙色 setBalance(address,uint256)与 transfer;底部终端显示成功勾、logs: 1、hash 0x484…;界面简体中文,2026-10-09

接着在已部署实例里:用 setBalance(某地址, 新余额) 写入,再点蓝色 balanceOf 读回;或调用 transfer 看终端是否出现更多 logs。回执字段怎么读,对照 交易回执 Logs 即可——Remix 终端里的 decoded logs 与浏览器 Logs 标签是同一类信息。

为什么 mapping 要配 event
存储改了,外部索引器 / 前端通常靠日志重建「谁的余额变了」;只靠轮询 balanceOf 既慢也容易漏历史。
indexed 参数
Updated.who、Transfer.from/to 进 topics,便于按地址过滤;非 indexed 的数值进 data。
本场边界
只在 Remix VM / 本地 eth-tester。不要切 Injected Provider 连主网,也不要粘贴私钥。

五、本机 eth-tester:把 mapping 读值与事件打印出来

同一份 solc 0.8.28 字节码,用 web3.py 8.0.0 + eth-tester 本地假链再跑:部署 → setBalance(acct2, 42) → transfer(acct2, 100)。终端应看到构造 Updated(…,0,1000)、写入后 balanceOf(acct2)=42,再 transfer 后 owner 900 / acct2 142,且 transfer 回执 logs: 3(1 条 Transfer + 2 条 Updated):

Solidity mapping 与事件:怎么在浏览器核对-程序旅途
图4:本机 xterm 实跑:web3 8.0.0 + eth-tester;deployed 0xF2E2…;constructor Updated (owner,0,1000);setBalance 后 balanceOf(acct2)=42;transfer 后 owner=900、acct2=142;注明仅本地、非主网;2026-10-09
solc --bin --abi --optimize -o build Ledger.sol --overwrite
# 再用 web3.py + EthereumTesterProvider 加载 ABI/bytecode 部署并 call

六、本机 solc 再核 ABI(可选)

确认 ABI 里确实有 Transfer / Updated 与 balanceOf / setBalance / transfer,字节码约 1129 字节:

Solidity mapping 与事件:怎么在浏览器核对-程序旅途
图5:本机 solc Version 0.8.28+commit.7893614a;Compiler run successful;events=[Transfer, Updated];functions 含 balanceOf/owner/setBalance/transfer;bytecode bytes 1129

常见坑

  • 以为 mapping 能 for 循环扫全表:不能;要自己维护索引数组,或靠事件重建。
  • 写了 storage 却忘了 emit:链上能 call 到新值,但浏览器 / 索引器很难追溯历史变更。
  • 用 float 心算余额: Solidity 用整数;换算小数位是另一层(见 ERC-20 decimals 文)。
  • 环境选成 Injected Provider:可能弹钱包、误连主网。第一课/第二课都停在 Remix VM。
  • 把 Remix VM 地址当测试网地址去区块浏览器搜:VM 交易只在当前会话有效,刷新即失效。

相关阅读

说明:配图为 2026-10-09 在 app.remix.live(中文)、本机 solc 0.8.28 与 eth-tester 实拍/实跑。Remix VM 与 eth-tester 地址/哈希仅本地有效。本文不构成投资建议,不引导主网部署。若要用钱包做链上操作,可另见 欧易 Web3 钱包教程(推荐码 CHENGXULVTU 写在创建步骤即可)。