How to issue an NFT on the Bitshares DEX with NodeJS

Pre-requisites

  • You'll need a production Bitshares account.
  • Purchase a 'Lifetime membership', this will dramatically reduce network fees.
  • Export your active WIF & memo private keys for use in the script.
    • Never share/expose these keys, store them in a password manager.
  • Install the latest NodeJS on your computer
  • Enough BTS to purchase your asset name & issue sub-assets for each of your planned NFTs.
  • Your own art in png format.

Install node packages

npm i bitsharesjs
npm i bitsharesjs-ws
npm i hasha
npm i web3.storage

For each NFT create a folder & file

Create a folder, within this folder create the file nft.json with the following example contents:

{
    symbol: 'ASSETNAME.NFT_SYMBOL',
    name: 'NFT_TITLE'
}

Store image to IPFS & update NFT.json file

Go create an account on web3.storage, retrieve your API key and place in the token variable (Don't share this key with others).

Save the following code to store.js, then run via the command node store.js, this will upload every png in the current folder to web3 for storage.

The following script was used for the NFTEA AI NFTs, it would fetch multiple images from a folder which were incrementally named like 1.png, 2.png etc. So if you've not got more than one image you should ommit the media_png_multihashes code.

Once the script is complete the required IPFS references will have been stored in the NFT.json file.

  const fs = require('fs');
  const hasha = require('hasha');
  const prompts = require('prompts');
  const { Web3Storage, getFilesFromPath } = require('web3.storage');
  let token = "YOUR_SECRET_KEY";
  const storage = new Web3Storage({ token: token });

  (async () => {
    const response = await prompts({
      type: 'text',
      name: 'directory',
      message: 'Enter the directory',
    });

    if (!response) {
      console.log("invalid directory")
      return;
    }

    let directory = response.directory;

    let nft = require(`./${directory}/nft.json`);

    let pathFiles = await getFilesFromPath(`./${directory}/`);
    let pngFiles = pathFiles.filter(file => file.name.includes(".png"));

    let withoutFolder = pngFiles.map(file => {
      let currentFileName = file.name.split("/");
      file["name"] = currentFileName[currentFileName.length - 1]
      return file;
    })

    let currentCID = await storage.put(withoutFolder);

    let fileList = fs.readdirSync(`./${directory}/`);
    let filteredFiles = fileList.filter(file => file.includes(".png"));
    let fileNumbers = filteredFiles.map(file => parseInt(file.split(".")[0])).sort((a,b) => b-a);
    let topFile = Math.max(...fileNumbers).toString() + ".png";
    nft["media_png_multihash"] = `/ipfs/${currentCID}/${topFile}`;

    let sortedFileList = fileNumbers.map(file => file.toString() + ".png");
    let media_png_multihashes = [];
    for (let i = 0; i < sortedFileList.length; i++) {
      let currentFile = sortedFileList[i];

      let hash = await hasha.fromFile(`./${directory}/${currentFile}`, {algorithm: 'sha512'});

      media_png_multihashes.push({
        sha512: hash,
        url: `/ipfs/${currentCID}/${currentFile}`
      });
    }

    nft["media_png_multihashes"] = media_png_multihashes;

    fs.writeFile(`./${directory}/nft.json`, JSON.stringify(nft, null, 4), (err) => {
       if (err) console.log(err);
    });

  })();

Issue NFT on the Bitshares platform

Whilst issuing your NFTs, temporarily store your exported active wif wifKey & memo key memoKey in the script.

Change BITSHARES_ACCOUNT_ID to your 1.2.x Bitshares account ID within the operationJSON within the script.

Change the contents of the nft_object & the common_options within operationJSON - some of these options are permanent, others are highly relevant for tweaking NFT settings (like quantity, divisibility, permissions, etc).

For more info on the nft_object best practices see the Bitshares NFT Spec for more info.

Save the script as create.js and run via node create.js. If successful it'll output a success message, otherwise look into what caused errors.

If you've successfully created the asset but spot a mistake, do not run the same script, you'll need to run an update command instead.

Now you've issued your NFT, remove the active & memo keys from the script so they're not lying around unsecured.

const { Apis } = require("bitsharesjs-ws");
const {
    TransactionBuilder,
    ChainStore,
    FetchChain,
    PrivateKey,
    hash,
    Signature
} = require("bitsharesjs");
const prompts = require('prompts');

const wifKey = "";
const pKey = PrivateKey.fromWif(wifKey);

const memoKey = "";
const pMemoKey = PrivateKey.fromWif(memoKey);

(async () => {
  const response = await prompts({
    type: 'text',
    name: 'directory',
    message: 'Enter the directory',
  });

  if (!response) {
    console.log("invalid directory")
    return;
  }

  let directory = response.directory;
  let nftJSON = require(`./${directory}/nft.json`);

  Apis.instance("wss://eu.nodes.bitshares.ws", true).init_promise.then(
      res => {
          console.log("connected to:", res[0].network_name, "network");

          ChainStore.init().then(() => {
              let tr = new TransactionBuilder();

              let nft_object = {
                acknowledgements: "",
                artist: "",
                attestation: "",
                encoding: "ipfs",
                holder_license: "",
                license: "",
                media_png_multihash: nftJSON.media_png_multihash,
                media_png_multihashes: nftJSON.media_png_multihashes,
                narrative: "",
                sig_pubkey_or_address: "",
                title: nftJSON.name,
                tags: "",
                type: ""
              };

              let nft_signature = Signature.signBuffer(Buffer.from(JSON.stringify(nft_object)), pMemoKey);

              let description = JSON.stringify({
                  main: "",
                  market: "BTS",
                  nft_object: nft_object,
                  nft_signature: nft_signature.toHex(),
                  short_name: nftJSON.symbol
              });

              let operationJSON = {
                issuer: "BITSHARES_ACCOUNT_ID", // change to your bitshares account id
                symbol: nftJSON.symbol,
                precision: 0,
                common_options: {
                    max_supply: 1,
                    market_fee_percent: 0,
                    max_market_fee: 0,
                    issuer_permissions: 0,
                    flags: 0,
                    core_exchange_rate: {
                        base: {
                            amount: 100000, // change to initial value
                            asset_id: "1.3.0"
                        },
                        quote: {
                            amount: 1,
                            asset_id: "1.3.1"
                        }
                    },
                    whitelist_authorities: [],
                    blacklist_authorities: [],
                    whitelist_markets: [],
                    blacklist_markets: [],
                    description: description,
                    extensions: {
                      reward_percent: 0,
                      whitelist_market_fee_sharing: []
                    }
                },
                is_prediction_market: false,
                extensions: null
              };

              tr.add_type_operation("asset_create", operationJSON);

              tr.set_required_fees().then(() => {
                  tr.add_signer(pKey, pKey.toPublicKey().toPublicKeyString());
                  console.log(
                      "serialized transaction:",
                      tr.serialize().operations
                  );
                  tr
                      .broadcast()
                      .then(result => {
                          console.log(
                              "asset was succesfully updated. result raw tx: \n" +
                                  JSON.stringify(result)
                          );
                      })
                      .catch(err => {
                          console.error(err);
                      });
              });

          });
      }
  );

})();

After issuance let's get the id!

Let's say we need to update the NFT, we require the asset ID as reference, so let's fetch this and store it in the nft.json file!

Save the following as addID.js, run via node addID.js and enter the directory name when prompted.

The nft.json file has now been updated with the id.

const { Apis } = require("bitsharesjs-ws");
const {
    TransactionBuilder,
    ChainStore,
    FetchChain,
    PrivateKey,
    hash,
    Signature
} = require("bitsharesjs");

const fs = require('fs');
const prompts = require('prompts');

(async () => {
  const response = await prompts({
    type: 'text',
    name: 'directory',
    message: 'Enter the directory',
  });

  if (!response) {
    console.log("invalid directory")
    return;
  }

  let directory = response.directory;

  let nft = require(`./${directory}/nft.json`);

  Apis.instance("wss://eu.nodes.bitshares.ws", true).init_promise.then(
      res => {
        ChainStore.init().then(async () => {
          //await Apis.instance("wss://node.testnet.bitshares.eu", true).init_promise;
            let asset = await Apis.db.get_assets([nft.symbol]);
            //let asset = await ChainStore.get_asset(nft.symbol);
            nft['id'] = asset[0].id;

            await fs.writeFile(`./${directory}/nft.json`, JSON.stringify(nft, null, 4), (err) => {
               if (err) console.log(err);
            });

            return;
        });
      }
  );

})();

Updating an NFT

Don't worry if you've made a mistake when creating your NFT (especially prior to sale), it's easy enough to update whilst you have ownership of the asset.

Duplicate the create.js script, and replace the Bitshares API chunk with the following and save as update.js. Update the contents of the nft_object and description in the script (fixing any mistakes) then run node update.js.

  Apis.instance("wss://eu.nodes.bitshares.ws", true).init_promise.then(
      res => {
          console.log("connected to:", res[0].network_name, "network");

          ChainStore.init().then(() => {
              let tr = new TransactionBuilder();

              let nft_object = {
                acknowledgements: "",
                artist: "",
                attestation: "",
                encoding: "ipfs",
                holder_license: "",
                license: "",
                media_png_multihash: nftJSON.media_png_multihash,
                media_png_multihashes: nftJSON.media_png_multihashes,
                narrative: "",
                sig_pubkey_or_address: "",
                title: nftJSON.name,
                tags: "",
                type: ""
              };

              let nft_signature = Signature.signBuffer(Buffer.from(JSON.stringify(nft_object)), pMemoKey);

              let description = JSON.stringify({
                  main: "",
                  market: "BTS",
                  nft_object: nft_object,
                  nft_signature: nft_signature.toHex(),
                  short_name: nftJSON.symbol
              });

              let operationJSON = {
                  issuer: "YOUR_ACCOUNT_ID",
                  asset_to_update: nftJSON.id,
                  new_options: {
                      max_supply: 1,
                      market_fee_percent: 0,
                      max_market_fee: 0,
                      issuer_permissions: 0,
                      flags: 0,
                      core_exchange_rate: {
                          base: {
                              amount: 100000,
                              asset_id: "1.3.0"
                          },
                          quote: {
                              amount: 1,
                              asset_id: nftJSON.id
                          }
                      },
                      whitelist_authorities: [],
                      blacklist_authorities: [],
                      whitelist_markets: [],
                      blacklist_markets: [],
                      description: description,
                      extensions: {
                        reward_percent: 0,
                        whitelist_market_fee_sharing: []
                      }
                  },
                  is_prediction_market: false,
                  extensions: null
              };

              tr.add_type_operation("asset_update", operationJSON);

              tr.set_required_fees().then(() => {
                  tr.add_signer(pKey, pKey.toPublicKey().toPublicKeyString());
                  console.log(
                      "serialized transaction:",
                      tr.serialize().operations
                  );

                  tr
                      .broadcast()
                      .then(result => {
                          console.log(
                              "asset was succesfully updated. result raw tx: \n" +
                                  JSON.stringify(result)
                          );
                      })
                      .catch(err => {
                          console.error(err);
                      });

              });
          });
      }
  );

NFTEA Gallery

An open source NFT gallery powered by the Bitshares blockchain!
All displayed NFTs are tradeable on the Bitshares decentralized exchange, get collecting!
Created using Next.js and running on Vercel