|
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}) |
|
} |
|
|