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. Owners use Solidity bytes32 so all 32 bytes of
a Shell address survive calldata decoding, storage, reads and events. Standard
Solidity address is 20 bytes; encoding a full Shell identity in the SDK cannot
widen that compiled type. The example targets Cancun instructions supported by
the node.
Redeploy the example after this owner ABI change. Previously deployed contracts
using address owners are incompatible with the new mint and event signatures.
The contract exposes:
mint(bytes32 to, string uri)ownerOf(uint256 tokenId) returns (bytes32)tokenURI(uint256 tokenId) returns (string)totalSupply() returns (uint256)TransferShell(bytes32 from, bytes32 to, uint256 tokenId)
Prerequisites
- Node.js 20+
- A local Shell Chain node
- A funded Shell keystore from the quickstart
shell-nft-dappchecked out locally
Clone the example app:
git clone https://github.com/ShellDAO/shell-nft-dapp.git
cd shell-nft-dapp
The example lockfile installs published [email protected]; standalone
installation and compilation do not require a sibling SDK repository.
Transaction compatibility: SDK 0.13.0 signs V1 transactions, while current node source expects V2. A compatible public SDK release is still required for the standalone deploy/mint flow against that node. A successful compile or browser build does not establish transaction compatibility.
For source development, keep a compatible built SDK in a sibling checkout.
After npm ci, override only the installed dependency with
npm install --no-save --package-lock=false ../shell-sdk. Record both source
commits when running the smoke flow; this override does not validate the
published-package path.
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:
- Compiles
contracts/ShellNft.solthroughshell-sdk/contracts/compiler. - Detects chain ID, balance, and pending nonce.
- Deploys
ShellNftthroughdeployContract. - Mints token
1to the signer address throughwriteContract. - Reads
totalSupply(),ownerOf(1), andtokenURI(1)throughreadContract. - Checks the mint event's full owner identity, rejects empty-URI and zero-owner mints, and verifies that supply, owner and URI remain unchanged.
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:
- Keep RPC URL as
http://127.0.0.1:8545. - Keep chain ID as
1337. - Paste the keystore JSON from
my-key.json. - Enter the keystore password.
- Paste
bytecodefromartifacts/ShellNft.compiled.json. - Click Deploy.
- Click Mint to Signer.
- 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 |