-
Notifications
You must be signed in to change notification settings - Fork 116
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -68,3 +68,7 @@ osxcross/ | |
target/ | ||
Cargo.lock | ||
**/*.rs.bk | ||
|
||
# simulator | ||
simulator | ||
!simulator/ |
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" |
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. |
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 |
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' |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is
simulator
adev-dependency
?There was a problem hiding this comment.
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.