Build a Shell NFT DApp

Create, deploy, mint, and read a Shell-native NFT contract with a reproducible local workflow.

See also: Quickstart Guide · Smart Contract Guide · JSON-RPC API Reference


What you will build

This tutorial uses shell-nft-dapp, a small Vite + React + TypeScript project with Node scripts for the same deploy/mint/read flow. The project uses shell-sdk/contracts for deployment, writes, reads, and receipt polling, and uses shell-sdk/contracts/compiler for Node-only Solidity compilation.

The contract is intentionally Shell-native NFT, not a marketplace-focused ERC-721 compatibility claim. The source still uses Solidity's address keyword for owners; Shell Chain's compiler/runtime and the Shell SDK contract helpers treat those address values as Shell-native 32-byte addresses.

The contract exposes:

  • mint(address to, string uri)
  • ownerOf(uint256 tokenId) returns (address)
  • tokenURI(uint256 tokenId) returns (string)
  • totalSupply() returns (uint256)
  • TransferShell(address from, address to, uint256 tokenId)

Prerequisites

  • Node.js 20+
  • A local Shell Chain node
  • A funded Shell keystore from the quickstart
  • shell-nft-dapp checked out locally

Clone the example app:

git clone https://github.com/ShellDAO/shell-nft-dapp.git
cd shell-nft-dapp

If you are developing the SDK and the example app together, keep the two repositories as sibling checkouts so the app can point at the current local SDK package. Standalone users should use [email protected] or newer.

Example sibling layout:

local-checkouts/
  shell-sdk/
  shell-nft-dapp/

1. Start a local Shell node

Follow the Quickstart Guide through key generation and genesis initialization. Start the node with CORS enabled for the browser DApp:

shell-node run \
  --datadir shell-data \
  --keystore my-key.json \
  --password-file .quickstart-password \
  --rpc-addr 127.0.0.1:8545 \
  --rpc-cors "http://127.0.0.1:5173,http://localhost:5173" \
  --block-time 2000 \
  --max-idle-interval 0 \
  --network dev \
  --rpc-api eth,net,web3,shell

Keep this terminal running.


2. Install the DApp

Open a second terminal:

cd shell-nft-dapp
npm ci
cp .env.example .env

Edit .env:

SHELL_RPC_URL=http://127.0.0.1:8545
SHELL_CHAIN_ID=1337
SHELL_KEYSTORE_PATH=../shell-chain/my-key.json
SHELL_KEYSTORE_PASSWORD=dev-password
NFT_BASE_URI=ipfs://example/
SHELL_NFT_CONTRACT=

Use the actual keystore path and password from the quickstart.

Leave SHELL_NFT_CONTRACT empty for the first deploy. After npm run deploy, the scripts save the deployed contract address in deployments/local.json; mint and read use that file automatically. Set SHELL_NFT_CONTRACT only when you want to target an existing deployed contract directly.


3. Compile the contract

npm run compile

Expected output:

compiled .../artifacts/ShellNft.compiled.json
contract=ShellNft bytecode=... bytes

The generated artifact is local build output and is not committed.


4. Run the complete smoke flow

npm run smoke

The smoke script:

  1. Compiles contracts/ShellNft.sol through shell-sdk/contracts/compiler.
  2. Detects chain ID, balance, and pending nonce.
  3. Deploys ShellNft through deployContract.
  4. Mints token 1 to the signer address through writeContract.
  5. Reads totalSupply(), ownerOf(1), and tokenURI(1) through readContract.

Expected output ends with:

smoke ok
contract: 0x...
mint tx: 0x...
owner: 0x...
tokenURI: ipfs://example/shell-nft-1.json

owner is a 32-byte Shell address.


5. Run each step manually

Deploy:

npm run deploy

Mint:

npm run mint -- ipfs://example/shell-nft-1.json

Read token 1:

npm run read -- 1

mint and read validate the configured or saved contract address before sending RPC calls, so malformed 20-byte or truncated addresses fail before a transaction is submitted.


6. Use the browser DApp

Start Vite:

npm run dev

Open http://127.0.0.1:5173.

In the page:

  1. Keep RPC URL as http://127.0.0.1:8545.
  2. Keep chain ID as 1337.
  3. Paste the keystore JSON from my-key.json.
  4. Enter the keystore password.
  5. Paste bytecode from artifacts/ShellNft.compiled.json.
  6. Click Deploy.
  7. Click Mint to Signer.
  8. Click Read.

The page displays totalSupply, the owner address, and the token URI.

The browser path uses shell-sdk/contracts runtime helpers. It does not import the compiler subpath; compilation stays in the Node CLI because solc is not a browser dependency.


Optional: testnet RPC

Set:

SHELL_RPC_URL=https://testnet-rpc.shell.org
SHELL_CHAIN_ID=10
SHELL_KEYSTORE_PATH=/path/to/funded-testnet-keystore.json
SHELL_KEYSTORE_PASSWORD=...

Testnet execution depends on external RPC availability and a funded account. Keep the local node path as the required reproducible tutorial path.


Troubleshooting

Symptom Fix
Browser fetch blocked by CORS Restart shell-node with --rpc-cors "http://127.0.0.1:5173,http://localhost:5173"
SHELL_CHAIN_ID mismatch Query eth_chainId or set the chain ID for the selected network
deployer has no balance Use the funded quickstart keystore or fund the account before deploying
SHELL_NFT_CONTRACT is required Run npm run deploy first, or set SHELL_NFT_CONTRACT to an existing 32-byte Shell contract address
0x + 64 hex chars error Use a Shell canonical 32-byte address, not a 20-byte Ethereum address
Transaction timeout Confirm the node is producing blocks and RPC eth_getTransactionReceipt is reachable