Learn how to build a web app that gives you NFTs upon successfully watching YouTube videos.
Most of us learn on the internet. We spend hours watching youtube videos and reading long articles to learn about new skills. Unlike educational institutions, we don't get certificates for learning on the internet.
This application will create 'Proof of viewership' NFTs which are rewarded for completing YouTube videos, and can therefore be used to represent the completion of educational video courses.
Let's get started!
Dashboard setup
The first thing we need to do is head over to the dashboard and create an Edition contract.
I'm naming this Edition "SuperNFT", and its symbol is $SUPR
.
Once you have created your contract, you'll reach the following page:
You won't need to add any NFTs at this stage as we'll be minting the NFTs programmatically.
Set up your React Frontend
I'll be using React for the front-end of our app, you can use anything you're comfortable with.
The objective of the front-end is to help the user watch a YouTube video, verify if they've seen it fully and then issue a command to the backend to mint an NFT for the user.
I have written down all the code for the frontend to run a YouTube video, prevent the user from skipping forward beyond 10 seconds and take a wallet address as input.
The wallet address + video URL + video title are sent to the backend as well!
import React from "react";
import "./App.css";
function MyApp() {
const [videoURL, setvideoURL] = React.useState("");
const [address, setAddress] = React.useState("");
const [loading, setLoading] = React.useState(false);
const [watchedVideo, setwatchedVideo] = React.useState(false);
const [image, setImage] = React.useState("");
const [name, setName] = React.useState("");
const [minted, setMinted] = React.useState(false);
let videolength = 0;
let watchLength = 0;
function myfetch(url, options, timeout = 7000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("timeout")), timeout),
),
]);
}
async function mintNFT() {
setMinted(true);
setLoading(true);
const data = JSON.stringify({
name: name,
address: address,
url: videoURL,
});
myfetch(
`your backend api URL`,
{
// Adding method type
method: "POST",
// Adding body or contents to send
body: data,
headers: {
"Content-type": "application/json; charset=UTF-8",
},
},
300000,
)
.then((result) => result.json())
.then((response) => {
setImage(response.image);
setLoading(false);
});
}
var player;
function onYouTubeIframeAPIReady() {
console.log(videoURL);
player = new YT.Player("player", {
height: "390",
width: "640",
videoId: videoURL,
playerVars: {
playsinline: 1,
},
events: {
onReady: onPlayerReady,
onStateChange: onPlayerStateChange,
},
});
}
function onPlayerReady(event) {
var total_time = player.getDuration();
console.log(total_time);
const title = player.getVideoData().title;
console.log(title);
setName(title);
videolength = parseInt(total_time);
event.target.playVideo();
}
function onPlayerStateChange(event) {
const current_time = player.getCurrentTime();
console.log(videolength, current_time, watchLength);
if (current_time - watchLength > 10) {
player.seekTo(watchLength, true);
setTimeout(() => {
onPlayerStateChange();
}, 200);
} else if (videolength - current_time < 2) {
setwatchedVideo(true);
watchLength = current_time;
console.log("Video watched");
setTimeout(stopVideo, 500);
} else if (current_time - watchLength < 10) {
console.log;
watchLength = current_time;
setTimeout(() => {
onPlayerStateChange();
}, 200);
}
}
function stopVideo() {
player.stopVideo();
}
function handleData(data) {
if (data.length != 42) {
window.alert("Please enter valid wallet address");
} else {
console.log(data);
setAddress(data);
}
setMinted(false);
}
return (
<div className="grid justify-items-center">
<input
onChange={(e) => {
setvideoURL(e.target.value.substr(32));
}}
className="m-5 focus:border-blue-400 border-3 p-2 focus:outline-none rounded-lg w-1/4 h-12 bg-gray-100"
placeholder="Youtube Video URL"
/>
<button
onClick={onYouTubeIframeAPIReady}
className="text-gray-100 text-xl p-2 rounded-lg hover:bg-blue-900 bg-blue-500"
>
Load Video
</button>
<div className="mt-10" id="player"></div>
{watchedVideo ? (
<div className="mt-10 w-full grid justify-items-center">
<input
onChange={(e) => {
handleData(e.target.value);
}}
className="m-5 focus:border-blue-400 border-3 p-2 focus:outline-none rounded-lg w-3/4 h-12 bg-gray-100"
placeholder="Polygon Public Address"
/>
<button
onClick={mintNFT}
className="text-gray-100 text-xl p-2 rounded-lg hover:bg-green-900 bg-green-500"
>
Mint NFT as Proof!
</button>
{minted ? (
<div>
{loading ? (
<div className="mt-20 grid justify-items-center">
<img src="https://c.tenor.com/5o2p0tH5LFQAAAAi/hug.gif" />
</div>
) : (
<div className="mt-20 grid justify-items-center">
<img src={"data:image/png;base64," + image} />
<h1 className="text-xl font-mono font-bold mt-10">
NFT successfully minted to {address} :)
</h1>
</div>
)}
;
</div>
) : null}
</div>
) : null}
</div>
);
}
export default MyApp;
The only line you'd want to change in this code is the URL space on line 32 where it says "your backend api URL"
.
Backend in Python
Now we come to creating programmable NFTs.
I'm going to be using FastAPI to make the backend, you can use whatever works best for you. The Python code to mint NFTs remains the same.
First off, we'll install FastAPI, the thirdweb Python SDK and a few more dependencies.
pip install fastapi uvicorn thirdweb-sdk firebase-admin Pillow python-dotenv
Let's first write the code for minting an NFT, then we'll wrap it up in a simple API.
You can go through the Python SDK docs here.
First, let's import the SDK.
from thirdweb import ThirdwebSDK
from thirdweb.types import NFTMetadataInput
To sign transactions from your wallet, we need to instantiate the SDK using our wallet's private key.
Learn how to export your private key from your wallet.
Ensure you store and access your private key securely.
- Never commit any file that may contain your private key to your source control.
# Learn more about securely accessing your private key: https://portal.thirdweb.com/web3-sdk/set-up-the-sdk/securing-your-private-key
PRIVATE_KEY = "<your-private-key>"
# Set the network you want to operate on, or add your own RPC URL here
NETWORK = "mumbai"
# Finally, you can create a new instance of the SDK to use
sdk = ThirdwebSDK.from_private_key(PRIVATE_KEY, NETWORK)
Now, we'll need the address that is found on your NFT Collection Module dashboard -
Copy the address and connect to our project's NFT Module. We do this by using the following function -
# Instantiate a new NFT Collection contract as described above.
NFT_COLLECTION_ADDRESS = "0x.."
nft_collection = sdk.get_nft_collection(NFT_COLLECTION_ADDRESS)
Great! You're almost done. Now we need to figure out a way to mint an NFT to our user's URL with some data.
The Python SDK provides a single-line function for all of this.
# Note that you can customize this metadata however you like
metadata = NFTMetadataInput.from_json({
"name": "Cool NFT",
"description": "This is a cool NFT",
"image": open("path/to/file.jpg", "rb"),
})
# You can pass in any address here to mint the NFT to
wallet_address = "<your-wallet-address-here>"
tx = nft_collection.mint_to(wallet_address, metadata)
One thing you'll notice here is that we need a URL for our content that needs to go behind the NFT. We have one more challenge here, we need to create an image based on the information we're getting from the front-end, store it somewhere, get the URL and then pass it to the NFT.
I'm going to be using pillow and textwrap to create an image based on the information I'm getting from the frontend. And I'll be using firebase to store the image and get a URL to access it -
from PIL import Image, ImageDraw
import textwrap
from firebase_admin import credentials, initialize_app, storage
from datetime import datetime
# creds.json can be downloaded once you setup your firebase project
# bucket id is seen on the storage page of your firebase project
cred = credentials.Certificate("creds.json")
initialize_app(cred, {'storageBucket': 'your storage bucket id'})
# test.png is a plain grey background image I want to create my NFTs on
img = Image.open('test.png')
now = datetime.now()
dt_string = now.strftime("%d/%m/%Y %H:%M:%S")
draw = ImageDraw.Draw(img)
spacing = 20
text = "this is text that goes on top"
text2 = "this is text that goes in the bottom" + dt_string
# drawing text size
draw.text((8, 8), text, fill="white",
spacing=spacing, align="left")
draw.text((8, 180), text2, fill="yellow", spacing=spacing, align="left")
img.save('img.png')
fileName = 'img.png'
bucket = storage.bucket()
blob = bucket.blob(fileName)
blob.upload_from_filename(fileName)
blob.make_public()
print("your file url", blob.public_url)
The above code snippet writes text
and text2
on top of img.png
which is a simple grey background image I found on the internet.
It also takes the datestamp of the time you run this code and adds that to the text written on the image.
That way, you know when the image and subsequently the NFT were created.
It then puts the edited image on Firebase and gets back a URL to access the image.
You'll have to set up a Firebase project and get your JSON keys + bucket name from there for this to work.
Check out these docs for more information.
Now let's make a quick FastAPI wrapper that takes all data from front-end, uses all the code we wrote above and mints an NFT.
from thirdweb import ThirdwebSDK
from thirdweb.types import NFTMetadataInput
from dotenv import load_dotenv
import os
from fastapi import FastAPI, File, UploadFile, Response
from pydantic import BaseModel
import base64
import math
import uvicorn
from datetime import datetime
from PIL import Image, ImageDraw, ImageFont
import textwrap
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from fastapi.middleware.cors import CORSMiddleware
from firebase_admin import credentials, initialize_app, storage
# Init firebase with your credentials
cred = credentials.Certificate("creds.json")
initialize_app(cred, {'storageBucket': 'your bucket URL'})
app = FastAPI()
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class Item(BaseModel):
name: str
url: str
address: str
@app.post("/mint")
def mint(item: Item):
print(item.name)
local_name = item.name+item.address
local_name = local_name.replace(" ", "") + '.png'
img = Image.open('test.png')
now = datetime.now()
dt_string = now.strftime("%d/%m/%Y %H:%M:%S")
draw = ImageDraw.Draw(img)
spacing = 20
text = """ You have
successfully completed the video
""" + item.name + """
On Youtube"""
text2 = """
URL - """ + item.url + """
Minter Address - """ + item.address + """
Datestamp - """ + dt_string
# drawing text size
draw.text((8, 8), text, fill="white",
spacing=spacing, align="left")
draw.text((8, 180), text2, fill="yellow", spacing=spacing, align="left")
img.save(local_name)
with open(local_name, "rb") as img_file:
my_string = base64.b64encode(img_file.read())
my_data = jsonable_encoder({"image": [my_string]})
# Put your local file path
fileName = local_name
bucket = storage.bucket()
blob = bucket.blob(fileName)
blob.upload_from_filename(fileName)
blob.make_public()
print("your file url", blob.public_url)
# Learn more about securely accessing your private key: https://portal.thirdweb.com/web3-sdk/set-up-the-sdk/securing-your-private-key
PRIVATE_KEY = "<your-private-key-here>"
# Set the network you want to operate on, or add your own RPC URL here
NETWORK = "mumbai"
# Finally, you can create a new instance of the SDK to use
sdk = ThirdwebSDK.from_private_key(PRIVATE_KEY, NETWORK)
# Instantiate a new NFT Collection contract as described above.
NFT_COLLECTION_ADDRESS = "0x.."
nft_collection = sdk.get_nft_collection(NFT_COLLECTION_ADDRESS)
print(nft_collection.mint_to(item.address, NFTMetadataInput.from_json({
"name":item.name,
"description": "The owner of this NFT has watched a youtube video in its entirety",
"image":blob.public_url,
})))
return JSONResponse(my_data)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Note - Once our NFT image is prepared, I convert the locally saved image to a base64 string that is passed to the frontend. The frontend renders that string in an IMG tag and shows the user how their NFT looks.
You can take this Python code snippet, replace it with your credentials and then run it anywhere. I recommend Replit's always-on feature to host this API while you're testing because it's super easy to debug if something goes wrong. After running this API, you can make an actual query from the frontend with your address after watching a video and you'll be able to see a NFT getting minted. In a couple minutes after minting, your NFT will also be available on Opensea under your wallet.
Hope you enjoyed building this! Web3 is just getting started and thirdweb is building the underlying infrastructure to help make web3 mainstream. Let me know if you have any questions on Twitter @aryxnsharma!