在以太坊这类支持智能合约的平台上,婚姻登记可以被建模为一种地址映射关系:两个地址通过一笔交易建立配偶关系,也可以通过另一笔交易解除。Node.js在这一流程里扮演链下脚本和后端角色,负责生成交易数据、签名或发送到节点、读取事件日志。本文以一个简化版DeMarriage合约为例,展示从合约编写到Node.js调用完整链路。

一、DeMarriage合约的状态与约束设计
合约核心是保存每个地址的婚姻状态。Solidity中的映射类型很适合这种一一对应的关系,用mapping(address => Marriage)记录配偶地址、结婚时间和状态枚举。枚举值分成未结婚、已婚和已离婚,这样查询接口可以返回可读的数字状态,而不是只靠地址是否为零来判断。
登记函数需要完成双向写入:调用者与配偶地址都要指向彼此,并记录同一个时间戳。状态判断放在修饰器中,如果一个地址已经处于已婚状态,再次调用登记就会直接回滚。离婚函数只让已婚地址调用,通过配偶字段找到另一方,再清除双方记录并触发事件。这样既保持了数据一致性,也避免出现单边删除导致错误状态。
事件是链上日志的重要部分。婚姻登记和解除都会写入Married和Divorced事件,Node.js端可以订阅这些事件来做实时提醒或数据同步。由于事件参数带有indexed标记,按地址过滤历史婚姻记录会比遍历映射方便很多。下面是一个可运行的合约示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract DeMarriage {
enum Status { Single, Married, Divorced }
struct Marriage {
address partner;
uint256 since;
Status status;
}
mapping(address=>Marriage) public marriages;
event Married(address indexed left, address indexed right, uint256 at);
event Divorced(address indexed left, address indexed right, uint256 at);
modifier onlySingle() {
require(marriages[msg.sender].status == Status.Single, "Already married");
_;
}
function marry(address partner) external onlySingle {
require(partner != address(0), "Invalid partner");
require(marriages[partner].status == Status.Single, "Partner not single");
marriages[msg.sender] = Marriage(partner, block.timestamp, Status.Married);
marriages[partner] = Marriage(msg.sender, block.timestamp, Status.Married);
emit Married(msg.sender, partner, block.timestamp);
}
function divorce() external {
Marriage storage mine = marriages[msg.sender];
require(mine.status == Status.Married, "Not married");
address partner = mine.partner;
marriages[partner] = Marriage(address(0), block.timestamp, Status.Divorced);
delete marriages[msg.sender];
emit Divorced(msg.sender, partner, block.timestamp);
}
}
二、用Node.js部署合约前的准备
部署合约需要一个可用的以太坊节点。本地开发通常使用Ganache,它会开放http://127.0.0.1:8545这个JSON-RPC地址,同时准备若干测试账户和私钥。Node.js端安装web3库,并用solc编译合约得到ABI和字节码。实际项目中可以在编译后把ABI保存成JSON,让部署脚本读取。
下面先准备依赖:
- Node.js 18或更高版本
- 本地以太坊节点,如Ganache
- web3.js库
- Solidity编译器solc
部署脚本的核心逻辑是:读取ABI和字节码,创建合约对象,再调用deploy方法发送交易。注意gas不能设置得太低,否则合约部署会因资源不足失败。交易发出后可以等待回执得到合约地址,这个地址后续查询和调用时都要使用。
const Web3 = require('web3');
const fs = require('fs');
const web3 = new Web3('http://127.0.0.1:8545');
const abi = JSON.parse(fs.readFileSync('./DeMarriage.json', 'utf8')).abi;
const bytecode = JSON.parse(fs.readFileSync('./DeMarriage.json', 'utf8')).bytecode;
async function deploy() {
const accounts = await web3.eth.getAccounts();
const from = accounts[0];
const contract = new web3.eth.Contract(abi);
const instance = await contract.deploy({ data: bytecode })
.send({ from, gas: 2000000 });
console.log('Deployed at:', instance.options.address);
fs.writeFileSync('./contract-address.txt', instance.options.address);
return instance.options.address;
}
deploy().catch(console.error);
本地测试节点通常会解锁账户,因此脚本可以直接使用from发送交易。生产环境如果接入真实节点,最好不要长期解锁账户,改用私钥签名交易或借助钱包服务。部署完成后,把地址写入文件是为了让后续脚本不用重新部署。
三、登记结婚与离婚函数的Node.js调用
有了合约地址和ABI之后,就可以在Node.js中构造合约实例。登记结婚需要两个不同账户,分别代表夫妻双方。调用marry方法时,交易从一方账户发出,把另一方地址作为参数传入。发送成功后返回回执,回执的events属性中包含Married事件的参数。
查询婚姻状态属于只读操作,不需要消耗gas,所以用call方法而不是send。因为marriages被声明为public,Solidity会自动生成对应读函数,Node.js可以直接调用contract.methods.marriages(account).call()。返回对象的partner和status字段可以用于界面展示。
离婚调用只需从已婚账户发起,不需要额外参数。合约会根据msg.sender找到配偶地址,然后清除双方记录并写入事件。以下是完整调用脚本:
const Web3 = require('web3');
const fs = require('fs');
const web3 = new Web3('http://127.0.0.1:8545');
const abi = JSON.parse(fs.readFileSync('./DeMarriage.json', 'utf8')).abi;
const address = fs.readFileSync('./contract-address.txt', 'utf8').trim();
async function main() {
const accounts = await web3.eth.getAccounts();
const a = accounts[0];
const b = accounts[1];
const contract = new web3.eth.Contract(abi, address);
const receipt = await contract.methods.marry(b)
.send({ from: a, gas: 200000 });
console.log('Married event:', receipt.events.Married.returnValues);
const mine = await contract.methods.marriages(a).call();
console.log('Partner:', mine.partner, 'Status:', mine.status);
const divorceReceipt = await contract.methods.divorce()
.send({ from: a, gas: 200000 });
console.log('Divorced event:', divorceReceipt.events.Divorced.returnValues);
}
main().catch(console.error);
如果账户顺序不确定,可以先从节点获取账户列表,再把第一个和第二个分别作为双方。测试时若账户未解锁,发送交易会报错,这是链下环境最常见的配置问题。
四、事件监听与错误排查方向
除了等待回执里的events,还可以用contract.events.Married()创建一个订阅,当新婚姻登记上链时收到通知。事件订阅通常从最新区块开始,如果需要回放历史记录,可以调用getPastEvents按过滤条件查询。
async function listen() {
const contract = new web3.eth.Contract(abi, address);
contract.events.Married({ fromBlock: 'latest' })
.on('data', (event) => {
console.log('New marriage:', event.returnValues.left, event.returnValues.right);
})
.on('error', console.error);
}
开发中常见的问题包括gas上限不足、ABI与合约字节码不匹配、nonce冲突等。node端如果直接复用同一个nonce发送多笔交易,容易出现替换交易或报错。建议每个交易发出后等待回执,再继续下一条,本地测试阶段尤其要注意。
DeMarriage这类合约还可以扩展成双方共同确认解除、设置冷静期、加入证人地址等更贴近现实婚姻登记的机制。Node.js端只需要根据新增的方法调整参数和事件名称,整体调用模式仍然一致。把状态、事件、链下脚本三者分开理解,后续开发其他业务合约也会顺手很多。
Node.jsDeMarriage智能合约修改时间:2026-09-30 15:43:48