WAVS NFT DEMO
Template for getting started with developing building dynamic NFTs with WAVS. NOT PRODUCTION READY.
This example demonstrates a simple Dynamic NFT + Minter contract that can even communicate with each other cross-chain.
There are two contracts WavsNft.sol
and WavsMinter.sol
, as well as two components autonomous-artist
and simple-relay
.
Mint flow:
- User pays
WavsMinter.sol
contract which emits anWavsNftTrigger
event, user gets a receipt for their purchase, which after a certain timeout period they can use to get a refund if the AVS fails to run. - WAVS listens for event and triggers the registered WASI component
autonomous_artist.wasm
autonomous-artist
component runs, generates description and image, adds different attributes based on EVM queries, uploads NFT metadata to IPFS, and outputs an NFT tokenURI- WAVS operators sign output with their keys and send results to aggregator, aggregator agregates signatures and puts results on chain
handleSignedData
is called on theWavsNft.sol
contract, it mints an NFT with the tokenURI and emits anWavsNftMint
event.- WAVS listens for event and triggers the registered WASI component
simple_relayer.wasm
simple-relay
component runs an outputs the TriggerId that has been completed- Operators sign output, Aggregator aggregates signatures and submits them on chain
handleSignedData
is called on theWavsMinter.sol
contract, it deletes the Receipt
Update flow:
- Owner of an NFT calls
triggerUpdate
onWavsNft.sol
and emits aWavsNftTrigger
with the update type. - WAVS listens for event and triggers the registered WASI component
autonomous_artist.wasm
autonomous-artist
component runs, generates new description and image, adds different attributes based on EVM queries, uploads NFT metadata to IPFS, and outputs a new NFT tokenURI- WAVS operators sign output with their keys and send results to aggregator, aggregator agregates signatures and puts results on chain
handleSignedData
is called on theWavsNft.sol
contract, it updates the NFT with the tokenURI.
Core (Docker, Compose, Make, JQ, Node v21+)
- MacOS:
brew install --cask docker
- Linux:
sudo apt -y install docker.io
- Windows WSL: docker desktop wsl &
sudo chmod 666 /var/run/docker.sock
- Docker Documentation
- MacOS: Already installed with Docker installer
- Linux + Windows WSL:
sudo apt-get install docker-compose-v2
- Compose Documentation
- MacOS:
brew install make
- Linux + Windows WSL:
sudo apt -y install make
- Make Documentation
- MacOS:
brew install jq
- Linux + Windows WSL:
sudo apt -y install jq
- JQ Documentation
- Required Version: v21+
- Installation via NVM
Rust v1.84+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup toolchain install stable
rustup target add wasm32-wasip2
# Remove old targets if present
rustup target remove wasm32-wasi || true
rustup target remove wasm32-wasip1 || true
# Update and add required target
rustup update stable
rustup target add wasm32-wasip2
Cargo Components
# Install required cargo components
# https://github.com/bytecodealliance/cargo-component#installation
cargo install cargo-binstall
cargo binstall cargo-component warg-cli wkg --locked --no-confirm --force
# Configure default registry
wkg config --default-registry wa.dev
Install Ollama and Stable Diffusion
### Install OllamaThis example use an LLM configured for determinism, run locally with Ollama. The model is llama3.1, but other open source models can be used if you change the config in components/automous-artist/src
.
For more information about AVSs and deterministic AI, see our blog post on the subject.
You can download Ollama here: https://ollama.com/
Get the llama 3.1 model.
ollama pull llama3.1
In a separate terminal run Ollama in the background with:
ollama serve
In a separate terminal, run stable diffusion locally.
git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui
cd stable-diffusion-webui
./webui.sh --api
For testing, you can alternately set WAVS_ENV_SD_API_URL
and WAVS_ENV_SD_API_KEY
with a stable diffusion API.
In a production AVS environment, you would need to ship an bundles that bundles WAVS, Ollama, and Stable Diffusion together into a new docker image. More information on support for WAVS sidecars will be forthcoming in a future release. For deterministic output, every AVS operator MUST use the same GPU.
IPFS: Lighthouse API keys
This example currently uses [Lighthouse](https://lighthouse.storage/) to store NFT metadata.You can get a free API key by signing up, simply set it in your .env
file.
WAVS_ENV_IPFS_API_URL="https://node.lighthouse.storage/api/v0/add"
WAVS_ENV_LIGHTHOUSE_API_KEY="your-lighthouse-api-key"
Install the required packages to build the Solidity contracts. This project supports both submodules and npm packages.
# Install packages (npm & forge submodules)
make setup
# Build the contracts
forge build
# Run the solidity tests
forge test
Now build the WASI rust components into the compiled
output directory.
Warning
If you get: error: no registry configured for namespace "wavs"
run, wkg config --default-registry wa.dev
make wasi-build # or `make build` to include solidity compilation.
Note: under the hood this uses cargo component build --release
for each component in the components
directory and moves them to the compiled
directory. See Makefile
for more details.
Test run the component locally to validate the business logic works. Nothing will be saved on-chain, just the output of the component is shown.
PROMPT="How to become a great artist?" make wasi-exec
Note
If you are running on a Mac with an ARM chip, you will need to do the following:
- Set up Rosetta:
softwareupdate --install-rosetta
- Enable Rosetta (Docker Desktop: Settings -> General -> enable "Use Rosetta for x86_46/amd64 emulation on Apple Silicon")
Configure one of the following networking:
- Docker Desktop: Settings -> Resources -> Network -> 'Enable Host Networking'
brew install chipmk/tap/docker-mac-net-connect && sudo brew services start chipmk/tap/docker-mac-net-connect
Start an Ethereum node (anvil), the WAVS service, and deploy EigenLayer contracts to the local network.
cp .env.example .env
# Start the backend
#
# This must remain running in your terminal. Use another terminal to run other commands.
# You can stop the services with `ctrl+c`. Some MacOS terminals require pressing it twice.
make start-all
In a separate terminal, start Ollama:
ollama serve
Upload your service's trigger and submission contracts. The trigger contract is where WAVS will watch for events, and the submission contract is where the AVS service operator will submit the result on chain.
export SERVICE_MANAGER_ADDR=`jq -r '.eigen_service_managers.local | .[-1]' .docker/deployments.json`
forge script ./script/Deploy.s.sol:Deploy ${SERVICE_MANAGER_ADDR} --sig "run(string)" --rpc-url http://localhost:8545 --broadcast
Tip
You can see the deployed NFT address with jq -r '.nft' "./.docker/script_deploy.json"
,
the deployed minter address with jq -r '.minter' "./.docker/script_deploy.json"
,
and the deployed submission address with jq -r '.service_handler' "./.docker/script_deploy.json"
Deploy the compiled component with the contracts from the previous steps. Review the makefile for more details.
TRIGGER_EVENT
is the event signature that the trigger contract emits and WAVS watches for. By altering SERVICE_TRIGGER_ADDR
you can watch events for even contracts others have deployed.
The SERVICE_SUBMISSION_ADDR
is the contract to which results from the AVS are submitted and implements the IWavsServiceHandler
interface which is simply function handleSignedData(bytes calldata data, bytes calldata signature) external
.
Let's set these based on our recently run deployment script, and deploy the component.
# Get deployed service trigger and submission contract addresses
export WAVS_MINTER=`jq -r '.minter' "./.docker/script_deploy.json"`
export WAVS_NFT=`jq -r '.nft' "./.docker/script_deploy.json"`
# Deploy autonmous artist component for the minting flow. Triggered here by the WavsMinter.sol contract
COMPONENT_FILENAME=autonomous_artist.wasm \
TRIGGER_EVENT="WavsNftTrigger(address,string,uint64,uint8,uint256)" \
SERVICE_TRIGGER_ADDR=$WAVS_MINTER \
SERVICE_SUBMISSION_ADDR=$WAVS_NFT \
SERVICE_CONFIG='{"fuel_limit":100000000,"max_gas":5000000,"host_envs":["WAVS_ENV_IPFS_API_URL","WAVS_ENV_LIGHTHOUSE_API_KEY"],"kv":[["nft_contract","'$WAVS_NFT'"]],"workflow_id":"default","component_id":"default"}' \
make deploy-service
# Deploy simple relayer component, triggered by successful minting from the WavsNft.sol contract
COMPONENT_FILENAME=simple_relay.wasm TRIGGER_EVENT="WavsNftMint(address,uint256,string,uint64)" SERVICE_TRIGGER_ADDR=$WAVS_NFT SERVICE_SUBMISSION_ADDR=$WAVS_MINTER make deploy-service
# Deploy autonmous artist component for the update flow. Triggered here by the WavsNft.sol contract
COMPONENT_FILENAME=autonomous_artist.wasm \
TRIGGER_EVENT="WavsNftTrigger(address,string,uint64,uint8,uint256)" \
SERVICE_TRIGGER_ADDR=$WAVS_NFT \
SERVICE_SUBMISSION_ADDR=$WAVS_NFT \
SERVICE_CONFIG='{"fuel_limit":100000000,"max_gas":5000000,"host_envs":["WAVS_ENV_IPFS_API_URL","WAVS_ENV_LIGHTHOUSE_API_KEY"],"kv":[["nft_contract","'$WAVS_NFT'"]],"workflow_id":"default","component_id":"default"}' \
make deploy-service
To see all options for deploying services, run make wavs-cli -- deploy-service -h
and consider customizing deploy service
in the Makefile
.
# Run the mint script with the minter address and prompt
forge script ./script/Mint.s.sol:Trigger $WAVS_MINTER "How do I become a great Artist?" \
--sig "run(address,string)" --rpc-url http://localhost:8545 --broadcast
After triggering the service, you can check the status of both the NFT and the mint receipts using the Show script:
# Run the show script with the NFT and minter addresses
forge script ./script/Show.s.sol:ShowResults $WAVS_NFT $WAVS_MINTER \
--sig "run(address,address)" --rpc-url http://localhost:8545
This will display information about:
- The last minted NFT (including its tokenURI and how to decode it)
- All mint receipts in the system (including whether they're fulfilled)
You can also check and decode the NFT's token URI directly in one line:
cast call $WAVS_NFT "tokenURI(uint256)(string)" 0 | grep -o 'base64,[^"]*' | cut -d',' -f2 | base64 -d | jq
You can update an existing NFT using the Update script. This will trigger the autonomous artist component to generate new content based on your prompt.
# Run the update script with the NFT address, token ID, and new prompt
forge script ./script/Update.s.sol:Update $WAVS_NFT 0 "How do I become a great Engineer?" \
--sig "run(address,uint256,string)" --rpc-url http://localhost:8545 --broadcast
This will:
- Send the update fee (currently 0.01 ETH) to the contract
- Trigger the update process
- Emit the
WavsNftTrigger
event with the tokenId parameter - The WAVS service will pick up the event and process the update
- The NFT's tokenURI will be updated with the new AI-generated content
- Use the queries above to see updated NFT
tokenURI