🎥 如何创建一个安全的 NFT 限制访问网站

  • QuickNode
  • 发布于 2024-05-01 16:27
  • 阅读 13

本文介绍了如何构建一个安全的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 上观看

订阅我们的 YouTube 频道以获得更多视频! 订阅

代码

以下是视频中展示的代码示例。或者,你可以访问 GitHub 上的完整代码库 或使用以下命令将其克隆到本地:

git clone https://github.com/kshitijofficial/nftGating.git

api/destination.js

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));

api/notification.js

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));

api/server.js

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("已连接")
})

api/ABI.json

[
    {
        "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"
    }
]

client/.eslintrc.cjs

/* 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 },
    ],
  },
}

client/index.html

<!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>

client/src/components/Home.css

.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; }
}

client/src/components/Home.jsx

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;

client/src/components/Members.jsx

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;

client/src/components/Wallet.jsx

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;

client/src/images/GM.png

我们❤️反馈!

让我们知道 如果你有任何反馈或对新主题的请求。我们很乐意听取你的意见。

  • 原文链接: quicknode.com/guides/eth...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。