Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[x/programs] Add NFT program example #511

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is simulator a dev-dependency?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature is tracked in rust-lang/cargo#7916. It's in nightly but not sure if it's in stable.

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,
exdx marked this conversation as resolved.
Show resolved Hide resolved
}

/// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything below this line in this function is a perfect example of what I meant by "batch" ops.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My impression was batch ops are something that the underlying SDK would add support for behind the scenes? I am not aware of changes that should be made to the program to support batching.

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()
}
exdx marked this conversation as resolved.
Show resolved Hide resolved

#[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
Copy link
Contributor

@patrick-ogrady patrick-ogrady Oct 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Counter is never updated but you check against it when burning? Is it supposed to be the total count ever created or the current supply?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Counter is the total count of outstanding NFTs. It goes up but does not go down. Basically it's there to ensure that the max supply is never exceeded. Burning an NFT does not decrement the counter because the NFT itself is gone at that point and should not be able to be re-minted.

.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
Loading