Neo N3 NFT Creation

A developers guide to creating Neo N3 NFTs compatible with GhostMarket

Introduction

This page provides step-by-step instructions for deploying GhostMarket compatible NFTs on the Neo N3 blockchain. The Neo 2 legacy blockchain is not supported.

The NFT creation process requires interaction with the blockchain. For retrieving existing NFT metadata, see Accessing NFT data, which uses the GhostMarket APIs.

The snippets used in this guide are javascript.

The code snippets also assume a node.js environment.

See our Metadata Reference for full details and requirements of GhostMarket NFT metadata.

NFT Creation

The NFT creation process on all blockchains broadly consists of the following steps.

  1. Prepare Off-Chain Metadata and persist it.

  2. Assemble On-Chain Metadata and build a transaction.

  3. Sign the transaction.

  4. Broadcast to the blockchain.

1. Prepare Off-Chain Metadata

The preferred decentralised platform for persisting image/media data for your NFT is IPFS.

This guide will use Piñata to upload and pin your images to IPFS.

Set up a Piñata account and get started here.

You can either manually upload you metadata, noting the IPFS hash, or dive into the Pinata js-sdk to interact with Piñata programatically.

Take note of the IPFS hash of the uploaded metadata, you will need it in the following step.

2. Assemble On-Chain Metadata

GhostMarket Neo N3 NFT contract stores all metadata on-chain as a JSON string.

We will use the neon-js npm module to work with the Neo N3 blockchain. The full neon-js docs can be found here

Install the needed modules

% npm install @cityofzion/neon-js@next
% npm install unicode-encode

Import the library

const { utoa, atou } = require("unicode-encode");
const { CONST, rpc, sc, wallet, tx, u } = require("@cityofzion/neon-core");

Assemble the Minting Parameter JS Object

GhostMarket Neo N3 NFT contract allows you to serialize the NFT's metadata object to JSON and store On-Chain. Let's create the Metadata.

In this example, for clarity, we will create the attribute and properties objects separately:

Royalties and locked contend do not form part of the MetaData, they are specified later in the contract parameters.

Example using On-Chain Metadata

// Define the NFT attributes
const attributes = [
  {
    trait_type: "color", // The attribute type/key
    value: "red", // The attribute value
    display_type: "", // The display format
  },
  {
    trait_type: "size", // The attribute type/key
    value: "small", // The attribute value
    display_type: "", // The display format
  },
  // No third attribute
  // An arbitrary number of attributes may be added for custom apps
];

// Define the NFT properties
const properties = {
  has_locked: true, // Is there locked content
  type: 1,
};

// Put it together
const jsonMetadata = JSON.stringify({
  name: "My Shiny NFT",
  description: "This NFT will be a classic",
  image: "ipfs://QmTy8w65yBXgyfG2ZBg5TrfB2hPjrDQH3RCQFJGkARStJb",
  tokenURI: "",
  attributes,
  properties,
});

Build the transaction script

Define account and RPC Network objects needed later

const creatorPrivateKey =
  "L1QqQJnpBwbsPGAuutuzPTac8piqvbR1HRjrY5qHup48TBCBFe4g";
const creatorAccount = new wallet.Account(creatorPrivateKey);

const rpcClient = new rpc.RPCClient("http://neo3.edgeofneo.com:10332");

// The locked content - ToDo - Describe fully
// Node.js only - add module "npm i buffer" for browsers
const lockedContent = Buffer.from("My secret Locked Content", "utf8").toString(
  "hex"
);

Define the royalties that the creator wishes to receive from each sale of the NFT.

// construct the royalties ARRAY.
const royaltyBPS = 2000; // royalties - 20% expressed in Basis Points (BPS) (0.01%)

const contractRoyalties = JSON.stringify([
  {
    address: creatorAccount.address,
    value: royaltyBPS.toString(),
  },
  // Additional royalty address/value pairs may be added here
]);
// Define the minting arguments to be used in the transaction script
const mintArgs = [
  sc.ContractParam.hash160(creatorAccount.address),
  sc.ContractParam.byteArray(utoa(jsonMetadata)),
  sc.ContractParam.byteArray(utoa(lockedContent)),
  sc.ContractParam.byteArray(btoa(contractRoyalties.toString())),
  sc.ContractParam.string(""), //Data - empty
];

// Build the script - ready for transaction
const script = sc.createScript({
  scriptHash: "577a51f7d39162c9de1db12a6b319c848e4c54e5", // GhostMarket NFT contract
  operation: "mint",
  args: mintArgs,
});

let currentHeight = 0;
// Retrieve the current block height to calculate expiry
currentHeight = await rpcClient.getBlockCount();

// specify the contracts that this transaction may interract with
const allowedContracts = [
  "d2a4cff31913016155e38e474a2c06d08be276cf", // GAS script hash
  "577a51f7d39162c9de1db12a6b319c848e4c54e5", // GhostMarket NFT Contract
];

3. Construct & Sign the transaction

// Build the transaction
txn = new tx.Transaction({
  sender: creatorAccount.scriptHash,
  signers: [
    {
      account: creatorAccount.scriptHash,
      scopes: tx.WitnessScope.CustomContracts,
      allowedContracts,
    },
  ],
  validUntilBlock: currentHeight + 1000000,
  systemFee: 0,
  script: script,
});

// Sign the transaction
const signedTransaction = txn.sign(
  creatorAccount,
  CONST.MAGIC_NUMBER["MainNet"]
);

4. Finally, broadcast to the network

const result = await rpcClient.sendRawTransaction(
  signedTransaction.serialize(true)
);

console.log("Txn ID: ", result);

Congratulations, you have successfully deployed a GhostMarket compatible NFT to GhostMarket contract on Neo N3 blockchain.

For clarity, all of the above snippets exclude error and promise handling for clarity. Production code should include thorough exception management and handle promises appropriately.

Last updated