[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
|
@ -31,3 +31,5 @@ hfuzz_workspace
|
|||
**/.DS_Store
|
||||
.cache
|
||||
js/packages/web/.env
|
||||
traits
|
||||
traits-configuration.json
|
||||
|
|
|
@ -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
|
||||
|
||||
```
|
||||
|
|
|
@ -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":{}}
|
After Width: | Height: | Size: 8.8 KiB |
|
@ -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":{}}
|
After Width: | Height: | Size: 9.1 KiB |
|
@ -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":{}}
|
After Width: | Height: | Size: 9.3 KiB |
|
@ -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":{}}
|
After Width: | Height: | Size: 7.7 KiB |
|
@ -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":{}}
|
After Width: | Height: | Size: 6.7 KiB |
|
@ -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":{}}
|
After Width: | Height: | Size: 8.0 KiB |
|
@ -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":{}}
|
After Width: | Height: | Size: 9.0 KiB |
|
@ -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":{}}
|
After Width: | Height: | Size: 8.8 KiB |
|
@ -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":{}}
|
After Width: | Height: | Size: 9.0 KiB |
|
@ -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":{}}
|
After Width: | Height: | Size: 7.9 KiB |
|
@ -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
|
||||
}
|
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 28 KiB |
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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[],
|
||||
|
|