Skip to content

Commit

Permalink
nft: Add program example
Browse files Browse the repository at this point in the history
Signed-off-by: Dan Sover <dan.sover@avalabs.org>
  • Loading branch information
exdx committed Nov 2, 2023
1 parent f735d94 commit 33263ed
Show file tree
Hide file tree
Showing 9 changed files with 464 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,7 @@ osxcross/
target/
Cargo.lock
**/*.rs.bk

# simulator
simulator
!simulator/
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"x/programs/rust/wasmlanche_sdk",
"x/programs/rust/examples/token",
"x/programs/rust/examples/counter",
"x/programs/rust/examples/nft",
]
resolver = "2"

Expand Down
Binary file not shown.
20 changes: 20 additions & 0 deletions x/programs/rust/examples/nft/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "nft"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
wasmlanche_sdk = { version = "0.1.0", path = "../../wasmlanche_sdk", features = ["simulator"]}
serde = "1.0.188"

[dev-dependencies]
serde_json = "1.0.68"

[lib]
crate-type = ["cdylib"] # set the crate(needed for cargo build to work properly)

[[integration]]
name = "nft"
path = "tests/nft.rs"
18 changes: 18 additions & 0 deletions x/programs/rust/examples/nft/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# nft

The `nft` HyperSDK Program is an example of an NFT (non-fungible token).

For more information on NFTs, see the [resources at nft
school](https://nftschool.dev/concepts/non-fungible-tokens/#a-bit-of-history).

## Usage

The program exposes `mint` and `burn` methods which are publicly accessible.
Building a `Plan` with `Steps` to invoke these methods is the standard way to
interact with `nft`. See [the integration test](./tests/nft.rs) for a simple
invocation of `nft` via the WASM runtime.

## Testing

Use the [simulator](../../wasmlanche_sdk/src/simulator.rs) provided to run a
custom program. Detailed documentation on the simulator will soon be available.
32 changes: 32 additions & 0 deletions x/programs/rust/examples/nft/scripts/build_and_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash

set -euo pipefail

if ! [[ "$0" =~ scripts/build_and_test.sh ]]; then
echo "must be run from crate root"
exit 255
fi

root="$(pwd)"

simulator_path="${PWD}"/../../../cmd/simulator
simulator_bin="${simulator_path}"/bin/simulator

echo "Downloading dependencies..."
cd "${simulator_path}"
go mod download

echo "Building Simulator..."
go build -o "${simulator_bin}" "${simulator_path}"/simulator.go

# Set environment variables for the test

# The path to the simulator binary
export SIMULATOR_PATH="${simulator_bin}"

# The path to the compiled Wasm program to be tested
export PROGRAM_PATH="${root}"/../examples/testdata/nft.wasm

echo "Running Simulator Tests..."

cargo test -p nft -- --include-ignored
22 changes: 22 additions & 0 deletions x/programs/rust/examples/nft/scripts/copy_wasm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash

set -euo pipefail

if ! [[ "$0" =~ scripts/copy_wasm.sh ]]; then
echo "must be run from crate root"
exit 255
fi

root="$(pwd)"

# Build the program
build_script_path="${root}"/../../scripts/build.sh
sh "${build_script_path}" out

# Copy wasm file over
cp out/nft.wasm "${root}"/../examples/testdata/nft.wasm

# Delete build artifacts
rm -rf "${root}"/out

echo 'Successfully copied nft.wasm to examples/testdata'
164 changes: 164 additions & 0 deletions x/programs/rust/examples/nft/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//! A basic NFT contract.
//! The program serves as a non-fungible token with the ability to mint and burn.
//! Only supports whole units with no decimal places.
//!
//! The NFT must support the common NFT metadata format.
//! This includes the name, symbol, and URI of the NFT.
use wasmlanche_sdk::{
memory::{Memory, Pointer},
program::Program,
public, state_keys,
types::Address,
};

/// The program storage keys.
#[state_keys]
enum StateKey {
/// The total supply of the token. Key prefix 0x0.
MaxSupply,
/// The name of the token. Key prefix 0x1.
Name,
/// The symbol of the token. Key prefix 0x2.
Symbol,
/// Metadata of the token. Key prefix 0x3.
Uri,
/// Balances of the NFT token by address. Key prefix 0x4(address).
Balances(Address),
/// Counter -- used to keep track of total NFTs minted. Key prefix 0x5.
Counter,
}

/// Initializes the NFT with all required metadata.
/// This includes the name, symbol, image URI, owner, and total supply.
/// Returns true if the initialization was successful.
#[public]
// TODO: update the macro to enable String arguments
#[allow(clippy::too_many_arguments)]
pub fn init(
program: Program,
nft_name_ptr: i64,
nft_name_length: i64,
nft_symbol_ptr: i64,
nft_symbol_length: i64,
nft_uri_ptr: i64,
nft_uri_length: i64,
nft_max_supply: i64,
) -> bool {
let name_ptr = Memory::new(Pointer::from(nft_name_ptr));
let nft_name = unsafe { name_ptr.range(nft_name_length as usize) };

let nft_symbol_ptr = Memory::new(Pointer::from(nft_symbol_ptr));
let nft_symbol = unsafe { nft_symbol_ptr.range(nft_symbol_length as usize) };

let nft_uri_ptr = Memory::new(Pointer::from(nft_uri_ptr));
let nft_uri = unsafe { nft_uri_ptr.range(nft_uri_length as usize) };

let counter = program
.state()
.get::<i64, _>(StateKey::Counter.to_vec())
.unwrap_or_default();

assert_eq!(counter, 0, "init already called");

// Set token name
program
.state()
.store(StateKey::Name.to_vec(), &nft_name)
.expect("failed to store nft name");

// Set token symbol
program
.state()
.store(StateKey::Symbol.to_vec(), &nft_symbol)
.expect("failed to store nft symbol");

// Set token URI
program
.state()
.store(StateKey::Uri.to_vec(), &nft_uri)
.expect("failed to store nft uri");

// Set total supply
program
.state()
.store(StateKey::MaxSupply.to_vec(), &nft_max_supply)
.expect("failed to store total supply");

// Initialize counter
program
.state()
.store(StateKey::Counter.to_vec(), &0_i64)
.expect("failed to store counter");

true
}

/// Mints NFT tokens and sends them to the recipient.
#[public]
pub fn mint(program: Program, recipient: Address, amount: i64) -> bool {
let counter = program
.state()
.get::<i64, _>(StateKey::Counter.to_vec())
.unwrap_or_default();

let max_supply = program
.state()
.get::<i64, _>(StateKey::MaxSupply.to_vec())
.unwrap_or_default();

assert!(
counter + amount <= max_supply,
"max supply for nft exceeded"
);

let balance = program
.state()
.get::<i64, _>(StateKey::Balances(recipient).to_vec())
.unwrap_or_default();

// TODO: check for overflow
program
.state()
.store(StateKey::Balances(recipient).to_vec(), &(balance + amount))
.expect("failed to store balance");

// TODO: check for overflow
program
.state()
.store(StateKey::Counter.to_vec(), &(counter + amount))
.is_ok()
}

#[public]
pub fn burn(program: Program, from: Address, amount: i64) -> bool {
let balance = program
.state()
.get::<i64, _>(StateKey::Balances(from).to_vec())
.unwrap_or_default();

assert!(
balance >= amount,
"balance must be greater than or equal to amount burned"
);

let counter = program
.state()
.get::<i64, _>(StateKey::Counter.to_vec())
.unwrap_or_default();

assert!(counter >= amount, "cannot burn more nfts");

// TODO: check for underflow
program
.state()
.store(StateKey::Balances(from).to_vec(), &(balance - amount))
.is_ok()
}

#[public]
fn balance(program: Program, owner: Address) -> i64 {
program
.state()
.get::<i64, _>(StateKey::Balances(owner).to_vec())
.unwrap_or_default()
}
Loading

0 comments on commit 33263ed

Please sign in to comment.