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. This
example retains its existing bytes32 ABI. New contracts using Solidity
address can select the SDK compiler's target: "pqvm" option for Shell-native
address semantics. 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+
- The public Shell Chain 0.27.10 native release running locally
- 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 pins the published [email protected] prerelease,
which signs V2 transactions for Shell Chain 0.27.10. npm ci installs this exact
SDK without a sibling checkout or a local dependency override. SDK 0.13.0 signs
V1 and fails signature verification against this node.
The CLI deploy/mint/read flow was checked with these public packages on Node.js 24 and macOS arm64, using a local full RocksDB node and an ML-DSA-65 keystore. Full owner identity, mint events, rejected mints and reads after restarting the node were checked. Browser verification covers type checking and the production build. This does not establish compatibility with the public testnet's node version or activate a protocol upgrade there.
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 \
--db rocksdb \
--rpc-api eth,net,web3,shell
The --db rocksdb setting keeps deployments and tokens in shell-data across
node restarts. Dev mode otherwise selects an ephemeral in-memory database.
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 |