├── .gitignore ├── chunkies.png ├── projects ├── react-demo │ ├── .eslintrc.json │ ├── next.config.js │ ├── public │ │ ├── favicon.ico │ │ ├── vercel.svg │ │ └── chunky-dump.json │ ├── lib │ │ ├── are-props-equal.ts │ │ └── get-base-parts.ts │ ├── next-env.d.ts │ ├── pages │ │ ├── _app.tsx │ │ ├── api │ │ │ └── hello.ts │ │ └── index.tsx │ ├── styles │ │ ├── globals.css │ │ └── Home.module.css │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ ├── README.md │ └── components │ │ └── rmrk-svg-composer.tsx └── scripts │ ├── .env.example │ ├── run-simple-script.ts │ ├── .gitignore │ ├── assets │ └── chunky │ │ ├── Chunky Preview.png │ │ ├── chunky_altresource.jpg │ │ ├── Chunky Items │ │ ├── Chunky_bone_thumb.png │ │ ├── Chunky_flag_thumb.png │ │ ├── Chunky_spear_thumb.png │ │ ├── Chunky_pencil_thumb.png │ │ ├── Chunky_bone_left.svg │ │ ├── Chunky_bone_right.svg │ │ ├── Chunky_flag_left.svg │ │ ├── Chunky_flag_right.svg │ │ ├── Chunky_spear_right.svg │ │ ├── Chunky_spear_left.svg │ │ ├── Chunky_pencil_right.svg │ │ └── Chunky_pencil_left.svg │ │ ├── v1 │ │ ├── Chunky_head_v1.svg │ │ └── Chunky_hand_v1.svg │ │ ├── v2 │ │ ├── Chunky_hand_v2.svg │ │ └── Chunky_head_v2.svg │ │ ├── v3 │ │ ├── Chunky_hand_v3.svg │ │ └── Chunky_head_v3.svg │ │ └── v4 │ │ ├── Chunky_hand_v4.svg │ │ └── Chunky_head_v4.svg │ ├── devaccs.ts │ ├── tsconfig.json │ ├── README.md │ ├── constants.ts │ ├── run-mint-sequence.ts │ ├── package.json │ ├── utils.ts │ ├── simple-send.ts │ ├── pinata-utils.ts │ ├── get-polkadot-api.ts │ ├── create-base.ts │ ├── sign-and-send-with-retry.ts │ ├── mint-chunky.ts │ ├── mint-chunky-items.ts │ ├── mint-ukraine-stamp-book.ts │ └── mint-in-chunks.ts ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .env 3 | node_modules 4 | 5 | -------------------------------------------------------------------------------- /chunkies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmrk-team/rmrk2-examples/HEAD/chunkies.png -------------------------------------------------------------------------------- /projects/react-demo/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /projects/scripts/.env.example: -------------------------------------------------------------------------------- 1 | PRIVAKE_KEY="//Alice" 2 | PINATA_KEY="XXX" 3 | PINATA_SECRET="XXX" 4 | -------------------------------------------------------------------------------- /projects/scripts/run-simple-script.ts: -------------------------------------------------------------------------------- 1 | import { simpleSend } from "./simple-send"; 2 | 3 | simpleSend(); 4 | -------------------------------------------------------------------------------- /projects/react-demo/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | } 5 | -------------------------------------------------------------------------------- /projects/react-demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmrk-team/rmrk2-examples/HEAD/projects/react-demo/public/favicon.ico -------------------------------------------------------------------------------- /projects/scripts/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .env 3 | consolidated-from-dumps-unconsolidated.json 4 | node_modules 5 | dumps-unconsolidated.json 6 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/Chunky Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmrk-team/rmrk2-examples/HEAD/projects/scripts/assets/chunky/Chunky Preview.png -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/chunky_altresource.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmrk-team/rmrk2-examples/HEAD/projects/scripts/assets/chunky/chunky_altresource.jpg -------------------------------------------------------------------------------- /projects/react-demo/lib/are-props-equal.ts: -------------------------------------------------------------------------------- 1 | import { equals } from 'ramda'; 2 | 3 | export const arePropsEqual = (prevProps: any, nextProps: any) => equals(prevProps, nextProps); 4 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/Chunky Items/Chunky_bone_thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmrk-team/rmrk2-examples/HEAD/projects/scripts/assets/chunky/Chunky Items/Chunky_bone_thumb.png -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/Chunky Items/Chunky_flag_thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmrk-team/rmrk2-examples/HEAD/projects/scripts/assets/chunky/Chunky Items/Chunky_flag_thumb.png -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/Chunky Items/Chunky_spear_thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmrk-team/rmrk2-examples/HEAD/projects/scripts/assets/chunky/Chunky Items/Chunky_spear_thumb.png -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/Chunky Items/Chunky_pencil_thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmrk-team/rmrk2-examples/HEAD/projects/scripts/assets/chunky/Chunky Items/Chunky_pencil_thumb.png -------------------------------------------------------------------------------- /projects/react-demo/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /projects/react-demo/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import { ChakraProvider } from "@chakra-ui/react" 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return ( 7 | 8 | 9 | 10 | ) 11 | } 12 | export default MyApp 13 | -------------------------------------------------------------------------------- /projects/react-demo/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /projects/scripts/devaccs.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | import { Keyring } from "@polkadot/keyring"; 3 | import { KeyringPair } from "@polkadot/keyring/types"; 4 | 5 | export default (seed?: string): KeyringPair[] => { 6 | const k = []; 7 | const keyring = new Keyring({ type: "sr25519" }); 8 | k.push(keyring.addFromUri(seed || process.env.MNEMONIC_PHRASE)); 9 | return k; 10 | }; 11 | -------------------------------------------------------------------------------- /projects/react-demo/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /projects/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "lib": [ 5 | "es2017", 6 | "es2019", 7 | "es2020", 8 | "esnext" 9 | ], 10 | "target": "ES2017", 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "emitDecoratorMetadata": true, 14 | "experimentalDecorators": true, 15 | "resolveJsonModule": true, 16 | "baseUrl": ".", 17 | "esModuleInterop": true, 18 | "allowSyntheticDefaultImports": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /projects/react-demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rmrk2-demo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "RMRK2 demo", 6 | "repository": "git@github.com:rmrk-team/rmrk2-examples.git", 7 | "author": "Yuri ", 8 | "license": "MIT", 9 | "devDependencies": { 10 | "prettier": "^2.4.1" 11 | }, 12 | "scripts": { 13 | "dev": "yarn workspace rmrk2-demo dev" 14 | }, 15 | "workspaces": { 16 | "packages": [ 17 | "projects/*" 18 | ], 19 | "nohoist": [ 20 | "**/scripts", 21 | "**/scripts/**" 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /projects/scripts/README.md: -------------------------------------------------------------------------------- 1 | # RMRK2 Chunky minting scripts 2 | 3 | This is a collection of script to mint Chunky composable nested NFTs using RMRK2 4 | 5 | Please Run scripts in following order: 6 | 7 | - `npx ts-node ./run-mint-sequence.ts` 8 | - `yarn fetch --prefixes=0x726d726b,0x524d524b --append=dumps-unconsolidated.json` 9 | - `yarn consolidate --json=dumps-unconsolidated.json` 10 | 11 | Then look at `consolidated-from-dumps-unconsolidated.json` to verify there's no invalid calls. If everything is ok, you can copy this json fil to `react-demo` 12 | project under `public/chunky-dump.json` 13 | -------------------------------------------------------------------------------- /projects/scripts/constants.ts: -------------------------------------------------------------------------------- 1 | export const isProd = false; 2 | 3 | export const WS_URL = isProd ? 'wss://kusama-rpc.polkadot.io' : 'wss://staging.node.rmrk.app'; 4 | const backupWsEndpoint = isProd ? 'wss://kusama.api.onfinality.io/public-ws' : 'wss://staging.node.rmrk.app' 5 | 6 | export const RPC_ENDPOINTS = [WS_URL, backupWsEndpoint] 7 | 8 | export const ASSETS_CID = 'Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT'; 9 | export const CHUNKY_ITEMS_COLLECTION_SYMBOL = 'CHNKITMS'; 10 | export const CHUNKY_COLLECTION_SYMBOL = 'CHNK'; 11 | export const CHUNKY_BASE_SYMBOL = 'CHNKBS'; 12 | 13 | export const CHUNK_SIZE = 70; 14 | -------------------------------------------------------------------------------- /projects/scripts/run-mint-sequence.ts: -------------------------------------------------------------------------------- 1 | import { createBase } from "./create-base"; 2 | import { mintChunky } from "./mint-chunky"; 3 | import { addBaseResource } from "./mint-chunky"; 4 | import { mintItems } from "./mint-chunky-items"; 5 | 6 | export const runMintSequence = async () => { 7 | try { 8 | const baseBlock = await createBase(); 9 | const chunkiesBlock = await mintChunky(); 10 | await addBaseResource(chunkiesBlock, baseBlock); 11 | await mintItems(chunkiesBlock, baseBlock); 12 | process.exit(0); 13 | } catch (error: any) { 14 | console.error(error); 15 | process.exit(0); 16 | } 17 | }; 18 | 19 | runMintSequence(); 20 | -------------------------------------------------------------------------------- /projects/react-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "paths": { 17 | "components/*": ["./components/*"], 18 | "lib/*": ["./lib/*"], 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /projects/scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rmrk2-demo-scripts", 3 | "version": "1.0.0", 4 | "description": "RMRK2 demo scripts", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "fetch": "rmrk-tools-fetch", 9 | "consolidate": "rmrk-tools-consolidate" 10 | }, 11 | "dependencies": { 12 | "@pinata/sdk": "^1.1.26", 13 | "@polkadot/api": "^9.4.3", 14 | "@polkadot/keyring": "^10.1.9", 15 | "@polkadot/util-crypto": "^10.1.9", 16 | "@types/node": "^17.0.21", 17 | "dotenv": "^10.0.0", 18 | "nanoid": "^3.1.25", 19 | "p-limit": "^3.1.0", 20 | "prettier": "^2.3.2", 21 | "rmrk-tools": "2.0.95", 22 | "ts-node": "^10.9.1", 23 | "typescript": "^4.8.4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /projects/react-demo/lib/get-base-parts.ts: -------------------------------------------------------------------------------- 1 | import { IBasePart } from 'rmrk-tools/dist/classes/base'; 2 | import { ConsolidatorReturnType } from 'rmrk-tools/dist/tools/consolidator/consolidator'; 3 | import { IResourceConsolidated } from 'rmrk-tools/dist/classes/nft'; 4 | 5 | export const getBaseParts = async ( 6 | setBaseParts: (baseParts: IBasePart[]) => void, 7 | baseResource?: IResourceConsolidated, 8 | ) => { 9 | if (baseResource) { 10 | const payload = await fetch('/chunky-dump.json'); 11 | const data: ConsolidatorReturnType = await payload.json(); 12 | const base = data.bases?.[baseResource.base || '']; 13 | const baseParts = base?.parts 14 | ? base.parts.filter((part) => (baseResource.parts || []).includes(part.id)) 15 | : []; 16 | 17 | setBaseParts(baseParts); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /projects/react-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rmrk2-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@chakra-ui/react": "^1.6.8", 13 | "@emotion/react": "^11", 14 | "@emotion/styled": "^11", 15 | "framer-motion": "^4", 16 | "next": "11.1.2", 17 | "ramda": "^0.27.1", 18 | "react": "17.0.2", 19 | "react-dom": "17.0.2", 20 | "react-inlinesvg": "^2.3.0", 21 | "rmrk-tools": "^2.0.11" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "17.0.24", 25 | "eslint": "7.32.0", 26 | "eslint-config-next": "11.1.2", 27 | "prettier": "^2.4.1", 28 | "typescript": "4.4.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RMRK2 Examples 2 | 3 | This repository showcases a collection of examples on how to use RMRK2 in it's current implementation (using `system.remark` exstrinsic) 4 | You can find Crowdcast demo of using this code here: https://www.crowdcast.io/e/buidl 5 | 6 | This is a monorepo, so do `yarn install` from the root of this repo 7 | 8 | ![Chunkies](chunkies.png) 9 | 10 | This example shows how to create a composable NFT using [Base](https://github.com/rmrk-team/rmrk-spec/blob/2.0-wip/standards/rmrk2.0.0/entities/base.md) entity of type SVG. As well as nested NFTs equippable into chunky hands 11 | 12 | ## Scripts 13 | 14 | Under `/projects/scripts` you can find all the minting scripts that you can run from CLI 15 | 16 | ## React demo 17 | 18 | Under `/projects/react-demo` you can find a simple Next.js react app and an example of SVG composer component that composes single image from all the Base parts and nested NFTs of each parent Chunky 19 | -------------------------------------------------------------------------------- /projects/scripts/utils.ts: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | import { KeyringPair } from "@polkadot/keyring/types"; 3 | import { Keyring } from "@polkadot/api"; 4 | 5 | export const getKeys = (): KeyringPair[] => { 6 | const k = []; 7 | const keyring = new Keyring({ type: "sr25519" }); 8 | k.push(keyring.addFromUri(process.env.PRIVAKE_KEY)); 9 | return k; 10 | }; 11 | 12 | export const sleep = (ms: number): Promise => { 13 | return new Promise((resolve) => { 14 | setTimeout(() => resolve(), ms); 15 | }); 16 | }; 17 | 18 | export const getKeyringFromUri = (phrase: string): KeyringPair => { 19 | const keyring = new Keyring({ type: "sr25519" }); 20 | return keyring.addFromUri(phrase); 21 | }; 22 | 23 | 24 | export const chunkArray = (array: any[], size: number) => { 25 | let result = []; 26 | for (let i = 0; i < array.length; i += size) { 27 | let chunk = array.slice(i, i + size); 28 | result.push(chunk); 29 | } 30 | return result; 31 | }; 32 | -------------------------------------------------------------------------------- /projects/react-demo/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /projects/scripts/simple-send.ts: -------------------------------------------------------------------------------- 1 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 2 | import { getKeyringFromUri } from "./utils"; 3 | import { getApi } from "./get-polkadot-api"; 4 | import { signAndSendWithRetry } from "./sign-and-send-with-retry"; 5 | 6 | export const simpleSend = async () => { 7 | try { 8 | console.log("SIMPLE SEND NFT START -------"); 9 | 10 | const nftId = process.argv[2]; 11 | if (!nftId) { 12 | throw new Error("NFT ID NOT PASSED"); 13 | } 14 | 15 | const recipient = process.argv[3]; 16 | if (!recipient) { 17 | throw new Error("RECIPIENT NOT PASSED"); 18 | } 19 | await cryptoWaitReady(); 20 | const phrase = process.env.PRIVAKE_KEY; 21 | const kp = getKeyringFromUri(phrase); 22 | 23 | const api = await getApi(); 24 | 25 | const remark = api.tx.system.remark( 26 | `RMRK::SEND::2.0.0::${nftId}::${recipient}` 27 | ); 28 | 29 | const tx = api.tx.utility.batchAll([remark]); 30 | const { block } = await signAndSendWithRetry(tx, kp); 31 | console.log(`Sent NFT ${nftId} at block: `, block); 32 | return block; 33 | } catch (error: any) { 34 | console.error(error); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /projects/react-demo/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /projects/react-demo/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import Head from 'next/head'; 3 | import Image from 'next/image'; 4 | import styles from '../styles/Home.module.css'; 5 | import { useEffect, useState } from 'react'; 6 | import {Badge, Box, SimpleGrid, Spinner} from '@chakra-ui/react'; 7 | import { 8 | ConsolidatorReturnType, 9 | NFTConsolidated, 10 | } from 'rmrk-tools/dist/tools/consolidator/consolidator'; 11 | import SvgResourceComposer from '../components/rmrk-svg-composer'; 12 | 13 | export const fetchData = async (setNfts: (nfts: NFTConsolidated[]) => void) => { 14 | try { 15 | const payload = await fetch('/chunky-dump.json'); 16 | const data: ConsolidatorReturnType = await payload.json(); 17 | if (data?.nfts) { 18 | setNfts(Object.values(data.nfts)); 19 | } 20 | console.log(data); 21 | } catch (error: any) { 22 | console.log(error); 23 | } 24 | }; 25 | 26 | const Home: NextPage = () => { 27 | const [nfts, setNfts] = useState([]); 28 | useEffect(() => { 29 | fetchData(setNfts); 30 | }, []); 31 | 32 | if (!nfts) { 33 | return ; 34 | } 35 | 36 | console.log(nfts) 37 | 38 | return ( 39 |
40 | 41 | Create Next App 42 | 43 | 44 | 45 | 46 | 47 | 48 | {nfts.filter(nft => nft.collection === 'd43593c715a56da27d-CHNK').map((nft, index) => ( 49 | 50 | {nft.sn.slice(nft.sn.length - 4)} 51 | 52 | 53 | ))} 54 | 55 | 56 |
57 | ); 58 | }; 59 | 60 | export default Home; 61 | -------------------------------------------------------------------------------- /projects/scripts/pinata-utils.ts: -------------------------------------------------------------------------------- 1 | import {sleep} from "./utils"; 2 | 3 | require('dotenv').config(); 4 | import { Metadata } from 'rmrk-tools/dist/tools/types'; 5 | import pLimit from 'p-limit'; 6 | import { Readable } from 'stream'; 7 | import fs from 'fs'; 8 | // @ts-ignore 9 | import pinataSDK, { PinataOptions, PinataPinOptions } from '@pinata/sdk'; 10 | 11 | const defaultOptions: Partial = { 12 | pinataOptions: { 13 | cidVersion: 1, 14 | }, 15 | }; 16 | 17 | export const pinata = pinataSDK(process.env.PINATA_KEY, process.env.PINATA_SECRET); 18 | 19 | const fsPromises = fs.promises; 20 | export type StreamPinata = Readable & { 21 | path?: string; 22 | }; 23 | const limit = pLimit(1); 24 | 25 | export const pinFileStreamToIpfs = async (file: StreamPinata, name?: string) => { 26 | const options = { ...defaultOptions, pinataMetadata: { name } }; 27 | const result = await pinata.pinFileToIPFS(file, options); 28 | return result.IpfsHash; 29 | }; 30 | 31 | export const uploadAndPinIpfsMetadata = async (metadataFields: Metadata): Promise => { 32 | const options = { 33 | ...defaultOptions, 34 | pinataMetadata: { name: metadataFields.name }, 35 | }; 36 | try { 37 | const metadata = { ...metadataFields }; 38 | const metadataHashResult = await pinata.pinJSONToIPFS(metadata, options); 39 | return `ipfs://ipfs/${metadataHashResult.IpfsHash}`; 40 | } catch (error) { 41 | return ''; 42 | } 43 | }; 44 | 45 | export const pinSingleMetadataFromDir = async ( 46 | dir: string, 47 | path: string, 48 | name: string, 49 | metadataBase: Partial, 50 | ) => { 51 | try { 52 | const imageFile = await fsPromises.readFile(`${process.cwd()}${dir}/${path}`); 53 | if (!imageFile) { 54 | throw new Error('No image file'); 55 | } 56 | 57 | const stream: StreamPinata = Readable.from(imageFile); 58 | stream.path = path; 59 | 60 | const imageCid = await pinFileStreamToIpfs(stream, name); 61 | console.log(`NFT ${path} IMAGE CID: `, imageCid); 62 | const metadata: Metadata = { ...metadataBase, name, mediaUri: `ipfs://ipfs/${imageCid}` }; 63 | const metadataCid = await uploadAndPinIpfsMetadata(metadata); 64 | await sleep(500); 65 | console.log(`NFT ${name} METADATA: `, metadataCid); 66 | return metadataCid; 67 | } catch (error) { 68 | console.log(error); 69 | console.log(JSON.stringify(error)); 70 | return ''; 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /projects/react-demo/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | height: 100vh; 9 | } 10 | 11 | .main { 12 | padding: 5rem 0; 13 | flex: 1; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | .footer { 21 | width: 100%; 22 | height: 100px; 23 | border-top: 1px solid #eaeaea; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | 29 | .footer a { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | flex-grow: 1; 34 | } 35 | 36 | .title a { 37 | color: #0070f3; 38 | text-decoration: none; 39 | } 40 | 41 | .title a:hover, 42 | .title a:focus, 43 | .title a:active { 44 | text-decoration: underline; 45 | } 46 | 47 | .title { 48 | margin: 0; 49 | line-height: 1.15; 50 | font-size: 4rem; 51 | } 52 | 53 | .title, 54 | .description { 55 | text-align: center; 56 | } 57 | 58 | .description { 59 | line-height: 1.5; 60 | font-size: 1.5rem; 61 | } 62 | 63 | .code { 64 | background: #fafafa; 65 | border-radius: 5px; 66 | padding: 0.75rem; 67 | font-size: 1.1rem; 68 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 69 | Bitstream Vera Sans Mono, Courier New, monospace; 70 | } 71 | 72 | .grid { 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | flex-wrap: wrap; 77 | max-width: 800px; 78 | margin-top: 3rem; 79 | } 80 | 81 | .card { 82 | margin: 1rem; 83 | padding: 1.5rem; 84 | text-align: left; 85 | color: inherit; 86 | text-decoration: none; 87 | border: 1px solid #eaeaea; 88 | border-radius: 10px; 89 | transition: color 0.15s ease, border-color 0.15s ease; 90 | width: 45%; 91 | } 92 | 93 | .card:hover, 94 | .card:focus, 95 | .card:active { 96 | color: #0070f3; 97 | border-color: #0070f3; 98 | } 99 | 100 | .card h2 { 101 | margin: 0 0 1rem 0; 102 | font-size: 1.5rem; 103 | } 104 | 105 | .card p { 106 | margin: 0; 107 | font-size: 1.25rem; 108 | line-height: 1.5; 109 | } 110 | 111 | .logo { 112 | height: 1em; 113 | margin-left: 0.5rem; 114 | } 115 | 116 | @media (max-width: 600px) { 117 | .grid { 118 | width: 100%; 119 | flex-direction: column; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/Chunky Items/Chunky_bone_left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/Chunky Items/Chunky_bone_right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/Chunky Items/Chunky_flag_left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/Chunky Items/Chunky_flag_right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/Chunky Items/Chunky_spear_right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/Chunky Items/Chunky_spear_left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/react-demo/components/rmrk-svg-composer.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useState } from 'react'; 2 | import { Flex } from '@chakra-ui/react'; 3 | import SVG from 'react-inlinesvg'; 4 | import { 5 | ConsolidatorReturnType, 6 | NFTConsolidated, 7 | } from 'rmrk-tools/dist/tools/consolidator/consolidator'; 8 | import { getBaseParts } from '../lib/get-base-parts'; 9 | import { IBasePart } from 'rmrk-tools/dist/classes/base'; 10 | import { arePropsEqual } from '../lib/are-props-equal'; 11 | 12 | interface IProps { 13 | nft: NFTConsolidated; 14 | } 15 | 16 | export const getEquippedInventoryItems = async ( 17 | setInventory: (basePart: Partial[]) => void, 18 | resources_parts?: IBasePart[], 19 | children?: NFTConsolidated['children'], 20 | ) => { 21 | const payload = await fetch('/chunky-dump.json'); 22 | const data: ConsolidatorReturnType = await payload.json(); 23 | 24 | const equippedChildren = (children || []).map((child) => { 25 | const nft = data?.nfts[child.id]; 26 | const matchingResource = nft.resources.find((resource) => resource.slot === child.equipped); 27 | 28 | return matchingResource; 29 | }); 30 | 31 | const slotParts = (resources_parts || []).map((resources_part) => { 32 | // Find base slot for each equipped children 33 | const matchingResource = equippedChildren.find( 34 | (resource) => resource?.slot && resource.slot.split('.')[1] === resources_part.id, 35 | ); 36 | 37 | if (resources_part.type !== 'slot') { 38 | return null; 39 | } 40 | 41 | return { 42 | z: resources_part.z, 43 | src: matchingResource?.src || resources_part.src, 44 | id: resources_part.id, 45 | }; 46 | }); 47 | 48 | const filteredParts = slotParts.filter((part): part is { z: number; src: string; id: string } => 49 | Boolean(part), 50 | ); 51 | 52 | setInventory(filteredParts); 53 | }; 54 | 55 | const SvgResourceComposer = ({ nft }: IProps) => { 56 | const [baseParts, setBaseParts] = useState(); 57 | const [equippedInventory, setEquippedInventory] = useState[]>(); 58 | 59 | useEffect(() => { 60 | getBaseParts( 61 | setBaseParts, 62 | nft.resources.find((resource) => Boolean(resource.base)), 63 | ); 64 | }, []); 65 | 66 | useEffect(() => { 67 | if (baseParts) { 68 | getEquippedInventoryItems(setEquippedInventory, baseParts, nft.children); 69 | } 70 | }, [baseParts]); 71 | 72 | const fixedParts = (baseParts || []).filter((resources_part) => resources_part.type === 'fixed'); 73 | 74 | const parts = [...(equippedInventory || []), ...fixedParts].sort( 75 | (first, second) => first?.z! - second.z!, 76 | ); 77 | 78 | return ( 79 | 85 | 90 | {parts.map( 91 | (part) => 92 | part.src && ( 93 | 102 | ), 103 | )} 104 | 105 | 106 | ); 107 | }; 108 | 109 | export default memo(SvgResourceComposer, arePropsEqual); 110 | -------------------------------------------------------------------------------- /projects/scripts/get-polkadot-api.ts: -------------------------------------------------------------------------------- 1 | import { ApiPromise, WsProvider } from '@polkadot/api'; 2 | import { sleep } from './utils'; 3 | import {RPC_ENDPOINTS} from "./constants"; 4 | 5 | const MAX_RETRIES = 5; 6 | const WS_DISCONNECT_TIMEOUT_SECONDS = 20; 7 | 8 | let wsProvider: WsProvider; 9 | let polkadotApi: ApiPromise; 10 | let healthCheckInProgress = false; 11 | 12 | /** 13 | * 14 | * @param wsEndpoints - array of rpc ws endpoints. In the order of their priority 15 | */ 16 | const providerHealthCheck = async (wsEndpoints: string[]) => { 17 | const [primaryEndpoint, secondaryEndpoint, ...otherEndpoints] = wsEndpoints; 18 | console.log( 19 | `Performing ${WS_DISCONNECT_TIMEOUT_SECONDS} seconds health check for WS Provider fro rpc ${primaryEndpoint}.`, 20 | ); 21 | healthCheckInProgress = true; 22 | await sleep(WS_DISCONNECT_TIMEOUT_SECONDS * 1000); 23 | if (wsProvider.isConnected) { 24 | console.log(`All good. Connected back to ${primaryEndpoint}`); 25 | healthCheckInProgress = false; 26 | return true; 27 | } else { 28 | console.log( 29 | `rpc endpoint ${primaryEndpoint} still disconnected after ${WS_DISCONNECT_TIMEOUT_SECONDS} seconds. Disconnecting from ${primaryEndpoint} and switching to a backup rpc endpoint ${secondaryEndpoint}`, 30 | ); 31 | await wsProvider.disconnect(); 32 | 33 | healthCheckInProgress = false; 34 | throw new Error( 35 | `rpc endpoint ${primaryEndpoint} still disconnected after ${WS_DISCONNECT_TIMEOUT_SECONDS} seconds.`, 36 | ); 37 | } 38 | }; 39 | 40 | /** 41 | * 42 | * @param wsEndpoints - array of rpc ws endpoints. In the order of their priority 43 | */ 44 | const getProvider = async (wsEndpoints: string[]) => { 45 | const [primaryEndpoint, ...otherEndpoints] = wsEndpoints; 46 | return await new Promise((resolve, reject) => { 47 | wsProvider = new WsProvider(primaryEndpoint); 48 | wsProvider.on('disconnected', async () => { 49 | console.log(`WS provider for rpc ${primaryEndpoint} disconnected!`); 50 | if (!healthCheckInProgress) { 51 | try { 52 | await providerHealthCheck(wsEndpoints); 53 | resolve(wsProvider); 54 | } catch (error: any) { 55 | reject(error); 56 | } 57 | } 58 | }); 59 | wsProvider.on('connected', () => { 60 | console.log(`WS provider for rpc ${primaryEndpoint} connected`); 61 | resolve(wsProvider); 62 | }); 63 | wsProvider.on('error', async () => { 64 | console.log(`Error thrown for rpc ${primaryEndpoint}`); 65 | if (!healthCheckInProgress) { 66 | try { 67 | await providerHealthCheck(wsEndpoints); 68 | resolve(wsProvider); 69 | } catch (error: any) { 70 | reject(error); 71 | } 72 | } 73 | }); 74 | }); 75 | }; 76 | 77 | /** 78 | * 79 | * @param wsEndpoints - array of rpc ws endpoints. In the order of their priority 80 | * @param retry - retry count 81 | */ 82 | export const getApi = async ( 83 | wsEndpoints: string[] = RPC_ENDPOINTS, 84 | retry = 0, 85 | ): Promise => { 86 | if (wsProvider && polkadotApi && polkadotApi.isConnected) return polkadotApi; 87 | const [primaryEndpoint, secondaryEndpoint, ...otherEndpoints] = wsEndpoints; 88 | 89 | try { 90 | const provider = await getProvider(wsEndpoints); 91 | polkadotApi = await ApiPromise.create({ provider }); 92 | await polkadotApi.isReady; 93 | return polkadotApi; 94 | } catch (error: any) { 95 | if (retry < MAX_RETRIES) { 96 | // If we have reached maximum number of retries on the primaryEndpoint, let's move it to the end of array and try the secondary endpoint 97 | return await getApi([secondaryEndpoint, ...otherEndpoints, primaryEndpoint], retry + 1); 98 | } else { 99 | return polkadotApi; 100 | } 101 | } 102 | }; 103 | -------------------------------------------------------------------------------- /projects/scripts/create-base.ts: -------------------------------------------------------------------------------- 1 | import { IBasePart } from "rmrk-tools/dist/classes/base"; 2 | import { 3 | ASSETS_CID, 4 | CHUNKY_BASE_SYMBOL, 5 | CHUNKY_ITEMS_COLLECTION_SYMBOL, 6 | WS_URL, 7 | } from "./constants"; 8 | import { cryptoWaitReady, encodeAddress } from "@polkadot/util-crypto"; 9 | import { getKeyringFromUri, getKeys } from "./utils"; 10 | import { Collection, Base } from "rmrk-tools"; 11 | import { u8aToHex } from "@polkadot/util"; 12 | import {getApi} from "./get-polkadot-api"; 13 | import {signAndSendWithRetry} from "./sign-and-send-with-retry"; 14 | 15 | export const fixedParts: IBasePart[] = [ 16 | { 17 | type: "fixed", 18 | id: "chunky_body_1", 19 | src: `ipfs://ipfs/${ASSETS_CID}/v1/Chunky_body_v1.svg`, 20 | z: 0, 21 | }, 22 | { 23 | type: "fixed", 24 | id: "chunky_body_2", 25 | src: `ipfs://ipfs/${ASSETS_CID}/v2/Chunky_body_v2.svg`, 26 | z: 0, 27 | }, 28 | { 29 | type: "fixed", 30 | id: "chunky_body_3", 31 | src: `ipfs://ipfs/${ASSETS_CID}/v3/Chunky_body_v3.svg`, 32 | z: 0, 33 | }, 34 | { 35 | type: "fixed", 36 | id: "chunky_body_4", 37 | src: `ipfs://ipfs/${ASSETS_CID}/v4/Chunky_body_v4.svg`, 38 | z: 0, 39 | }, 40 | { 41 | type: "fixed", 42 | id: "chunky_head_1", 43 | src: `ipfs://ipfs/${ASSETS_CID}/v1/Chunky_head_v1.svg`, 44 | z: 4, 45 | }, 46 | { 47 | type: "fixed", 48 | id: "chunky_head_2", 49 | src: `ipfs://ipfs/${ASSETS_CID}/v2/Chunky_head_v2.svg`, 50 | z: 4, 51 | }, 52 | { 53 | type: "fixed", 54 | id: "chunky_head_3", 55 | src: `ipfs://ipfs/${ASSETS_CID}/v3/Chunky_head_v3.svg`, 56 | z: 4, 57 | }, 58 | { 59 | type: "fixed", 60 | id: "chunky_head_4", 61 | src: `ipfs://ipfs/${ASSETS_CID}/v4/Chunky_head_v4.svg`, 62 | z: 4, 63 | }, 64 | { 65 | type: "fixed", 66 | id: "chunky_hand_1", 67 | src: `ipfs://ipfs/${ASSETS_CID}/v1/Chunky_hand_v1.svg`, 68 | z: 3, 69 | }, 70 | { 71 | type: "fixed", 72 | id: "chunky_hand_2", 73 | src: `ipfs://ipfs/${ASSETS_CID}/v2/Chunky_hand_v2.svg`, 74 | z: 3, 75 | }, 76 | { 77 | type: "fixed", 78 | id: "chunky_hand_3", 79 | src: `ipfs://ipfs/${ASSETS_CID}/v3/Chunky_hand_v3.svg`, 80 | z: 3, 81 | }, 82 | { 83 | type: "fixed", 84 | id: "chunky_hand_4", 85 | src: `ipfs://ipfs/${ASSETS_CID}/v4/Chunky_hand_v4.svg`, 86 | z: 3, 87 | }, 88 | ]; 89 | 90 | const getSlotKanariaParts = (equippable: string[] | "*" = []): IBasePart[] => { 91 | return [ 92 | { 93 | type: "slot", 94 | id: "chunky_objectLeft", 95 | equippable, 96 | z: 1, 97 | }, 98 | { 99 | type: "slot", 100 | id: "chunky_objectRight", 101 | equippable, 102 | z: 2, 103 | }, 104 | ]; 105 | }; 106 | 107 | export const createBase = async () => { 108 | try { 109 | console.log("CREATE CHUNKY BASE START -------"); 110 | await cryptoWaitReady(); 111 | const accounts = getKeys(); 112 | const ws = WS_URL; 113 | const phrase = process.env.PRIVAKE_KEY; 114 | const api = await getApi(); 115 | const kp = getKeyringFromUri(phrase); 116 | 117 | const collectionId = Collection.generateId( 118 | u8aToHex(accounts[0].publicKey), 119 | CHUNKY_ITEMS_COLLECTION_SYMBOL 120 | ); 121 | console.log("collectionId", collectionId); 122 | 123 | const baseParts = [...fixedParts, ...getSlotKanariaParts([collectionId])]; 124 | 125 | const baseEntity = new Base( 126 | 0, 127 | CHUNKY_BASE_SYMBOL, 128 | encodeAddress(kp.address, 2), 129 | "svg", 130 | baseParts 131 | ); 132 | 133 | const { block } = await signAndSendWithRetry( 134 | api.tx.system.remark(baseEntity.base()), 135 | kp 136 | ); 137 | console.log("Chunky Base created at block: ", block); 138 | return block; 139 | return block; 140 | } catch (error: any) { 141 | console.error(error); 142 | } 143 | }; 144 | -------------------------------------------------------------------------------- /projects/scripts/sign-and-send-with-retry.ts: -------------------------------------------------------------------------------- 1 | import { SubmittableExtrinsic } from '@polkadot/api/types'; 2 | import { ISubmittableResult } from '@polkadot/types/types'; 3 | import { KeyringPair } from '@polkadot/keyring/types'; 4 | import { getApi } from './get-polkadot-api'; 5 | import { sleep } from './utils'; 6 | import { CodecHash, EventRecord } from '@polkadot/types/interfaces'; 7 | 8 | const MAX_RETRIES = 5; 9 | const RETRY_DELAY_SECONDS = 4; 10 | 11 | interface ISendTxReturnType { 12 | success: boolean; 13 | hash?: CodecHash; 14 | included: EventRecord[]; 15 | finalized: EventRecord[]; 16 | block: number; 17 | } 18 | 19 | /** 20 | * 21 | * @param tx - polkadot.js api tx 22 | * @param account - Account keypair 23 | * @param resolvedOnFinalizedOnly - If you don't want to wait for promise to resolve only when the block is finalized, 24 | * it can resolve as soon as tx is added to a block. This doesn't guarantee that transaction block will be included in finalised chain. 25 | * true by default 26 | * @param retry - retry count in case of failure. 27 | */ 28 | export const signAndSendWithRetry = async ( 29 | tx: SubmittableExtrinsic<'promise', ISubmittableResult>, 30 | account: KeyringPair, 31 | resolvedOnFinalizedOnly = true, 32 | retry = 0, 33 | ): Promise => { 34 | return new Promise(async (resolve, reject) => { 35 | const api = await getApi(); 36 | 37 | const returnObject: ISendTxReturnType = { success: false, hash: undefined, included: [], finalized: [], block: 0 } 38 | 39 | try { 40 | const unsubscribe = await tx.signAndSend( 41 | account, 42 | { nonce: -1 }, 43 | async ({ events = [], status, dispatchError }) => { 44 | returnObject.success = !dispatchError; 45 | returnObject.included = [...events]; 46 | returnObject.hash = status.hash; 47 | 48 | const rejectPromise = (error: any) => { 49 | console.error(`Error sending tx`, error); 50 | console.log(`tx for the error above`, tx.toHuman()); 51 | unsubscribe(); 52 | reject(error); 53 | } 54 | 55 | if (status.isInBlock) { 56 | console.log( 57 | `📀 Transaction ${tx.meta.name} included at blockHash ${status.asInBlock} [success = ${!dispatchError}]`, 58 | ); 59 | 60 | // Get block number that this tx got into, to return back to user 61 | const signedBlock = await api.rpc.chain.getBlock(status.asInBlock); 62 | returnObject.block = signedBlock.block.header.number.toNumber(); 63 | 64 | // If we don't care about waiting for this tx to get into a finalized block, we can return early. 65 | if (!resolvedOnFinalizedOnly && !dispatchError) { 66 | unsubscribe(); 67 | resolve(returnObject); 68 | } 69 | } else if (status.isBroadcast) { 70 | console.log(`🚀 Transaction broadcasted.`); 71 | } else if (status.isFinalized) { 72 | console.log( 73 | `💯 Transaction ${tx.meta.name}(..) Finalized at blockHash ${status.asFinalized}`, 74 | ); 75 | if (returnObject.block === 0) { 76 | const signedBlock = await api.rpc.chain.getBlock(status.asInBlock); 77 | returnObject.block = signedBlock.block.header.number.toNumber(); 78 | } 79 | 80 | unsubscribe(); 81 | resolve(returnObject); 82 | } else if (status.isReady) { 83 | // let's not be too noisy.. 84 | } else if (status.isInvalid) { 85 | rejectPromise(new Error(`Extrinsic isInvalid`)) 86 | } else { 87 | console.log(`🤷 Other status ${status}`); 88 | } 89 | }, 90 | ); 91 | } catch (error: any) { 92 | console.log( 93 | `Error sending tx. Error: "${error.message}". TX: ${JSON.stringify(tx.toHuman())}`, 94 | ); 95 | if (retry < MAX_RETRIES) { 96 | console.log(`sendAndFinalize Retry #${retry} of ${MAX_RETRIES}`); 97 | await sleep(RETRY_DELAY_SECONDS * 1000); 98 | const result = await signAndSendWithRetry(tx, account, resolvedOnFinalizedOnly, retry + 1); 99 | resolve(result); 100 | } else { 101 | console.error(`Error initiating tx signAndSend`, error); 102 | reject(error); 103 | } 104 | } 105 | }); 106 | }; 107 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/Chunky Items/Chunky_pencil_right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/Chunky Items/Chunky_pencil_left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/scripts/mint-chunky.ts: -------------------------------------------------------------------------------- 1 | import { cryptoWaitReady, encodeAddress } from "@polkadot/util-crypto"; 2 | import { getKeyringFromUri, getKeys } from "./utils"; 3 | import { 4 | ASSETS_CID, 5 | CHUNKY_BASE_SYMBOL, 6 | CHUNKY_COLLECTION_SYMBOL, 7 | WS_URL, 8 | } from "./constants"; 9 | import { Base, Collection, NFT } from "rmrk-tools"; 10 | import { u8aToHex } from "@polkadot/util"; 11 | import { pinSingleMetadataFromDir } from "./pinata-utils"; 12 | import { nanoid } from "nanoid"; 13 | import {getApi} from "./get-polkadot-api"; 14 | import {signAndSendWithRetry} from "./sign-and-send-with-retry"; 15 | 16 | export const addBaseResource = async ( 17 | chunkyBlock: number, 18 | baseBlock: number 19 | ) => { 20 | try { 21 | console.log("ADD BASE RESOURCE TO CHUNKY NFT START -------"); 22 | await cryptoWaitReady(); 23 | const accounts = getKeys(); 24 | const phrase = process.env.PRIVAKE_KEY; 25 | const kp = getKeyringFromUri(phrase); 26 | 27 | const collectionId = Collection.generateId( 28 | u8aToHex(accounts[0].publicKey), 29 | CHUNKY_COLLECTION_SYMBOL 30 | ); 31 | 32 | const api = await getApi(); 33 | const serialNumbers = [1, 2, 3, 4]; 34 | 35 | const baseEntity = new Base( 36 | baseBlock, 37 | CHUNKY_BASE_SYMBOL, 38 | encodeAddress(kp.address, 2), 39 | "svg" 40 | ); 41 | 42 | const BASE_ID = baseEntity.getId(); 43 | 44 | const resourceRemarks = []; 45 | 46 | serialNumbers.forEach((sn) => { 47 | const nft = new NFT({ 48 | block: chunkyBlock, 49 | collection: collectionId, 50 | symbol: `chunky_${sn}`, 51 | transferable: 1, 52 | sn: `${sn}`.padStart(8, "0"), 53 | owner: encodeAddress(accounts[0].address, 2), 54 | metadata: "", 55 | }); 56 | 57 | const baseResId = nanoid(8); 58 | 59 | resourceRemarks.push( 60 | nft.resadd({ 61 | base: BASE_ID, 62 | id: baseResId, 63 | parts: [ 64 | `chunky_body_${sn}`, 65 | `chunky_head_${sn}`, 66 | `chunky_hand_${sn}`, 67 | "chunky_objectLeft", 68 | "chunky_objectRight", 69 | ], 70 | thumb: `ipfs://ipfs/${ASSETS_CID}/Chunky%20Preview.png`, 71 | }) 72 | ); 73 | 74 | if (sn === 4) { 75 | const secondaryArtResId = nanoid(8); 76 | resourceRemarks.push( 77 | nft.resadd({ 78 | src: `ipfs://ipfs/${ASSETS_CID}/chunky_altresource.jpg`, 79 | thumb: `ipfs://ipfs/${ASSETS_CID}/chunky_altresource.jpg`, 80 | id: secondaryArtResId, 81 | }) 82 | ); 83 | 84 | resourceRemarks.push(nft.setpriority([secondaryArtResId, baseResId])); 85 | } 86 | }); 87 | 88 | const txs = resourceRemarks.map((remark) => api.tx.system.remark(remark)); 89 | const tx = api.tx.utility.batch(txs); 90 | const { block } = await signAndSendWithRetry(tx, kp); 91 | console.log("Chunky base resources added at block: ", block); 92 | } catch (error: any) { 93 | console.error(error); 94 | } 95 | }; 96 | 97 | export const createChunkyCollection = async () => { 98 | try { 99 | console.log("CREATE CHUNKY COLLECTION START -------"); 100 | await cryptoWaitReady(); 101 | const accounts = getKeys(); 102 | const phrase = process.env.PRIVAKE_KEY; 103 | const api = await getApi(); 104 | const kp = getKeyringFromUri(phrase); 105 | 106 | const collectionId = Collection.generateId( 107 | u8aToHex(accounts[0].publicKey), 108 | CHUNKY_COLLECTION_SYMBOL 109 | ); 110 | 111 | const collectionMetadataCid = await pinSingleMetadataFromDir( 112 | "/assets/chunky", 113 | "Chunky Preview.png", 114 | "RMRK2 demo chunky collection", 115 | { 116 | description: "This is Chunky! RMRK2 demo nested NFT", 117 | externalUri: "https://rmrk.app", 118 | properties: {}, 119 | } 120 | ); 121 | 122 | const ItemsCollection = new Collection( 123 | 0, 124 | 10000, 125 | encodeAddress(accounts[0].address, 2), 126 | CHUNKY_COLLECTION_SYMBOL, 127 | collectionId, 128 | collectionMetadataCid 129 | ); 130 | 131 | const { block } = await signAndSendWithRetry( 132 | api.tx.system.remark(ItemsCollection.create()), 133 | kp 134 | ); 135 | console.log("COLLECTION CREATION REMARK: ", ItemsCollection.create()); 136 | console.log("Chunky collection created at block: ", block); 137 | 138 | return block; 139 | } catch (error: any) { 140 | console.error(error); 141 | } 142 | }; 143 | 144 | export const mintChunky = async () => { 145 | try { 146 | console.log("CREATE CHUNKY NFT START -------"); 147 | await cryptoWaitReady(); 148 | const accounts = getKeys(); 149 | const phrase = process.env.PRIVAKE_KEY; 150 | const kp = getKeyringFromUri(phrase); 151 | 152 | const collectionId = Collection.generateId( 153 | u8aToHex(accounts[0].publicKey), 154 | CHUNKY_COLLECTION_SYMBOL 155 | ); 156 | 157 | await createChunkyCollection(); 158 | 159 | const api = await getApi(); 160 | 161 | const serialNumbers = [1, 2, 3, 4]; 162 | 163 | const promises = serialNumbers.map(async (sn) => { 164 | const metadataCid = await pinSingleMetadataFromDir( 165 | "/assets/chunky", 166 | "Chunky Preview.png", 167 | `RMRK2 demo chunky NFT #${sn}`, 168 | { 169 | description: `This is Chunky #${sn}! RMRK2 demo nested NFT`, 170 | externalUri: "https://rmrk.app", 171 | properties: { 172 | rarity: { 173 | type: "string", 174 | value: sn === 4 ? "epic" : "common", 175 | }, 176 | }, 177 | } 178 | ); 179 | 180 | const nft = new NFT({ 181 | block: 0, 182 | collection: collectionId, 183 | symbol: `chunky_${sn}`, 184 | transferable: 1, 185 | sn: `${sn}`.padStart(8, "0"), 186 | owner: encodeAddress(accounts[0].address, 2), 187 | metadata: metadataCid, 188 | }); 189 | 190 | return nft.mint(); 191 | }); 192 | 193 | const remarks = await Promise.all(promises); 194 | 195 | const txs = remarks.map((remark) => api.tx.system.remark(remark)); 196 | const tx = api.tx.utility.batchAll(txs); 197 | const { block } = await signAndSendWithRetry(tx, kp); 198 | console.log("Chunky NFT minted at block: ", block); 199 | return block; 200 | } catch (error: any) { 201 | console.error(error); 202 | } 203 | }; 204 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/v1/Chunky_head_v1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/scripts/mint-chunky-items.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ASSETS_CID, 3 | CHUNKY_COLLECTION_SYMBOL, 4 | CHUNKY_ITEMS_COLLECTION_SYMBOL, 5 | WS_URL, 6 | CHUNKY_BASE_SYMBOL 7 | } from "./constants"; 8 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 9 | import { getKeyringFromUri, getKeys } from "./utils"; 10 | import { Collection, NFT, Base } from "rmrk-tools"; 11 | import { u8aToHex } from "@polkadot/util"; 12 | import { encodeAddress } from "@polkadot/keyring"; 13 | import { nanoid } from "nanoid"; 14 | import {pinSingleMetadataFromDir} from "./pinata-utils"; 15 | import {getApi} from "./get-polkadot-api"; 16 | import {signAndSendWithRetry} from "./sign-and-send-with-retry"; 17 | 18 | const chunkyItems = [ 19 | { 20 | symbol: "chunky_bone", 21 | thumb: "Chunky_bone_thumb.png", 22 | resources: ["Chunky_bone_left.svg", "Chunky_bone_right.svg"], 23 | name: "The Bone", 24 | description: "Chunky likes his bone!", 25 | }, 26 | { 27 | symbol: "chunky_flag", 28 | thumb: "Chunky_flag_thumb.png", 29 | resources: ["Chunky_flag_left.svg", "Chunky_flag_right.svg"], 30 | name: "The Flag", 31 | description: "Chunky likes his flag!", 32 | }, 33 | { 34 | symbol: "chunky_pencil", 35 | thumb: "Chunky_pencil_thumb.png", 36 | resources: ["Chunky_pencil_left.svg", "Chunky_pencil_right.svg"], 37 | name: "The Pencil", 38 | description: "Chunky likes his pencil!", 39 | }, 40 | { 41 | symbol: "chunky_spear", 42 | thumb: "Chunky_spear_thumb.png", 43 | resources: ["Chunky_spear_left.svg", "Chunky_spear_right.svg"], 44 | name: "The Spear", 45 | description: "Chunky likes his spear!", 46 | }, 47 | ]; 48 | 49 | export const mintItems = async (chunkyBlock: number, baseBlock: number) => { 50 | try { 51 | console.log("CREATE CHUNKY ITEMS START -------"); 52 | await cryptoWaitReady(); 53 | const accounts = getKeys(); 54 | const ws = WS_URL; 55 | const phrase = process.env.PRIVAKE_KEY; 56 | const api = await getApi(); 57 | const kp = getKeyringFromUri(phrase); 58 | 59 | const collectionId = Collection.generateId( 60 | u8aToHex(accounts[0].publicKey), 61 | CHUNKY_ITEMS_COLLECTION_SYMBOL 62 | ); 63 | 64 | const chunkyCollectionId = Collection.generateId( 65 | u8aToHex(accounts[0].publicKey), 66 | CHUNKY_COLLECTION_SYMBOL 67 | ); 68 | 69 | const baseEntity = new Base( 70 | baseBlock, 71 | CHUNKY_BASE_SYMBOL, 72 | encodeAddress(kp.address, 2), 73 | "svg" 74 | ); 75 | 76 | await createItemsCollection(); 77 | 78 | const promises = chunkyItems.map(async (item, index) => { 79 | const sn = index + 1; 80 | 81 | const metadataCid = await pinSingleMetadataFromDir( 82 | "/assets/chunky/Chunky Items", 83 | item.thumb, 84 | item.name, 85 | { 86 | description: item.description, 87 | externalUri: "https://rmrk.app", 88 | } 89 | ); 90 | 91 | const nft = new NFT({ 92 | block: 0, 93 | sn: sn.toString().padStart(8, "0"), 94 | owner: encodeAddress(accounts[0].address, 2), 95 | transferable: 1, 96 | metadata: metadataCid, 97 | collection: collectionId, 98 | symbol: item.symbol, 99 | }); 100 | 101 | return nft.mint(); 102 | }); 103 | 104 | const remarks = await Promise.all(promises); 105 | 106 | const txs = remarks.map((remark) => api.tx.system.remark(remark)); 107 | const batch = api.tx.utility.batch(txs); 108 | const { block } = await signAndSendWithRetry(batch, kp); 109 | console.log("CHUNKY ITEMS MINTED AT BLOCK: ", block); 110 | 111 | const resaddSendRemarks = []; 112 | 113 | chunkyItems.forEach((item, index) => { 114 | const sn = index + 1; 115 | const nft = new NFT({ 116 | block, 117 | sn: sn.toString().padStart(8, "0"), 118 | owner: encodeAddress(accounts[0].address, 2), 119 | transferable: 1, 120 | metadata: `ipfs://ipfs/trololo`, 121 | collection: collectionId, 122 | symbol: item.symbol, 123 | }); 124 | 125 | item.resources.forEach((resource) => { 126 | resaddSendRemarks.push( 127 | nft.resadd({ 128 | src: `ipfs://ipfs/${ASSETS_CID}/Chunky Items/${resource}`, 129 | thumb: `ipfs://ipfs/${ASSETS_CID}/Chunky Items/${item.thumb}`, 130 | id: nanoid(8), 131 | slot: resource.includes("left") 132 | ? `${baseEntity.getId()}.chunky_objectLeft` 133 | : `${baseEntity.getId()}.chunky_objectRight`, 134 | }) 135 | ); 136 | }); 137 | 138 | const chunkyNft = new NFT({ 139 | block: chunkyBlock, 140 | collection: chunkyCollectionId, 141 | symbol: `chunky_${sn}`, 142 | transferable: 1, 143 | sn: `${sn}`.padStart(8, "0"), 144 | owner: encodeAddress(accounts[0].address, 2), 145 | metadata: "", 146 | }); 147 | 148 | resaddSendRemarks.push(nft.send(chunkyNft.getId())); 149 | resaddSendRemarks.push( 150 | nft.equip( 151 | `${baseEntity.getId()}.${ 152 | index % 2 ? "chunky_objectLeft" : "chunky_objectRight" 153 | }` 154 | ) 155 | ); 156 | }); 157 | 158 | const restxs = resaddSendRemarks.map((remark) => 159 | api.tx.system.remark(remark) 160 | ); 161 | const resbatch = api.tx.utility.batch(restxs); 162 | const { block: resaddSendBlock } = await signAndSendWithRetry(resbatch, kp); 163 | console.log("CHUNKY ITEMS RESOURCE ADDED AND SENT: ", resaddSendBlock); 164 | return true; 165 | } catch (error: any) { 166 | console.error(error); 167 | } 168 | }; 169 | 170 | export const createItemsCollection = async () => { 171 | try { 172 | console.log("CREATE CHUNKY ITEMS COLLECTION START -------"); 173 | await cryptoWaitReady(); 174 | const accounts = getKeys(); 175 | const phrase = process.env.PRIVAKE_KEY; 176 | const api = await getApi(); 177 | const kp = getKeyringFromUri(phrase); 178 | 179 | const collectionId = Collection.generateId( 180 | u8aToHex(accounts[0].publicKey), 181 | CHUNKY_ITEMS_COLLECTION_SYMBOL 182 | ); 183 | 184 | const collectionMetadataCid = await pinSingleMetadataFromDir( 185 | "/assets/chunky", 186 | "Chunky Preview.png", 187 | "RMRK2 demo chunky items collection", 188 | { 189 | description: "This is Chunky items! RMRK2 demo nested NFTs", 190 | externalUri: "https://rmrk.app", 191 | properties: {}, 192 | } 193 | ); 194 | 195 | const ItemsCollection = new Collection( 196 | 0, 197 | 0, 198 | encodeAddress(accounts[0].address, 2), 199 | CHUNKY_ITEMS_COLLECTION_SYMBOL, 200 | collectionId, 201 | collectionMetadataCid 202 | ); 203 | 204 | const { block } = await signAndSendWithRetry( 205 | api.tx.system.remark(ItemsCollection.create()), 206 | kp 207 | ); 208 | console.log("Chunky items collection created at block: ", block); 209 | 210 | return block; 211 | } catch (error: any) { 212 | console.error(error); 213 | } 214 | }; 215 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/v1/Chunky_hand_v1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/v2/Chunky_hand_v2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/v3/Chunky_hand_v3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/v4/Chunky_hand_v4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/v4/Chunky_head_v4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/react-demo/public/chunky-dump.json: -------------------------------------------------------------------------------- 1 | {"nfts":{"209-d43593c715a56da27d-CHNK-chunky_bird_1-00000001":{"changes":[],"children":[{"id":"526-d43593c715a56da27d-CHNKITMS-chunky_bone-00000001","pending":false,"equipped":"base-13-CHNKBS.chunky_objectRight"}],"resources":[{"pending":false,"id":"nCkINZNg","base":"base-13-CHNKBS","parts":["chunky_body_1","chunky_head_1","chunky_hand_1","chunky_objectLeft","chunky_objectRight"],"thumb":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Preview.png"}],"block":209,"collection":"d43593c715a56da27d-CHNK","symbol":"chunky_bird_1","transferable":1,"sn":"00000001","metadata":"ipfs://ipfs/bafkreiefmtvvjnqnlqs2dzavsyble5ft5ly7xtu4pg7tpo5gf4sovputwy","priority":["nCkINZNg"],"owner":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","rootowner":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","reactions":{},"forsale":"0","burned":"","properties":{},"pending":false,"id":"209-d43593c715a56da27d-CHNK-chunky_bird_1-00000001"},"209-d43593c715a56da27d-CHNK-chunky_bird_2-00000002":{"changes":[],"children":[{"id":"526-d43593c715a56da27d-CHNKITMS-chunky_flag-00000002","pending":false,"equipped":"base-13-CHNKBS.chunky_objectLeft"}],"resources":[{"pending":false,"id":"b4HIImJD","base":"base-13-CHNKBS","parts":["chunky_body_2","chunky_head_2","chunky_hand_2","chunky_objectLeft","chunky_objectRight"],"thumb":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Preview.png"}],"block":209,"collection":"d43593c715a56da27d-CHNK","symbol":"chunky_bird_2","transferable":1,"sn":"00000002","metadata":"ipfs://ipfs/bafkreidfmr2jukykcxdnkt4qtgttr64ob5oidpi3gakxqqnx7k5zeiinvi","priority":["b4HIImJD"],"owner":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","rootowner":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","reactions":{},"forsale":"0","burned":"","properties":{},"pending":false,"id":"209-d43593c715a56da27d-CHNK-chunky_bird_2-00000002"},"209-d43593c715a56da27d-CHNK-chunky_bird_3-00000003":{"changes":[],"children":[{"id":"526-d43593c715a56da27d-CHNKITMS-chunky_pencil-00000003","pending":false,"equipped":"base-13-CHNKBS.chunky_objectRight"}],"resources":[{"pending":false,"id":"gIs2u9fw","base":"base-13-CHNKBS","parts":["chunky_body_3","chunky_head_3","chunky_hand_3","chunky_objectLeft","chunky_objectRight"],"thumb":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Preview.png"}],"block":209,"collection":"d43593c715a56da27d-CHNK","symbol":"chunky_bird_3","transferable":1,"sn":"00000003","metadata":"ipfs://ipfs/bafkreia45odhqo53gqzi7gfa7a56zp6m6vmjidgurrvrlg5i7ces7b7xri","priority":["gIs2u9fw"],"owner":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","rootowner":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","reactions":{},"forsale":"0","burned":"","properties":{},"pending":false,"id":"209-d43593c715a56da27d-CHNK-chunky_bird_3-00000003"},"209-d43593c715a56da27d-CHNK-chunky_bird_4-00000004":{"changes":[],"children":[{"id":"526-d43593c715a56da27d-CHNKITMS-chunky_spear-00000004","pending":false,"equipped":"base-13-CHNKBS.chunky_objectLeft"}],"resources":[{"pending":false,"id":"JoFWZKXr","base":"base-13-CHNKBS","parts":["chunky_body_4","chunky_head_4","chunky_hand_4","chunky_objectLeft","chunky_objectRight"],"thumb":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Preview.png"},{"pending":false,"id":"pibnOWHo","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/chunky_altresource.jpg","thumb":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/chunky_altresource.jpg"}],"block":209,"collection":"d43593c715a56da27d-CHNK","symbol":"chunky_bird_4","transferable":1,"sn":"00000004","metadata":"ipfs://ipfs/bafkreiafyggf5wgxgpoftd4xzjdkyxwdmr2qhg75nx5y3kfelmj234eq7m","priority":["pibnOWHo","JoFWZKXr"],"owner":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","rootowner":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","reactions":{},"forsale":"0","burned":"","properties":{},"pending":false,"id":"209-d43593c715a56da27d-CHNK-chunky_bird_4-00000004"},"526-d43593c715a56da27d-CHNKITMS-chunky_bone-00000001":{"changes":[{"field":"owner","old":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","new":"209-d43593c715a56da27d-CHNK-chunky_bird_1-00000001","caller":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","block":529,"opType":"SEND"}],"children":[],"resources":[{"pending":false,"id":"8DicBEHt","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_bone_left.svg","slot":"base-13-CHNKBS.chunky_objectLeft","thumb":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_bone_thumb.png"},{"pending":false,"id":"wKnVWhlT","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_bone_right.svg","slot":"base-13-CHNKBS.chunky_objectRight","thumb":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_bone_thumb.png"}],"block":526,"collection":"d43593c715a56da27d-CHNKITMS","symbol":"chunky_bone","transferable":1,"sn":"00000001","metadata":"ipfs://ipfs/bafkreifbdlldfpfz4vpuv6jmgbtcvbux3djgeps6n4ldhobagdp6ntan54","priority":["8DicBEHt","wKnVWhlT"],"owner":"209-d43593c715a56da27d-CHNK-chunky_bird_1-00000001","rootowner":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","reactions":{},"forsale":"0","burned":"","properties":{},"pending":false,"id":"526-d43593c715a56da27d-CHNKITMS-chunky_bone-00000001"},"526-d43593c715a56da27d-CHNKITMS-chunky_flag-00000002":{"changes":[{"field":"owner","old":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","new":"209-d43593c715a56da27d-CHNK-chunky_bird_2-00000002","caller":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","block":529,"opType":"SEND"}],"children":[],"resources":[{"pending":false,"id":"Ntc9RUMD","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_flag_left.svg","slot":"base-13-CHNKBS.chunky_objectLeft","thumb":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_flag_thumb.png"},{"pending":false,"id":"7rPrE7OV","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_flag_right.svg","slot":"base-13-CHNKBS.chunky_objectRight","thumb":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_flag_thumb.png"}],"block":526,"collection":"d43593c715a56da27d-CHNKITMS","symbol":"chunky_flag","transferable":1,"sn":"00000002","metadata":"ipfs://ipfs/bafkreicbipz6whc3zzr35sxmxe5acmovlwbf6hklklwueg3fqi4quecnq4","priority":["Ntc9RUMD","7rPrE7OV"],"owner":"209-d43593c715a56da27d-CHNK-chunky_bird_2-00000002","rootowner":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","reactions":{},"forsale":"0","burned":"","properties":{},"pending":false,"id":"526-d43593c715a56da27d-CHNKITMS-chunky_flag-00000002"},"526-d43593c715a56da27d-CHNKITMS-chunky_pencil-00000003":{"changes":[{"field":"owner","old":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","new":"209-d43593c715a56da27d-CHNK-chunky_bird_3-00000003","caller":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","block":529,"opType":"SEND"}],"children":[],"resources":[{"pending":false,"id":"P4DrAeb9","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_pencil_left.svg","slot":"base-13-CHNKBS.chunky_objectLeft","thumb":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_pencil_thumb.png"},{"pending":false,"id":"cC72og79","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_pencil_right.svg","slot":"base-13-CHNKBS.chunky_objectRight","thumb":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_pencil_thumb.png"}],"block":526,"collection":"d43593c715a56da27d-CHNKITMS","symbol":"chunky_pencil","transferable":1,"sn":"00000003","metadata":"ipfs://ipfs/bafkreibvrtwnap4uxqszojrbluju6pgaogji4rtznce6nl6mesqn63745i","priority":["P4DrAeb9","cC72og79"],"owner":"209-d43593c715a56da27d-CHNK-chunky_bird_3-00000003","rootowner":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","reactions":{},"forsale":"0","burned":"","properties":{},"pending":false,"id":"526-d43593c715a56da27d-CHNKITMS-chunky_pencil-00000003"},"526-d43593c715a56da27d-CHNKITMS-chunky_spear-00000004":{"changes":[{"field":"owner","old":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","new":"209-d43593c715a56da27d-CHNK-chunky_bird_4-00000004","caller":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","block":529,"opType":"SEND"}],"children":[],"resources":[{"pending":false,"id":"deOEn2mu","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_spear_left.svg","slot":"base-13-CHNKBS.chunky_objectLeft","thumb":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_spear_thumb.png"},{"pending":false,"id":"IPfzmKE3","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_spear_right.svg","slot":"base-13-CHNKBS.chunky_objectRight","thumb":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/Chunky Items/Chunky_spear_thumb.png"}],"block":526,"collection":"d43593c715a56da27d-CHNKITMS","symbol":"chunky_spear","transferable":1,"sn":"00000004","metadata":"ipfs://ipfs/bafkreihk23kbw5sfn7vcd777vaj3d7jz6nw5zocwt6gvfvnun7gipuly7m","priority":["deOEn2mu","IPfzmKE3"],"owner":"209-d43593c715a56da27d-CHNK-chunky_bird_4-00000004","rootowner":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","reactions":{},"forsale":"0","burned":"","properties":{},"pending":false,"id":"526-d43593c715a56da27d-CHNKITMS-chunky_spear-00000004"}},"collections":{"d43593c715a56da27d-CHNK":{"changes":[],"block":204,"max":10000,"issuer":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","symbol":"CHNK","id":"d43593c715a56da27d-CHNK","metadata":"ipfs://ipfs/bafkreibzeyetfssguxrzoltvluyjac7hp3bzvzgpa27jkbpek23tqkfpmi"},"d43593c715a56da27d-CHNKITMS":{"changes":[],"block":522,"max":0,"issuer":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","symbol":"CHNKITMS","id":"d43593c715a56da27d-CHNKITMS","metadata":"ipfs://ipfs/bafkreidaprw5rjxkmnahoqtxnfvlj6zw4sje7ogqqq4itbzjgkp3sqkomq"}},"bases":{"base-13-CHNKBS":{"changes":[],"block":13,"symbol":"CHNKBS","type":"svg","issuer":"HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F","parts":[{"type":"fixed","id":"chunky_body_1","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/v1/Chunky_body_v1.svg","z":0},{"type":"fixed","id":"chunky_body_2","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/v2/Chunky_body_v2.svg","z":0},{"type":"fixed","id":"chunky_body_3","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/v3/Chunky_body_v3.svg","z":0},{"type":"fixed","id":"chunky_body_4","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/v4/Chunky_body_v4.svg","z":0},{"type":"fixed","id":"chunky_head_1","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/v1/Chunky_head_v1.svg","z":4},{"type":"fixed","id":"chunky_head_2","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/v2/Chunky_head_v2.svg","z":4},{"type":"fixed","id":"chunky_head_3","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/v3/Chunky_head_v3.svg","z":4},{"type":"fixed","id":"chunky_head_4","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/v4/Chunky_head_v4.svg","z":4},{"type":"fixed","id":"chunky_hand_1","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/v1/Chunky_hand_v1.svg","z":3},{"type":"fixed","id":"chunky_hand_2","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/v2/Chunky_hand_v2.svg","z":3},{"type":"fixed","id":"chunky_hand_3","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/v3/Chunky_hand_v3.svg","z":3},{"type":"fixed","id":"chunky_hand_4","src":"ipfs://ipfs/Qmax4X3v382ZsWY6msE7vcnRw351BKMo5uZ8G8oBBQBDKT/v4/Chunky_hand_v4.svg","z":3},{"type":"slot","id":"chunky_objectLeft","equippable":["d43593c715a56da27d-CHNKITMS"],"z":1},{"type":"slot","id":"chunky_objectRight","equippable":["d43593c715a56da27d-CHNKITMS"],"z":2}],"id":"base-13-CHNKBS"}},"invalid":[],"lastBlock":535} 2 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/v3/Chunky_head_v3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/scripts/assets/chunky/v2/Chunky_head_v2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/scripts/mint-ukraine-stamp-book.ts: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | import { Base, Collection, NFT } from "rmrk-tools"; 3 | import { 4 | chunkArray, 5 | getKeyringFromUri, 6 | sleep, 7 | } from "./utils"; 8 | import { u8aToHex } from "@polkadot/util"; 9 | import { encodeAddress } from "@polkadot/keyring"; 10 | import getKeys from "./devaccs"; 11 | import { uploadAndPinIpfsMetadata } from "./pinata-utils"; 12 | import { WS_URL } from "./constants"; 13 | import { IBasePart } from "rmrk-tools/dist/classes/base"; 14 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 15 | import { nanoid } from "nanoid"; 16 | import { Resource } from "rmrk-tools/dist/classes/nft"; 17 | import { signAndSendWithRetry } from './sign-and-send-with-retry'; 18 | import { getApi } from './get-polkadot-api'; 19 | 20 | const STAMPS_FOR_UKRAINE_ASSETS_CID = 21 | "QmW8kMq1rQEj7ayiPU3ii1fVhR2gN5MFTohVxwLENTCMv6"; 22 | 23 | const SYMBOL = "STAMPFRUKRN"; 24 | const SYMBOL_BOOK = "STMPBK"; 25 | const SYMBOL_STAMP = "STMP"; 26 | 27 | const slotParts = { 28 | "Slot 1": 100, 29 | "Slot 2": 100, 30 | "Slot 3": 100, 31 | "Slot 4": 100, 32 | "Slot 5": 100, 33 | "Slot 6": 100, 34 | "Slot 7": 100, 35 | "Slot 8": 100, 36 | "Slot 9": 100, 37 | "Slot 10": 100, 38 | "Slot 11 deluxe": 50, 39 | "Slot 12 deluxe": 25, 40 | }; 41 | 42 | const stampMetadata = { 43 | "Slot 1": { 44 | name: "St. Andrews Church, Kyiv", 45 | description: 46 | "To support the Ukrainian people in these and future days.\n" + 47 | "Thank you", 48 | thumbs: ["Stamp_1_preview_b_n.png", "Stamp_1_preview_b_y.png"], 49 | }, 50 | "Slot 2": { 51 | name: "Monument of the Founders of KYIV", 52 | description: 53 | "To support the Ukrainian people in these and future days.\n" + 54 | "Thank you", 55 | thumbs: ["Stamp_2_preview_b_n.png", "Stamp_2_preview_b_y.png"], 56 | }, 57 | "Slot 3": { 58 | name: "Sofiyivka park, Uman", 59 | description: 60 | "To support the Ukrainian people in these and future days.\n" + 61 | "Thank you", 62 | thumbs: ["Stamp_3_preview_b_n.png", "Stamp_3_preview_b_y.png"], 63 | }, 64 | "Slot 4": { 65 | name: "Swallow's Nest, Gaspra", 66 | description: 67 | "To support the Ukrainian people in these and future days.\n" + 68 | "Thank you", 69 | thumbs: ["Stamp_4_preview_b_n.png", "Stamp_4_preview_b_y.png"], 70 | }, 71 | "Slot 5": { 72 | name: "Kamianets podilskyi castle", 73 | description: 74 | "To support the Ukrainian people in these and future days.\n" + 75 | "Thank you", 76 | thumbs: ["Stamp_5_preview_b_n.png", "Stamp_5_preview_b_y.png"], 77 | }, 78 | "Slot 6": { 79 | name: "Catherine's Church, Chernihiv", 80 | description: 81 | "To support the Ukrainian people in these and future days.\n" + 82 | "Thank you", 83 | thumbs: ["Stamp_6_preview_b_n.png", "Stamp_6_preview_b_y.png"], 84 | }, 85 | "Slot 7": { 86 | name: "Independence Square, Kyiv", 87 | description: 88 | "To support the Ukrainian people in these and future days.\n" + 89 | "Thank you", 90 | thumbs: ["Stamp_7_preview_b_n.png", "Stamp_7_preview_b_y.png"], 91 | }, 92 | "Slot 8": { 93 | name: "Lviv National Academic Opera", 94 | description: 95 | "To support the Ukrainian people in these and future days.\n" + 96 | "Thank you", 97 | thumbs: ["Stamp_8_preview_b_n.png", "Stamp_8_preview_b_y.png"], 98 | }, 99 | "Slot 9": { 100 | name: "Museum of Zaporizhian Cossacks", 101 | description: 102 | "To support the Ukrainian people in these and future days.\n" + 103 | "Thank you", 104 | thumbs: ["Stamp_9_preview_b_n.png", "Stamp_9_preview_b_y.png"], 105 | }, 106 | "Slot 10": { 107 | name: "Odessa National Academic Theater of Opera and Ballet", 108 | description: 109 | "To support the Ukrainian people in these and future days.\n" + 110 | "Thank you", 111 | thumbs: ["Stamp_10_preview_y_b.png", "Stamp_10_preview_y_n.png"], 112 | }, 113 | "Slot 11 deluxe": { 114 | name: "Ukraine!", 115 | description: 116 | "To support the Ukrainian people in these and future days.\n" + 117 | "Thank you", 118 | thumbs: ["Stamp_11_preview.png"], 119 | }, 120 | "Slot 12 deluxe": { 121 | name: "Ukraine!", 122 | description: 123 | "To support the Ukrainian people in these and future days.\n" + 124 | "Thank you", 125 | thumbs: ["Stamp_12_preview.png"], 126 | }, 127 | }; 128 | 129 | export const createBase = async () => { 130 | try { 131 | await cryptoWaitReady(); 132 | 133 | const ws = WS_URL; 134 | const phrase = process.env.STAND_WITH_UKRAINE_SEED; 135 | 136 | const kp = getKeyringFromUri(phrase); 137 | const accounts = getKeys(phrase); 138 | const issuer = encodeAddress(accounts[0].address, 2); 139 | 140 | const collectionId = Collection.generateId( 141 | u8aToHex(accounts[0].publicKey), 142 | SYMBOL 143 | ); 144 | const collectionMetadataCid = await uploadAndPinIpfsMetadata({ 145 | mediaUri: `ipfs://ipfs/${STAMPS_FOR_UKRAINE_ASSETS_CID}/PFP.png`, 146 | description: 147 | "Postage Stamp for Ukraine is a collection with the purpose of fundraising for the people of Ukraine who suffered from the war.\n" + 148 | "\n" + 149 | "The collection uses the RMRK2.0 standard, and consists of a book on which stamps can be collected. Each stamp is from a symbolic place of Ukraine and are limited edition. Other stamps can always be created for the purpose of the Ukrainian people.\n" + 150 | "\n" + 151 | "We must not stand still in front of the events of these days, supporting the Ukrainian people is a moral duty, even a small gesture is important. All the funds raised will be sent directly to Ukrainian associations which will use them to support the population.\n" + 152 | "\n" + 153 | "Support Ukraine too\n" + 154 | "Thank you", 155 | name: "Stamp For Ukraine", 156 | }); 157 | 158 | const baseMetadataCid = await uploadAndPinIpfsMetadata({ 159 | mediaUri: `ipfs://ipfs/${STAMPS_FOR_UKRAINE_ASSETS_CID}/PFP.png`, 160 | name: "Stamp For Ukraine", 161 | }); 162 | 163 | const Cl = new Collection( 164 | 0, 165 | 0, 166 | issuer, 167 | SYMBOL, 168 | collectionId, 169 | collectionMetadataCid 170 | ); 171 | 172 | const remarks = [Cl.create()]; 173 | const baseParts: IBasePart[] = [ 174 | { 175 | type: "fixed", 176 | id: "Book", 177 | src: `ipfs://ipfs/${STAMPS_FOR_UKRAINE_ASSETS_CID}/Book_empty.svg`, 178 | z: 0, 179 | }, 180 | { 181 | type: "slot", 182 | id: "Stamp 1", 183 | equippable: [collectionId], 184 | z: 1, 185 | }, 186 | { 187 | type: "slot", 188 | id: "Stamp 2", 189 | equippable: [collectionId], 190 | z: 2, 191 | }, 192 | { 193 | type: "slot", 194 | id: "Stamp 3", 195 | equippable: [collectionId], 196 | z: 3, 197 | }, 198 | { 199 | type: "slot", 200 | id: "Stamp 4", 201 | equippable: [collectionId], 202 | z: 4, 203 | }, 204 | { 205 | type: "slot", 206 | id: "Stamp 5", 207 | equippable: [collectionId], 208 | z: 5, 209 | }, 210 | { 211 | type: "slot", 212 | id: "Stamp 6", 213 | equippable: [collectionId], 214 | z: 6, 215 | }, 216 | ]; 217 | 218 | const base = new Base( 219 | 0, 220 | SYMBOL, 221 | issuer, 222 | "svg", 223 | baseParts, 224 | undefined, 225 | baseMetadataCid 226 | ); 227 | 228 | remarks.push(base.base()); 229 | const api = await getApi(); 230 | 231 | const txs = remarks.map((remark) => api.tx.system.remark(remark)); 232 | const tx = api.tx.utility.batch(txs); 233 | const { block } = await signAndSendWithRetry(tx, kp); 234 | 235 | console.log("done at block:", block); 236 | 237 | return block; 238 | } catch (error: any) { 239 | console.error(error); 240 | process.exit(1); 241 | } 242 | }; 243 | 244 | const CHUNK_SIZE = 70; 245 | 246 | export const mintBooks = async () => { 247 | try { 248 | const baseBlock = await createBase(); 249 | await cryptoWaitReady(); 250 | 251 | const ws = WS_URL; 252 | const phrase = process.env.STAND_WITH_UKRAINE_SEED; 253 | 254 | const kp = getKeyringFromUri(phrase); 255 | const accounts = getKeys(phrase); 256 | const issuer = encodeAddress(accounts[0].address, 2); 257 | 258 | const collectionId = Collection.generateId( 259 | u8aToHex(accounts[0].publicKey), 260 | SYMBOL 261 | ); 262 | const base = new Base(baseBlock, SYMBOL, issuer, "png", []); 263 | 264 | const metadataCid = await uploadAndPinIpfsMetadata({ 265 | mediaUri: `ipfs://ipfs/QmQCTD9h9ejWtnPyR6tDNwJzhLHDFq8FtABM7NceBA5tpo`, 266 | description: 267 | "Create your stamp collection!\n" + 268 | "\n" + 269 | "To support the Ukrainian people in these and future days.\n" + 270 | "Thank you", 271 | name: "Collector's book", 272 | }); 273 | 274 | const total = 200; 275 | 276 | const arrayForTheSakeOfIt = Array(total).fill(issuer); 277 | 278 | let recipientsChunked = chunkArray(arrayForTheSakeOfIt, CHUNK_SIZE); 279 | let chunkIndex = 0; 280 | 281 | for (const recipientChunk of recipientsChunked) { 282 | await sleep(1500); 283 | const remarks = []; 284 | 285 | recipientChunk.forEach((recipient, index) => { 286 | const sn = `${chunkIndex * CHUNK_SIZE + index + 1}`.padStart(8, "0"); 287 | 288 | const nft = new NFT({ 289 | block: 0, 290 | collection: collectionId, 291 | symbol: SYMBOL_BOOK, 292 | transferable: 1, 293 | sn, 294 | owner: issuer, 295 | metadata: metadataCid, 296 | }); 297 | 298 | remarks.push(nft.mint()); 299 | }); 300 | 301 | const api = await getApi(); 302 | const txs = remarks.map((remark) => api.tx.system.remark(remark)); 303 | 304 | const tx = api.tx.utility.batch(txs); 305 | const { block } = await signAndSendWithRetry(tx, kp); 306 | 307 | const resaddAndSendRemarks = []; 308 | recipientChunk.forEach((recipient, index) => { 309 | const sn = `${chunkIndex * CHUNK_SIZE + index + 1}`.padStart(8, "0"); 310 | const nft = new NFT({ 311 | block, 312 | collection: collectionId, 313 | symbol: SYMBOL_BOOK, 314 | transferable: 1, 315 | sn, 316 | owner: issuer, 317 | metadata: metadataCid, 318 | }); 319 | 320 | const res: Resource = { 321 | thumb: `ipfs://ipfs/QmQCTD9h9ejWtnPyR6tDNwJzhLHDFq8FtABM7NceBA5tpo`, 322 | base: base.getId(), 323 | parts: [ 324 | "Book", 325 | "Stamp 1", 326 | "Stamp 2", 327 | "Stamp 3", 328 | "Stamp 4", 329 | "Stamp 5", 330 | "Stamp 6", 331 | ], 332 | id: `${SYMBOL}_${nanoid(8)}`, 333 | metadata: metadataCid, 334 | }; 335 | 336 | resaddAndSendRemarks.push(nft.resadd(res)); 337 | }); 338 | 339 | const resaddtxs = resaddAndSendRemarks.map((remark) => 340 | api.tx.system.remark(remark) 341 | ); 342 | let rmrksChunked = chunkArray(resaddtxs, 100); 343 | 344 | console.log(resaddAndSendRemarks); 345 | 346 | for (const rmrkChunk of rmrksChunked) { 347 | console.log(`Chunk size: ${rmrkChunk.length}`); 348 | const tx = api.tx.utility.batch(rmrkChunk); 349 | console.log("tx create"); 350 | const { block } = await signAndSendWithRetry(tx, kp); 351 | console.log("Book base resource added: ", block); 352 | } 353 | 354 | chunkIndex = chunkIndex + 1; 355 | } 356 | 357 | console.log("ALL MINTED"); 358 | return baseBlock; 359 | } catch (error: any) { 360 | console.error(error); 361 | process.exit(1); 362 | } 363 | }; 364 | 365 | export const mintStamps = async () => { 366 | try { 367 | const baseBlock = await mintBooks(); 368 | await cryptoWaitReady(); 369 | 370 | const ws = WS_URL; 371 | const phrase = process.env.STAND_WITH_UKRAINE_SEED; 372 | 373 | const kp = getKeyringFromUri(phrase); 374 | const accounts = getKeys(phrase); 375 | const issuer = encodeAddress(accounts[0].address, 2); 376 | 377 | const collectionId = Collection.generateId( 378 | u8aToHex(accounts[0].publicKey), 379 | SYMBOL 380 | ); 381 | const base = new Base(baseBlock, SYMBOL, issuer, "png", []); 382 | 383 | for (const slotPartId of Object.keys(slotParts)) { 384 | await sleep(1500); 385 | const stampIndex = Object.keys(slotParts).indexOf(slotPartId); 386 | const total = slotParts[slotPartId]; 387 | const metadata = stampMetadata[slotPartId]; 388 | 389 | const metadataCidEven = await uploadAndPinIpfsMetadata({ 390 | mediaUri: `ipfs://ipfs/${STAMPS_FOR_UKRAINE_ASSETS_CID}/${metadata.thumbs[0]}`, 391 | description: metadata.description, 392 | name: metadata.name, 393 | }); 394 | 395 | const metadataCidOdd = await uploadAndPinIpfsMetadata({ 396 | mediaUri: `ipfs://ipfs/${STAMPS_FOR_UKRAINE_ASSETS_CID}/${metadata.thumbs[1]}`, 397 | description: metadata.description, 398 | name: metadata.name, 399 | }); 400 | 401 | const arrayForTheSakeOfIt = Array(total).fill(issuer); 402 | 403 | let recipientsChunked = chunkArray(arrayForTheSakeOfIt, CHUNK_SIZE); 404 | let chunkIndex = 0; 405 | 406 | for (const recipientChunk of recipientsChunked) { 407 | await sleep(1500); 408 | const remarks = []; 409 | 410 | recipientChunk.forEach((recipient, index) => { 411 | const sn = `${chunkIndex * CHUNK_SIZE + index + 1}`.padStart(8, "0"); 412 | const isOdd = index % 2; 413 | const nft = new NFT({ 414 | block: 0, 415 | collection: collectionId, 416 | symbol: SYMBOL_STAMP, 417 | transferable: 1, 418 | sn, 419 | owner: issuer, 420 | metadata: isOdd ? metadataCidOdd : metadataCidEven, 421 | }); 422 | 423 | remarks.push(nft.mint()); 424 | }); 425 | 426 | const api = await getApi(); 427 | const txs = remarks.map((remark) => api.tx.system.remark(remark)); 428 | 429 | const tx = api.tx.utility.batch(txs); 430 | const { block } = await signAndSendWithRetry(tx, kp); 431 | 432 | const resaddAndSendRemarks = []; 433 | recipientChunk.forEach((recipient, index) => { 434 | const sn = `${chunkIndex * CHUNK_SIZE + index + 1}`.padStart(8, "0"); 435 | const isOdd = index % 2; 436 | const nft = new NFT({ 437 | block, 438 | collection: collectionId, 439 | symbol: SYMBOL_STAMP, 440 | transferable: 1, 441 | sn, 442 | owner: issuer, 443 | metadata: isOdd ? metadataCidOdd : metadataCidEven, 444 | }); 445 | 446 | for (let i = 0; i < 6; i++) { 447 | const res: Resource = { 448 | thumb: isOdd 449 | ? `ipfs://ipfs/${STAMPS_FOR_UKRAINE_ASSETS_CID}/${metadata.thumbs[0]}` 450 | : `ipfs://ipfs/${STAMPS_FOR_UKRAINE_ASSETS_CID}/${metadata.thumbs[1]}`, 451 | src: `ipfs://ipfs/${STAMPS_FOR_UKRAINE_ASSETS_CID}/Stamp_${ 452 | stampIndex + 1 453 | }_slot_position_${i + 1}.svg`, 454 | id: `${SYMBOL_STAMP}_${nanoid(8)}`, 455 | metadata: isOdd ? metadataCidOdd : metadataCidEven, 456 | slot: `${base.getId()}.Stamp ${i + 1}`, 457 | }; 458 | 459 | resaddAndSendRemarks.push(nft.resadd(res)); 460 | } 461 | }); 462 | 463 | const resaddtxs = resaddAndSendRemarks.map((remark) => 464 | api.tx.system.remark(remark) 465 | ); 466 | let rmrksChunked = chunkArray(resaddtxs, 100); 467 | 468 | console.log(resaddAndSendRemarks); 469 | 470 | for (const rmrkChunk of rmrksChunked) { 471 | console.log(`Chunk size: ${rmrkChunk.length}`); 472 | const tx = api.tx.utility.batch(rmrkChunk); 473 | console.log("tx create"); 474 | const { block } = await signAndSendWithRetry(tx, kp); 475 | console.log("Stamp base resource added: ", block); 476 | } 477 | 478 | chunkIndex = chunkIndex + 1; 479 | } 480 | } 481 | 482 | console.log("ALL MINTED"); 483 | process.exit(0); 484 | } catch (error: any) { 485 | console.error(error); 486 | process.exit(1); 487 | } 488 | }; 489 | 490 | mintStamps(); 491 | 492 | // createBase(); 493 | -------------------------------------------------------------------------------- /projects/scripts/mint-in-chunks.ts: -------------------------------------------------------------------------------- 1 | import {getApi} from "./get-polkadot-api"; 2 | 3 | require("dotenv").config(); 4 | 5 | import { Resource } from "rmrk-tools/dist/classes/nft"; 6 | import { cryptoWaitReady } from "@polkadot/util-crypto"; 7 | import { isProd, WS_URL } from "./constants"; 8 | import { 9 | chunkArray, 10 | getKeyringFromUri, 11 | getKeys, 12 | sleep, 13 | } from "./utils"; 14 | import { Collection, NFT } from "rmrk-tools"; 15 | import { encodeAddress } from "@polkadot/keyring"; 16 | import { nanoid } from "nanoid"; 17 | import fs from "fs"; 18 | import { 19 | uploadAndPinIpfsMetadata, 20 | pinFileStreamToIpfs, 21 | StreamPinata, 22 | } from "./pinata-utils"; 23 | import { IProperties } from "rmrk-tools/dist/tools/types"; 24 | import { Readable } from "stream"; 25 | import { u8aToHex } from "@polkadot/util"; 26 | import {signAndSendWithRetry} from "./sign-and-send-with-retry"; 27 | const BASE_ID = isProd ? "CHANGEME" : "CHANGEME"; 28 | 29 | const fsPromises = fs.promises; 30 | 31 | interface TomoMintingResource { 32 | description: string; 33 | base?: string; 34 | src: string; 35 | slot?: string; 36 | thumb?: string; 37 | } 38 | 39 | interface IMintingRoyalties { 40 | royaltyPercentFloat: string; 41 | reciever: string; 42 | } 43 | 44 | const BANNERVERSE_COLLECTION_ID = "e0b9bdcc456a36497a-RMRKBNNRS"; 45 | const BANNERVERSE_COLLECTION_SYMBOL = "RMRKBNNRS"; 46 | const COMMISSION_ADDRESS = "CHANGEME"; 47 | const MINTER_ADDRESS = "CHANGEME"; 48 | 49 | interface BatchMintingObject { 50 | collection: { 51 | id: string; 52 | symbol: string; 53 | }; 54 | symbol: string; 55 | title: string; 56 | description: string; 57 | recipients: Record; 58 | properties: { context?: string; type?: string }[]; 59 | royalties: IMintingRoyalties[]; 60 | resources: TomoMintingResource[]; 61 | } 62 | 63 | const mintObject: Record = { 64 | 1: { 65 | collection: { 66 | id: BANNERVERSE_COLLECTION_ID, 67 | symbol: BANNERVERSE_COLLECTION_SYMBOL, 68 | }, 69 | symbol: "BNNRX", 70 | title: "X", 71 | description: 72 | "February '22 RMRK Banner. The search continues. Where is the treasure buried? X Marks the Spot.", 73 | properties: [], 74 | royalties: [ 75 | { 76 | reciever: COMMISSION_ADDRESS, 77 | royaltyPercentFloat: "5", 78 | }, 79 | ], 80 | resources: [ 81 | { 82 | description: 83 | "February '22 RMRK Banner. The search continues. Where is the treasure buried? X Marks the Spot.", 84 | src: "banners_02_22/2022 Feb_Banner 01 - Multiresource.png", 85 | thumb: "banners_02_22/2022 Feb_Banner 01 - Multiresource.png", 86 | }, 87 | ], 88 | recipients: { 89 | [MINTER_ADDRESS]: "30", 90 | }, 91 | }, 92 | 2: { 93 | collection: { 94 | id: BANNERVERSE_COLLECTION_ID, 95 | symbol: BANNERVERSE_COLLECTION_SYMBOL, 96 | }, 97 | symbol: "BNNRCND", 98 | title: "Candyland", 99 | description: 100 | "February '22 RMRK Banner. Sometimes all you need is candy and color.", 101 | properties: [], 102 | royalties: [ 103 | { 104 | reciever: MINTER_ADDRESS, 105 | royaltyPercentFloat: "5", 106 | }, 107 | ], 108 | resources: [ 109 | { 110 | description: 111 | "February '22 RMRK Banner. Sometimes all you need is candy and color.", 112 | src: "banners_02_22/2022 Feb_Banner 02 - Candy.png", 113 | thumb: "banners_02_22/2022 Feb_Banner 02 - Candy.png", 114 | }, 115 | ], 116 | recipients: { 117 | [MINTER_ADDRESS]: "47", 118 | DhvJ4ZEKr75kBtr3VSwem84jbZCfmjUCZ771sf33Z5mX8Ta: "1", 119 | H2SwfhnSStX91iGZGye9L3wetRKahk1BZMCuSk7aXU66aFQ: "1", 120 | CpjsLDC1JFyrhm3ftC9Gs4QoyrkHKhZKtK7YqGTRFtTafgp: "1", 121 | }, 122 | }, 123 | 3: { 124 | collection: { 125 | id: BANNERVERSE_COLLECTION_ID, 126 | symbol: BANNERVERSE_COLLECTION_SYMBOL, 127 | }, 128 | symbol: "BNNRDSTRT", 129 | title: "Distort", 130 | description: 131 | "February '22 RMRK Banner. Distortion. Disturbance. Deformity. What do you see?", 132 | properties: [], 133 | royalties: [ 134 | { 135 | reciever: MINTER_ADDRESS, 136 | royaltyPercentFloat: "5", 137 | }, 138 | ], 139 | resources: [ 140 | { 141 | description: 142 | "February '22 RMRK Banner. Distortion. Disturbance. Deformity. What do you see?", 143 | src: "banners_02_22/2022 Feb_Banner 03 - RMRK 2.0.png", 144 | thumb: "banners_02_22/2022 Feb_Banner 03 - RMRK 2.0.png", 145 | }, 146 | ], 147 | recipients: { 148 | [MINTER_ADDRESS]: "95", 149 | DhvJ4ZEKr75kBtr3VSwem84jbZCfmjUCZ771sf33Z5mX8Ta: "1", 150 | H2SwfhnSStX91iGZGye9L3wetRKahk1BZMCuSk7aXU66aFQ: "1", 151 | CpjsLDC1JFyrhm3ftC9Gs4QoyrkHKhZKtK7YqGTRFtTafgp: "1", 152 | DJiT2VbsbvA6EMiUjtoXLuBXNCscexS3GUx3fxR1JwK8KMr: "1", 153 | EDQwsrUJQbBRxc5K5p1xKb55jhsr18fngdHghQQeTxEZLoM: "1", 154 | }, 155 | }, 156 | 4: { 157 | collection: { 158 | id: BANNERVERSE_COLLECTION_ID, 159 | symbol: BANNERVERSE_COLLECTION_SYMBOL, 160 | }, 161 | symbol: "BNNRDRK2", 162 | title: "RMRK2 Dark", 163 | description: 164 | "February '22 RMRK Banner. Finally, it's here! Darkly commemorating the launch of RMRK2 on Singular. But what's that in the back?", 165 | properties: [], 166 | royalties: [ 167 | { 168 | reciever: MINTER_ADDRESS, 169 | royaltyPercentFloat: "5", 170 | }, 171 | ], 172 | resources: [ 173 | { 174 | description: 175 | "February '22 RMRK Banner. Finally, it's here! Darkly commemorating the launch of RMRK2 on Singular. But what's that in the back?", 176 | src: "banners_02_22/2022 Feb_Banner 04 - On singular dark.png", 177 | thumb: "banners_02_22/2022 Feb_Banner 04 - On singular dark.png", 178 | }, 179 | ], 180 | recipients: { 181 | [MINTER_ADDRESS]: "228", 182 | DhvJ4ZEKr75kBtr3VSwem84jbZCfmjUCZ771sf33Z5mX8Ta: "1", 183 | H2SwfhnSStX91iGZGye9L3wetRKahk1BZMCuSk7aXU66aFQ: "1", 184 | CpjsLDC1JFyrhm3ftC9Gs4QoyrkHKhZKtK7YqGTRFtTafgp: "1", 185 | DJiT2VbsbvA6EMiUjtoXLuBXNCscexS3GUx3fxR1JwK8KMr: "1", 186 | EDQwsrUJQbBRxc5K5p1xKb55jhsr18fngdHghQQeTxEZLoM: "1", 187 | EB1oqZ5MEnEwxhJ5DySCH3pyY55a2CUDfAbYKmLz2QcqWgx: "1", 188 | EuJAYheXvPywhDqB9YYG9RYbp2iENUqT261FPRhhTioPxSu: "1", 189 | ErU55Vp3AGFGEbrL5Tj1Kv47xYBDU1McBA1ALPewgVjRZDn: "1", 190 | CmkHUWRBZWp8vh3t9ZMyddx3DHjqfv2jErXrECtcizxoGc2: "1", 191 | GhQgLK4MAj564SUQgbV5EeQRva8L9AUEkqT28mZsmCS1egv: "1", 192 | HpbFHQjLCCuBpBwS2wJe8pB4ZrgVxD6w2SGEE1ckzT3Uxs4: "1", 193 | GbqtkjvQKWsgovW69Ga4WBnJxH3fSv3XkP7yLWC33j4yaQB: "1", 194 | G2e4YrQ8CQ4yw7iy5yu9yB5vHxJjfS3q4T46ojazqsYkKjV: "1", 195 | HZXYG4jYFXwqwBrhnbjMUSXH49zV2CW8T1pEuHeQTfBNfq5: "1", 196 | HnnwFDsJiyThghLCmkbbQ2mtVrwzxjF5pWFUJbzWngDTvzj: "1", 197 | Fr6ovaQd8ysmjXEF76gQVCErsU8UYzXjYVKV2cTw9myK7A3: "1", 198 | HydwuGTL6vbThMEyZq5ygYqMeHvmSpTLRsZdLx313rcPv6M: "1", 199 | FRqF9tyZmKdSGoULHZC16GTf9RVNDvjhquq8tNwuDRjAaic: "1", 200 | G28YgXSEHW4EE7uR9N7vi7ULF2NEEtgsMCJLARD5Yj5a1Y7: "1", 201 | F2toN3eA5EWiWhggt14wDrmAiercTJvF8ye1tqHorQPyEM6: "1", 202 | EZv3htNfDpYt6m42xBaEbLr2Y2ZcPjHvY9y6tvZEcWBmefJ: "1", 203 | Fys7d6gikP6rLDF9dvhCJcAMaPrrLuHbGZRVgqLPn26fWmr: "1", 204 | }, 205 | }, 206 | 5: { 207 | collection: { 208 | id: BANNERVERSE_COLLECTION_ID, 209 | symbol: BANNERVERSE_COLLECTION_SYMBOL, 210 | }, 211 | symbol: "BNNRLGHT2", 212 | title: "RMRK2 Light", 213 | description: 214 | "February '22 RMRK Banner. Finally, it's here! Brightly commemorating the launch of RMRK2 on Singular. But what's that in the back?", 215 | properties: [], 216 | royalties: [ 217 | { 218 | reciever: MINTER_ADDRESS, 219 | royaltyPercentFloat: "5", 220 | }, 221 | ], 222 | resources: [ 223 | { 224 | description: 225 | "February '22 RMRK Banner. Finally, it's here! Brightly commemorating the launch of RMRK2 on Singular. But what's that in the back?", 226 | src: "banners_02_22/2022 Feb_Banner 05 - On singular light.png", 227 | thumb: "banners_02_22/2022 Feb_Banner 05 - On singular light.png", 228 | }, 229 | ], 230 | recipients: { 231 | [MINTER_ADDRESS]: "228", 232 | DhvJ4ZEKr75kBtr3VSwem84jbZCfmjUCZ771sf33Z5mX8Ta: "1", 233 | H2SwfhnSStX91iGZGye9L3wetRKahk1BZMCuSk7aXU66aFQ: "1", 234 | CpjsLDC1JFyrhm3ftC9Gs4QoyrkHKhZKtK7YqGTRFtTafgp: "1", 235 | DJiT2VbsbvA6EMiUjtoXLuBXNCscexS3GUx3fxR1JwK8KMr: "1", 236 | EDQwsrUJQbBRxc5K5p1xKb55jhsr18fngdHghQQeTxEZLoM: "1", 237 | EB1oqZ5MEnEwxhJ5DySCH3pyY55a2CUDfAbYKmLz2QcqWgx: "1", 238 | EuJAYheXvPywhDqB9YYG9RYbp2iENUqT261FPRhhTioPxSu: "1", 239 | ErU55Vp3AGFGEbrL5Tj1Kv47xYBDU1McBA1ALPewgVjRZDn: "1", 240 | CmkHUWRBZWp8vh3t9ZMyddx3DHjqfv2jErXrECtcizxoGc2: "1", 241 | GhQgLK4MAj564SUQgbV5EeQRva8L9AUEkqT28mZsmCS1egv: "1", 242 | HpbFHQjLCCuBpBwS2wJe8pB4ZrgVxD6w2SGEE1ckzT3Uxs4: "1", 243 | GbqtkjvQKWsgovW69Ga4WBnJxH3fSv3XkP7yLWC33j4yaQB: "1", 244 | G2e4YrQ8CQ4yw7iy5yu9yB5vHxJjfS3q4T46ojazqsYkKjV: "1", 245 | HZXYG4jYFXwqwBrhnbjMUSXH49zV2CW8T1pEuHeQTfBNfq5: "1", 246 | HnnwFDsJiyThghLCmkbbQ2mtVrwzxjF5pWFUJbzWngDTvzj: "1", 247 | Fr6ovaQd8ysmjXEF76gQVCErsU8UYzXjYVKV2cTw9myK7A3: "1", 248 | HydwuGTL6vbThMEyZq5ygYqMeHvmSpTLRsZdLx313rcPv6M: "1", 249 | FRqF9tyZmKdSGoULHZC16GTf9RVNDvjhquq8tNwuDRjAaic: "1", 250 | G28YgXSEHW4EE7uR9N7vi7ULF2NEEtgsMCJLARD5Yj5a1Y7: "1", 251 | F2toN3eA5EWiWhggt14wDrmAiercTJvF8ye1tqHorQPyEM6: "1", 252 | EZv3htNfDpYt6m42xBaEbLr2Y2ZcPjHvY9y6tvZEcWBmefJ: "1", 253 | Fys7d6gikP6rLDF9dvhCJcAMaPrrLuHbGZRVgqLPn26fWmr: "1", 254 | }, 255 | }, 256 | }; 257 | 258 | const CHUNK_SIZE = 70; 259 | 260 | export const mintInChunksDemo = async () => { 261 | try { 262 | console.log("CREATE batched NFTs START -------"); 263 | const mintedIds = []; 264 | await cryptoWaitReady(); 265 | 266 | const ws = WS_URL; 267 | 268 | const chunksMintDemoMint: Record = mintObject; 269 | const chunksMintDemoMintArray = Object.values(chunksMintDemoMint); 270 | 271 | const accounts = getKeys(); 272 | const kp = getKeyringFromUri(process.env.PRIVATE_KEY as string); 273 | 274 | for (const chunksMintDemoMintItem of chunksMintDemoMintArray) { 275 | const collectionId = Collection.generateId( 276 | u8aToHex(accounts[0].publicKey), 277 | chunksMintDemoMintItem.collection.symbol 278 | ); 279 | const owner = encodeAddress(accounts[0].address, 2); 280 | const recipients = Object.keys(chunksMintDemoMintItem.recipients) 281 | .map((recipient) => { 282 | return Array(parseInt(chunksMintDemoMintItem.recipients[recipient])).fill( 283 | recipient 284 | ); 285 | }) 286 | .flat(); 287 | 288 | const thumbFile = await fsPromises.readFile( 289 | `${process.cwd()}/data/${chunksMintDemoMintItem.resources[0].thumb}` 290 | ); 291 | const thumbStream: StreamPinata = Readable.from(thumbFile); 292 | thumbStream.path = chunksMintDemoMintItem.resources[0].thumb; 293 | const thumbCid = await pinFileStreamToIpfs( 294 | thumbStream, 295 | chunksMintDemoMintItem.title 296 | ); 297 | 298 | const properties: IProperties = {}; 299 | 300 | const typeProperty = chunksMintDemoMintItem.properties.find((property) => 301 | Boolean(property.type) 302 | ); 303 | if (typeProperty) { 304 | properties.type = { 305 | value: typeProperty.type, 306 | type: "string", 307 | }; 308 | } 309 | 310 | const contextProperty = chunksMintDemoMintItem.properties.find((property) => 311 | Boolean(property.context) 312 | ); 313 | if (contextProperty) { 314 | properties.context = { 315 | value: contextProperty.context, 316 | type: "string", 317 | }; 318 | } 319 | 320 | const metadataCid = await uploadAndPinIpfsMetadata({ 321 | thumbnailUri: `ipfs://ipfs/${thumbCid}`, 322 | description: chunksMintDemoMintItem.description, 323 | name: chunksMintDemoMintItem.title, 324 | properties, 325 | }); 326 | 327 | let recipientsChunked = chunkArray(recipients, CHUNK_SIZE); 328 | let chunkIndex = 0; 329 | 330 | for (const recipientChunk of recipientsChunked) { 331 | await sleep(1500); 332 | const remarks = []; 333 | 334 | recipientChunk.forEach((recipient, index) => { 335 | const sn = `${chunkIndex * CHUNK_SIZE + index + 1}`.padStart(8, "0"); 336 | 337 | const isOdd = index % 2; 338 | const multipleRoyaltyRecipients = 339 | chunksMintDemoMintItem.royalties.length > 1; 340 | const royalties = multipleRoyaltyRecipients 341 | ? isOdd 342 | ? chunksMintDemoMintItem.royalties[1] 343 | : chunksMintDemoMintItem.royalties[0] 344 | : chunksMintDemoMintItem.royalties[0]; 345 | 346 | const nft = new NFT({ 347 | block: 0, 348 | collection: collectionId, 349 | symbol: chunksMintDemoMintItem.symbol, 350 | transferable: 1, 351 | sn, 352 | owner, 353 | metadata: metadataCid, 354 | properties: { 355 | royaltyInfo: { 356 | type: "royalty", 357 | value: { 358 | royaltyPercentFloat: parseFloat( 359 | royalties.royaltyPercentFloat 360 | ), 361 | receiver: royalties.reciever, 362 | }, 363 | }, 364 | }, 365 | }); 366 | 367 | remarks.push(nft.mint()); 368 | }); 369 | 370 | const api = await getApi(); 371 | const txs = remarks.map((remark) => api.tx.system.remark(remark)); 372 | 373 | const tx = api.tx.utility.batch(txs); 374 | const { block } = await signAndSendWithRetry(tx, kp); 375 | console.log("Chunks mint demo NFTs minted at block: ", block); 376 | 377 | const resourcesPinned: TomoMintingResource[] = []; 378 | for (const resource of chunksMintDemoMintItem.resources) { 379 | const thumbFile = await fsPromises.readFile( 380 | `${process.cwd()}/data/${resource.thumb}` 381 | ); 382 | const thumbStream: StreamPinata = Readable.from(thumbFile); 383 | thumbStream.path = resource.thumb; 384 | const thumbCid = await pinFileStreamToIpfs( 385 | thumbStream, 386 | chunksMintDemoMintItem.title 387 | ); 388 | 389 | const mediaFile = await fsPromises.readFile( 390 | `${process.cwd()}/data/${resource.src}` 391 | ); 392 | const mediaStream: StreamPinata = Readable.from(mediaFile); 393 | mediaStream.path = resource.src; 394 | const mediaCid = await pinFileStreamToIpfs( 395 | mediaStream, 396 | chunksMintDemoMintItem.title 397 | ); 398 | 399 | resourcesPinned.push({ 400 | ...resource, 401 | src: `ipfs://ipfs/${mediaCid}`, 402 | thumb: `ipfs://ipfs/${thumbCid}`, 403 | }); 404 | } 405 | 406 | const resaddAndSendRemarks = []; 407 | recipientChunk.forEach((recipient, index) => { 408 | const sn = `${chunkIndex * CHUNK_SIZE + index + 1}`.padStart(8, "0"); 409 | const nft = new NFT({ 410 | block, 411 | collection: collectionId, 412 | symbol: chunksMintDemoMintItem.symbol, 413 | transferable: 1, 414 | sn, 415 | owner, 416 | metadata: metadataCid, 417 | }); 418 | 419 | mintedIds.push(nft.getId()); 420 | 421 | resourcesPinned.forEach((resource) => { 422 | const res: Resource = { 423 | src: resource.src, 424 | thumb: resource.thumb, 425 | id: `${chunksMintDemoMintItem.symbol}_${nanoid(8)}`, 426 | metadata: metadataCid, 427 | }; 428 | 429 | if (resource.slot) { 430 | res.slot = `${BASE_ID}.${resource.slot}`; 431 | } 432 | resaddAndSendRemarks.push(nft.resadd(res)); 433 | }); 434 | 435 | if (owner !== recipient) { 436 | resaddAndSendRemarks.push(nft.send(recipient)); 437 | } 438 | }); 439 | 440 | const resaddtxs = resaddAndSendRemarks.map((remark) => 441 | api.tx.system.remark(remark) 442 | ); 443 | let rmrksChunked = chunkArray(resaddtxs, 100); 444 | 445 | console.log(resaddAndSendRemarks); 446 | 447 | for (const rmrkChunk of rmrksChunked) { 448 | console.log(`Chunk size: ${rmrkChunk.length}`); 449 | const tx = api.tx.utility.batch(rmrkChunk); 450 | console.log("tx create"); 451 | const { block } = await signAndSendWithRetry(tx, kp); 452 | console.log("Chunks mint demo resource added: ", block); 453 | } 454 | 455 | chunkIndex = chunkIndex + 1; 456 | } 457 | } 458 | 459 | console.log("ALL MINTED"); 460 | fs.writeFileSync( 461 | `${process.cwd()}/data/minted-item-ids.json`, 462 | JSON.stringify(mintedIds) 463 | ); 464 | 465 | process.exit(0); 466 | } catch (error: any) { 467 | console.error(error); 468 | process.exit(1); 469 | } 470 | }; 471 | 472 | mintInChunksDemo(); 473 | --------------------------------------------------------------------------------