Skip to content

Instantly share code, notes, and snippets.

@nythrox
Last active April 7, 2023 22:01
Show Gist options
  • Save nythrox/a2072b9dcb34fd3b04f0453e56865ad0 to your computer and use it in GitHub Desktop.
Save nythrox/a2072b9dcb34fd3b04f0453e56865ad0 to your computer and use it in GitHub Desktop.

Creating Generative Art using Capi and Polkadot NFTs

In this guide we'll learn how to create algorithmically generated NFTs. We'll cover creating a Deno project, generating the images using the Canvas API, uploading them to IPFS, and minting them to Westmint using Capi.

Getting stared

In this guide we'll be using Deno, but you can also use node. If you don't have deno installed, make sure to get it installed [here].(https://deno.land/[email protected]/getting_started/installation)

To create a new Deno application, you can open your command line and run these commands to setup the project folder and create main.ts:

mkdir gen-art-nfts
cd gen-art-nfts
touch main.ts

Generating the art

We'll be using this tutorial as a reference to create the generative art, all credits due!

You can skip ahead if you don't need a line-by-line explanation.

The first thing we do is create a sketch.ts file and add a new function called genImage. We'll be using the deno-canvas package.

import { createCanvas } from "https://deno.land/x/[email protected]/mod.ts";

Inside of genImage, we'll create a 2500x2500 canvas.

export function genImage() { 
  const canvas = createCanvas(2500, 2500);
}

As with any canvas operation, we need to grab its context in order to draw on it. We then create a 2500x2500 canvas and set its background using fillRect with the color antiquewhite for the fillStyle.

export function genImage() { 
  // ...
  const ctx = canvas.getContext("2d");
  ctx.fillStyle = "antiquewhite";
  ctx.fillRect(0, 0, 2500, 2500);
}

Next, we'll create a drawLine function, passing the context and looping over it five times.

export function genImage() { 
  // ...
  for (let i = 0; i < 5; i++) {
    drawLine(ctx);
  }
}

The drawLine function will be responsible for creating a single line (that may look like a square, depending on the brush size). We'll give it a thickness from 1 to 250 using lineWidth and a random color using HSL color values:

export function drawLine() { 
  // ...
  ctx.lineWidth = random(1, 250);
  ctx.strokeStyle = `hsl(${random(0, 50)}, ${random(0, 100)}%, ${random(0, 100)}%)`;
}

The random function is a simple helper we created that simply returns a random value between min and max.

const random = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1) + min);

The next step consists of actually drawing the line. Now that we've given the path a color and a line width, we draw it by calling moveTo to make it start at a random position of X and Y between 500 and 2000, then end at another position with the same random values using lineTo. Once we call ctx.stroke() the line is created.

export function drawLine() { 
  // ...
  ctx.beginPath();
  ctx.moveTo(random(500, 2000), random(500, 2000));
  ctx.lineTo(random(500, 2000), random(500, 2000));
  ctx.stroke();
}

Once the drawLine function is called five times in genImage, our painting is done and what's left is to turn it into a png image by returning the following:

export function genImage() { 
  // ...
  return canvas.toBuffer("image/png");
}

To generate the images, let's go back to our main.ts and add a few lines of code:

// main.ts
for (let i = 0; i < 10; i++) {
  const image = genImage();
  await Deno.writeFile(`./images/item-${i}.png`, image, { create: true });
}
console.log("Images generated")

Here we'll loop over genImage in order to create ten images (you can create as many as you want) and save the file to a images folder.

If all was done correctly, you can now run the code:

$ deno run main.ts

Images generated

The resulting images should look something like this:

Image

Here's the complete code for sketch.ts.

import { createCanvas } from "https://deno.land/x/[email protected]/mod.ts";

export function genImage() {
  const canvas = createCanvas(2500, 2500);
  const ctx = canvas.getContext("2d");
  ctx.fillStyle = "antiquewhite";
  ctx.fillRect(0, 0, 2500, 2500);
  
  for (let i = 0; i < 5; i++) {
    drawLine(ctx);
  }
  
  return canvas.toBuffer("image/png");
}

const random = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1) + min);

function drawLine(ctx: CanvasRenderingContext2D) {
  ctx.lineWidth = random(0, 250);
  ctx.strokeStyle = `hsl(${random(0, 50)}, ${random(0, 100)}%, ${random(0, 100)}%)`;
  ctx.beginPath();
  ctx.moveTo(random(500, 2000), random(500, 2000));
  ctx.lineTo(random(500, 2000), random(500, 2000));
  ctx.stroke();
}

Uploading to IPFS

In order to make our image files avalible in ipfs, we'll want to use Pinata awesome Pinning feature to make sure there is always a version of each one available.

To use Pinata you have to first create an account, and then generate an API key that we can use for uploading the files.

We're gonna be using the pinFileToIPFS and pinJSONToIPFS functions from their API in order to upload our images and metadata for our NFTs. You can find the documentation for Pinata's API here.

Here are some simple helper functions that call their API using fetch and return the result.

const token = "[YOUR_TOKEN_HERE]"

export async function pinFile(path: string) {
  const formData = new FormData()
  const file = await Deno.readFile(path)
  formData.append("file", new Blob([file]), window.crypto.randomUUID())
  const res = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
    method: "POST",
    headers: { Authorization: `Bearer ${token}` },
    body: formData,
  })
  const data = await res.json()
  return data.IpfsHash as string
}

export async function pinJson(json: Record<string, any>) {
  const res = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", {
    method: "POST",
    headers: { Authorization: `Bearer ${token}` },
    body: JSON.stringify({ pinataContent: json }),
  })
  const data = await res.json()
  return data.IpfsHash as string
}

Setting up Capi

Capi offers a library with a simplified API for managing NFTs which we'll be making good use of. To interact with the Westmint parachain you must first add the Capi url to your import_map.json together with the parachain:

{
  "imports": {
    "@capi": "https://capi.dev/",
    "@capi/westmint": "https://capi.dev/frame/wss/westmint-rpc.polkadot.io/@latest"
  }
}

Minting the NFTs

Now that we have everything setup including the Pinata API, we can start writing our main flow. It will go as following:

  • Generate our images from sketch.ts and save them as .png files
  • Upload them to IPFS using Pinata's pinning service
  • Create the JSON files with the metadata for our NFTs and upload them to IPFS
  • Mint the NFTs and set their metadata to the IPFS hashes we've created

In order to sign the transactions we'll be sending to Polkadot, we can create a keypair with our private key:

const SECRET_KEY = "[your secret key here]";
const keypair = Sr25519.fromSecret(decode(SECRET_KEY))

Before minting our NFTs, we must first create a collection. If we want to set its metadata (image, name, description, etc) then we first have to upload the image to IPFS and later its metadata:

const name = "My Collection";
const description = "My Collection's description here"
const image = await pinFile("./collection-logo.png");
const metadata = await pinJson({ name, image, description });

Once we've got the IPFS hash of the metadata, we can create the collection and grab the emitted events.

const createCollectionEvents = await Nfts.create(Rune.rec({
  admin: wallet.address,
  settings: CollectionSettings.default,
  mintSettings: Rune.rec({
    mintType: MintType.Issuer(),
    defaultItemSettings: ItemSetting.default,
  }),
}))
  .sign(signature({ sender: wallet }))
  .send()
  .finalizedEvents()
  .run()  

Now we extract the collectionId from the events.

const collectionCreatedEvent = createCollectionEvents.find((event) =>
  RuntimeEvent.isNfts(event.event) && Event.isCreated(event.event.value)
)?.event.value!

const collectionId = collectionCreatedEvent.collection

And set the metadata of our collection.

await Nfts.setCollectionMetadata({
  collection,
  data: $.str.encode(metadata),
})
  .sign(signature({ sender: wallet }))
  .send()
  .finalizedEvents()
  .run()

console.log(`[Collection]: ID ${await collection.id.run()} created`)

Next, we'll iterate through the images we've created and repeat the same process to upload them to IPFS and save their metadata.

const nftMetadatas = Array.from({ length: 10 }.map(async (, i) => {
	const name = `Item #${i + 1}`;
	const description = `Description for Item #${i + 1}`;
	const image = await pinFile(`./images/item-${i}.png`);
	const metadata = await pinJson({ name, image, description })
	return metadata
}

When we have the IPFS hashes, we can finally mint our NFTs. It's ideal that we batch them into a single transaction in order to save time and fees (?)

const mintNfts = nftMetadatas.map((metadata) =>
  Nfts.mint({ collection, item, mintTo: wallet.address })
)

await Utility.batchAll({
  calls: Rune.tuple(mintNfts),
})
  .sign(signature({ sender: wallet }))
  .send()
  .finalized()
  .run()

Finally, we'll set the metadata of the NFTs we've just minted

const setNftsMetadata = nftMetadatas.map((metadata) =>
  Nfts.setNftsMetadata({ collection, item, data: $.str.encode(metadata) })
)

await Utility.batchAll({
  calls: Rune.tuple(setNftsMetadata),
})
  .sign(signature({ sender: wallet }))
  .send()
  .finalized()
  .run()

console.log(`[NFT]: ${name} minted`)

Once you're 100% certain your collection is ready and you no longer need to mint new items, update metadata or change any configurations, you should relinquish the permissions of the collection so it can no longer be modified. Be careful, this is not reversible.

await Nfts.setTeam({
  admin: null,
  issuer: null,
  freezer: null,
})

Done! Now we can run our program, and wait for it to finish executing. If everything went well, you should see the following logs:

$ deno run main.ts

[Collection]: ID 0 created 
[NFT]: Item #0 minted
[NFT]: Item #1 minted
[NFT]: Item #2 minted
...

Here's what the final code should look like:

import { Sr25519 } from "@capi/crypto/Sr25519.ts"
import { signature } from "@capi/patterns/signature/nfts.ts"
import { Nfts, RuntimeEvent, Utility } from "@capi/westmint"
import { EventRecord } from "@capi/westmint/types/frame_system/mod.js"
import { RuntimeEvent } from "@capi/westmint/types/westmint_runtime.js"
import { pinFile, pinJson } from "./pinata.ts"
import { genImage } from "./sketch.ts"

for (let i = 0; i < 10; i++) {
  const image = genImage()
  await Deno.writeFile(`./images/item-${i}.png`, image, { create: true })
}
console.log("Images generated")

const SECRET_KEY = "[your secret key here]"
const wallet = Sr25519.fromSecret(SECRET_KEY)

const name = "Artsy Collection"
const description = ""
const image = await pinFile("./collection-logo.png")
const metadata = await pinJson({ name, image, description })

const createCollectionEvents = await Nfts.create(Rune.rec({
  admin: wallet.address,
  settings: CollectionSettings.default,
  mintSettings: Rune.rec({
    mintType: MintType.Issuer(),
    defaultItemSettings: ItemSetting.default,
  }),
}))
  .sign(signature({ sender: wallet }))
  .send()
  .finalizedEvents()
  .run()

const collectionCreatedEvent = createCollectionEvents.find((event) =>
  RuntimeEvent.isNfts(event.event) && Event.isCreated(event.event.value)
)?.event.value!

const collectionId = collectionCreatedEvent.collection

await Nfts.setCollectionMetadata({
  collection,
  data: $.str.encode(metadata),
})
  .sign(signature({ sender: wallet }))
  .send()
  .finalizedEvents()
  .run()

console.log(`[Collection]: ID ${await collection.id.run()} created`)

for (let i = 0; i < 10; i++) {
  const item = i + 1
  const name = `Item #${item}`
  const description = ""
  const image = await pinFile(`./images/item-${i}.png`)
  const metadata = await pinJson({ name, image, description })

  const mintNfts = nftMetadatas.map((metadata) =>
    Nfts.mint({ collection, item, mintTo: wallet.address })
  )

  await Utility.batchAll({
    calls: Rune.tuple(mintNfts),
  })
    .sign(signature({ sender: wallet }))
    .send()
    .finalized()
    .run()

  const setNftsMetadata = nftMetadatas.map((metadata) =>
    Nfts.setNftsMetadata({ collection, item, data: $.str.encode(metadata) })
  )

  await Utility.batchAll({
    calls: Rune.tuple(setNftsMetadata),
  })
    .sign(signature({ sender: wallet }))
    .send()
    .finalized()
    .run()

  console.log(`[NFT]: ${name} minted`)
}

await Nfts.setTeam({
  admin: null,
  issuer: null,
  freezer: null,
})

There you go! We've got a fully functioning script for creating generative art and using it to create NFTs.

You can view your collection in the Singular.app marketplace or in any other NFT marketplace that shows items from Polkadot.


If you want to access your collection directly from the blockchain you can also use Capi to do so. Let's grab our collectionId from the logs above and use it to read from the blockchain:

const collectionId = 0

const nfts = Nfts.ItemMetadataOf.entryPage(10, [collectionId])
  .into(ValueRune)
  .map((entries) =>
    entries.map(([[collectionId, itemId], metadata]) => ({
      imageUrl: $.str.decode(metadata.data),
      collection: collectionId,
      id: itemId,
    }))
  )
  
console.log(await nfts.run())

Here we read the first 10 nfts from our collection, grab their metadata and decode it using $.str.decode.


We can also send a transaction to bid on a NFT:

await Nfts.createSwap({
  desiredCollection: collectionId,
  maybeDesiredItem: 0,
  price: {
    ammount: 100n,
    direction: "Send",
  },
  duration: 213123,
})
  .sign(signature({ sender: wallet }))
  .send()
  .finalized()
  .run()

List bids that have been done on your items:

const offers = await Nfts.PendingSwapOf.entryPage(10, [collectionId]).run()

And accept the bids that you desire:

const offersToAccept = offers
  .filter(([_, swap]) => {
    const price = swap.price
    return price && price.amount > 99 && price.direction == "Send"
  })

const acceptSwaps = offersToAccept.map(([[collectionId, itemId], { swap }]) => (
  Nfts.claimSwap({
    sendCollection: collectionId,
    sendItem: itemId,
    swap,
  })
))

await Utility.batch({ calls: Rune.tuple(acceptSwaps) })
  .sign(signature({ sender: wallet }))
  .send()
  .finalized()
  .run()

To learn more about how to use Capi, check out the docs at capi.dev.

To see the full capabilities of the NFT pallet, you can find its repository here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment