ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于以太坊的电子合同DAPP:完整生命周期实现

基于以太坊的电子合同DAPP:完整生命周期实现 简介本资源是一个基于区块链DApp的电子合同签署系统完整实现面向区块链初学者、课程设计学生及毕业设计开发者解决传统电子合同可信存证与多方协同签署难题。项目采用Truffle框架开发集成Solidity智能合约、前端HTML/JS交互界面与管理员后台覆盖合同发布、双签再确认、链上状态查询、中止申请与审核等全流程功能并包含身份证校验、重复操作拦截等实用安全机制。压缩包共36个文件含11个HTML页面如new.html、confirm1.html、admin.html等核心流程页、7个JS脚本实现前端逻辑与Web3交互、2个.sol合约文件EContract.sol为核心业务合约、3个JSON配置文件及配套CSS、字体与图片资源整体体积仅584KB结构紧凑、开箱即用。目前已有89人学习下载提供可直接部署运行的完整工程目录含迁移脚本、合约编译产物与README说明适合快速理解区块链应用落地逻辑并开展二次开发。1. 这不是“上链存个PDF”一个能跑通合同生命周期的DAPP从发布、双签、再确认到管理员审核全闭环你见过太多“区块链电子合同”demo——上传文件哈希、点一下按钮、弹出“已上链”然后就没了。但真实业务里合同不是单次写入就完事甲方签了乙方没签、双方都点了但没二次确认、管理员还没审核、有人突然要中止……这些状态流转和权限校验才是电子合同系统真正的硬骨头。这个E-ContractSigning-master项目就是少有的、把「合同生命周期」完整落地到以太坊测试链Truffle Ganache上的 DAPP 实战工程。它不只存哈希而是用 Solidity 实现了合同状态机New → Signed → Confirmed → Approved/Aborted嵌入身份证号正则校验、重复签署拦截、序列号存在性查询等生产级约束并配齐前端页面流发布页、双签页、确认页、查询页、管理后台。适合毕设、课程设计或想真正理解“智能合约如何驱动业务流程”的开发者——它不是概念演示是能照着跑通、改参数、加字段、接真实UI的脚手架。尤其对刚学完 Truffle 基础、卡在“合约怎么和页面联动”环节的同学这份代码就是血泪经验的实体化。2. 合约层EContract.sol 的状态机设计与关键校验逻辑拆解2.1 合同状态机5个状态4类角色权限的硬编码约束contracts/EContract.sol是整个系统的核心。它没有用 ERC 标准而是自定义了一套轻量但严谨的状态流转规则。打开源码第一眼就能看到这个枚举enum ContractStatus { New, // 初始状态发布后未签署 Signed, // 甲乙双方均完成首次签署 Confirmed, // 双方均完成二次确认 Approved, // 管理员审核通过 Aborted // 管理员中止或申请后批准 }状态变更不是随意调用函数而是被require严格锁死。比如confirmSign()函数二次确认的前置校验function confirmSign(address _signer) public { require(msg.sender _signer, Only signer can confirm); require(status ContractStatus.Signed, Must be in Signed state to confirm); require(_signer partyA || _signer partyB, Only parties can confirm); if (_signer partyA) { confirmedA true; } else if (_signer partyB) { confirmedB true; } if (confirmedA confirmedB) { status ContractStatus.Confirmed; } }提示这里confirmedA/confirmedB是独立布尔变量而非依赖signerA/signerB是否为空——这是为防“签了又反悔”做的状态隔离。很多新手直接用地址是否为空判断结果导致二次确认无法触发状态跃迁。2.2 身份校验与防重机制Solidity 里的字符串处理玄学合同创建时要求输入身份证号合约层做了两层校验格式合法性 防重复提交。注意Solidity 0.5.x 不支持原生正则所以作者用纯逻辑实现18位身份证校验含末位校验码function isValidIdCard(string memory id) internal pure returns (bool) { bytes memory idBytes bytes(id); if (idBytes.length ! 18) return false; // 检查前17位是否全为数字 for (uint i 0; i 17; i) { if (idBytes[i] 0x30 || idBytes[i] 0x39) return false; } // 末位校验码逻辑省略具体系数计算源码中有完整实现 // ... }而防重复的关键在于createContract函数里这行require(!idExists[id], ID already used for contract); idExists[id] true;idExists是一个mapping(string bool)它把身份证号作为 key 存储。这里踩坑点在于Solidity 中 string 作为 mapping key 会自动进行 keccak256 哈希实际存储的是哈希值而非明文。所以前端传来的身份证号必须和合约里校验的完全一致不能有多余空格、大小写差异否则idExists[id]查不到导致重复提交成功。我一般会在前端trim()toLowerCase()后再传避免黑匣子式失败。2.3 管理员权限模型非 Owner 的多角色授权设计项目没用 OpenZeppelin 的Ownable而是自定义了管理员角色address public admin; constructor(address _admin) public { admin _admin; } modifier onlyAdmin() { require(msg.sender admin, Only admin can call this); _; }注意admin是在部署时由2_deploy_contracts.js传入的不是合约部署者自动成为 admin。这意味着部署者可以指定任意地址为 admin比如测试时填自己的 Ganache 账户后续所有approveContract()、abortContract()都强制走onlyAdmin检查如果部署时填错 admin 地址整个系统将失去审核能力——没有后悔药。这是比简单msg.sender owner更贴近真实场景的设计管理员是业务角色不是技术角色。3. 前端交互HTML 页面流与 Web3.js 1.0 的实操集成3.1 页面路由与状态映射从index.html到admin.html的数据驱动整个前端是纯静态 HTML JavaScript无框架。核心逻辑在src/js/index.js和各页面的内联脚本中。以合同发布页add1.html为例它不是简单表单提交而是分三步前端校验用 JS 正则检查身份证号、手机号格式合约调用准备调用EContract.new()创建新实例获取合约地址状态同步将新合约地址写入本地localStorage跳转到confirm1.html并带参。关键代码段add1.html中// 获取用户输入 const idCard document.getElementById(idCard).value.trim(); const phone document.getElementById(phone).value.trim(); // 前端校验与合约层互补 if (!/^[1-9]\d{17}$/.test(idCard)) { alert(身份证号格式错误); return; } // 初始化 web3需先注入 MetaMask 或 Ganache 提供的 provider if (typeof web3 ! undefined) { web3 new Web3(web3.currentProvider); } else { web3 new Web3(new Web3.providers.HttpProvider(http://127.0.0.1:7545)); } // 部署新合约实例 const EContract web3.eth.contract(ABI); // ABI 来自 build/contracts/EContract.json EContract.new( idCard, phone, { from: web3.eth.accounts[0], gas: 3000000 }, function(err, contract) { if (err) { console.error(err); alert(部署失败请检查Ganache是否运行); return; } if (contract.address) { // 成功后跳转并传递合约地址 localStorage.setItem(currentContract, contract.address); window.location.href confirm1.html; } } );注意gas: 3000000是硬编码值。实际部署时若合约逻辑变复杂如增加日志事件、循环可能因 gas 不足失败。建议先在 Truffle Console 中estimateGas再设值。3.2 双签与再确认前端如何区分 PartyA/PartyB 并触发不同合约方法confirm1.html和confirm2.html表面相似但背后逻辑完全不同。confirm1.html对应 PartyA发布者的首次签署调用signAsPartyA()confirm2.html对应 PartyB 的首次签署调用signAsPartyB()。而二次确认页confirm1.html注意名字没变靠上下文区分则调用confirmSign()并传入当前账户地址。关键区别在于前端必须明确知道当前用户是 PartyA 还是 PartyB。项目用了一个朴素但有效的方案——在add1.html发布合同时把 PartyA 地址即发布者地址存入localStorage并在confirm1.html加载时读取// confirm1.html 中 const partyA localStorage.getItem(partyA); const currentAccount web3.eth.accounts[0]; if (currentAccount partyA) { // 当前用户是 PartyA显示 PartyA 签署按钮 document.getElementById(signBtn).onclick signAsPartyA; } else { // 否则认为是 PartyB显示 PartyB 签署按钮实际跳转 confirm2.html window.location.href confirm2.html; }这种设计把角色判定逻辑从前端完成避免合约层做复杂地址匹配也降低了 Gas 消耗。3.3 查询与管理query.html的链上状态拉取与admin.html的批量操作query.html的核心是实时查询合约状态。它不依赖事件监听Event而是用轮询contract.status()function refreshStatus() { const contract EContract.at(localStorage.getItem(currentContract)); contract.status.call(function(err, status) { if (err) return; document.getElementById(status).innerText [New,Signed,Confirmed,Approved,Aborted][status.toNumber()]; }); } setInterval(refreshStatus, 3000); // 每3秒刷新一次而admin.html则更进一步它从Migrations.sol的lastCompletedMigration推导出所有已部署的EContract实例地址再逐个调用status()构建合同列表。源码中有一段关键逻辑// 从 Migrations 合约获取最新 migration ID migrations.lastCompletedMigration.call(function(err, id) { // id 是整数对应 migrations/ 目录下的文件序号 // 但实际合同地址需从部署记录中读取Truffle 的 artifacts // 项目采用简化方案手动维护一个 contracts.json 数组 });由于 Truffle 5 的 artifact 结构较深项目没直接解析build/contracts/*.json而是让开发者在src/js/admin.js中手动维护一个contractAddresses [0x..., 0x...]数组。这是权衡——牺牲自动化换取调试直观性。如果你要升级建议用truffle-artifactor库动态读取。4. 部署与环境Truffle v5.1.26 下的四步启动法与常见报错排查4.1 四步启动法从 Ganache 到浏览器可访问的完整链路这不是 npm install 就完事的项目。必须按顺序执行以下四步缺一不可启动 Ganache# 确保 Ganache GUI 或 CLI 运行在 http://127.0.0.1:7545 # 若用 CLIganache-cli -p 7545 -a 10编译并迁移合约cd E-ContractSigning-master truffle compile truffle migrate --network development注意truffle-config.js中development网络指向http://127.0.0.1:7545且from字段必须是 Ganache 中第一个账户0x...地址否则迁移会因 nonce 错误失败。生成前端 ABI 与地址迁移成功后build/contracts/EContract.json会更新。你需要从中提取abi和networks[5777].address5777 是 Ganache 默认 chainId粘贴到src/js/index.js的对应位置const ABI [/* 复制 abi 数组 */]; const CONTRACT_ADDRESS 0x...; // 复制 networks.5777.address启动静态服务器# 推荐用 http-server全局安装npm install -g http-server http-server src -p 8080 # 然后访问 http://localhost:80804.2 避坑Truffle 5.1.26 下的 5 个高频翻车点现象1truffle migrate报错Error: Error: Cannot find module web3原因Truffle 5.1.26 依赖web31.2.11但全局或项目中安装了web30.x或web32.x版本冲突。解决rm -rf node_modules package-lock.json npm install web31.2.11 truffle5.1.26 # 确保 package.json 中 web3: ^1.2.11现象2前端调用contract.signAsPartyA()时提示invalid address原因truffle migrate后build/contracts/EContract.json中的networks字段为空或address字段是null。解决检查truffle-config.js中networks.development的host、port、network_id是否与 Ganache 一致手动打开build/contracts/EContract.json确认networks下有5777键且address不为空若为空删掉build/目录重新truffle compile truffle migrate。现象3confirm1.html点击签署按钮无反应控制台报Uncaught TypeError: Cannot read property new of undefined原因src/js/index.js中web3.eth.contract(ABI)的ABI变量未正确定义或ABI数组复制时漏了逗号导致语法错误。解决在浏览器控制台console.log(ABI)确认输出是数组检查ABI复制是否完整build/contracts/EContract.json中abi字段不是bytecode用 JSONLint 验证ABI是否为合法 JSON 数组。现象4Ganache 显示交易成功但前端status()返回始终是0New原因合约方法未加public或external修饰符或状态变量未用storage关键字声明如string public idCard写成string idCard。解决检查EContract.sol中所有状态变量是否带public如address public partyA检查所有修改状态的函数是否带public或external且无view/pure修饰重新truffle compile truffle migrate。现象5admin.html列表为空console.log(contractAddresses)输出undefined原因src/js/admin.js中contractAddresses数组未按实际部署地址更新。解决运行truffle console --network development输入EContract.deployed().then(i console.log(i.address))获取最新地址将地址粘贴到admin.js的contractAddresses [0x...]数组中。5. 功能扩展给合同加时间戳、PDF 附件与链下签名验证的三步改造5.1 加时间戳在createContract中嵌入block.timestamp规避中心化时间源当前合约没有记录创建时间导致“合同发布时间”只能靠前端 JSnew Date()不可信。改造只需两步修改EContract.sol增加createdAt状态变量uint256 public createdAt; constructor(address _partyA, address _partyB, string memory _idCard, string memory _phone) public { partyA _partyA; partyB _partyB; idCard _idCard; phone _phone; status ContractStatus.New; createdAt block.timestamp; // 关键使用区块时间戳 }前端query.html中调用contract.createdAt()contract.createdAt.call(function(err, ts) { document.getElementById(createdAt).innerText new Date(ts.toNumber() * 1000).toLocaleString(); });注意block.timestamp精确到秒且矿工可微调±15秒适合业务时间锚点不适合金融级精确计时。若需更高精度需引入 Chainlink Oracle但本项目复杂度不允许。5.2 PDF 附件用 IPFS 替代链上存储保持合约轻量把 PDF 二进制直接存链上Gas 费爆炸。正确做法是存哈希文件放 IPFS。改造如下前端上传 PDF 时用ipfs-http-client上传并获取 CIDnpm install ipfs-http-clientimport { create } from ipfs-http-client; const client create(https://ipfs.infura.io:5001); const file document.getElementById(pdfFile).files[0]; const result await client.add(file); const cid result.cid.toString(); // 例如QmXyZ...修改合约增加pdfCid字段并在createContract中存入string public pdfCid; constructor(..., string memory _pdfCid) public { // ... 其他初始化 pdfCid _pdfCid; }前端query.html中拼接 IPFS 网关 URLcontract.pdfCid.call(function(err, cid) { const ipfsUrl https://ipfs.io/ipfs/${cid}; document.getElementById(pdfLink).href ipfsUrl; });这样PDF 文件存在去中心化网络合约只存 46 字符 CIDGas 消耗几乎不变。5.3 链下签名验证用ecrecover验证 PDF 哈希签名绕过 MetaMask 限制MetaMask 不允许直接签名任意二进制如 PDF但允许签名哈希。我们可以让甲方用私钥对 PDF 的 SHA256 哈希签名再在合约中用ecrecover验证前端生成 PDF 哈希并签名用ethereumjs-utilnpm install ethereumjs-utilimport { sha3, ecsign, pubToAddress } from ethereumjs-util; const hash sha3(pdfBytes); // pdfBytes 是 Uint8Array const sig ecsign(hash, privateKey); // privateKey 来自用户钱包导出 const signature bufferToHex(Buffer.concat([sig.r, sig.s, Buffer.from([sig.v])]));合约新增verifyPdfSignature函数function verifyPdfSignature(bytes32 _pdfHash, bytes memory _signature, address _expectedSigner) public view returns (bool) { bytes32 r; bytes32 s; uint8 v; // 解析 signature省略解析逻辑源码中有标准切分 address recovered ecrecover(_pdfHash, v, r, s); return (recovered _expectedSigner); }调用时传入sha3(pdfBytes)、签名、预期签署人地址。这样PDF 真实性由密码学保证不依赖中心化 CA且签名过程完全链下用户体验无阻塞。从那以后我每次给学生讲区块链 DAPP都会先让他们跑通这个E-ContractSigning项目——不是因为它多炫酷而是因为它把“状态机”、“角色权限”、“前后端协同”、“Gas 敏感设计”这些抽象概念钉死在每一行可执行的代码里。它不教你怎么发币但教会你怎么让一段业务逻辑在去中心化环境下既安全又可用。希望帮到你。本文还有配套的精品资源点击获取
返回列表