ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

单文件HTML实现加密货币付费墙:MetaMask与智能合约实战

单文件HTML实现加密货币付费墙:MetaMask与智能合约实战 1. 背景与核心概念1.1 什么是加密货币付费墙Crypto Paywall先从一个直观场景说起。你在个人网站上发布了一篇深度技术教程、一组高清壁纸或者一个在线计算工具希望只有付费用户才能看到完整内容。传统做法是接入 Stripe、支付宝、微信支付但这类方案通常需要注册商户账号、配置回调接口、处理退款甚至还需要企业资质。对于一个个人开发者或者小团队来说这套链路并不轻松。加密货币付费墙Crypto Paywall则提供了一种更轻量的思路访客通过 MetaMask 等加密钱包向指定地址转账一定金额的数字货币支付成功后自动解锁网页中的隐藏内容。整个过程不依赖中心化支付平台没有退款和风控纠纷交易记录在链上公开可查。而 Onefile-unlock 这个名字非常直白——“one file unlock”即把所有逻辑打包到一个自包含的 HTML 文件中打开浏览器即可运行不需要安装 Node.js不需要后端服务器不需要数据库。1.2 单文件 HTML 方案为什么值得关注传统 Web 项目通常分为前端、后端、数据库三层。前端页面负责展示后端负责验证支付状态数据库负责记录哪些用户已经付费。这样做的好处是安全可控但部署成本也高。Onefile-unlock 选择了一条不同的路径所有功能集中在一个 HTML 文件中前端即应用浏览器即运行时。这种方案适合以下场景个人作品集网站中的付费内容解锁临时活动页面的付费访问技术演示和概念验证不想维护服务器的小型数字产品。当然单文件方案也有明显边界比如纯前端逻辑无法保证内容绝对安全用户可以通过阅读源码绕过付费。因此它更适合“防君子不防小人”的场景。本文后面会专门讨论这个问题。1.3 读懂这个方案需要哪些基础这篇文章定位是中级偏入门教程需要你具备以下基础熟悉 HTML、CSS、JavaScript 基本语法了解 MetaMask 钱包的基本操作对以太坊交易和智能合约有初步认识能看懂并修改简单的 Solidity 合约示例合约并不复杂。如果你完全不了解区块链也不要着急。下文会先讲解工作原理再给出可直接运行的代码最后通过“常见问题”帮你避坑。2. 运行原理拆解2.1 整体架构Onefile-unlock 的简化版实现由三部分构成一个可部署的智能合约通常部署在以太坊测试网或主网用于接收付款并记录付款状态一个钱包地址作为收款方一个 HTML 文件内部包含 Web3 逻辑、内容锁定逻辑和解锁后的页面展示。用户操作流程如下打开 HTML 页面某段正文被遮罩遮挡。点击“解锁内容”按钮MetaMask 弹窗弹出交易确认请求。用户确认支付交易被广播到区块链网络。页面监听交易状态一旦确认立即解锁内容。这里的关键点在于交易状态是通过钱包地址和交易哈希Transaction Hash来确定的。2.2 为什么需要智能合约有人可能会问“我直接用 MetaMask 转账不就行了吗为什么还要写合约”直接转账确实可以完成支付但有一个问题你怎么证明某个地址已经付过款纯前端只能通过交易哈希来查询记录而 MetaMask 弹出的交易确认窗口中用户未必能准确区分付款对象。引入一个极简合约后我们可以把“支付动作”和“解锁状态”绑定在一起。用户调用合约的pay()函数并附带金额合约记录调用者的地址前端再通过hasPaid(address)查询该地址是否已解锁。这样做的好处是状态统一记录在合约中不需要额外维护数据库多用户场景下每个地址独立记录互不影响后续可以扩展出不同级别的付费档位。2.3 前端如何与区块链交互HTML 文件通过ethers.js或 Web3.js连接 MetaMask 注入的以太坊 Provider。浏览器访问页面时MetaMask 会自动向页面注入window.ethereum对象前端通过这个对象获取用户账户、发起交易、监听事件。基本交互步骤检测window.ethereum是否存在请求用户授权连接钱包构造合约实例调用合约的支付函数监听交易确认。这些步骤在后面的完整代码中会逐一体现。3. 环境准备与版本说明3.1 环境工具清单由于这是一个单文件 HTML 项目环境准备非常简单。你只需要以下工具工具说明浏览器Chrome、Edge、Firefox 均可推荐 ChromeMetaMask 钱包插件用于连接以太坊网络、签名交易文本编辑器VS Code、Sublime Text甚至记事本都可以本地 HTTP 服务器可选推荐用 VS Code Live Server 或 Python 自带服务测试网代币用于测试支付流程可在 Sepolia 水龙头免费领取3.2 网络与版本说明因为这是一个示例项目本文使用的关键库和网络信息如下Solidity 版本^0.8.0ethers.js通过 CDN 引入使用 v5 版本v6 部分 API 有变化需要按实际情况调整智能合约部署网络默认使用以太坊 Sepolia 测试网MetaMask安装最新稳定版即可。版本需要根据你的项目实际情况调整。正式部署到主网前一定先在小额测试网验证完整流程。4. 完整实战案例4.1 项目结构设计虽然最终只有一个 HTML 文件但为了演示清晰我们分两步走先编写并部署一个极简合约再编写一个自包含的 HTML 文件集成 ABI、合约地址和前端逻辑。如果你想把合约也编译成字节码嵌入 HTML也可以实现但会增加文件体积。本文采用“合约部署到链上HTML 只存 ABI 和地址”的方式这是最通用的做法。推荐本地目录结构onefile-unlock/ ├── index.html # 最终的单文件应用 ├── Paywall.sol # 智能合约源码开发时使用不属于最终文件 └── README.md # 说明文档4.2 编写智能合约合约的作用是接收用户付款并记录每个地址的付款状态。这里不搞复杂功能只保留核心逻辑。文件路径Paywall.sol// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Paywall { address public owner; uint256 public unlockPrice; mapping(address bool) public hasPaid; event ContentUnlocked(address indexed user, uint256 amount); constructor(uint256 _price) { owner msg.sender; unlockPrice _price; } function pay() external payable { require(msg.value unlockPrice, Insufficient payment); require(!hasPaid[msg.sender], Already unlocked); hasPaid[msg.sender] true; // 转账给 owner (bool sent, ) owner.call{value: msg.value}(); require(sent, Failed to send Ether); emit ContentUnlocked(msg.sender, msg.value); } function setPrice(uint256 _newPrice) external onlyOwner { unlockPrice _newPrice; } modifier onlyOwner() { require(msg.sender owner, Not owner); _; } }关键点说明unlockPrice是解锁价格单位是 weihasPaid是一个映射记录地址是否完成支付pay()是付费函数用户调用并附带足够金额后hasPaid[msg.sender]会被标记为trueowner.call{value: msg.value}()会把收到的以太币实时转给合约部署者。该合约是示例性质生产环境建议加入暂停开关、提款保护、价格修改时的事件通知等机制。4.3 部署合约部署方式有两种使用 Remix IDE或者使用 Hardhat 脚本。这里推荐 Remix因为不需要额外安装环境。操作步骤打开 Remix IDE 新建文件Paywall.sol粘贴上面的代码在编译面板选择 Solidity0.8.x版本点击 Compile切换到 Deploy 面板Environment 选择Injected Provider - MetaMask确认 MetaMask 当前网络是 Sepolia并且账户里有测试 ETH在构造函数参数中输入价格例如1000000000000000表示 0.001 ETH点击 DeployMetaMask 弹窗确认交易。部署成功后记下合约地址后面会用到。4.4 编写单文件 HTML现在进入核心部分编写一个直接双击打开就能运行的index.html。文件路径index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleOnefile Unlock - Crypto Paywall Demo/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; background: #f5f7fb; color: #1f2937; display: flex; justify-content: center; padding: 40px 16px; } .container { max-width: 720px; width: 100%; background: #fff; border-radius: 16px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06); padding: 32px; } .article-header { margin-bottom: 24px; } .article-header h1 { font-size: 24px; margin-bottom: 8px; } .article-header .meta { color: #6b7280; font-size: 14px; } .article-content { line-height: 1.8; font-size: 15px; } .article-content .preview p { margin-bottom: 16px; } .locked-content { position: relative; filter: blur(6px); user-select: none; pointer-events: none; background: #f9fafb; padding: 16px; border-radius: 12px; } .locked-content.unlocked { filter: none; user-select: auto; pointer-events: auto; background: transparent; padding: 0; } .paywall-overlay { text-align: center; padding: 32px 16px; background: linear-gradient(135deg, #f6d365 0%, #fda085 100%); border-radius: 12px; margin-top: 16px; } .paywall-overlay h3 { margin-bottom: 8px; } .paywall-overlay p { color: #4b5563; font-size: 14px; margin-bottom: 20px; } button { cursor: pointer; border: none; border-radius: 8px; padding: 12px 24px; font-size: 14px; font-weight: 500; transition: background 0.2s ease; } button.primary { background: #111827; color: #fff; } button.primary:hover { background: #1f2937; } button:disabled { cursor: not-allowed; opacity: 0.5; } .status { margin-top: 16px; font-size: 14px; color: #2563eb; text-align: center; word-break: break-all; } .status.error { color: #dc2626; } .unlocked-message { background: #ecfdf5; border: 1px solid #a7f3d0; color: #065f46; padding: 12px; border-radius: 8px; margin-top: 16px; font-size: 14px; text-align: center; } /style /head body div classcontainer div classarticle-header h1 付费解锁完整内容演示/h1 div classmeta这是一个单文件 HTML 实现的加密货币付费墙示例/div /div div classarticle-content div classpreview p这是文章开头的一段免费内容所有访客都可以阅读。/p p在下方可以看到一部分被模糊遮挡的付费内容预览。当交易确认后遮挡会自动消失。/p /div div idlockedContent classlocked-content h3付费内容区域/h3 p恭喜你已经成功解锁了这篇付费文章。/p p在这里可以放置你真正想售卖的完整内容代码示例、高清资源链接、深度教程、工具下载地址等。/p p如果这是你的正式项目建议结合 IPFS、云存储或者后端接口来承载大量付费资源避免把敏感信息直接嵌在前端。/p /div div idpaywallOverlay classpaywall-overlay h3解锁完整内容/h3 p使用 MetaMask 支付 0.001 ETH测试网即可解锁本篇文章/p button idunlockBtn classprimary立即解锁/button div idstatus classstatus/div /div div idunlockedMessage classunlocked-message styledisplay: none; ✅ 支付成功内容已解锁 /div /div /div !-- 引入 ethers.js v5 -- script srchttps://cdn.jsdelivr.net/npm/ethers5.7.2/dist/ethers.umd.min.js/script script // // 核心配置部署合约后替换成你的实际地址 // const CONTRACT_ADDRESS 0xYourContractAddressHere; // 合约 ABI与 Paywall.sol 对应 const CONTRACT_ABI [ constructor(uint256 _price), function pay() external payable, function hasPaid(address) external view returns (bool), function owner() external view returns (address), function unlockPrice() external view returns (uint256), function setPrice(uint256 _newPrice) external, event ContentUnlocked(address indexed user, uint256 amount) ]; let provider, signer, contract; let alreadyUnlocked false; // // 工具函数 // const statusEl document.getElementById(status); const unlockBtn document.getElementById(unlockBtn); const lockedContent document.getElementById(lockedContent); const unlockedMessage document.getElementById(unlockedMessage); const overlay document.getElementById(paywallOverlay); function setStatus(msg, isError false) { statusEl.textContent msg; statusEl.classList.toggle(error, isError); } async function init() { if (typeof window.ethereum undefined) { setStatus(未检测到 MetaMask请先安装浏览器钱包插件。, true); unlockBtn.disabled true; return; } try { provider new ethers.providers.Web3Provider(window.ethereum); await provider.send(eth_requestAccounts, []); signer provider.getSigner(); contract new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer); // 检查当前用户是否已经付过费 const address await signer.getAddress(); const paid await contract.hasPaid(address); if (paid) { unlockContent(); } else { setStatus(钱包已连接等待解锁操作。); } } catch (err) { console.error(err); setStatus(连接钱包失败 err.message, true); } } async function handleUnlock() { if (!contract) { setStatus(请先连接钱包再执行解锁操作。, true); return; } try { setStatus(正在请求支付请在 MetaMask 中确认交易...); unlockBtn.disabled true; const tx await contract.pay({ value: ethers.utils.parseEther(0.001) }); setStatus(交易已提交等待链上确认交易哈希 tx.hash); // 等待 1 个区块确认测试环境 1-2 个即可 const receipt await tx.wait(1); if (receipt.status 1) { unlockContent(); } else { setStatus(交易失败请检查网络状态后重试。, true); unlockBtn.disabled false; } } catch (err) { console.error(err); // 用户取消交易或交易失败 setStatus(解锁失败 err.message, true); unlockBtn.disabled false; } } function unlockContent() { alreadyUnlocked true; lockedContent.classList.add(unlocked); overlay.style.display none; unlockedMessage.style.display block; setStatus(内容已解锁。); } // 初始化监听 unlockBtn.addEventListener(click, init); // 页面加载时先尝试连接不强制弹窗 window.addEventListener(load, async () { if (typeof window.ethereum ! undefined) { try { provider new ethers.providers.Web3Provider(window.ethereum); const accounts await provider.listAccounts(); if (accounts.length 0) { signer provider.getSigner(); contract new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer); const paid await contract.hasPaid(accounts[0]); if (paid) { unlockContent(); } } } catch (err) { console.warn(自动检查钱包状态失败, err); } } }); /script /body /html4.5 代码逐段解析上面这个 HTML 文件很长但逻辑并不复杂。我们逐个关键部分说明。样式部分被锁定的内容使用filter: blur(6px)做了模糊处理同时禁用鼠标选择和点击事件。这样访问者能看到内容确实存在但无法阅读。解锁后移除这些样式。这种方式只适合演示。有经验的用户打开开发者工具直接删除locked-content样式类就能看到内容因此正式项目需要在内容传输机制上做更强的保护。合约连接部分provider new ethers.providers.Web3Provider(window.ethereum); await provider.send(eth_requestAccounts, []); signer provider.getSigner(); contract new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer);这段代码的作用是获取 MetaMask 注入的 Provider请求用户授权然后创建一个可写合约实例。已支付检查const paid await contract.hasPaid(address);如果当前钱包地址已经调用过pay()hasPaid返回true页面直接解锁不会重复扣费。支付流程const tx await contract.pay({ value: ethers.utils.parseEther(0.001) });调用合约的pay()函数并附带 0.001 ETH。这里要求 MetaMask 当前网络和合约部署网络一致。4.6 运行与验证完成后的操作步骤如下用 VS Code 打开项目文件夹右键index.html选择 “Open with Live Server”。确保 MetaMask 已连接 Sepolia 测试网账户中有测试 ETH。点击页面中的“立即解锁”按钮。MetaMask 弹出交易确认窗口检查金额和地址无误后点击确认。大约几秒到十几秒后页面提示“交易已提交”随后自动解锁内容。预期结果免费内容正常显示付费内容初始状态被模糊遮挡点击按钮后MetaMask 弹出交易确认交易确认后模糊效果消失解锁成功提示出现再次刷新页面仍然保持解锁状态因为合约中已经记录了该地址的支付记录。4.7 将合约地址写入 HTML在正式使用前务必修改下面这行代码const CONTRACT_ADDRESS 0xYourContractAddressHere;改成你自己部署的合约地址。如果地址写错前端调用合约时会报错或者完全无法交互。5. 常见问题与排查思路5.1 常见报错场景问题现象常见原因解决思路提示“未检测到 MetaMask”未安装 MetaMask 或浏览器插件未启用安装并启用 MetaMask刷新页面点击按钮后 MetaMask 没有弹出页面未正确连接钱包或 MetaMask 被浏览器拦截刷新页面后再次点击检查页面是否通过 HTTP 协议访问交易一直处于 Pending 状态测试网拥挤或 Gas 费用设置过低等待区块确认或在 MetaMask 中提高 Gas 限额交易完成后内容仍未解锁前端监听状态失败或合约地址/ABI 配置错误刷新页面查看浏览器控制台报错信息提示“Insufficient payment”支付金额小于unlockPrice调整前端parseEther参数或调整合约价格重复付款当前地址未在合约中查询到已支付记录或前端状态未同步先检查合约中的hasPaid(address)返回值5.2 测试网如何获取测试 ETHSepolia 测试网的 ETH 需要通过水龙头Faucet获取。常见方式是在搜索引擎搜索 “Sepolia Faucet”进入支持 Sepolia 的免费水龙头网站输入你 MetaMask 中的 Sepolia 地址点击领取等待测试 ETH 到账。不同水龙头可能有注册或验证码要求选择一个你能正常使用的即可。5.3 为什么内容没有绝对保护这是整个方案最容易被质疑的地方纯前端 HTML 文件无法真正保护内容。用户阅读 HTML 源码后可以直接看到被模糊遮挡的文字或者修改 CSS 去掉模糊效果。如果你需要更严格的内容保护可以考虑以下思路后端接口鉴权方案内容不直接写在 HTML 中而是放在服务端用户支付后获取访问令牌再请求对应内容IPFS 加密方案将内容加密后上传到 IPFS支付后将解密密钥通过页面脚本返回给用户服务端渲染方案支付完成后服务器返回完整页面但这样就不能称为“一个 HTML 文件”了。5.4 在本地双击打开 HTML 会怎样双击本地文件时页面通过file://协议打开。MetaMask 在某些浏览器配置下可能无法正常注入window.ethereum即使注入也可能会出现连接问题。更稳妥的方式是使用本地 HTTP 服务器。最简单的办法是使用 VS Code 的 Live Server 插件或者运行 Python 命令python3 -m http.server 8000然后访问http://localhost:8000。6. 最佳实践与工程建议6.1 安全边界与内容保护如果只是把内容直接写在 HTML 里加密和付费墙都只是一个“过程仪式”。正式项目中建议把真正值钱的内容放服务端或加密存储。这里提供三个保护等级供参考第一级前端模糊遮挡适合展示演示、测试流程第二级服务端记录用户地址支付确认后返回内容适合个人作品站第三级内容加密 密钥按账户分发适合数字商品销售。6.2 合约设计的注意事项生产环境部署合约前至少要审视以下几个点pay()函数是否允许重复支付当前示例中已经用了hasPaid限制但如果用户想再次支付支持作者需要额外的捐赠函数合约中转移 ETH 用的是call而不是transfer因为transfer的 Gas 限制已经过时是否设置了暂停功能遇到紧急情况可以暂停支付并保护用户资金是否有调整价格的事件日志价格变动最好记录NewPrice事件方便前端同步。6.3 前端错误处理用户取消交易、网络切换、钱包未授权这些都是常见异常。代码中已经通过try-catch捕获并展示错误信息但在真实项目中建议补充以下处理监听 MetaMaskchainChanged事件网络切换后重新加载合约实例监听accountsChanged事件账户切换后重新检查hasPaid状态按钮状态切换要覆盖所有分支避免用户重复点击提交多次交易。6.4 避免把私钥和敏感信息写进前端Onefile-unlock 是纯前端方案因此绝不能把合约部署者私钥、API Key 等敏感信息写入 HTML。所有用户交互都通过 MetaMask 签名完成前端只接触公钥地址和合约 ABI。6.5 测试流程建议在测试网上的完整验证流程如下使用测试 ETH 部署合约使用一个新的钱包地址访问页面确认内容被锁定使用同一钱包发起支付确认内容解锁刷新页面确认解锁状态保留使用另一个未支付的钱包访问页面确认内容仍然锁定检查合约地址中的 ETH 余额是否正确累计。7. 总结与学习路线本文从一个简单的场景出发完整拆解了 Onefile-unlock 这个单文件 HTML 实现的加密付费墙方案。核心收获有三点第一理解了一个最小可用的链上付费墙架构包括智能合约、前端交互和状态查询这三个环节。第二掌握了一套可运行的前端代码基于 ethers.js 连接 MetaMask调用合约支付函数并根据交易状态解锁内容。第三明确了这种方案的边界它适合轻量级、部署简单、不追求高安全性的场景不适合处理需要强内容保护的商业业务。如果你还想继续深入建议按以下方向进阶学习 Hardhat 或 Foundry用脚本自动化部署合约和编写测试研究 ERC-20 代币支付比如让用户用 USDC 支付而不是 ETH了解智能合约安全审计重点学习重入攻击、拒绝服务攻击和 gas 限制的相关知识尝试把内容迁移到 IPFS 加密方案增强内容保护能力。动手把合约部署到 Sepolia 测试网替换掉 HTML 里的合约地址跑通一遍完整流程你就能真正掌握这个单文件付费墙的实现了。如果部署过程中遇到本文没有覆盖的报错可以翻一翻浏览器控制台和 MetaMask 的交易详情通常能找到具体的失败原因。
返回列表