search instagram arrow-down

Enter your email address to follow this blog and receive notifications of new posts by email.

Recent Posts

This post is primarily for me to document the path I took to learning how to develop a dApp that interfaces with a Smart Contract on the Ethereum Blockchain (note that it is incomplete and I am updating it as I learn…). If you get some value out of my journey great… I am most interested in understanding Non Fungible Tokens (or NFTs), how they are created specifically, and their value in the context of physical assets (versus digital assets). I’ll continue to add to this content as my journey continues. If you see something in here that I misinterpreted please let me know by leaving a comment.

I am not going to repeat what the definition, or value, is of Blockchain, Ethereum, and Distributed Applications (dApps) as you can find many other great articles and videos that go into each in detail (example).

As a general background- a fungible asset such as BitCoin, gold or fiat dollars is interchangeable (you could change one dollar in for another). Non-Fungible assets such as a dog, artwork, real estate or a rare coin are not interchangeable. In the world of Ethereum fungible assets generally use the ERC20 token standard and Non-Fungible assets use the ERC721 standard.

The 100,000 Foot View — OK, what is it again?

An easy way to think about an NFT is to compare it to a deed for a parcel of real estate–where a deed is the official document (or for an NFT, the official cryptographic record) declaring a person’s legal ownership of a specific parcel of real estate/property.

The Value of an NFT in the Context of a Physical Asset

I’ll use an example, to explain:

Imagine someone passes away and leaves an appreciating asset (like a rare painting) to be split between multiple people in a will. The beneficiaries could sell the item and split the money between themselves or they may want to hold onto it and tokenize the artwork. This allows them to all own equal “shares” of the painting and benefit as it grows in value over the years. Tokenizing the asset may open up the opportunity for one of the shareholders to sell their shares or even partial shares. It could also allow a shareholder to use their ownership as collateral for a loan or to acquire insurance on their share of the asset.

Creating an NFT also provides a mechanism to start storing provenance (a record of ownership) and a history of activities such as grading/valuation (stored as metadata).

You could think of an NFT’s value in the context of its ability to enable additional ‘utility’ and ‘provenance’. Utility being what it enables the owner to do (I’m willing to buy an NFT ticket because it lets me into a conference, I’m willing to buy a piece of art if I can show it off in a virtual world, I’m willing to buy an item if it gives me special abilities in a game). Provenance encapsulates the story behind an NFT’s asset (Where did it come from? Who’s owned it in the past?). As the stories of the asset grow more complex they start to have a meaningfully impact on a token’s value.

NFTs could be used to secure and immutably store birth certificates, academic credentials, warranties, identities, and property ownership if and when a reliable repository of trusted issuers of such certificates can be established. Imagine having your diploma issued on the blockchain as a digital document that is recognized by all authorities around the world  with  no need to translate, notarize, verify it. Wallet-like software could manage all personal identity-related data as well as certificates of a person. This empowers us to reclaim ownership of our data from birth certificates to university certificates and all other important identity information, without the need for centralized institutions storing our data. NFTs could therefore represent identities on the web, with attached transaction histories, content, and reputation.

Caveat: There are several areas where laws are not clear in the context of NFTs. Some are securities laws and others are in the context of ownership (do you own the asset or the NFT)… The real estate deed comparison above is so simple but the subject actually quite complicated. (here is a good high level on NFTs and securities law and another on NFTs and Intellectual Property law). This thread will show you some of that confusion… As an example, Jack Dorsey sold his first Tweet–but, What was the “asset” because tweets are generated in real time by querying a database and generating some HTML, CSS and possibly some JSON via an API. So what does the owner own–the Twitter code that generates the Tweet? An image of the Tweet? The text of the Tweet?

If you are building an NFT marketplace, you need to consider:

  • If an NFT originated in Iran or North Korea or some other sanctioned country how would a US-based marketplace treat that transaction from an OFAC/FinCEN sanctions perspective?
  • Since FinCEN rules govern things that substitute for value are marketplaces treated as money transmission businesses? What are the implications?
  • Does a marketplace that records the sale of the NFT need to conduct know-your-customer/anti-money laundering procedures?
  • Does the marketplace need to collect sale tax on the asset and the NFT transaction?
  • Can a marketplace be involved in secondary trading or fractional ownership and not violate US securities laws?  What about Commodity Futures Trading Commission (CFTC) laws?
  • How not to violate US securities laws-If the NFT relates to an already existing asset and is marketed as a collectible with a public assurance of authenticity on the blockchain is it unlikely that such an NFT would be deemed security? What if the NFT is being created and sold as a way for members of the public to earn investment returns then is that type of NFT more likely to be considered a security? The SEC put out this guidance in 2019.

If you are building an NFT for an asset you also need to know exactly what to include in the metadata to ensure it’s clear exactly what you are selling and the best smart contract to use for the transaction. You also need to understand what laws protect you (the grantor) and the grantee (who is purchasing the asset) in each jurisdiction. You can find some good content on these legal subject here.

What about other BlockChains?

Which blockchain to use given there are so many–why Ethereum? There are many including the Binance Smart Chain, Flow by Dapper Labs, Tron, EOS, Polkadot, Tezos, Cosmos, and hundreds of others and each blockchain has its own separate NFT token standard, compatible wallet services, and marketplaces. This creates a problem because certain NFTs are only available on specific platforms. For example, if you want to purchase NBA Top Shot (on Flow blockchain) packs you will need to open an account with NBA Top Shot, create a Dapper wallet, and fund it with either the USDC stablecoin or supported fiat currency options.

I’m focused here on Ethereum and its ecosystem given it is the largest and most mature. However, I’m keeping a close eye on a couple of the others. Ethereum also has many marketplaces (OpenSea, Rarible, SuperRare, Nifty Gateway, Foundation, Axie Marketplace, BakerySwap, NFT ShowRoom, VIV3) and most make it easy for non-developers to create NFTs for their assets—just connect your Ethereum Wallet.

Note: Everything on Rarible is on OpenSea and most of the other marketplaces. This is because all the marketplaces read from the blockchain! So even if marketplace platforms disappear, the blockchain will persist the tokens and contracts.

What is a Smart Contract?

A smart contract is a self-executing contract with the terms of the agreement between buyer and seller being directly written into lines of code. The code and the agreements contained therein exist across a distributed, decentralized blockchain network. Smart contracts allow developers to place hard caps on the supply of NFTs and enforce persistent properties that cannot be modified after the NFTs are issued. For example, a developer can enforce programmatically that only a specific number of a specific rare item can be created, while keeping the supply of more common items infinite. NFTs are fully programmable.

Example: if a certain amount of Ether is deposited into a smart contract by a certain date, then payment will be released to the fundraiser — if it is not, then payment will be returned to donors. Because smart contracts exist on a blockchain, they are immutable (can’t be changed) and verifiable (everyone can see them), guaranteeing a high level of trust among parties that they accurately reflect the stated parameters of the agreement and will execute if, and only if, those parameters are met.

On the Ethereum blockchain, smart contracts are written in a language called Solidity.

Because smart contracts are programmable they can utilize other services offered by third parties. Oracle networks provide for this ability. One example of an Oracle is Chainlink. It is a decentralized oracle network that connects smart contracts on any blockchain to data providers, web APIs, enterprise systems, cloud services, IoT devices, payment systems, other blockchains.

ERC721 Smart contracts usually point to metadata as well. This metadata is usually not stored on the blockchain because the costs are preventative however it is usually stored on a decentralized network such as IPFS (InterPlanetary File System). There are several ‘pinning services’ that provide easy access to IPFS such as Pinata. Here is a good article on how to use Pinata to store your image and metadata and get a URL for use in your Solidity smart contract (how-to).

Risk: Before you acquire a NFT you need to know where these links are hosted and are they on a decentralized platform (like IPFS) versus a server that can one day go away. Here is an example of why this is important.

Risk: The format of metadata can also lock you into a platform. Keep an eye on standards evolving so this is not the case. One to follow is here. Opensea’s metadata standards are documented here.

Developing Smart Contracts

There are several videos on how to get started with Solidity and building smart contracts. Dapp University has several. Also, check out this one by the guy behind Dapp University but on the FreeCodeCamp.org channel.

Note: Solidity is changing fast so some of the examples no longer work with the newer versions of Solidity. Make sure you use the same version of Solidity as used in the video if you want to follow along. I did not, and it was very frustrating… Learn from my mistake.

What is an ERC721 Smart Contract?

Each ERC721 token is tied to a different identifier, making each token unique to its owner and each token within the contract holds a different value. This is different than ERC20 tokens, where developers can create any number of tokens within one contract, but in the ERC721 token standard.

What is the Difference Between a ERC20 and ERC721 contract?

While an ERC20 token represents a single type of asset (BitCoin or Ether are good examples), an ERC721 token represents a class of assets (a CryptoKitty is an example), its ERC721 token contract represents ALL the unique assets (unique kitties), as well as who owns which asset.

To repeat, ERC721 provides the ability for a mapping of unique identifiers (each of which represents a single asset) to addresses, which represent the owner of that identifier. So basically it when you are coding an ERC721 contract you are providing a way to check who owns what and a way to move tokens around.

Token metadata is also very important in the context of an ERC721 contract. Most projects store their metadata off-chain simply due to the current storage limitations of the Ethereum blockchain. The ERC721 standard includes a method called tokenURI (example: https://myipfslocationforallmystuff/lostvangoghmetadata ) that developers can implement to tell applications where to find the metadata for a given item.

{
  "name": "The lost Van Gogh",
  "image": "https://myipfslocationforallmystuff/1500718.png",
  "description": "I bought on eBay for $1.00."
}

From a programming / functional perspective — here are the differences:

ERC20
(article)
ERC721
(article)
Description
Name (optional)NameThis function is used to tell outside contracts and applications the name of this token.
Symbol (optional)SymbolThis function also helps in providing compatibility with the ERC20 token standard. It provides outside programs with the token’s shorthand name or symbol.
totalSupplytotalSupplyThis function returns the total number of coins available on the blockchain. The supply does not have to be constant.
balanceOfbalanceOfThis function is used to find the number of tokens that a given address owns.
transfertransferlets the owner of a token send it to another user. However, a transfer can only be initiated if the receiving account has previously been approved to own the token by the sending account.
approveapproveThis function approves, or grants, another entity permission to transfer a token on the owner’s behalf
tokenOfOwnerByIndex (optional)Each non-fungible token owner can own more than one token at one time. Because each token is referenced by its unique ID, however, it can get difficult to keep track of the individual tokens that a user may own. To do this, the contract keeps a record of the IDs of each token that each user owns. Because of this, each token owned by a user can be retrieved by its index in the list (array) of tokens owned by the user. tokenOfOwnerByIndex lets us retrieve a token in this method.
tokenMetadata (optional)lets us discover a token’s metadata or the link to the its data.
ownerOfThis function returns the address of the owner of a token.
takeOwnership This function acts like a withdraw function since an outside party can call it to take tokens out of another user’s account. Therefore, takeOwnership can be used when a user has been approved to own a certain amount of tokens and wishes to withdraw said tokens from another user’s balance.
Transfer (Event)This event is fired whenever a token changes hands. It’s broadcasted when a token’s ownership moves from one user to another. It details which account sent the token, which account received the token, and which token (by ID) was transferred.
Approval (Event)This event is fired whenever a user approves another user to take ownership of a token (i.e. whenever approve is executed). It details which account currently own the token, which account is allowed to own the token in the future, and which token (by ID) is approved to have its ownership transferred.
Decimal (optional)An optional field used to determine to what decimal place the amount of the token will be calculated. The most common number of decimals to consider is 18.
transferFromAllows a smart contract to automate the transfer process and send a given amount of the token on behalf of the owner. Similar to your bank sending money to pay off a bill on your behalf, automatically.
allowanceGive an allowance to another address to be able to retrieve tokens from it. It returns the remaining number of tokens that the ‘spender’ will be allowed to spend on behalf of ‘owner’.

How It All Fits Together (Smart Contracts and dApps)

Creating an NFT is one thing and several platforms have made it pretty easy… however, creating an application that can use a smart contract is a bit more complex.

If you are not a developer, many platforms such as OpenSea have made creating an NFT a point and click exercise.

But, if you want to create an application (dApp) you are going to want to follow these high-level steps. Each of these steps has a lot of nuances so we are going to go through each one in detail.

  1. Write a smart contract using Solidity (explained in detail here). I’d recommend starting with a 3rd party NFT contract. Two great examples are from these two projects:
  2. Debug on Remix – An awesome online IDE and compiler (documentation found here)
  3. Once you are confident the contract is perfect, download an Ethereum development environment such as Hardhat or Trufflle to test code against the contract locally.
  4. Once you think the code and contract are near perfect, deploy the contract to an Ethereum test network such as Rinkeby or Ropsten.
  5. When your contract is bulletproof (remember, it’s immutable) deploy it to the production Ethereum blockchain
  6. Print and NFT and verify it in a platform marketplace such as Opensea (explained here). Note marketplace metadata requirements.

The most difficult part of this endeavor is understanding the environment (how tools fit together and what each of them does) and learning the Solidity programming language.

ToolsDescription
npm and node.jsNode.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine. npm (part of node.js) makes it easy for JavaScript developers to share and reuse code.
MetamaskMetaMask is a key vault, secure login, token wallet, and token exchange—everything you need to manage your digital assets. Available as a browser extension and as a mobile app.
PinataEnables you to easily store metadata on IPFS for use in your smart contracts
0xcert & OpenZeppelinHelps minimize risk by using battle-tested libraries of smart contracts
Hardhat or TrufflleEthereum development environments
npm install truffle
npm install –save-dev hardhat
*install locally (versus -g globally) so you can control the version per project
GanacheLocal test blockchain that you host to test on your laptop (use with Truffle)
*Note: Hardhat’s local test network is the Hardhat Network
Rinkeby & RopstenEthereum test networks
RemixHosted IDE to test Solidity Contracts
BrownieAs an alternative, if you want to use Python versus Javascript to test your Solidity contract.
infura.ioprovides APIs to interact with the Ethereum blockchain using web3 and JavaScript
web3.jsThe web3.js library is a collection of modules that contain specific functionality for the Ethereum ecosystem.
ethereumjs-txJavaScript Etherium support library

Step 1: Write a smart contract using Solidity

Pointer to Solidity docs (here) and a great video (here).

We are going to import a 0xcert ERC721 contract and add 1 function to it “mint”. For more information on the 0xcert ERC721 contracts look here –Check out the ‘mock‘ contracts that you can use to test these.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
 

//https://github.com/0xcert/ethereum-erc721/blob/master/src/contracts/tokens/nf-token-metadata.sol
import "@0xcert/ethereum-erc721/src/contracts/tokens/nf-token-metadata.sol";
//https://github.com/0xcert/ethereum-erc721/blob/master/src/contracts/ownership/ownable.sol
import "@0xcert/ethereum-erc721/src/contracts/ownership/ownable.sol";

contract Icollect is NFTokenMetadata, Ownable {
 
 uint256 public tokenCounter;

  constructor() NFTokenMetadata () {
    nftName = "Icollect";
    nftSymbol = "ICOL";
    tokenCounter = 0;
  }
 
  function mint(string calldata _uri) external onlyOwner {
    super._mint(msg.sender, tokenCounter);
    super._setTokenUri(tokenCounter, _uri);
    tokenCounter = tokenCounter + 1;
  }
 
}

Step 2: Test the Solidity Contract in REMIX

Compile the contract:

Deploy the contract in a JavaScript VM:

Mint a token:

Get the balance, name, owner, ownerof, symbol & tokenURI:

The VM provides several accounts…. copy one of them and then test a few of the function calls that the contract provides. One thing to realize is that none of these functions require ‘gas‘.

Test a function call that requires gas (note that ‘mint’ requires gas as well). Transfer ownership of the token you minted above to another VM user account (transfer the token from: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 to: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2)

Step 3: Test Solidity Contract Locally Using A Development Environment

We’ll use Hardhat… Note that OpenZeppelin does a great job of going through this part of the tutorial as well (here and here).

Install Hardhat c:\mycode\learningsolidity\npm install –save-dev hardhat

Run a local blockchain c:\mycode\learningsolidity\npx hardhat node

Create a hardhat.config.js file and ensure the version of solidity is the same as in your contract.sol file

Compile Your Solditiy Contract c:\mycode\learningsolidity\contracts\npx hardhat compile

Create a deployment script (c:\mycode\learningsolidity\scripts\deploy.js)

Deploy the contract ( c:\mycode\learningsolidity\scripts\npx hardhat run –network localhost scripts/deploy.js )

Interact with the contract in the hardhat console: ( c:\mycode\learningsolidity\npx hardhat console –network localhost)

Create some JavaScript to interact with the locally deployed contract: (c:\mycode\learningsolidity\scripts\index.js)

To interact with the smart contract we use an ethers contract instance

An ethers contract instance is a JavaScript object that represents our contract on the blockchain, which we can use to interact with our contract. To attach it to our deployed contract we need to provide the contract address. Update the javascript with the contract ID we received when we deployed the contract (0x5FbDB2315678afecb367f032d93F642f64180aa3)

Note: In a real-world application, you may want to estimate the gas of your transactions, and check a gas price oracle to know the optimal values to use on every transaction.

// scripts/index.js
async function main() {

    const contract_address = "0x5FbDB2315678afecb367f032d93F642f64180aa3";      
    const Icollect = await ethers.getContractFactory("Icollect");
    const icollect = await Icollect.attach(contract_address);

    const mint_return = await icollect.mint("https://icollect.money");    
    console.log("mint returned: ", mint_return);
    
    console.log("owner:", await icollect.owner());
    console.log("owner:", await icollect.ownerOf(0)); 
    console.log("symbol:", await icollect.symbol());
    console.log("URI:", await icollect.tokenURI(0));
    console.log("token counter:", await icollect.tokenCounter());    
  }
  
  main()
    .then(() => process.exit(0))
    .catch(error => {
      console.error(error);
      process.exit(1);
    });

Execute the JavaScript ( c:\mycode\learningsolidity\npx hardhat run –network localhost ./scripts/index.js):

Step 4: Deploy The Contract To An Ethereum Test Network

We will deploy to the Ropsten test network. Here is a great tutorial.

In Metamask, connect to the Ropsten test network

Get some test Ethereum to use during development

Here are 3 places to pick up some:

Use Remix to connect to Ropsten (Use Environment: Injected Web3)

Deploy your contract (requires Gas – note what it costs to the right in Metamask)

Interact with the contract by calling some of its functions to test if it works:

Get the address of the contract from REMIX: (we will need this for the JavaScript test below). It will look like this: 0x97E0175415cB7D758cFB0ffc27Be727360664B90

Install web3 (npm install -g web3) – Here is a great tutorial

Get API keys from Infura.io:

Build out your JavaScript for the dApp (great tutorial here)

Here is what we will build as a simple test — a webapp that can:

  1. Get contract information
  2. Get Account information
  3. Mint a token
  4. Transfer ownership of the token to another account

Here is the supporting code for the web app. I won’t get into the details of the code (there are way too many videos/articles out there that do a great job on this such as the series from Dapp University found here) but I want to point a few things out to look for. The first is getting the ‘abi’. You can easily get this from REMIX:

The second thing to note below is the transaction object and how it is built and used. There is a very good stackoverflow answer that explains it in detail found here. One part of the transaction object is called ‘nonce’ and it is an important concept to understand–you can read more about it here.

<!doctype html>

<html>
  <head>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.min.css" />
    <title>Ethereum Tests</title>    
  </head>

  <body>
    <div class="container">
        <div class="block">
            <div class="block-content block-content-full text-center bg-black-75 pt-3">
                <div class="input-group mb-3">
                    <div class="input-group-prepend">
                      <span class="input-group-text" id="basic-contract_address">Contract Address</span>
                    </div>
                    <input type="text" id="contract_address" class="form-control"              
                             value="0x97E0175415cB7D758cFB0ffc27Be727360664B90" aria-label="contract_address" 
                                aria-describedby="basic-contract_address">
                    <button id="getcontractinfobutton" type="button" class="btn btn-success ">Get Contract Info</button>
                  </div>                
            </div>            
            <div class="pt-2"></div>
            <div id="showDIV1" style="display: none;">
                <div id="contract_name" class="alert alert-primary" role="alert"></div>
                <div id="contract_owner" class="alert alert-primary" role="alert"></div>
                <div id="contract_symbol" class="alert alert-primary" role="alert"></div>
                <div id="contract_tokenCounter" class="alert alert-primary" role="alert"></div>
            </div>
        </div>
        <div class="block">
            <div class="block-content block-content-full text-center bg-black-75 pt-3">
                <div class="input-group mb-3">
                    <div class="input-group-prepend">
                      <span class="input-group-text" id="basic-account_address">Account 1 Address</span>
                    </div>
                    <input type="text" id="account_address" class="form-control" 
                               value="0x1957d5124A3dC2b44c0AEbDaF8E8D9F89FF68276" aria-label="account_address" 
                                    aria-describedby="basic-account_address">
                    <button id="getaccountinfobutton" type="button" class="btn btn-success ">Get Account 1 Info</button>
                </div>                
            </div>
            <div class="pt-2"></div>
            <div id="showDIV2"  style="display: none;">
                <div id="account_balance" class="alert alert-primary" role="alert"></div>
                <div id="account_net" class="alert alert-primary" role="alert"></div>
                <div id="account_networktype" class="alert alert-primary" role="alert"></div>
                <div id="account_blocknumber" class="alert alert-primary" role="alert"></div>
                <div id="account_balanceOf" class="alert alert-primary" role="alert"></div>
                <div class="input-group mb-3">
                    <div class="input-group-prepend">
                        <span class="input-group-text" id="basic-transfer_to_account_address">Transfer To Address</span>
                    </div>
                    <input type="text" id="transfer_to_account_address" class="form-control" 
                              value="0xfC241D33a6845Bbee50EcF7674f23218Ac407B8C" aria-label="transfer_to_account_address" 
                                 aria-describedby="basic-transfer_to_account_address">
                    <div class="input-group-prepend">
                        <span class="input-group-text" id="basic-tokenID">TokenID</span>
                    </div>
                    <input type="text" id="tokenID" class="" value="0" aria-label="tokenID" aria-describedby="basic-tokenID">
                    <button id="transferNFTbutton" type="button" class="btn btn-success ">Transfer NFT</button>
                </div>
                <div id="transfer_return" class="alert alert-primary" role="alert"></div>
            </div>
        </div>

        <div class="block">
            <div class="block-content block-content-full text-center bg-black-75 pt-3">
                <div class="input-group mb-3">
                    <div class="input-group-prepend">
                      <span class="input-group-text" id="basic-account2_address">account 2 Address</span>
                    </div>
                    <input type="text" id="account2_address" class="form-control"
                         value="0xfC241D33a6845Bbee50EcF7674f23218Ac407B8C" aria-label="account2_address" 
                            aria-describedby="basic-account2_address">
                    <button id="getaccount2infobutton" type="button" class="btn btn-success ">Get account2 Info</button>
                </div>                
            </div>
            <div class="pt-2"></div>
            <div id="showDIV4"  style="display: none;">
                <div id="account2_balance" class="alert alert-primary" role="alert"></div>
                <div id="account2_net" class="alert alert-primary" role="alert"></div>
                <div id="account2_networktype" class="alert alert-primary" role="alert"></div>
                <div id="account2_blocknumber" class="alert alert-primary" role="alert"></div>
                <div id="account2_balanceOf" class="alert alert-primary" role="alert"></div>                
            </div>
        </div>

        <div class="block">
            <div class="block-content block-content-full text-center bg-black-75 pt-3">
                <div class="input-group mb-3">
                    <div class="input-group-prepend">
                      <span class="input-group-text" id="basic-nft_minting">NFT URI</span>
                    </div>
                    <input type="text" id="nft_uri" class="form-control" value="https://icollect.money" 
                           aria-label="nft_url" aria-describedby="basic-nft_minting">
                    <button id="mintbutton" type="button" class="btn btn-success ">Mint new NFT</button>  
                </div>                
            </div>
            <div class="pt-2"></div>
            <div id="showDIV3"  style="display: none;">
                <div id="mint_return" class="alert alert-primary" role="alert"></div>            
            </div>
        </div>
    </div>
  </body>

https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js
https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js
https://cdn.jsdelivr.net/gh/ethereumjs/browser-builds/dist/ethereumjs-tx/ethereumjs-tx-1.3.3.min.js

<script>    
    //npm install ethereumjs-tx  from https://github.com/ethereumjs/ethereumjs-tx
    //npm install web3 from https://github.com/ChainSafe/web3.js
       
    let privateKey = "******GET YOUR OWN******";

    const provider = new Web3.providers.HttpProvider("https://ropsten.infura.io/v3/******GET YOUR OWN******");
    const web3 = new Web3(provider);

    web3.eth.net.isListening()
        .then(() => $('#message').html("Web3 is connected"))
        .catch(e => $('#message').html("Wow. Something went wrong"));

    var abi = [
        {
            "inputs": [],
            "stateMutability": "nonpayable",
            "type": "constructor"
        },
        {
            "anonymous": false,
            "inputs": [
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "_owner",
                    "type": "address"
                },
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "_approved",
                    "type": "address"
                },
                {
                    "indexed": true,
                    "internalType": "uint256",
                    "name": "_tokenId",
                    "type": "uint256"
                }
            ],
            "name": "Approval",
            "type": "event"
        },
        {
            "anonymous": false,
            "inputs": [
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "_owner",
                    "type": "address"
                },
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "_operator",
                    "type": "address"
                },
                {
                    "indexed": false,
                    "internalType": "bool",
                    "name": "_approved",
                    "type": "bool"
                }
            ],
            "name": "ApprovalForAll",
            "type": "event"
        },
        {
            "inputs": [
                {
                    "internalType": "address",
                    "name": "_approved",
                    "type": "address"
                },
                {
                    "internalType": "uint256",
                    "name": "_tokenId",
                    "type": "uint256"
                }
            ],
            "name": "approve",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        },
        {
            "inputs": [
                {
                    "internalType": "string",
                    "name": "_uri",
                    "type": "string"
                }
            ],
            "name": "mint",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        },
        {
            "anonymous": false,
            "inputs": [
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "previousOwner",
                    "type": "address"
                },
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "newOwner",
                    "type": "address"
                }
            ],
            "name": "OwnershipTransferred",
            "type": "event"
        },
        {
            "inputs": [
                {
                    "internalType": "address",
                    "name": "_from",
                    "type": "address"
                },
                {
                    "internalType": "address",
                    "name": "_to",
                    "type": "address"
                },
                {
                    "internalType": "uint256",
                    "name": "_tokenId",
                    "type": "uint256"
                }
            ],
            "name": "safeTransferFrom",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        },
        {
            "inputs": [
                {
                    "internalType": "address",
                    "name": "_from",
                    "type": "address"
                },
                {
                    "internalType": "address",
                    "name": "_to",
                    "type": "address"
                },
                {
                    "internalType": "uint256",
                    "name": "_tokenId",
                    "type": "uint256"
                },
                {
                    "internalType": "bytes",
                    "name": "_data",
                    "type": "bytes"
                }
            ],
            "name": "safeTransferFrom",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        },
        {
            "inputs": [
                {
                    "internalType": "address",
                    "name": "_operator",
                    "type": "address"
                },
                {
                    "internalType": "bool",
                    "name": "_approved",
                    "type": "bool"
                }
            ],
            "name": "setApprovalForAll",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        },
        {
            "anonymous": false,
            "inputs": [
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "_from",
                    "type": "address"
                },
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "_to",
                    "type": "address"
                },
                {
                    "indexed": true,
                    "internalType": "uint256",
                    "name": "_tokenId",
                    "type": "uint256"
                }
            ],
            "name": "Transfer",
            "type": "event"
        },
        {
            "inputs": [
                {
                    "internalType": "address",
                    "name": "_from",
                    "type": "address"
                },
                {
                    "internalType": "address",
                    "name": "_to",
                    "type": "address"
                },
                {
                    "internalType": "uint256",
                    "name": "_tokenId",
                    "type": "uint256"
                }
            ],
            "name": "transferFrom",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        },
        {
            "inputs": [
                {
                    "internalType": "address",
                    "name": "_newOwner",
                    "type": "address"
                }
            ],
            "name": "transferOwnership",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        },
        {
            "inputs": [
                {
                    "internalType": "address",
                    "name": "_owner",
                    "type": "address"
                }
            ],
            "name": "balanceOf",
            "outputs": [
                {
                    "internalType": "uint256",
                    "name": "",
                    "type": "uint256"
                }
            ],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [],
            "name": "CANNOT_TRANSFER_TO_ZERO_ADDRESS",
            "outputs": [
                {
                    "internalType": "string",
                    "name": "",
                    "type": "string"
                }
            ],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [
                {
                    "internalType": "uint256",
                    "name": "_tokenId",
                    "type": "uint256"
                }
            ],
            "name": "getApproved",
            "outputs": [
                {
                    "internalType": "address",
                    "name": "",
                    "type": "address"
                }
            ],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [
                {
                    "internalType": "address",
                    "name": "_owner",
                    "type": "address"
                },
                {
                    "internalType": "address",
                    "name": "_operator",
                    "type": "address"
                }
            ],
            "name": "isApprovedForAll",
            "outputs": [
                {
                    "internalType": "bool",
                    "name": "",
                    "type": "bool"
                }
            ],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [],
            "name": "name",
            "outputs": [
                {
                    "internalType": "string",
                    "name": "_name",
                    "type": "string"
                }
            ],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [],
            "name": "NOT_CURRENT_OWNER",
            "outputs": [
                {
                    "internalType": "string",
                    "name": "",
                    "type": "string"
                }
            ],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [],
            "name": "owner",
            "outputs": [
                {
                    "internalType": "address",
                    "name": "",
                    "type": "address"
                }
            ],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [
                {
                    "internalType": "uint256",
                    "name": "_tokenId",
                    "type": "uint256"
                }
            ],
            "name": "ownerOf",
            "outputs": [
                {
                    "internalType": "address",
                    "name": "_owner",
                    "type": "address"
                }
            ],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [
                {
                    "internalType": "bytes4",
                    "name": "_interfaceID",
                    "type": "bytes4"
                }
            ],
            "name": "supportsInterface",
            "outputs": [
                {
                    "internalType": "bool",
                    "name": "",
                    "type": "bool"
                }
            ],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [],
            "name": "symbol",
            "outputs": [
                {
                    "internalType": "string",
                    "name": "_symbol",
                    "type": "string"
                }
            ],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [],
            "name": "tokenCounter",
            "outputs": [
                {
                    "internalType": "uint256",
                    "name": "",
                    "type": "uint256"
                }
            ],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [
                {
                    "internalType": "uint256",
                    "name": "_tokenId",
                    "type": "uint256"
                }
            ],
            "name": "tokenURI",
            "outputs": [
                {
                    "internalType": "string",
                    "name": "",
                    "type": "string"
                }
            ],
            "stateMutability": "view",
            "type": "function"
        }
    ];


    document.getElementById('getcontractinfobutton').onclick = async function() {
        let contract_address = document.getElementById("contract_address").value;                                    
        const contract = await new web3.eth.Contract(abi, contract_address);        
        console.log(contract);
        await contract.methods.name().call((err, result) => { $('#contract_name').html("Contract Name: " + result) });
        await contract.methods.owner().call((err, result) => { $('#contract_owner').html("Contract Owner: " + result) });
        await contract.methods.symbol().call((err, result) => { $('#contract_symbol').html("Contract Symbol: " + result) });
        await contract.methods.tokenCounter().call((err, result) => { $('#contract_tokenCounter').html("Contract Token Counter: " + result) });
        var x = document.getElementById("showDIV1");        
        x.style.display = "block";
    };

    document.getElementById('getaccountinfobutton').onclick = async function() {
        let account_address = document.getElementById("account_address").value;                                    
        
        await web3.eth.getBalance(account_address , function(err, result){                
            $('#account_balance').html("Eth Account Balance: " + result);
        });
        await web3.eth.net.getId(function(err, result){
            $('#account_net').html("Net: " + result);                    
        });
        await web3.eth.net.getNetworkType(function(err, result){
            $('#account_networktype').html("Network Type: " + result);                    
        });  
        await web3.eth.getBlockNumber(function(error, result){
            $('#account_blocknumber').html("Block Number: " + result);                
        });
        let contract_address = document.getElementById("contract_address").value;                                    
        const contract = await new web3.eth.Contract(abi, contract_address);
        await contract.methods.balanceOf(account_address).call((err, result) => { $('#account_balanceOf').html("NFT Account Balance: " + result) });        
        
        var x = document.getElementById("showDIV2");        
        x.style.display = "block";
    };

    document.getElementById('getaccount2infobutton').onclick = async function() {
        let account_address = document.getElementById("account2_address").value;                                    
        
        await web3.eth.getBalance(account_address , function(err, result){                
            $('#account2_balance').html("Eth Account Balance: " + result);
        });
        await web3.eth.net.getId(function(err, result){
            $('#account2_net').html("Net: " + result);                    
        });
        await web3.eth.net.getNetworkType(function(err, result){
            $('#account2_networktype').html("Network Type: " + result);                    
        });  
        await web3.eth.getBlockNumber(function(error, result){
            $('#account2_blocknumber').html("Block Number: " + result);                
        });
        let contract_address = document.getElementById("contract_address").value;                                    
        const contract = await new web3.eth.Contract(abi, contract_address);
        await contract.methods.balanceOf(account_address).call((err, result) => { $('#account2_balanceOf').html("NFT Account Balance: " + result) });        
        
        var x = document.getElementById("showDIV4");        
        x.style.display = "block";
    };

    document.getElementById('mintbutton').onclick = async function() {
        let NFT_URI = document.getElementById("nft_uri").value;                                            
        let contract_address = document.getElementById("contract_address").value;                                            
        const contract = await new web3.eth.Contract(abi, contract_address);  
        let account_address = document.getElementById("account_address").value;  
        const account = web3.eth.accounts.privateKeyToAccount(privateKey).address;        
        const transaction = contract.methods.mint(NFT_URI);
        let nonce_count = await web3.eth.getTransactionCount(account_address);        

        //build the transaction            
        const txObject = {
            nonce: nonce_count, 
            to: contract_address,
            gasPrice: web3.utils.toHex(web3.utils.toWei('10','gwei')),            
            gas: await transaction.estimateGas({from: account_address}),
            data: transaction.encodeABI()
        };

        const signed  = await web3.eth.accounts.signTransaction(txObject, privateKey);        
        const return_from_send = await web3.eth.sendSignedTransaction(signed.rawTransaction);
                        
        return_string = 
            "blockHash: " + return_from_send.blockHash + "<br>" +
            "blockNumber: <a href='https://ropsten.etherscan.io/block/"  + return_from_send.blockNumber + "'>" + return_from_send.blockNumber + "</a><br>" +
            "contractAddress: " + return_from_send.contractAddress + "<br>" +
            "cumulativeGasUsed: " + return_from_send.cumulativeGasUsed + "<br>" +            
            "from: <a href='https://ropsten.etherscan.io/address/" + return_from_send.from  + "'>" + return_from_send.from + "</a><br>" +
            "gasUsed: " + return_from_send.gasUsed + "<br>" +
            "status: " + return_from_send.status + "<br>" +
            "to:  <a href='https://ropsten.etherscan.io/address/" + return_from_send.to + "'>" + return_from_send.to + "</a><br>" +
            "transactionHash: <a href='https://ropsten.etherscan.io/tx/" + return_from_send.transactionHash + "'>" + return_from_send.transactionHash + "</a><br>" +
            "transactionIndex: " + return_from_send.transactionIndex + "<br>" +
            "type: " + return_from_send.type + "<br>"; 

        $('#mint_return').html(return_string);            
        
        var x = document.getElementById("showDIV3");        
        x.style.display = "block";

    }
    
    document.getElementById('transferNFTbutton').onclick = async function() {        
        let contract_address = document.getElementById("contract_address").value;                                            
        const contract = await new web3.eth.Contract(abi, contract_address);  
        let account_address = document.getElementById("account_address").value;  
        const account = web3.eth.accounts.privateKeyToAccount(privateKey).address;        
        let transfer_to_account_address = document.getElementById("transfer_to_account_address").value;  
        let tokenID = document.getElementById("tokenID").value;  

        const transaction = contract.methods.transferFrom(account_address, transfer_to_account_address, tokenID);
        let nonce_count = await web3.eth.getTransactionCount(account_address);        

        //build the transaction            
        const txObject = {
            nonce: nonce_count, 
            to: contract_address,     
            gasPrice: await web3.utils.toHex(web3.utils.toWei('10','gwei')),            
            gas: await transaction.estimateGas({from: account_address}),
            data: await transaction.encodeABI()
        };
        
        const signed  = await web3.eth.accounts.signTransaction(txObject, privateKey);        
        
        const return_from_send = await web3.eth.sendSignedTransaction(signed.rawTransaction)
            .on('error', e => {              
                $('#transfer_return').html('Unable to send signed transaction! ' + e); 
            });
                        
        return_string = 
            "blockHash: " + return_from_send.blockHash + "<br>" +
            "blockNumber: <a href='https://ropsten.etherscan.io/block/"  + return_from_send.blockNumber + "'>" + return_from_send.blockNumber + "</a><br>" +
            "contractAddress: " + return_from_send.contractAddress + "<br>" +
            "cumulativeGasUsed: " + return_from_send.cumulativeGasUsed + "<br>" +            
            "from: <a href='https://ropsten.etherscan.io/address/" + return_from_send.from  + "'>" + return_from_send.from + "</a><br>" +
            "gasUsed: " + return_from_send.gasUsed + "<br>" +
            "status: " + return_from_send.status + "<br>" +
            "to:  <a href='https://ropsten.etherscan.io/address/" + return_from_send.to + "'>" + return_from_send.to + "</a><br>" +
            "transactionHash: <a href='https://ropsten.etherscan.io/tx/" + return_from_send.transactionHash + "'>" + return_from_send.transactionHash + "</a><br>" +
            "transactionIndex: " + return_from_send.transactionIndex + "<br>" +
            "type: " + return_from_send.type + "<br>"; 

        $('#transfer_return').html(return_string);            
        
    }

</script>
</html>

Once your code is live and you have uploaded your contract and minted your first NFT verify your work using EtherScan. The etherscan.io block explorer can be used to view internal state and EVM execution logs.

You can use the links created by the test web app when you ‘mint’ a new token. Here is an example of the contract I uploaded to Ropsten:

You can also decompile the bytecode to see the contract. Here is an example.

You can also see the transactions of a user. Here is the transactions from the owner of the contract:

Here is an example of the ‘minting’ of a new token:

Here is a token

You can also get a great deal of good analytics from the Infura.io dashboard you setup earlier

Step 5: Deploying to Mainnet – coming.. will update as I learn more…

Read (this) OpenZeppelin article

Step 6: Verifying Your NFT With A Public Marketplace – coming.. will update as I learn more

Step 7: Upgrading Your Smart Contracts – coming… will update as I learn more

Read (this and this) OpenZeppelin article

Read (this)

Leave a Reply
Your email address will not be published. Required fields are marked *

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: