本文介绍了如何构建一个安全的NFT门控网站,重点在于防止NFT转移和验证用户持有情况。通过使用QuickAlerts,可以实时监控任何NFT转移,从而实现有效的内容保护。文章包含了详细的代码示例,并提供了一个YouTube视频课程链接,使读者能够深入理解技术的实现过程。
现在,NFT 被作为会员卡、活动门票和地位象征,网站正在寻找方法根据用户是否拥有特定集合中的 NFT 来限制其内容。我们看到 Spotify、Shopify 等推出了这种“代币门控”功能,并有无数教程教你如何自己实现。然而,几乎所有这些实现都缺乏防护,防止简单的黑客攻击,即一旦用户通过检查,就将 NFT 发送给另一个用户,而该用户可以访问限制内容。
在下面的视频中,我们演示了如何创建一个安全的 NFT 门控网站,通过使用 QuickAlerts 实时提醒任何代币转移,以防止重复消费和代币共享。
一个端到端的视频课程,说明如何使用 JavaScript 构建一个安全的 NFT 门控网站
什么是代币门控? | 从头开始创建 NFT 门控网站 | QuickNode - YouTube
QuickNode
131K 订阅者
什么是代币门控? | 从头开始创建 NFT 门控网站 | QuickNode
QuickNode
搜索
信息
购物
点击取消静音
如果播放没有及时开始,请尝试重新启动设备。
你已退出登录
你观看的视频可能会添加到电视的观看历史记录中并影响电视推荐。为避免这种情况,请在计算机上取消并登录 YouTube。
取消确认
分享
包含播放列表
检索共享信息时发生错误。请稍后再试。
稍后观看
分享
复制链接
观看
0:00
/ •直播
•
订阅我们的 YouTube 频道以获得更多视频! 订阅
以下是视频中展示的代码示例。或者,你可以访问 GitHub 上的完整代码库 或使用以下命令将其克隆到本地:
git clone https://github.com/kshitijofficial/nftGating.git
const axios = require('axios');
const headers = {
'accept': 'application/json',
'x-api-key': 'YOUR API KEY'
};
const data = {
name: 'My Destination',
to_url: 'https://7de9-103-171-168-142.ngrok-free.app/webhook',
webhook_type: 'POST',
service: 'webhook',
payload_type: 5
};
axios.post('https://api.quicknode.com/quickalerts/rest/v1/destinations', data, { headers })
.then(response => console.log("Response Data",response.data))
.catch(error => console.log('error', error));
const axios = require('axios');
const headers = {
'accept': 'application/json',
'x-api-key': 'YOUR API KEY'
};
const data = {
name: 'NFT Transfer',
expression: 'KHR4X2xvZ3NfdG9waWMxID1+ICdhMmM4MzQ1YjMwQjRjMjZlMTE4RjMwNzI2OWRFOTY5MTNEYmRCNGJDJykgJiYgDQoodHhfbG9nc19hZGRyZXNzID09ICcweEQ2MTg1ODE0MDIyMjZjOTJiMTRDOWY0ODcwNzk5YjMwMDBBQzRDNzcnKSAmJiANCih0eF9sb2dzX3RvcGljMCA9PSAnMHhkZGYyNTJhZDFiZTJjODliNjljMmIwNjhmYzM3OGRhYTk1MmJhN2YxNjNjNGExMTYyOGY1NWE0ZGY1MjNiM2VmJyk=',
network: 'ethereum-sepolia',
destinationIds: ['fa375e89-8c4c-4260-8c0b-91cb20cd4da9']
};
axios.post('https://api.quicknode.com/quickalerts/rest/v1/notifications', data, { headers })
.then(response => console.log(response.data))
.catch(error => console.log('error', error));
const express = require('express');
const cors = require('cors')
const {Web3} = require('web3');
const ABI =require('./ABI.json')
const socketIO = require('socket.io')
const app = express();
app.use(cors())
app.use(express.json());
const web3 =new Web3('YOUR API KEY')
const contractAddress = 'YOUR CONTRACT ADDRESS';
const contract = new web3.eth.Contract(ABI,contractAddress);
// console.log(contract)
const fetchNFTs = async(account)=>{
try{
const nftBalance = await contract.methods.balanceOf(account).call();
return {userNFTs:Number(nftBalance)}
}catch(error){
console.log('错误 fetching NFTs',error);
}
}
app.post('/members',async(req,res)=>{
try{
const account = req.body.from;
console.log(account)
const numNFTs = await fetchNFTs(account)
if(numNFTs.userNFTs>0){
res.status(200).json({status:200,numNFTs})
}else{
res.status(404).json({status:404,message:"你没有拥有任何 NFT",numNFTs});
}
}catch(error){
res.status(500).json({status:500,message:"内部服务器错误"});
}
})
app.post('/webhook',async(req,res)=>{
try{
const account = req.body[0].from;
const numNFTs = await fetchNFTs(account);
io.emit('nftsUpdated',{userNFTs:numNFTs.userNFTs})
res.status(200).json({status:200,message:"Webhook 被触发"})
}catch(error){
console.error(error)
}
})
const PORT=3000;
const server = app.listen(PORT,()=>{
console.log(`服务器运行在 ${PORT}`)
})
const io = socketIO(server);
io.on('connection',()=>{
console.log("已连接")
})
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "approved",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "_fromTokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "_toTokenId",
"type": "uint256"
}
],
"name": "BatchMetadataUpdate",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "_tokenId",
"type": "uint256"
}
],
"name": "MetadataUpdate",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "player",
"type": "address"
},
{
"internalType": "string",
"name": "tokenURI",
"type": "string"
}
],
"name": "awardItem",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getApproved",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ownerOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "tokenURI",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
/* eslint-env node */
module.exports = {
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
.beautiful-sentence {
font-family: "Arial", sans-serif;
font-size: 18px;
color: #ff4081;
text-align: center;
animation: rainbow 5s infinite;
}
@keyframes rainbow {
0% { color: #ff4081; }
14% { color: #ff7f00; }
28% { color: #ffff00; }
42% { color: #00ff00; }
57% { color: #0000ff; }
71% { color: #4b0082; }
85% { color: #9400d3; }
100% { color: #ff4081; }
}
import {useNavigate,useLocation} from "react-router-dom"
import './Home.css'
const Home=()=>{
const location = useLocation()
const navigateTo=useNavigate()
const revealMsg=async()=>{
try{
const account = location.state.address;
const res = await fetch(`http://localhost:3000/members`,{
method:"POST",
headers:{
"content-type":"application/json"
},
body:JSON.stringify({from:account})
})
const data = await res.json();
if(data.status===200){
navigateTo("/members")
}else{
window.alert("你当前不持有任何地址为 0xd618581402226c92b14c9f4870799b3000ac4c77 的集合中的 NFT")
}
}catch(error){
console.error(error)
}
}
return(
<>
<span className="beautiful-sentence">我有一个秘密消息给持有我 NFT 集合的用户,地址为 <br></br>0xd618581402226c92b14c9f4870799b3000ac4c77</span>
<br></br>
<br></br>
<button onClick={revealMsg}>揭示消息</button>
</>
)
}
export default Home;
import {useState,useEffect} from "react";
import {useNavigate} from "react-router-dom"
import io from 'socket.io-client'
import welcomeImg from '../images/GM.png'
const Members=()=>{
const [socket,setSocket]=useState(null);
const navigateTo = useNavigate();
useEffect(()=>{
const socketInstance = io('http://localhost:3000');
setSocket(socketInstance);
return()=>{
socketInstance.disconnect()
}
},[])
useEffect(()=>{
if(socket){
socket.on('nftsUpdated',(data)=>{
if(data.userNFTs<1){
navigateTo('/')
alert("你已被登出,因为你不再持有地址为 0xd618581402226c92b14c9f4870799b3000ac4c77 的集合中的 NFT")
}
})
}
},[socket])
return<>
<p>感谢你成为我 NFT 集合的持有者,这是你的消息:</p>
<img src={welcomeImg}></img>
</>
}
export default Members;
import { useNavigate } from "react-router-dom";
const Wallet=()=>{
const navigateTo =useNavigate()
const connectWallet =async()=>{
try{
if(window.ethereum){
const accounts = await window.ethereum.request({method:'eth_requestAccounts'});
navigateTo("/home",{state:{address:accounts[0]}})
}else{
alert("请安装 Metamask")
}
}catch(error){
console.error(error)
}
}
return<><button onClick={connectWallet}>连接钱包</button></>
}
export default Wallet;
让我们知道 如果你有任何反馈或对新主题的请求。我们很乐意听取你的意见。
- 原文链接: quicknode.com/guides/eth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!