Skip to content
This repository has been archived by the owner on May 21, 2024. It is now read-only.

feat: generate + mass create NFTs with pallet-uniques #299

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ zombienet-linux
# IDEs
.vscode/
.idea/

# JS / TS
**/node_modules/
41 changes: 41 additions & 0 deletions nft-creator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# NFT Creator

These files provide the building blocks to:
- Bulk create mock NFTs on a local network, no images required
- Generate NFT images from trait .pngs
- Upload files to IPFS (implementation in progress)
- And an e2e example of generating NFTs + metadata from traits, storing them in IPFS (in progres), and creating them on a parachain

## Running
Launch trappist parachain locally.
Instructions found [here](../README.md)

```
cd nft-creator
```

```
yarn
```

```
ts-node create-mock-nfts.ts
```


## Scripts
- `mock-nft-config.json` example config passed for mock NFTs. No images.
- `trappist-nft-config.json` config used for trappist NFTs with traits
- `create-mock-nfts.ts` will generate mock metadata, create collection, and populate each NFT on-chain with the mock metadata CID and attributes.
- `create-trappist-nfts.ts` will generate NFT images & metadata from the `trappist-nfts/traits` folder. Populates the NFT metadata on-chain

The creation scripts include examples on implementing custom interfaces to provide to the `NftGenerator` and `NftCreator` classes.

## Building Blocks
- `nft-generator.ts` is a reusable NFT generator class that will create NFT images from trait images and provide metadata
- `nft-creator.ts` is reusable class that will bulk populate NFT metadata and images on-chain. It is configurable to upload metadata & images on-chain based on the `IpfsManager` implementation
- `interfaces/config.ts` the interface that config files must follow
- `interfaces/ipfs-manager.ts` a simple interface for IPFS to allow configurability for IPFS connections & apis
- `interfaces/metadata-interface.ts` the metadata interface that the NFTs must use. Based on common metadata standards. Example [here](https://docs.opensea.io/docs/metadata-standards)
- `interfaces/name-and-description.ts` interfaces to implement custom NFT naming and description generation. Will be provided to the `NftGenerator` class.

107 changes: 107 additions & 0 deletions nft-creator/create-mock-nfts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import beer from "beer-names";
import superb from "superb";
import fs from "fs";

import { NftMetadata } from "./interfaces/metadata-interface";
import { IpfsManager } from "./interfaces/ipfs-manager";
import { ApiPromise, WsProvider, Keyring } from "@polkadot/api";
import { NftCreator } from "./nft-creator";
import { createDirSync } from "./utils";

import { CID } from "multiformats/cid";
import * as json from 'multiformats/codecs/json'
import { sha256 } from 'multiformats/hashes/sha2'

const config = require("./mock-nft-config.json");

// Color trait to randomly choose for mock nfts
const colors = [
"Red",
"Blue",
"Green",
"Yellow",
"Orange",
"Purple",
"Pink",
"Brown",
"Gray",
"White",
"Black",
"Cyan",
"Magenta",
"Turquoise"
];

// Generates mock NFT metadata files and saves them to `outputDir`
function generateMockNfts(amount: number, outputDir: string, cleanDir: boolean = true) {
// remove trailing slashes
outputDir = outputDir.replace(/\/$/, "");
createDirSync(outputDir, cleanDir);

for (let i = 1; i <= amount; i++) {
const randomName = beer.random();
// get the last word, which is the beer type (ale, lager, etc.)
const beerType: string = randomName.split(" ").slice(-1).join("");
// add superb word + beer type
let randomDescription = superb.random() + " " + beerType;
const randomColor = colors[Math.floor(Math.random() * colors.length)];

let metadata: NftMetadata = {
attributes: [
{
trait_type: "type",
value: beerType
},
{
trait_type: "color",
value: randomColor
}
],
description: randomDescription,
image: "",
name: randomName,
itemId: i
};

// pad id with 0's for file name
const fileId = i.toString().padStart(amount.toString().length, "0");
const fileName = outputDir + "/beer_nft_" + fileId + ".json";
fs.writeFileSync(fileName, JSON.stringify(metadata, null, 2));
}
}

class MockIpfs implements IpfsManager {
// Mock implementation. There is no actual uploading
async uploadContent(content: string): Promise<CID> {
const bytes = json.encode(content);

const hash = await sha256.digest(bytes);
return CID.create(1, json.code, hash);
}

async uploadFile(filePath: string): Promise<CID> {
// simple implementation, just read the file and get CID
return this.uploadContent(fs.readFileSync(filePath, 'utf8'));
}
}

async function main() {
const wsProvider = new WsProvider(config.substrateEndpoint);
const dotApi = await ApiPromise.create({ provider: wsProvider });

const keyring = new Keyring({ type: 'sr25519' });
const signer = keyring.addFromUri('//Alice');

// generate metadata for `numNfts`. Save the metadata files to `metadataDir`
generateMockNfts(config.numNfts, config.out.metadataDir);

let nftCreator = new NftCreator(config, dotApi, signer, new MockIpfs());

console.log("Creating NFT collection");
await nftCreator.createNftCollection();
console.log("Creating NFTs...");
await nftCreator.bulkCreateNfts(config.numNfts);
console.log("Done!");
}

main();
68 changes: 68 additions & 0 deletions nft-creator/create-trappist-nfts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import beer from "beer-names";
import superb from "superb";
import fs from "fs";

import { IpfsManager } from "./interfaces/ipfs-manager";
import { ApiPromise, WsProvider, Keyring } from "@polkadot/api";
import { NftCreator } from "./nft-creator";
import { NftGenerator } from "./nft-generator";
import { NameGenerator, DescriptionGenerator } from "./interfaces/name-and-description";

import { CID } from "multiformats/cid";
import * as json from 'multiformats/codecs/json'
import { sha256 } from 'multiformats/hashes/sha2'

const config = require("./trappist-nft-config.json");


class TrappistNftNameGenerator implements NameGenerator {
generateName(attributes: any, id: number): string {
let label = attributes.find((attribute: any) => attribute.trait_type === "label").value;
// Replace last word (beer type) with label (polkastout, kusamale, squinkist)
return beer.random().replace(/\b\w+\b(?![^\s]*\s)/, label);
}
}

class TrappistDescriptionGenerator implements DescriptionGenerator {
generateDescription(attributes: any, id: number): string {
let label = attributes.find((attribute: any) => attribute.trait_type === "label").value;
// add superb word + beer type
return superb.random() + " " + label;
}
}

class MockIpfs implements IpfsManager {
// Mock implementation. There is no actual uploading
async uploadContent(content: string): Promise<CID> {
const bytes = json.encode(content);

const hash = await sha256.digest(bytes);
return CID.create(1, json.code, hash);
}

async uploadFile(filePath: string): Promise<CID> {
// simple implementation, just read the file and get CID
return this.uploadContent(fs.readFileSync(filePath, 'utf8'));
}
}

async function main() {
const wsProvider = new WsProvider(config.substrateEndpoint);
const dotApi = await ApiPromise.create({ provider: wsProvider });

const keyring = new Keyring({ type: 'sr25519' });
const signer = keyring.addFromUri('//Alice');

const generator = new NftGenerator(config, new TrappistNftNameGenerator(), new TrappistDescriptionGenerator());
await generator.generateNfts(10);

let nftCreator = new NftCreator(config, dotApi, signer, new MockIpfs());

console.log("Creating NFT collection");
await nftCreator.createNftCollection();
console.log("Creating NFTs...");
await nftCreator.bulkCreateNfts(config.numNfts);
console.log("Done!");
}

main();
13 changes: 13 additions & 0 deletions nft-creator/interfaces/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface Config {
substrateEndpoint: string;
collectionId: number;
numNfts: number;
imageInfo: {
traitsDir: string;
width: number;
}
out: {
metadataDir: string;
imageDir: string;
};
}
8 changes: 8 additions & 0 deletions nft-creator/interfaces/ipfs-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { NftMetadata } from "./metadata-interface";
import { CID } from "multiformats/cid";


export interface IpfsManager {
uploadFile(filePath: string): Promise<CID>;
uploadContent(content: string): Promise<CID>;
}
13 changes: 13 additions & 0 deletions nft-creator/interfaces/metadata-interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

export interface NftAttribute {
trait_type: string;
value: string;
}

export interface NftMetadata {
attributes: NftAttribute[];
description: string;
image: string;
name: string;
itemId: number;
}
9 changes: 9 additions & 0 deletions nft-creator/interfaces/name-and-description.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { NftAttribute } from "./metadata-interface"

export interface NameGenerator {
generateName(attributes: NftAttribute[], id: number): string;
}

export interface DescriptionGenerator {
generateDescription(attributes: NftAttribute[], id: number): string;
}
10 changes: 10 additions & 0 deletions nft-creator/mock-nft-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"collectionId": 1,
"numNfts": 10,
"imageInfo": null,
"out": {
"metadataDir": "nft-metadata",
"imageDir": null
},
"substrateEndpoint": "ws://127.0.0.1:9920"
}
105 changes: 105 additions & 0 deletions nft-creator/nft-creator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { ApiPromise} from "@polkadot/api";
import { NftAttribute, NftMetadata } from "./interfaces/metadata-interface";
import { IpfsManager } from "./interfaces/ipfs-manager";
import { Config } from "./interfaces/config";
import { KeyringPair } from "@polkadot/keyring/types";

import * as fs from 'fs';

export class NftCreator {
private config : Config;
private dotApi: ApiPromise;
private signer: KeyringPair;
private ipfsManager: IpfsManager;

constructor(config: Config, dotApi: ApiPromise, signer: KeyringPair, ipfsManager: IpfsManager) {
this.config = config;
this.dotApi = dotApi;
this.signer = signer;
this.ipfsManager = ipfsManager;
}

// return tx for creating uniques collection. No call is made
formCreateNftCollectionTx(): any {
return this.dotApi.tx.uniques.create(this.config.collectionId, this.signer.address);
}

// return tx for setting item attribute. No call is made
formSetItemAttributeTx(itemId: number, attribute: NftAttribute): any {
return this.dotApi.tx.uniques.setAttribute(this.config.collectionId, itemId, attribute.trait_type, attribute.value);
}

formBatchedSetItemAttributesTxs(itemId: number, attributes: NftAttribute[]): any {
let txs = [];
for (let attribute of attributes) {
const nftCall = this.dotApi.tx.uniques.setAttribute(this.config.collectionId, itemId, attribute.trait_type, attribute.value);
txs.push(nftCall);
}

return txs;
}

formSetItemMetadataTx(itemId: number, data: string, isFrozen: boolean = false): any {
return this.dotApi.tx.uniques.setMetadata(this.config.collectionId, itemId, data, isFrozen);
}

async sendBatchedTxs(txs: any[]): Promise<any> {
const batchTx = this.dotApi.tx.utility.batchAll(txs);

return await batchTx.signAndSend(this.signer, { nonce: -1 });
}

async createNftCollection(): Promise<any> {
const nftCall = this.formCreateNftCollectionTx();

// return txHash
return await nftCall.signAndSend(this.signer, { nonce: -1 });
}

async setItemAttributes(itemId: number, attributes: NftAttribute[]): Promise<any> {
let txs = this.formBatchedSetItemAttributesTxs(itemId, attributes);
// batch set attribute calls
return await this.sendBatchedTxs(txs);
}

async setItemMetadata(itemId: number, data: string, isFrozen: boolean = false): Promise<any> {
const nftCall = this.formSetItemMetadataTx(itemId, data, isFrozen);

return await nftCall.signAndSend(this.signer, { nonce: -1 });
}

async bulkCreateNfts(max: number) {
const dir = this.config.out.metadataDir;

let metadataTxs = [];
let attributeTxs = [];

const metadataFiles = fs.readdirSync(dir);
let count = 0;
for (let fileName of metadataFiles) {
// limit number of NFTs created
if (count++ >= max) {
break;
}

const content = fs.readFileSync(dir + "/" + fileName, 'utf8');
let imageCid = null;
if (this.config.imageInfo !== null) {
const imagePath = this.config.out.imageDir + "/" + fileName.replace(".json", ".png");

imageCid = await this.ipfsManager.uploadFile(imagePath);
}

const metadata: NftMetadata = JSON.parse(content);
metadata.image = imageCid === null ? metadata.image : imageCid.toString();

const metadataCid = await this.ipfsManager.uploadContent(JSON.stringify(metadata));

metadataTxs.push(this.formSetItemMetadataTx(metadata.itemId, metadataCid.toString()));
attributeTxs = attributeTxs.concat(this.formBatchedSetItemAttributesTxs(metadata.itemId, metadata.attributes));
};

await this.sendBatchedTxs(attributeTxs);
await this.sendBatchedTxs(metadataTxs);
}
}
Loading