[Feature] Generative art within the Candy Machine Command line (#506)

* Created randomized configuration file

* Added packages

* Updated with logs

* Created the ability to generate json files

* Added the image creation algorithm

* Updated README

* Added example traits config file

* Added example assets
This commit is contained in:
Stanley Yang 2021-09-28 08:28:42 -07:00 committed by GitHub
parent c519beb458
commit a733ebb226
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1667 additions and 40 deletions

2
.gitignore vendored
View File

@ -31,3 +31,5 @@ hfuzz_workspace
**/.DS_Store
.cache
js/packages/web/.env
traits
traits-configuration.json

View File

@ -2,10 +2,88 @@
https://user-images.githubusercontent.com/81876372/133098938-dc2c91a6-1280-4ee1-bf0e-db0ccc972ff7.mp4
## Creating generative art
1. Create a `traits` folder and create a list of directories for the traits (i.e. background, shirt, sunglasses). Look at the `example-traits` for guidance
2. Run the following command to create a configuration file called `traits-configuration.json`:
```
metaplex generate_art_configurations <directory>
ts-node cli generate_art_configurations <directory>
```
The following file will be generated (based off of `example-traits`):
```json
{
"name": "",
"symbol": "",
"description": "",
"creators": [],
"collection": {},
"breakdown": {
"background": {
"blue.png": 0.04,
"brown.png": 0.04,
"flesh.png": 0.05,
"green.png": 0.02,
"light-blue.png": 0.06,
"light-green.png": 0.01,
"light-pink.png": 0.07,
"light-purple.png": 0.05,
"light-yellow.png": 0.06,
"orange.png": 0.07,
"pink.png": 0.02,
"purple.png": 0.03,
"red.png": 0.05,
"yellow.png": 0.43
},
"eyes": {
"egg-eyes.png": 0.3,
"heart-eyes.png": 0.12,
"square-eyes.png": 0.02,
"star-eyes.png": 0.56
},
"face": {
"cyan-face.png": 0.07,
"dark-green-face.png": 0.04,
"flesh-face.png": 0.03,
"gold-face.png": 0.11,
"grapefruit-face.png": 0.07,
"green-face.png": 0.05,
"pink-face.png": 0.05,
"purple-face.png": 0.02,
"sun-face.png": 0.1,
"teal-face.png": 0.46
},
"mouth": {
"block-mouth.png": 0.23,
"smile-mouth.png": 0.09,
"triangle-mouth.png": 0.68
}
},
"order": ["background", "face", "eyes", "mouth"],
"width": 1000,
"height": 1000
}
```
3. Go through and customize the fields in the `traits-configuration.json`, such as `name`, `symbol`, `description`, , `creators`, `collection`, `width`, and `height`.
4. After you have adjusted the configurations to your heart's content, you can run the following command to generate the JSON files along with the images.
```
metaplex create_generative_art -c <configuration_file_location> -n <number_of_images>
ts-node cli create_generative_art -c <configuration_file_location> -n <number_of_images>
```
5. This will create an `assets` folder, with a set of the JSON and PNG files to make it work!
## assets folder
* Folder with file pairs named with inrementing integer numbers starting from 0.png and 0.json
* the image HAS TO be a `PNG`
* JSON format can be checked out here: https://docs.metaplex.com/nft-standard. example below:
- Folder with file pairs named with incrementing integer numbers starting from 0.png and 0.json
- the image HAS TO be a `PNG`
- JSON format can be checked out here: https://docs.metaplex.com/nft-standard. example below:
```json
{
"name": "Solflare X NFT",
@ -23,15 +101,15 @@ https://user-images.githubusercontent.com/81876372/133098938-dc2c91a6-1280-4ee1-
{
"trait_type": "mobile",
"value": "yes"
},
{
},
{
"trait_type": "extension",
"value": "yes"
}
],
"collection": {
"name": "Solflare X NFT",
"family": "Solflare"
"name": "Solflare X NFT",
"family": "Solflare"
},
"properties": {
"files": [
@ -61,61 +139,70 @@ https://user-images.githubusercontent.com/81876372/133098938-dc2c91a6-1280-4ee1-
```
Install and build
```
yarn install
yarn install
yarn build
yarn run package:linuxb
OR
yarn run package:linux
OR
OR
yarn run package:macos
```
You can now either use `metaplex` OR the `ts-node cli` to execute the following commands.
You can now either use `metaplex` OR the `ts-node cli` to execute the following commands.
1. Upload your images and metadata. Refer to the NFT [standard](https://docs.metaplex.com/nft-standard) for the correct format.
```
metaplex upload ~/nft-test/mini_drop --keypair ~/.config/solana/id.json
metaplex upload ~/nft-test/mini_drop --keypair ~/.config/solana/id.json
ts-node cli upload ~/nft-test/mini_drop --keypair ~/.config/solana/id.json
```
2. Verify everything is uploaded. Rerun the first command until it is.
```
metaplex verify --keypair ~/.config/solana/id.json
ts-node cli verify --keypair ~/.config/solana/id.json
metaplex verify --keypair ~/.config/solana/id.json
ts-node cli verify --keypair ~/.config/solana/id.json
```
3. Create your candy machine. It can cost up to ~15 solana per 10,000 images.
3. Create your candy machine. It can cost up to ~15 solana per 10,000 images.
```
metaplex create_candy_machine -k ~/.config/solana/id.json -p 1
ts-node cli create_candy_machine -k ~/.config/solana/id.json -p 3
```
4. Set the start date and update the price of your candy machine.
```
metaplex update_candy_machine -k ~/.config/solana/id.json -d "20 Apr 2021 04:20:00 GMT" -p 0.1
ts-node cli update_candy_machine -k ~/.config/solana/id.json -d "20 Apr 2021 04:20:00 GMT" -p 0.1
```
5. Test mint a token (provided it's after the start date)
```
metaplex mint_one_token -k ~/.config/solana/id.json
ts-node cli mint_one_token -k ~/.config/solana/id.json
```
6. Check if you received any tokens.
```
spl-token accounts
spl-token accounts
```
7. If you are listed as a creator, run this command to sign your NFTs post sale. This will sign only the latest candy machine that you've created (stored in .cache/candyMachineList.json).
```
metaplex sign_candy_machine_metadata -k ~/.config/solana/id.json
ts-node cli sign_candy_machine_metadata -k ~/.config/solana/id.json
```
8. If you wish to sign metadata from another candy machine run with the --cndy flag.
```
metaplex sign_candy_machine_metadata -k ~/.config/solana/id.json --cndy CANDY_MACHINE_ADDRESS_HERE
ts-node cli sign_candy_machine_metadata -k ~/.config/solana/id.json --cndy CANDY_MACHINE_ADDRESS_HERE
```

View File

@ -0,0 +1 @@
{"name":"1","symbol":"","image":"0.png","properties":{"files":[{"uri":"0.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"blue"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"teal-face"}],"collection":{}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -0,0 +1 @@
{"name":"2","symbol":"","image":"1.png","properties":{"files":[{"uri":"1.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"light-blue"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"teal-face"}],"collection":{}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -0,0 +1 @@
{"name":"3","symbol":"","image":"2.png","properties":{"files":[{"uri":"2.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"light-pink"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"sun-face"}],"collection":{}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -0,0 +1 @@
{"name":"4","symbol":"","image":"3.png","properties":{"files":[{"uri":"3.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"light-yellow"},{"trait_type":"eyes","value":"square-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"teal-face"}],"collection":{}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -0,0 +1 @@
{"name":"5","symbol":"","image":"4.png","properties":{"files":[{"uri":"4.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"brown"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"block-mouth"},{"trait_type":"face","value":"teal-face"}],"collection":{}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1 @@
{"name":"6","symbol":"","image":"5.png","properties":{"files":[{"uri":"5.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"yellow"},{"trait_type":"eyes","value":"square-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"purple-face"}],"collection":{}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -0,0 +1 @@
{"name":"7","symbol":"","image":"6.png","properties":{"files":[{"uri":"6.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"light-green"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"cyan-face"}],"collection":{}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -0,0 +1 @@
{"name":"8","symbol":"","image":"7.png","properties":{"files":[{"uri":"7.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"green"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"teal-face"}],"collection":{}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -0,0 +1 @@
{"name":"9","symbol":"","image":"8.png","properties":{"files":[{"uri":"8.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"light-yellow"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"grapefruit-face"}],"collection":{}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -0,0 +1 @@
{"name":"10","symbol":"","image":"9.png","properties":{"files":[{"uri":"9.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"red"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"sun-face"}],"collection":{}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -0,0 +1,51 @@
{
"name": "",
"symbol": "",
"description": "",
"creators": [],
"collection": {},
"breakdown": {
"background": {
"blue.png": 0.04,
"brown.png": 0.04,
"flesh.png": 0.05,
"green.png": 0.02,
"light-blue.png": 0.06,
"light-green.png": 0.01,
"light-pink.png": 0.07,
"light-purple.png": 0.05,
"light-yellow.png": 0.06,
"orange.png": 0.07,
"pink.png": 0.02,
"purple.png": 0.03,
"red.png": 0.05,
"yellow.png": 0.43
},
"eyes": {
"egg-eyes.png": 0.3,
"heart-eyes.png": 0.12,
"square-eyes.png": 0.02,
"star-eyes.png": 0.56
},
"face": {
"cyan-face.png": 0.07,
"dark-green-face.png": 0.04,
"flesh-face.png": 0.03,
"gold-face.png": 0.11,
"grapefruit-face.png": 0.07,
"green-face.png": 0.05,
"pink-face.png": 0.05,
"purple-face.png": 0.02,
"sun-face.png": 0.1,
"teal-face.png": 0.46
},
"mouth": {
"block-mouth.png": 0.23,
"smile-mouth.png": 0.09,
"triangle-mouth.png": 0.68
}
},
"order": ["background", "face", "eyes", "mouth"],
"width": 1000,
"height": 1000
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -52,7 +52,14 @@
"ipfs-http-client": "^52.0.3",
"jsonschema": "^1.4.0",
"loglevel": "^1.7.1",
"node-fetch": "^2.6.1"
"node-fetch": "^2.6.1",
"weighted": "^0.3.0",
"canvas": "^2.8.0",
"image-data-uri": "^2.0.1",
"imagemin": "^7.0.1",
"imagemin-pngquant": "^9.0.2",
"lodash": "^4.17.21",
"merge-images": "^2.0.0"
},
"devDependencies": {
"@babel/preset-env": "^7.15.6",

View File

@ -24,11 +24,14 @@ import {
import { Config } from './types';
import { upload } from './commands/upload';
import { verifyTokenMetadata } from './commands/verifyTokenMetadata';
import { generateConfigurations } from './commands/generateConfigurations';
import { loadCache, saveCache } from './helpers/cache';
import { mint } from './commands/mint';
import { signMetadata } from './commands/sign';
import { signAllMetadataFromCandyMachine } from './commands/signAll';
import log from 'loglevel';
import { createMetadataFiles } from './helpers/metadata';
import { createGenerativeArt } from './commands/createArt';
program.version('0.0.2');
@ -139,7 +142,7 @@ programCommand('upload')
`ended at: ${new Date(endMs).toISOString()}. time taken: ${timeTaken}`,
);
if (warn) {
log.info('not all images have been uplaoded, rerun this step.');
log.info('not all images have been uploaded, rerun this step.');
}
});
@ -624,6 +627,56 @@ programCommand('sign_all')
);
});
programCommand('generate_art_configurations')
.argument('<directory>', 'Directory containing traits named from 0-n', val =>
fs.readdirSync(`${val}`),
)
.action(async (files: string[]) => {
log.info('creating traits configuration file');
const startMs = Date.now();
const successful = await generateConfigurations(files);
const endMs = Date.now();
const timeTaken = new Date(endMs - startMs).toISOString().substr(11, 8);
if (successful) {
log.info('traits-configuration.json has been created!');
log.info(
`ended at: ${new Date(endMs).toISOString()}. time taken: ${timeTaken}`,
);
} else {
log.info('The art configuration file was not created');
}
});
programCommand('create_generative_art')
.option(
'-n, --number-of-images <string>',
'Number of images to be generated',
'100',
)
.option(
'-c, --config-location <string>',
'Location of the traits configuration file',
'./traits-configuration.json',
)
.action(async (directory, cmd) => {
const { numberOfImages, configLocation } = cmd.opts();
log.info('Loaded configuration file');
// 1. generate the metadata json files
const randomSets = await createMetadataFiles(
numberOfImages,
configLocation,
);
log.info('JSON files have been created within the assets directory');
// 2. piecemeal generate the images
await createGenerativeArt(configLocation, randomSets);
log.info('Images have been created successfully!');
});
function programCommand(name: string) {
return program
.command(name)

View File

@ -0,0 +1,61 @@
import fs from 'fs';
import canvas from 'canvas';
import imagemin from 'imagemin';
import imageminPngquant from 'imagemin-pngquant';
import log from 'loglevel';
import { readJsonFile, sleep } from '../helpers/various';
import { ASSETS_DIRECTORY, TRAITS_DIRECTORY } from '../helpers/metadata';
const { createCanvas, loadImage } = canvas;
const createImage = async (order = [], image, width, height) => {
const canvas = createCanvas(width, height);
const context = canvas.getContext('2d');
const ID = parseInt(image.id, 10) - 1;
await Promise.all(
order.map(async cur => {
const imageLocation = `${TRAITS_DIRECTORY}/${cur}/${image[cur]}`;
const loadedImage = await loadImage(imageLocation);
context.patternQuality = 'best';
context.quality = 'best';
context.drawImage(loadedImage, 0, 0, width, height);
}),
);
const buffer = canvas.toBuffer('image/png');
const optimizedImage = await imagemin.buffer(buffer, {
plugins: [
imageminPngquant({
quality: [0.6, 0.95],
}),
],
});
log.info(`Placed ${ID}.png into the ${ASSETS_DIRECTORY}`);
fs.writeFileSync(`${ASSETS_DIRECTORY}/${ID}.png`, optimizedImage);
};
export async function createGenerativeArt(
configLocation: string,
randomizedSets,
) {
const { order, width, height } = await readJsonFile(configLocation);
const PROCESSING_LENGTH: number = 10;
const processImage = async (marker = 0) => {
const slice = randomizedSets.slice(marker, marker + PROCESSING_LENGTH + 1);
// generate images for the portion
await Promise.all(
slice.map(async image => {
await createImage(order, image, width, height);
}),
);
marker += PROCESSING_LENGTH;
await sleep(1000);
if (marker < randomizedSets.length - 1) {
processImage(marker);
}
};
// recurse until completion
processImage();
}

View File

@ -0,0 +1,53 @@
import fs from 'fs';
import log from 'loglevel';
import { generateRandoms } from '../helpers/various';
const { readdir, writeFile } = fs.promises;
export async function generateConfigurations(
traits: string[],
): Promise<boolean> {
let generateSuccessful: boolean = true;
const configs = {
name: '',
symbol: '',
description: '',
creators: [],
collection: {},
breakdown: {},
order: traits,
width: 1000,
height: 1000,
};
try {
await Promise.all(
traits.map(async trait => {
const attributes = await readdir(`./traits/${trait}`);
const randoms = generateRandoms(attributes.length - 1);
const tmp = {};
attributes.forEach((attr, i) => {
tmp[attr] = randoms[i] / 100;
});
configs['breakdown'][trait] = tmp;
}),
);
} catch (err) {
generateSuccessful = false;
log.error('Error created configurations', err);
throw err;
}
try {
await writeFile('./traits-configuration.json', JSON.stringify(configs));
} catch (err) {
generateSuccessful = false;
log.error('Error writing configurations to configs.json', err);
throw err;
}
return generateSuccessful;
}

View File

@ -0,0 +1,73 @@
import fs from 'fs';
import log from 'loglevel';
import _ from 'lodash';
import { generateRandomSet, getMetadata, readJsonFile } from './various';
const { writeFile, mkdir } = fs.promises;
export const ASSETS_DIRECTORY = './assets';
export const TRAITS_DIRECTORY = './traits';
export async function createMetadataFiles(
numberOfImages: string,
configLocation: string,
): Promise<any[]> {
let numberOfFilesCreated: number = 0;
const randomizedSets = [];
if (!fs.existsSync(ASSETS_DIRECTORY)) {
try {
await mkdir(ASSETS_DIRECTORY);
} catch (err) {
log.error('unable to create assets directory', err);
}
}
const {
breakdown,
name,
symbol,
creators,
description,
seller_fee_basis_points,
collection,
} = await readJsonFile(configLocation);
while (numberOfFilesCreated < parseInt(numberOfImages, 10)) {
const randomizedSet = generateRandomSet(breakdown);
if (!_.some(randomizedSets, randomizedSet)) {
randomizedSets.push(randomizedSet);
const metadata = getMetadata(
name,
symbol,
numberOfFilesCreated,
creators,
description,
seller_fee_basis_points,
randomizedSet,
collection,
);
try {
await writeFile(
`${ASSETS_DIRECTORY}/${numberOfFilesCreated}.json`,
JSON.stringify(metadata),
);
} catch (err) {
log.error(`${numberOfFilesCreated} failed to get created`, err);
}
numberOfFilesCreated += 1;
}
}
// map through after because IDs would make sets unique
const randomSetWithIds = randomizedSets.map((item, index) => ({
id: index + 1,
...item,
}));
return randomSetWithIds;
}

View File

@ -1,4 +1,25 @@
import { LAMPORTS_PER_SOL, AccountInfo } from '@solana/web3.js';
import fs from 'fs';
import weighted from 'weighted';
import path from 'path';
const { readFile } = fs.promises;
export async function readJsonFile(fileName: string) {
const file = await readFile(fileName, 'utf-8');
return JSON.parse(file);
}
export const generateRandomSet = breakdown => {
const tmp = {};
Object.keys(breakdown).forEach(attr => {
const randomSelection = weighted.select(breakdown[attr]);
tmp[attr] = randomSelection;
});
return tmp;
};
export const getUnixTs = () => {
return new Date().getTime() / 1000;
};
@ -97,6 +118,64 @@ export function chunks(array, size) {
);
}
export function generateRandoms(
numberOfAttrs: number = 1,
total: number = 100,
) {
const numbers = [];
const loose_percentage = total / numberOfAttrs;
for (let i = 0; i < numberOfAttrs; i++) {
const random = Math.floor(Math.random() * loose_percentage) + 1;
numbers.push(random);
}
const sum = numbers.reduce((prev, cur) => {
return prev + cur;
}, 0);
numbers.push(total - sum);
return numbers;
}
export const getMetadata = (
name: string = '',
symbol: string = '',
index: number = 0,
creators,
description: string = '',
seller_fee_basis_points: number = 500,
attrs,
collection,
) => {
const attributes = [];
for (const prop in attrs) {
attributes.push({
trait_type: prop,
value: path.parse(attrs[prop]).name,
});
}
return {
name: `${name}${index + 1}`,
symbol,
image: `${index}.png`,
properties: {
files: [
{
uri: `${index}.png`,
type: 'image/png',
},
],
category: 'image',
creators,
},
description,
seller_fee_basis_points,
attributes,
collection,
};
};
const getMultipleAccountsCore = async (
connection: any,
keys: string[],

File diff suppressed because it is too large Load Diff