Usando Pinata Submarine, Hardhat e Ethers.js
NFTs não são apenas obras de arte digitais. Eles são mecanismos em camadas para comprovar propriedade, acesso, direitos e muito mais. Avatares de NFT (às vezes conhecidos como PFP - ProFile Picture) têm sido um dos casos de uso mais populares para NFTs. Você pode ver isso com CryptoPunks, Bored Ape Yacht Club (BAYC) e mais. Mas o motivo pelo qual eles são populares não termina com a imagem do perfil à qual você tem acesso. Mais do que tudo, esses NFTs PFP são tokens de adesão.
Os proprietários de tokens BAYC podem atualizar suas imagens de perfil, certamente. Mas eles também têm acesso exclusivo a conteúdo, canais no Discord, páginas web e muito mais, apenas para membros. O mesmo é verdadeiro para CryptoPunks e dezenas de outros projetos PFP. Este é um dos casos de uso mais interessantes para NFTs. A parte do avatar do perfil é um complemento divertido, mas o verdadeiro motivo para possuir os tokens é o acesso exclusivo a outros conteúdos.
Hoje, vamos construir uma página que só é acessível para membros que possuem um determinado NFT. Os membros poderão acessar a página, a propriedade do token será verificada e eles poderão visualizar uma imagem especial à qual apenas os membros têm acesso. Como uma camada de verificação adicional, a imagem será servida usando um CID do IPFS, enquanto se mantém privada. Os membros podem baixar a imagem e verificar facilmente o CID e a autenticidade.
Vamos começar!
Sumário
1 . Configurando
6 . Fazendo o upload dos metadados
7 . Realmente implantando o contrato agora
8 . Servindo Conteúdo Protegido Baseado na Propriedade de NFT
9 . Fazendo Submarining no Pinata
10 . Programando nosso site com conteúdo restrito
11 . Concluindo
Configurando
Vamos usar nosso próprio contrato ERC721 para emitir nossos tokens. Estamos fazendo isso porque queremos vender os NFTs na data de lançamento em nosso próprio site. A randomização é importante e só podemos controlá-la se emitirmos cada token no momento da compra.
Então, tendo isso em mente, vamos precisar de algumas coisas:
- Node.js instalado > v12
- Uma conta com plano pago no Pinata
- Hardhat instalado
- Um editor de código
- Uma pasta com arquivos randomizados que servirão como seus NFTs
Uma observação rápida: a maioria dos projetos de NFT PFP usa algum tipo de algoritmo gerador para criar atributos para seus NFTs e alguns até usam um algoritmo para criar os arquivos de mídia. Um script desse tipo está fora do escopo deste tutorial. Em vez disso, vou criar uma pasta com Pinatas aleatórios. Vou escrever um script bem básico que determina qual Pinata está associado a qual ID de token.
Pronto para começar?
Criando o Contrato
Eu sempre gosto de começar com o contrato inteligente. É o mais fácil de falhar e o que mais impacta seu projeto se você cometer um erro. Felizmente, não vamos modificar muito em relação a um contrato ERC721 padrão.
Em uma postagem anterior, você viu como adicionar de volta o antigo método setTokenUri
que costumava aparecer em todos os contratos ERC721. Neste post, vamos aproveitar a variável baseUri
no contrato e definiremos esse URI quando implantarmos nosso contrato.
Vamos começar criando uma pasta para armazenar todo o nosso trabalho com contratos e scripts.
mkdir pfp-nfts
Mude para esse diretório e execute:
npm init -y
Agora, precisamos instalar o hardhat para nos ajudar no desenvolvimento e implantação do Solidity. Você pode instalá-lo assim:
npm install -D hardhat
O -D
apenas informa ao npm
que esta será uma dependência de desenvolvimento. Se alguma parte do nosso projeto precisasse ser construída para produção, as dependências de desenvolvimento não seriam incluídas.
Para nos ajudar a começar com o hardhat mais rapidamente, eles fornecem um início rápido com um projeto de exemplo como guia. Para iniciar isso, execute:
npx hardhat
Selecione o projeto básico de exemplo
e apenas pressione Enter nas outras perguntas.
Se você abrir a pasta do projeto no seu editor de código favorito, verá que nosso projeto de exemplo inclui um contrato inteligente muito simples chamado Greeter.sol
. Isso é encontrado na pasta contracts
. Não vamos usar esse contrato, mas sinta-se à vontade para dar uma olhada nele para entender o que foi gerado pelo hardhat.
Voltando à linha de comando, vamos instalar o OpenZeppelin e pegar um de seus contratos pré-construídos e auditados.
npm install @openzeppelin/contracts
Com isso instalado, vamos atualizar nosso contrato Greeter.sol
. Vou mudar o nome do contrato para PFPinatas.sol
, mas você pode alterar o nome para o que fizer sentido para você. Depois de fazer essa alteração, vamos substituir o conteúdo do contrato pelo exemplo que o OpenZeppelin fornece aqui.
Seu arquivo deve ficar assim agora:
// contracts/GameItem.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract GameItem is ERC721URIStorage {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
constructor() ERC721("GameItem", "ITM") {}
function awardItem(address player, string memory tokenURI)
public
returns (uint256)
{
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(player, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
}
Obviamente, não vamos manter isso. Precisamos modificar um pouco para lidar com nosso caso de uso. As alterações que vamos fazer são as seguintes:
- Mudar o nome do token
- Inicializar uma variável baseURI quando o contrato for implantado
- Subscrever o método
tokenURI
incluído no contrato ERC721
Parece muito, mas realmente não é. Na verdade, nossas atualizações adicionam apenas 5 linhas ao contrato inteiro. É uma atualização tão pequena que vou sugerir que você simplesmente substitua o arquivo inteiro:
// contracts/PFPinatas.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract PFPinatas is ERC721 {
using Strings for uint256;
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
string private baseURI;
constructor(string memory baseUri) ERC721("PFPinatas", "PFPP") {
baseURI = baseUri;
}
function tokenURI(uint256 tokenId) override view public returns (string memory) {
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}
function mintTo(address receiver)
public
returns (uint256)
{
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_safeMint(receiver, newItemId);
return newItemId;
}
Nessas atualizações, paramos de usar o utilitário ERC721URIStorage
. Gostei muito que o OpenZeppelin tenha começado a usar isso em seu exemplo, mas para o nosso tutorial de PFP, realmente queremos usar o conceito baseURI
.
Por quê?
A maioria dos projetos PFP trabalha com milhares de arquivos que se tornarão NFTs. Você não quer ter que escrever um script para enviar arquivos individuais ao provedor de armazenamento de sua escolha quando se pode fazer o upload de uma única pasta. A localização dessa pasta única se torna a baseURI.
Adicionamos um novo utilitário do contrato Strings
. Isso ocorre porque estamos substituindo o método tokenURI
e precisávamos usar a função toString()
. Falando nisso, modificamos o método base tokenURI
, pois queremos retornar a baseURI
com o tokenId
concatenado. A baseURI
está sendo definida quando o contrato é implantado. Você pode ver isso no construtor do contrato.
Ok, temos um contrato inteligente de 31 linhas. Nada mal. Vamos testar isso para ter certeza de que funciona.
Testando o Contrato
Você já deve ter uma pasta de teste que foi criada quando o projeto de exemplo do hardhat foi gerado. Dentro dessa pasta, há um arquivo chamado sample-test.js
. Nosso primeiro passo é renomear o arquivo. Dê um nome que faça sentido para o seu teste. Vou apenas usar pfpinata-test.js
.
Depois de alterar o nome, observe como os testes funcionam. Explore o arquivo existente. Conheça-o. Aprenda a amá-lo. E então destrua tudo!
Mas falando sério. Vamos substituir todo o conteúdo do arquivo por isto:
const { expect } = require("chai");
const { ethers } = require("hardhat");
const BASE_URI = "ipfs://FAKE_IPFS_CID/";
const TEST_WALLET = "0x243dc2F47EC5A0693C5c7bD39b31561cCd4B0e97";
describe("PFPinatas", function () {
it("Should mint a new token", async function () {
const PFPinata = await ethers.getContractFactory("PFPinatas");
const pfpPinata = await PFPinata.deploy(BASE_URI);
await pfpPinata.deployed();
await pfpPinata.mintTo(TEST_WALLET);
expect(await pfpPinata.ownerOf(1)).to.equal(TEST_WALLET);
});
it("Should return a valid token URI", async function () {
const PFPinata = await ethers.getContractFactory("PFPinatas");
const pfpPinata = await PFPinata.deploy(BASE_URI);
await pfpPinata.deployed(BASE_URI);
await pfpPinata.mintTo(TEST_WALLET);
expect(await pfpPinata.ownerOf(1)).to.equal(TEST_WALLET);
expect(await pfpPinata.tokenURI(1)).to.equal(`${BASE_URI}1`);
});
});
Como você pode ver no meu exemplo, eu tenho apenas dois testes. Eu queria testar se a cunhagem funcionava e se o tokenURI
correto seria retornado, já que foi algo para o qual adicionamos uma substituição em nosso contrato. Na realidade, você provavelmente deve escrever testes para tudo. Provavelmente existem alguns testes previamente escritos para o contrato OpenZeppelin ERC721 que você pode encontrar, mas não vamos fazer isso aqui.
Para executar esses testes, retorne à linha de comando e digite:
npx hardhat test
Você deve ver uma saída como esta:
Estou me sentindo bastante confiante com nosso contrato, e você? Vamos descobrir como implantá-lo na rede de teste (testnet) (desculpe, pessoal, não tenho dinheiro sobrando para implantar contratos na rede principal (mainnet) para tutoriais). Uma vez implantado na rede de teste (testnet), escreveremos um script para emitir alguns tokens para alguns endereços de carteira diferentes e, em seguida, podemos nos divertir com submarining usando o Pinata.
Implantando o Contrato
Para implantar nosso contrato na rede de teste, precisamos fazer três coisas:
- Escolher em qual rede de teste Ethereum implantar
- Certificar-se de que temos ETH da rede de teste para aquela rede de teste específica
- Atualizar nosso arquivo de configuração
Para o propósito deste tutorial, vamos usar a rede de teste Rinkeby. Portanto, o passo #1 está completo. Para obter ETH da rede de teste Rinkeby, você pode visitar a torneira (faucet) aqui. Quando tiver isso, o passo #2 estará completo. Vamos para o #3.
Antes de prosseguirmos, você precisará decidir como deseja implantar numa rede em tempo real. Existem muitos provedores de nós Ethereum por aí, então você pode escolher qualquer provedor que desejar. Conforme a documentação do Hardhat, vou usar o Alchemy. Eles têm um bom plano gratuito que funcionará bem para nós. Se você estiver usando um provedor diferente, basta alterar as chaves API e os URLs RPC adequadamente.
De volta ao diretório do seu projeto, encontre o arquivo hardhat.config.js. Você pode atualizá-lo para ficar assim:
require("@nomiclabs/hardhat-waffle");
// Este é um exemplo de tarefa do Hardhat. Para aprender como criar a sua própria, vá para
// https://hardhat.org/guides/create-task.html
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();
for (const account of accounts) {
console.log(account.address);
}
});
const ALCHEMY_API_KEY = "KEY";
const RINKEBY_PRIVATE_KEY = "YOUR RINKEBY PRIVATE KEY";
module.exports = {
solidity: "0.8.4",
networks: {
rinkeby: {
url: `https://eth-rinkeby.alchemyapi.io/v2/${ALCHEMY_API_KEY}`,
accounts: [`0x${RINKEBY_PRIVATE_KEY}`],
},
},
};
Novamente, se você estiver usando um provedor de nós diferente, substitua as referências ao Alchemy pelas variáveis apropriadas dos provedores que você selecionou.
O RINKEBY_PRIVATE_KEY
é a chave privada para a sua carteira Rinkeby na rede de teste, onde você envia fundos da torneira (faucet). É muito importante proteger essa chave. Inclua-a como a variável no seu arquivo de configuração, mas certifique-se de não incluir esse arquivo em um sistema de controle de versão, como o git.
Essas são todas as edições que precisamos fazer. Claro, ao implantar na rede principal, você seguiria essas etapas, mas com suas variáveis atualizadas para suportar a rede principal em vez da rede de teste.
Agora, só precisamos de um script para implantar nosso contrato. Felizmente, já temos uma pasta de scripts graças à nossa estrutura de projeto Hardhat. Encontre a pasta de scripts
e dê uma olhada no arquivo sample-script.js
. Vamos renomear este arquivo para deploy.js
. Vamos seguir o padrão nesse arquivo existente, mas atualizá-lo para o nosso contrato real. Você pode atualizar seu arquivo para ficar assim:
// Nós exigimos explicitamente o Hardhat Runtime Environment aqui. Isso é opcional,
// mas útil para executar o script de forma autônoma por meio de node <script>.
//
// Ao executar o script com npx hardhat run <script>, você encontrará os membros do
// Ambiente de Execução do Hardhat disponíveis no escopo global.
const hre = require("hardhat");
const BASE_URI = "";
async function main() {
// O Hardhat sempre executa a tarefa de compilação ao executar scripts com sua interface
// de linha de comando.
//
// Se este script for executado diretamente usando node, você pode querer chamar a compilação
// manualmente para garantir que tudo esteja compilado
// await hre.run('compile');
// Obtemos o contrato para implantar
const PFPinata = await hre.ethers.getContractFactory("PFPinatas");
const pfpPinata = await PFPinata.deploy(BASE_URI);
await pfpPinata.deployed();
console.log("PFPinatas deployed to:", pfpPinata.address);
}
// Recomendamos este padrão para poder usar async/await em todos os lugares
// e lidar adequadamente com os erros.
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Mas ESPERE! Você vê que nossa variável BASE_URI
está vazia. Precisamos disso antes de podermos implantar nosso contrato.
Carregando Imagens
Vamos criar outro arquivo na nossa pasta de scripts
chamado upload.js
.
Neste script, vamos fazer algumas coisas. Vamos gerar os metadados para nossos NFTs, vamos fazer upload de uma pasta de imagens e vamos fazer upload de uma pasta de metadados. Se você se lembra, vou criar NFTs a partir de algumas imagens pré-desenhadas. Estou criando e cunhando apenas 4 NFTs. O processo será semelhante para qualquer número de ativos que você planeja cunhar. Lembre-se de que não estamos abordando algoritmos generativos neste post, então vou manter as coisas simples.
Ao nomear seus arquivos de imagem, é importante dar-lhes nomes que corresponderão ao tokenId dos tokens cunhados. No meu caso, tenho apenas quatro imagens, então seriam 1.png, 2.png, 3.png, 4.png
. Precisamos que nossas imagens sejam nomeadas assim porque vamos simplificar o processo de criação de metadados com esses nomes. Acredite em mim.
Antes de começarmos, você vai querer instalar o Axios. Ele torna o processo de fazer requisições HTTP mais simples. Como vamos fazer upload de uma pasta, precisamos ser capazes de percorrer todos os arquivos em nossa pasta para construir os dados que são postados no Pinata. Isso requer alguns pacotes adicionais. Você deve instalar o seguinte:
npm i axios base-path-converter recursive-fs
Agora, no seu arquivo upload.js
, vamos começar fazendo o upload dos nossos arquivos de imagem. Para manter as coisas o mais simples possível, eu recomendo colocar a pasta de imagens que você planeja usar dentro do diretório do projeto. Depois de fazer isso, você precisará obter um JWT do Pinata. Você pode fazer isso se conectando, indo para a página de Chaves (Keys) e gerando uma nova chave (key). Em seguida, atualize seu arquivo upload.js
para ficar assim:
const PinataJWT = "YOUR PINATA JWT";
const fs = require("fs");
const axios = require("axios");
const FormData = require("form-data");
const recursive = require("recursive-fs");
const basePathConverter = require("base-path-converter");
async function main() {
try {
const path = "./YOUR_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT";
const url = `https://api.pinata.cloud/pinning/pinFileToIPFS`;
recursive.readdirr(path, function (err, dirs, files) {
let data = new FormData();
files.forEach((file) => {
data.append(`file`, fs.createReadStream(file), {
filepath: basePathConverter(path, file),
});
});
const metadata = JSON.stringify({
name: "Pinatas",
});
data.append("pinataMetadata", metadata);
return axios
.post(url, data, {
maxBodyLength: "Infinity",
headers: {
"Content-Type": `multipart/form-data; boundary=${data._boundary}`,
Authorization: `Bearer ${PinataJWT}`,
},
})
.then(function (response) {
console.log(response.data);
process.exit(0);
})
.catch(function (error) {
throw error;
});
});
} catch (error) {
console.log(error);
process.exit(1);
}
}
main();
Se você executar esse script na linha de comando com node scripts/upload.js
, você deve ver (quando terminar) uma impressão que inclui o CID para sua pasta.
Fazendo o upload dos metadados
Agora, podemos criar os metadados para nossos NFTs. Você provavelmente já tem um método para fazer isso, mas para fins de completude, vou criar um script que gerará arquivos de metadados para meus quatro tokens. Vou criar isso em seu próprio arquivo dentro da pasta de scripts e vou chamá-lo de metadata.js
.
Como vou gerar dados aleatórios, vou usar uma biblioteca chamada faker. Isso pode ser instalado assim:
npm i faker
Crie uma pasta de metadados na raiz do diretório do seu projeto e, em seguida, no seu arquivo metadata.json
, adicione este código:
const faker = require('faker');
const TOTAL = 4;
const folderCIDForImages = "YOUR_IMAGE_FOLDER_CID";
const gatewayCustomDomain = "YOUR_CUSTOM_GATEWAY_DOMAIN"
const generateRandomMetadata = (id) => {
return {
name: faker.name.findName(),
description: faker.lorem.sentences(),
image: `${gatewayCustomDomain}/ipfs/${folderCIDForImages}/${id}`
}
}
(async () => {
for(let i=1; i < TOTAL + 1; i++) {
const metadata = generateRandomMetadata(i)
fs.writeFileSync(`./metadata/${i}`, JSON.stringify(metadata));
}
console.log("Done!");
})();
Isso criará um objeto de metadados para cada um dos seus NFTs. Novamente, você certamente estará gerando seus metadados de alguma outra maneira do que usando uma biblioteca como o faker, mas isso ilustra como você pode querer fazer isso por meio de um script. No final das contas, meu script acima foi apenas para ter certeza de que eu tinha uma pasta de metadados que eu poderia então enviar para a Pinata.
Agora, vamos usar o mesmo script upload.js
que usamos antes. Modifique a variável de caminho para apontar para a sua pasta de metadados. Para mim, isso é simplesmente mudar ./pinatas
para ./metadata
. Depois de fazer isso, execute seu script de upload com:
node scripts/upload.js
Se você estiver criando um projeto com milhares de arquivos, esse upload pode demorar um pouco. Mas adivinhe o que você vai receber quando terminar? Isso mesmo - é o CID para a baseURI
do seu contrato inteligente.
O URI completo deve ter a seguinte aparência:
SUA_URL_CUSTOMIZADA_DO_GATEWAY/ipfs/IDENTIFICAÇÃO_DO_CID_DA_PASTA_DE_METADADOS
Com esse URI em mãos, acho que é hora de implantar nosso contrato na rede de teste.
Realmente implantando o contrato agora
Nós configuramos previamente nosso arquivo hardhat.config.js
com as chaves apropriadas (carteira e nó Ethereum) para podermos implantar na rede de teste rinkeby. Agora, só precisamos atualizar nosso script de implantação para usar a baseURI
que acabamos de criar.
Para implantar, volte para o seu arquivo scripts/deploy.js
e atualize a variável baseURI com a baseURI que construímos acima. Em seguida, execute este comando:
npx hardat run scripts/deploy.js --network rinkeby
Se tudo correr bem, você verá o endereço do contrato impresso na linha de comando. Parabéns! Você percorreu um longo caminho. Mas ainda não terminamos. Salve esse endereço do contrato, pois precisaremos dele para a próxima parte.
Antes de prosseguirmos, vamos facilitar nosso trabalho nos testes e criar um NFT do nosso novo contrato na nossa carteira. Crie mais um arquivo na pasta de scripts e chame-o de mint.js
. O arquivo deve ficar assim:
const hre = require("hardhat");
async function main() {
const PFPinata = await hre.ethers.getContractFactory("PFPinatas");
const contract = PFPinata.attach("SEU ENDEREÇO DE CONTRATO IMPLANTADO");
const mintedNft = await contract.mintTo("CARTEIRA RINKEBY A UTILIZAR");
console.log("token minted", mintedNft);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Você pode executar isso com o seguinte comando:
npx hardhat run scripts/mint.js --network rinkeby
Esses dois últimos scripts são os lugares mais comuns onde você pode enfrentar problemas, então eu preparei um pequeno guia de solução de problemas aqui:
Erro possível
`insufficient funds for gas * price + value`(fundos insuficientes para o gás * price + value)
Verifique novamente a rede no seu arquivo hardhat.config.js. No meu caso, eu atualizei tudo para Rinkeby, exceto a URL do meu provedor de nó.
Em seguida, verifique novamente se você está acrescentando `--network rinkeby` ao final dos seus scripts. O arquivo de configuração informa ao hardhat quais detalhes usar para uma determinada rede, mas se você não disser qual rede usar, ele usará a padrão (que é local).
Uma última coisa que podemos fazer agora é verificar se nossa carteira contém o NFT como esperamos. Vamos criar um pequeno script para verificar o proprietário de um ID de token.
const hre = require("hardhat");
async function main() {
const PFPinata = await hre.ethers.getContractFactory("PFPinatas");
const contract = PFPinata.attach("ENDEREÇO DO SEU CONTRATO");
const owner = await contract.ownerOf(1);
console.log({owner});
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Execute o script com:
npx hardhat run scripts/getOwner.js --network rinkeby
Isso deve imprimir o endereço da sua carteira Rinkeby.
Servindo Conteúdo Protegido Baseado na Propriedade de NFT
ATUALIZAÇÃO: Agradecimentos ao @Cr0wn_Gh0ul por identificar uma maneira de obter acesso ao conteúdo mesmo que você não seja realmente o proprietário do token. Os detalhes estão no link abaixo, e os exemplos aqui foram atualizados com a correção. 🤝
https://twitter.com/Cr0wn_Gh0ul/status/1441105277462319111
Agora que implantamos nosso contrato e o criamos, vamos criar algum conteúdo "somente para membros" que será acessível para aqueles que possuem um NFT do nosso contrato. Para fazer isso, vou criar um aplicativo simples em Next.js para que tenhamos funcionalidade de cliente e servidor. Aqui está um resumo de alto nível:
- Aplicativo React que utiliza a Metamask para autenticação e assinatura de transações
- Backend que verifica a mensagem assinada
- Backend que verifica se o token pertence ao endereço
- Servir conteúdo se for de propriedade
- Rejeitar se não for
É aqui que a nova funcionalidade Submarining do Pinata realmente brilha. Vamos criar nosso aplicativo Next.js e, em seguida, entraremos nos detalhes. Na linha de comando na raiz do seu projeto, execute:
npx create-next-app pfp-client
Isso criará um novo projeto Next.js dentro do seu diretório existente. Mude para esse diretório com cd pfp-client
. Vou manter isso simples e contar com o Metamask e o objeto window.ethereum
injetado no lado do cliente. Mas no servidor, precisamos do ethers.js
e de uma biblioteca auxiliar chamada ethereumjs-util
. Como também precisamos enviar dados para nosso servidor backend, vamos instalar o axios
.
npm i ethers ethereumjs-util axios uuid next-iron-session
Depois que tudo estiver instalado, vamos fazer uma pausa no código por alguns minutos e vamos submergir algum conteúdo no Pinata!
Fazendo Submarining no Pinata
Vá para sua conta no Pinata. Lembre-se de que você precisa estar em um plano pago para acessar o submarining. Uma vez lá, vá para a página do gerenciador de arquivos. Clique em "Upload" e, em seguida, escolha "File". Você será guiado pelo processo de upload. No Passo 2, certifique-se de ativar a opção submarining.
Vou submergir um gif incrível do Pinata (aviso para epilepsia: imagens piscantes e cores brilhantes). Você pode escolher algo mais prático, mas, para fins deste tutorial, vou mantê-lo simples. Quando terminar o upload, você verá um CID na interface. Copie-o e salve-o em algum lugar. Vamos precisar dele em breve.
Depois que o arquivo for carregado, você precisará gerar uma chave API. O Submarining é um recurso beta da nossa nova infraestrutura. Clique no menu suspenso do avatar no canto superior direito e clique em V2 API Keys. Uma vez lá, gere uma nova chave. Copie essa chave e salve-a em algum lugar, pois estamos prestes a voltar à programação.
Programando nosso site com conteúdo restrito
Antes de começarmos, vamos encontrar na raiz do nosso diretório do projeto a pasta artifacts
. Nela, você deve ver uma pasta contracts
. Dentro dessa pasta, você deve ter uma pasta chamada PFPinatas.so
. Copie o PFPinatas.json
de dentro dessa pasta. Cole-o na raiz da sua pasta pfp-client
. Estamos fazendo isso porque precisamos acessar a ABI do contrato.
Agora, a partir da configuração padrão do Next.js na nossa pasta pfp-client
, vamos editar apenas dois arquivos existentes. Estou tentando manter este exemplo o mais simples possível. No Next.js, nosso código do lado do servidor fica na pasta pages/api
. O aplicativo React está no arquivo pages.index.js
. Vamos atualizar o pages/api/hello.js
e o pages/index.js
. Simples, certo?
A primeira coisa que você deseja fazer é atualizar o pages/api/hello.js
para ser chamado de pages/api/verify.js
. Agora, vamos atualizar nosso arquivo pages/index.js
. Vamos manter todo o nosso código React em um único arquivo, mas você pode refatorar isso da maneira que achar melhor. Aqui está o código (não se preocupe, vamos passar por todo o código).
import Head from "next/head";
import Image from "next/image";
import styles from "../styles/Home.module.css";
import { useEffect, useState } from "react";
import axios from "axios";
export default function Home() {
const [ethereum, setEthereum] = useState(null);
const [isPFPinata, setIsPFPinata] = useState(null);
const [secretUrl, setSecretUrl] = useState(null);
useEffect(() => {
if (typeof window.ethereum !== "undefined") {
console.log("MetaMask is installed!");
setEthereum(window.ethereum);
}
if (ethereum) {
ethereum.request({ method: "eth_requestAccounts" });
}
}, [ethereum]);
const handleProveIt = async () => {
// Primeiro, obtemos a mensagem para assinar de volta a partir do servidor
const messageToSign = await axios.get("/api/verify");
const accounts = await ethereum.request({ method: "eth_requestAccounts" });
const account = accounts[0];
const signedData = await ethereum.request({
method: "personal_sign",
params: [JSON.stringify(messageToSign.data), account, messageToSign.data.id],
});
try {
const res = await axios.post("/api/verify", {
address: account,
signature: signedData
});
const url = res.data;
setIsPFPinata(true);
setSecretUrl(url);
} catch (error) {
if (error.response && error.response.status === 401) {
setIsPFPinata(false);
}
}
};
return (
<div className={styles.container}>
<Head>
<title>Welcome, Pinata</title>
<meta name="description" content="A Pinata Members Only Site" />
<link rel="icon" href="/logo.svg" />
</Head>
<main className={styles.main}>
<h1>Hey! Are you a PFPinata?</h1>
<p>
Members get access to a sweet flashy Pinata. But you have to prove
you're a member to get it...
</p>
{isPFPinata === false ? (
<div>
<h4>You're not one of us</h4>
<img
src="URL to a funny Gif (or whatever you want)"
alt="Not one of us"
/>
</div>
) : isPFPinata === true ? (
<div style={{textAlign: "center"}}>
<h4>Welcome to the club</h4>
<img style={{maxWidth: "90%"}} src={secretUrl} alt="One of us" />
</div>
) : (
<button className={styles.btn} onClick={handleProveIt}>
I'm A PFPinata
</button>
)}
</main>
<footer className={styles.footer}>
<a
href="https://pinata.cloud"
target="_blank"
rel="noopener noreferrer"
>
Powered by{" "}
<span className={styles.logo}>
<Image src="/logo.svg" alt="Pinata Logo" height={30} width={30} />
</span>
</a>
</footer>
</div>
);
}
Eu mencionei isso antes, mas novamente, estamos usando o objeto window.ethereum
injetado pela Metamask. Essa é a primeira coisa que verificamos e definimos como uma variável de estado. Também nos certificamos de solicitar a conta Ethereum do usuário imediatamente. Isso força o desbloqueio da Metamask se estiver mascarado. Também temos uma variável de estado para a URL secreta do conteúdo que queremos que possa ser acessado pelos membros. O servidor fornecerá isso se você possuir o NFT. A última variável de estado é isPFPinata
. Este é um indicador para atualizar a interface do usuário.
Em seguida, temos uma função chamada handleProveIt
. Essa função chamará nosso servidor e solicitará uma mensagem para ser assinada. O usuário será solicitado a assiná-la. Depois de assinada, os dados da assinatura são enviados para o servidor. É no servidor que essa mensagem será verificada. Supondo que o usuário tenha assinado corretamente e possua o NFT, o servidor responderá com uma URL com um token de acesso para o conteúdo restrito. Caso contrário, o servidor lançará um erro 401
. Definimos a variável isPFPinata
de acordo.
O restante do código é apenas a interface do usuário com um botão para acionar a função handleProveIt
. Eu fiz um pouco de estilização e adicionei a logo e favicon (favorite icon) do Pinata. Aqui está como minha página se parece:
Agora, isso ainda não fará nada. Precisamos configurar nosso código do servidor para lidar com a solicitação. Vamos fazer isso agora. Vá para o seu arquivo pages/api/verify.js
e adicione o seguinte:
import axios from "axios";
import * as util from "ethereumjs-util";
import {ethers} from "ethers";
import { v4 as uuidv4 } from 'uuid';
import { withIronSession } from 'next-iron-session'
const abi = require("../../PFPinatas.json").abi;
const contractAddress = "Your PFPinata Contract Address"
const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY;
const urlV2API = `https://managed.mypinata.cloud/api/v1`;
const API_KEY = process.env.PINATA_V2_API_KEY
const CID = "CID for your content that should be for members only"
const GATEWAY_URL = "Your dedicated gateway URL";
function withSession(handler) {
return withIronSession(handler, {
password: process.env.SECRET_COOKIE_PASSWORD,
cookieName: 'web3-auth-session',
cookieOptions: {
secure: process.env.NODE_ENV === 'production' ? true : false,
},
})
}
export default withSession(async (req, res) => {
if(req.method === "POST") {
try {
const message = req.session.get('message-session');
const provider = await new ethers.providers.JsonRpcProvider(`https://eth-rinkeby.alchemyapi.io/v2/${ALCHEMY_API_KEY}`);
const contract = await new ethers.Contract( contractAddress , abi , provider );
let nonce = "\x19Ethereum Signed Message:\n" + JSON.stringify(message).length + JSON.stringify(message)
nonce = util.keccak(Buffer.from(nonce, "utf-8"))
const { v, r, s } = util.fromRpcSig(req.body.signature)
const pubKey = util.ecrecover(util.toBuffer(nonce), v, r, s)
const addrBuf = util.pubToAddress(pubKey)
const addr = util.bufferToHex(addrBuf)
if(req.body.address === addr) {
const balance = await contract.balanceOf(addr);
if(balance.toString() !== "0") {
const config = {
headers: {
"x-api-key": `${API_KEY}`,
'Content-Type': 'application/json'
}
}
// Gerar Token de Acesso
const content = await axios.get(`${urlV2API}/content`, config)
const { data } = content;
const { items } = data;
const item = items.find(i => i.cid === CID);
const body = {
timeoutSeconds: 3600,
contentIds: [item.id]
}
const token = await axios.post(`${urlV2API}/auth/content/jwt`, body, config);
return res.send(`${GATEWAY_URL}/ipfs/${CID}?accessToken=${token.data}`);
} else {
return res.status(401).send("You aren't a PFPinata");
}
} else {
return res.status(401).send("Invalid signature");
}
} catch (error) {
console.log(error);
res.status(500).json(error);
}
} else if(req.method === "GET") {
try {
const message = { contractAddress, id: uuidv4()}
req.session.set('message-session', message)
await req.session.save()
res.json(message)
} catch (error) {
console.log(error);
const { response: fetchResponse } = error
res.status(fetchResponse?.status || 500).json(error.data)
}
} else {
res.status(200).json({ message: 'This is the way...wait, no it is not. What are you doing here?' })
}
})
Este arquivo parece assustador, e é. Mas é a natureza de verificar mensagens assinadas. Estamos fazendo o seguinte neste arquivo:
- Gerando uma mensagem única para ser assinada em cada solicitação GET
- Definindo um cookie de sessão para essa mensagem única
- Retornando a mensagem a ser assinada para o cliente
- Verificando se a solicitação POST inclui a mensagem assinada correta
- Usando
ethereumjs-util
para verificar a assinatura - Verificando se o signatário é o endereço da carteira correto
- Verificando se o endereço possui o NFT apropriado
- Gerando um token de acesso, se for o caso
- Retornando a URL do gateway de conteúdo oculto
Ufa! Isso é muita coisa.
Olhe o código algumas vezes. Absorva tudo. Depois implemente. É muita coisa, mas é necessário para verificar os dados da assinatura.
Uma rápida nota sobre variáveis codificadas diretamente: muitas das variáveis do lado do servidor devem ser variáveis de ambiente. Isso está fora do escopo deste tutorial, mas o Next.js tem um ótimo processo de env.
OK, vamos executar o aplicativo. Na linha de comando, execute:
npm run dev
Isso deve carregar a nossa página de conteúdo restrito. Provavelmente, você será solicitado na primeira vez a desbloquear a Metamask e fornecer o endereço da sua conta. Tente clicar no botão da página. Se a conta que você está usando possuir um dos NFTs que criamos, você deverá ver o conteúdo restrito exibido na tela:
Mas o que acontece se você não possuir o NFT? Lembra do espaço reservado para um gif engraçado? É aqui que o gif brilha. Aqui está o meu:
Se você estiver interessado no código-fonte completo deste tutorial, ele está localizado aqui. Eu não defini variáveis de ambiente, então você precisará fazer isso ou apenas atualizar as variáveis reservadas no código.
Concluindo
Esta foi uma postagem longa, mas espero que tenha te deixado animado. Espero que suas ideias criativas estejam fluindo. Como você pode aproveitar isso com seus projetos de NFT? Que coisas legais você pode construir?
O Submarining da Pinata abre um mundo completamente novo de funcionalidades no espaço NFT. Feliz submarining!
Esse artigo é uma tradução feita por @bananlabs. Você pode encontrar o artigo original aqui
Latest comments (0)