Во-первых, давайте определим, что такое белый список NFT: это процесс получения предварительного одобрения адреса криптокошелька для майнинга.
Это распространенный подход для предотвращения так называемых «газовых войн», когда люди повышают цену на газ, которую они готовы заплатить за майнинг NFT, чтобы их транзакции были приняты первыми, и может быть полезным маркетинговым инструментом, когда люди добавляются в белый список после совершения определенных действий (например, в обмен на подписку на рассылку по электронной почте).
В этой статье мы рассмотрим шаги, связанные с реализацией системы такого типа с использованием смарт-контрактов на блокчейне Ethereum.
Хотя существует множество подходов, мы будем использовать систему купонов, в которой адреса кошельков подписываются вне цепочки таким образом, чтобы смарт-контракт мог проверить, что они получены из надежного источника.
К концу этой статьи вы сможете:
- Добавить в смарт-контракт функцию «белых списков», которая позволит предварительно одобренным кошелькам майнить один NFT.
- Создать веб-решение, интегрированное с белым списком вашего смарт-контракта.
Как это будет работать?
Поколение
Каждый купон будет представлять собой простой объект javascript, содержащий адрес кошелька, который подписывается вне цепи с помощью закрытого ключа, известного только нам.
Получение
Наши купоны будут храниться в простом JSON-файле и будут доступны через простой API.
Потребление
Полученная подпись купона может быть использована при вызове нашего смарт-контракта для доказательства того, что полученные данные были сгенерированы нами.
Давайте создадим ее
Генерация
Начнем с генерации наших купонов. Мы будем использовать криптографию с открытым ключом для шифрования адреса кошелька внутри нашего «купона».
Приведенный ниже скрипт предоставляет функцию createCoupon, которая принимает адрес и privateKey и возвращает купон.
const { ethers } = require(‘ethers’);
async function createCoupon(address, privateKey) {
// We’ll leverage the ethers library to create a new wallet
// that we’ll use to sign the coupon with the private key.
const signer = new ethers.Wallet(privateKey);
// We need to encode the wallet address in a way that
// can be signed and later recovered from the smart contract.
// Hashing the address using the SHA-256 hashing algorithm
// is a good way to do this.
const message = ethers.utils.solidityKeccak256(
[‘address’],
[address]
);
// Now we can sign the message using the private key.
const signature = await signer.signMessage(ethers.utils.arrayify(message));
// The signature can be expanded into it’s underlying components
// which we can pass directly into the smart contract.
// If we didn’t split the signature here — we’d have to do it
// in the smart contract, which is a bit of a hassle.
let { r, s, v } = ethers.utils.splitSignature(signature);
return {r,s,v}
}
module.exports = {
createCoupon,
}
Нам понадобится пара ключей для работы — подойдет тот же, который использовался для развертывания вашего контракта, но если вам все же нужно сгенерировать ключ, вы можете быстро создать новый кошелек в Metamask и экспортировать закрытый ключ оттуда.
Вот небольшой скрипт на node.js, который будет генерировать наши купоны:
const { createCoupon } = require("./coupons");
const fs = require('fs');
require("dotenv").config();
// Insert private key corresponding to _couponSigner
const privateKey = process.env.PRIVATE_KEY;
// Populate with addresses to whitelist
let addresses = [
// ..
];
const main = async () => {
let output = [];
console.log('Generating...');
for (let i = 0; i < addresses.length; i++) {
let signature = await createCoupon(addresses[i], 0, privateKey);
output.push({
wallet: mint[i],
r: signature.r,
s: signature.s,
v: signature.v
})
}
// Save the generated coupons to a coupons.json file
let data = JSON.stringify(output);
fs.writeFileSync('coupons.json', data);
console.log('Done.');
console.log('Check the coupons.json file.');
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
Сначала убедитесь, что вы заполнили ваш .env файл закрытым ключом кошелька, который будет использоваться для подписи. Затем заполните массив адресов в скрипте списком адресов кошельков.
Запустите node generateCoupons.js для генерации и сохранения ваших купонов в файл coupons.json. Готово!
Получение
Поскольку каждый купон действителен только для одного адреса кошелька, нет никакого риска, если купоны будут раскрыты. Однако, чтобы сохранить белый список в тайне, неплохо было бы спрятать его за конечной точкой API, которая отвечает на адрес кошелька и возвращает соответствующий купон, если он найден.
Хотя написание API для обслуживания этих купонов выходит за рамки данной статьи, я могу показать вам, как легко использовать приведенный ниже код для поиска и возврата нужного купона:
// retrieve the wallet from the query wallet
const wallet = req.query.wallet
// Find a coupon for the passed wallet address
const c = coupons.filter(coupon => coupon.wallet.toLowerCase() === wallet.toLowerCase())
if (0 == c.length) {
return res.status(200).json({
coupon: null,
message: 'Coupon not found'
})
}
return res.status(200).json({coupon: c[0]})
💡 Фреймворк Next.js — отличный выбор для создания этого API и остального фронтенда майнингового сайта.
Потребление
В нашем смарт-контракте давайте начнем с определения struct для представления нашего купона.
struct Coupon {
bytes32 r;
bytes32 s;
uint8 v;
}
Вы можете заметить, что это уже похоже на купон, который мы сгенерировали с помощью Javascript.
В нашем смарт-контракте нам нужно сделать пару вещей, чтобы проверить, что купон действителен.
- Создайте тот же дайджест сообщения (содержащий адрес кошелька), который мы создали в нашем Javascript-коде.
- Использовать этот дайджест сообщения для восстановления подписанта нашего купона. Убедиться, что восстановленный подписант — это мы.
В Solidity мы можем добиться этого, написав две внутренние функции:
// Recover the original signer by using the message digest and
// the passed in coupon, to then confirm that the original
// signer is in fact the _couponSigner set on this contract.
function _isVerifiedCoupon(bytes32 digest, Coupon memory coupon)
internal
view
returns (bool)
{
address signer = ecrecover(digest, coupon.v, coupon.r, coupon.s);
require(signer != address(0), "ECDSA: invalid signature");
return signer == _couponSigner;
}
// Create the same message digest that we know the coupon created
// in our JavaScript code has created.
function _createMessageDigest(address _address)
internal
pure
returns (bytes32)
{
return keccak256(
abi.encodePacked(
"x19Ethereum Signed Message:n32",
keccak256(abi.encodePacked(_address))
)
);
}
Затем мы можем обновить нашу функцию майнинга, чтобы она использовала нашу новую систему купонов:
function mint(Coupon memory coupon)
external
payable
{
require(
_isVerifiedCoupon(_createMessageDigest(msg.sender), coupon),
"Coupon is not valid."
);
// require that each wallet can only mint one token
require(
!_mintedAddresses[msg.sender],
"Wallet has already minted."
);
// Keep track of the fact that this wallet has minted a token
_mintedAddresses[msg.sender] = true;
// ...
}
Вот и все! Важно отслеживать кошельки, которые были отчеканены, чтобы предотвратить повторное использование купонов.
На сайте майнинга нам нужно передать наш купон при вызове функции mint:
async function fetchCoupon(wallet) {
const res = await fetch(`/api/coupons?wallet=${wallet}`)
return await res.json()
}
async function mint(wallet) {
const coupon = await fetchCoupon(wallet)
let tx = await contract.mint(coupon)
// ...
}
Заключение
Вы узнали простой, безопасный и эффективный метод реализации белого списка NFT.
Эта статья является специально переработанным отрывком из моей готовящейся к выпуску книги: «Руководство разработчика по запуску коллекции NFT».
Следите за мной в twitter, чтобы получить больше советов и рекомендаций, связанных с блокчейном, и быть в курсе событий, связанных с книгой!