Skip to content

Instantly share code, notes, and snippets.

@fend25
Last active April 30, 2022 12:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fend25/9b167046f85379e91ffcc149e762e5e5 to your computer and use it in GitHub Desktop.
Save fend25/9b167046f85379e91ffcc149e762e5e5 to your computer and use it in GitHub Desktop.
exports.config = {
// WS_ENDPOINT: `wss://quartz.api.onfinality.io`,
// WS_ENDPOINT: `wss://quartz.unique.network`,
WS_ENDPOINT: `wss://ws-quartz-dev.comecord.com`,
seed: `//Alice`,
}

How to use:

  1. Clone this repo: https://github.com/UniqueNetwork/unique-playgrounds and install deps
  2. Place this file to the playgrounds repo, folder src/playgrounds: src/playgrounds/sadu_mint.dev.js.
  3. Place env file to the playgrounds repo, folder src/playgrounds: src/playgrounds/__env.dev.js.
  4. Place folder with all 24 token JSONs to the folder data/sadu_token_jsons
  5. Check and change config - __env.dev.js. Especially the ceed.
  6. REALLY CAREFULLY check var ACCOUNT_TO_WHERE_TO_MINT in the main file - line 17.
  7. Check collection name and token prefix - remove ${rnd} and change token prefix to 'SADU'
  8. Try to run the script on the DEV Quartz (wss://ws-quartz-dev.comecord.com).
  9. Command to run: npm run playground sadu_mint.dev
  10. When you are absolutely sure everything is ok and you have checked it, change the WS_ENDPOINT to the main Quartz RPC (file __env.dev.js).
const path = require('path');
const fs = require('fs');
const {getUsage} = require('../lib/cli');
const {Logger} = require('../lib/logger');
const {UniqueHelper, UniqueSchemaHelper, UniqueUtil} = require('../lib/unique');
const logger = new Logger(false, Logger.LEVEL.NONE);
const protobuf = require('protobufjs')
const {config} = require('./__env.dev.js')
const rnd = ` ${Math.ceil(Math.random() * 98)}`
// ------------------------------------------------------------------------------
// edit these vars
// ------------------------------------------------------------------------------
const ADDRESS_TO_WHERE_TO_MINT = `5CFrzx2ottkJ5GbeS5kGmJRBKH9kEWcKgE9gAJVwAgfLs6ip`
const JSONS_PATH = path.resolve(__dirname, '../../data/sadu_token_jsons')
const COLLECTION_NAME = `Quartz Summer${rnd}`
const COLLECTION_DESCRIPTION = `Sādu's first batch of eco-conscious NFTs allocate funding to Sādu, digital artist, Stacie Ant, and one of three ecosystem conservation and restoration partners. Partners include SeaTrees, The Haiti Tree Project, and Re-Climate.`
const COLLECTION_TOKEN_PREFIX = `S${rnd}`
// ------------------------------------------------------------------------------
const tokenList = fs.readdirSync(JSONS_PATH).sort((a, b) => {
return parseInt(a.split('.')[0]) - parseInt(b.split('.')[0])
})
const tokenJsons = {}
for (const tokenFileName of tokenList) {
const num = tokenFileName.split('.')[0]
tokenJsons[num] = JSON.parse(fs.readFileSync(path.resolve(JSONS_PATH, num.toString() + '.json')))
}
const __old__imageIpfsCids = {
"1": "QmVXsduWSzmDvMHLW9wxwcNr5mo2HWcNFu4SN2ywqTkRr4",
"2": "QmWWxKboNqN3cA2JrMF8q8Zi7X8dcCkaGo1aPFczM4icax",
"3": "Qme1v8qpMFp9ns4AWKXhpjD7oJvkrGicF7f7ejQXJUGtYy",
"4": "QmWZE4qc53fZJsHbhfSf3ubVXbKQmoSQ1QmSRvmcfjtCe8",
"5": "QmaHittBNrmhEDcnpVdpz1aGTRLtGSovpMAk5u1agNFRjQ",
"6": "QmaLPWvhVWBbP3F524Dqzrd33Q9Euo5jy87d6cFLzEQSuQ",
"7": "QmNXYDegSxCtEMMsDjW5EksDtgNzEyT9ZeJQfpJMEAHtdV",
"8": "QmeqKgyBFGh1kzQ96yKsBqz8A6ukqD8P8BsJCe7Pc4KRCs",
"9": "QmYGqpWt6JqRMN8XQ21YujKA9uQwy3rFsXPS3JwEk6Y8Nd",
"10": "QmW43MKRjLaJo6GacvuNEKE6STdahzmoTKKH6XQPNtZJ8d",
"11": "QmQ2RNdZoee2dqC65vLTKnwo6Nrb2T92i6rkM2uezNtApT",
"12": "Qme7xv4iw3TECB9QJXBsdmEvBvZ5kqKz17rKDJcaHbCGH7",
"13": "Qmb2kVMXYdwbN1NHd8WQdxkeVpq8bEdez6f1DYX67T8NGM",
"14": "Qmb4v33xtHM9Y9eqZuut6BcmtB94SdtZXL8FGfwG7CotSR",
"15": "QmWAbMx4id1MJU9jDWLVbsR7f8cKV8Snt9Bjz3Jn5ddC26",
"16": "QmS2Gr4LqRVSjAhHF979PK73WJU6mZrLhdLpo9BtJGogG8",
"17": "QmVwzcjsAsJmvbHCdpkqMG2ecJmKYUtooAh2TU9JMszKsb",
"18": "QmcP7X4GhL4TZJorzUdY2Jau43HRD8jFihJdvy8G2UxG2N",
"19": "QmYiGWNLQ34LjvaGhfHjCuZ7KPtHL5tDZjbvXES119KAYK",
"20": "QmanDVvupi6XPwnuHKyVySCzjQn5wxthb9mRSWnj3jG1Z9",
"21": "QmcvgzkBofRD2Vv68sJtphb6d4ZSfwf3PiQwU7VoCodbYi",
"22": "QmYZok18bPCRYz1XMaQsbjzpPg8WHRRoAxVptUyMvXnvdg",
"23": "QmcCZxDBhfMxBQ9kMewEPcKTYGkTSFECFrnufHwnbYqizn",
"24": "Qmc8J5zybfqiGykSBQVijmAFCwEiDAnxTWQZK7mLV8WUXE",
"25": "QmPy6E1EsNJbdQW87XR2MWL4GvvKHpNdyrytdpuAN5GLMV"
}
const imageIpfsCids = {
"1": "QmPWWoLRQ9D592tzEzybBAosKA4Vy9SjZrE41JbenWaARZ",
"2": "QmT6KhD9JcB4dVq3W57UfvuA3KWwRBQngSnyGec5NEmqUz",
"3": "QmauVRYmCNSD5DLaJwgsTAEnBmyEMD22mnW52Uo7ACAG7g",
"4": "QmePAnd6aBQURQPW9yQfeCgdn14ZvDR4QsCXPLoEA6vfVz",
"5": "QmZTd8Z97Xr4uyZmZfTvNL7LqDRUs1ZdeBj3kU2Ch583sV",
"6": "QmPzTRTQfZjWWGEUHpBksSRYVgF5h9gJK9TvcNBQbqQX9s",
"7": "QmTNbMZXF2yzkfiQMKz2EoUTCRTqtpqBWLAgTY3FGhnZ2L",
"8": "QmdXsLyPzTBU7dKp43xJ8az2V62LDxmzsNKXJbVg1aGX2u",
"9": "QmTVQsZf7SvJwGQQXroGb9qL3Qzu3v8dz9gCu7Ln2FN53m",
"10": "QmXZNF3hSGJRwezK5CA227BtJpjqTiKUgyJPFCo6x7rFy3",
"11": "QmNLxReEeU94ZL3hW2wtXrhcd8WnoMXnNtnemH7Bf8soUs",
"12": "QmR9T193VALhqnH3bKA2cEjEDmyF4Nsh65KjTqo1aXjsw7",
"13": "QmbD3Wv453WmY8L1aq6pU8LDEPLnbwvRebNfmm1SLuwQ4v",
"14": "QmbjjJ7t2wmV4ifAjt66ZvYW37ooLeNwL26om7chT5TwHb",
"15": "QmV9CCk9AWb47fnHtypwbSqrKRffwyxw3XiiEkiU5jUD39",
"16": "QmeVsVMdB8FVXUHKkM6UkkkUidChC2m8U7hpDsjcZySJD9",
"17": "QmeaZqb9FVkBiBR4yvyCe2J8qYCCKVTXtp7h5SUnhJjaqv",
"18": "QmVLBDsTqXXB4zhMNqB2Rq4aFHn9RxTgJUkLx4CFYFSgzo",
"19": "Qmc8fXjYWn9gzdqHyTY6jAVtcVa7NhwenPNUW5qbchKBFn",
"20": "QmTLgbnmubXmzpeHbmbhe47Ne3rgb64EMAHHoyGrzfAZof",
"21": "QmTTnY82jXGTNjk3ueXWsUYZyBYhm8iW5gERWqcrQ7pmip",
"22": "QmcuBgGHKNexCg2VkSiEgN68sNEKRhNx5LJodPHhU5SXTc",
"23": "QmYLBzfPkWqZAjkEDmY4Ew7j84Ph728agKDhobH4UfgAhT",
"24": "QmZHoYsi3t7FRPndLPWe5ePfzKUkpS5QRbth8ApoDm4ZZZ",
"25": "QmbX9cNCvXStf3kUzxteRNzoq53szdhVaU8VDcd5bs52c8"
}
const TRAIT_TYPES = {
Background: ['Blue Sky', 'Cosmos', 'Moonlight Sky'],
Body: ['Alien Skin', 'Dark Skin', 'Light Skin', 'Robot Skin'],
Hair: ['Blue Braids', 'Blue Pigtails', 'Pink Braids', 'Pink Pigtails'],
Headpiece: ['Alien Helmet', 'Oil Head'],
Outfit: ['Alien Armor', 'Oil', 'Pink Oil'],
Project: [
'This asset allocates 20 per cent of the primary sale to The Haiti Tree Project Public Address E8fMoCxXbKvHWbwgv2vV1KxeFoeedrps9SCWjg9k2UXTSr8',
'This asset allocates 20 per cent of the primary sale to Re-Climate Public Address GfQ48UJYXfLKpezBJQYka7qRmgc3soBe4TqwcguTZujCLjq',
'This asset allocates 20 per cent of the primary sale to SeaTrees Org Public Address ECqPeXiocyHYUNhaEGQA8PW3LwwZ7M9bXHqJyZgAaa1kkGc',
]
}
const generateSchema = (traitTypes) => {
const makeProtobufEnumValuesFromArray = (values) => {
const obj = {options: {}, values: {}}
values.forEach((value, index) => {
const fieldName = `field${index + 1}`
obj.options[fieldName] = `{"en":"${value}"}`
obj.values[fieldName] = index
})
return obj
}
const onChainMetaData = new protobuf.Root().define('onChainMetaData')
const NFTMeta = new protobuf.Type("NFTMeta")
.add(new protobuf.Field("ipfsJson", 1, "string", 'required'))
.add(new protobuf.Field("Headpiece", 2, "Headpiece", 'optional'))
.add(new protobuf.Field("Body", 3, "Body", 'optional'))
.add(new protobuf.Field("Outfit", 4, "Outfit", 'optional'))
.add(new protobuf.Field("Background", 5, "Background", 'optional'))
.add(new protobuf.Field("Hair", 6, "Hair", 'optional'))
.add(new protobuf.Field("Project", 7, "Project", 'required'))
onChainMetaData.add(NFTMeta)
for (const trait in traitTypes) {
const {options, values} = makeProtobufEnumValuesFromArray(traitTypes[trait])
onChainMetaData.add(new protobuf.Enum(trait, values, options))
}
const root = new protobuf.Root().add(onChainMetaData);
return root
}
const prepareTokenData = (schemaObject, num, tokenJson, ipfsCid) => {
const {attributes, description} = tokenJson
//for more encoding convenience
if (description) attributes.push({trait_type: 'Project', value: description})
const preparedData = {}
preparedData.ipfsJson = `{\"ipfs\":\"${ipfsCid}\",\"type\":\"image\"}`
for (const attribute of attributes) {
const {trait_type: traitName, value: traitValue} = attribute
const result = schemaObject.lookupEnum(traitName)
const fieldName = Object.entries(result.options).find(([_, translatesJson]) => {
return translatesJson.match(traitValue)
})?.[0]
const encodedValue = result.values[fieldName]
if (encodedValue !== undefined)
preparedData[traitName] = encodedValue
}
return preparedData
}
const encodeAllTokens = (schemaObject, schema, schemaHelper) => {
const encodedTokens = []
for (const tokenNumStr in tokenJsons) {
const num = parseInt(tokenNumStr)
const tokenJson = tokenJsons[tokenNumStr]
const preparedTokenData = prepareTokenData(schemaObject, num, tokenJson, imageIpfsCids[num])
const encodedToken = schemaHelper.encodeData(schema, preparedTokenData)
const validateResult = schemaHelper.validateData(schema, preparedTokenData)
if (!validateResult.success) {
console.log('ERROR', num, validateResult.error.message)
break;
}
encodedTokens.push({
num, preparedTokenData, encodedToken
})
}
return encodedTokens
}
const checkBalance = async (uniqueHelper, signer) => {
const props = await uniqueHelper.getChainProperties()
const tokenSymbol = props.tokenSymbol[0]
const decimals = parseInt(props.tokenDecimals[0])
const balanceBigInt = await uniqueHelper.getSubstrateAccountBalance(signer.address)
const balanceStr = balanceBigInt.toString().padStart(decimals, '0')
const balance = parseFloat(balanceStr.slice(0, -decimals) + '.' + balanceStr.slice(-decimals))
console.log(`Balance check: Your have ${balance} ${tokenSymbol}.`)
if (balance >= MIN_BALANCE) {
console.log(`Balance check: OK. Your have more than ${MIN_BALANCE} ${tokenSymbol}.`)
} else {
throw new Error(`YOU HAVE JUST ${balance} ${tokenSymbol}. Please get more than ${MIN_BALANCE} ${tokenSymbol} to mint collection and tokens`)
}
return {
tokenSymbol, decimals, balance
}
}
const main = async (args) => {
const schemaHelper = new UniqueSchemaHelper(logger)
const uniqueHelper = new UniqueHelper(logger)
await uniqueHelper.connect(config.WS_ENDPOINT)
const signer = UniqueUtil.fromSeed(config.seed)
const schemaObject = generateSchema(TRAIT_TYPES)
const schema = JSON.stringify(schemaObject.toJSON())
try {
await checkBalance(uniqueHelper, signer)
} catch (err) {
console.error(err)
if (config.WS_ENDPOINT.match('comecord')) {
console.log('RPC is dev (comecord), trying to send some money from Alice')
const aliceSigner = UniqueUtil.fromSeed('//Alice')
const result = await uniqueHelper.transferBalanceToSubstrateAccount(aliceSigner, signer.address, 100n * (10n**18n))
console.log('Money balance sufficience result', result)
await checkBalance(uniqueHelper, signer).catch()
}
}
const variableOnChainSchema = JSON.stringify({collectionCover: "Qmc8J5zybfqiGykSBQVijmAFCwEiDAnxTWQZK7mLV8WUXE"})
const constOnChainSchema = schema
const offchainSchema = JSON.stringify({
additionalType: 'video',
videoURLTemplate: `https://bafybeib5lxymirwhmoj6ofppxealqh7eyw5bu7wmvk64mll5dvcqfzykwu.ipfs.nftstorage.link/videos/{id}.mp4`
})
const collectionOptions = {
name: COLLECTION_NAME,
description: COLLECTION_DESCRIPTION,
tokenPrefix: COLLECTION_TOKEN_PREFIX,
offchainSchema,
schemaVersion: "Unique",
variableOnChainSchema,
constOnChainSchema,
limits: {
sponsorTransferTimeout: 0,
sponsorApproveTimeout: 0,
},
metaUpdatePermission: "Admin"
}
// const collectionId = 7
const {collectionId} = await uniqueHelper.mintNFTCollection(signer, collectionOptions)
console.log('collectionId', collectionId)
const realOwnerAddress = ADDRESS_TO_WHERE_TO_MINT || signer.address
const allTokensEncoded = encodeAllTokens(schemaObject, schema, schemaHelper)
// console.log(JSON.stringify(allTokensEncoded, null, 2))
const tokensForMint = allTokensEncoded.map(token => {
return {
owner: {Substrate: realOwnerAddress},
constData: token.encodedToken,
}
})
const mintedTokens = await uniqueHelper.mintMultipleNFTTokens(signer, collectionId, tokensForMint)
console.log(mintedTokens)
if (realOwnerAddress !== signer.address) {
console.log('changing collection owner')
const result = await uniqueHelper.changeNFTCollectionOwner(signer, collectionId, realOwnerAddress)
console.log('changeNFTCollectionOwner result:', result)
}
}
const DESCRIPTION = 'mint a collection and tokens for sadu'
module.exports = {
main,
description: DESCRIPTION,
help: getUsage('npm run -- playground image.dev', {help: DESCRIPTION})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment