From 4849202489ca7c9ea6fdbbeaaa1f51b9ff07f550 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:54:40 -0400 Subject: [PATCH 01/31] Update dev_dependencies -> dev-dependencies in examples --- crates/loam-cli/examples/soroban/calculator/Cargo.toml | 2 +- crates/loam-cli/examples/soroban/core/Cargo.toml | 2 +- crates/loam-cli/examples/soroban/ft/Cargo.toml | 2 +- crates/loam-cli/examples/soroban/status_message/Cargo.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/loam-cli/examples/soroban/calculator/Cargo.toml b/crates/loam-cli/examples/soroban/calculator/Cargo.toml index 99d423c1..39d144c4 100644 --- a/crates/loam-cli/examples/soroban/calculator/Cargo.toml +++ b/crates/loam-cli/examples/soroban/calculator/Cargo.toml @@ -15,5 +15,5 @@ loam-sdk = { workspace = true, features = ["loam-soroban-sdk"] } loam-subcontract-core = { workspace = true } -[dev_dependencies] +[dev-dependencies] loam-sdk = { workspace = true, features = ["soroban-sdk-testutils"] } diff --git a/crates/loam-cli/examples/soroban/core/Cargo.toml b/crates/loam-cli/examples/soroban/core/Cargo.toml index ab5fe825..54a816ad 100644 --- a/crates/loam-cli/examples/soroban/core/Cargo.toml +++ b/crates/loam-cli/examples/soroban/core/Cargo.toml @@ -17,7 +17,7 @@ loam-subcontract-core = { workspace = true } loam-soroban-sdk = { workspace = true } -[dev_dependencies] +[dev-dependencies] loam-sdk = { workspace = true, features = ["soroban-sdk-testutils"] } [package.metadata.loam] diff --git a/crates/loam-cli/examples/soroban/ft/Cargo.toml b/crates/loam-cli/examples/soroban/ft/Cargo.toml index b64983d9..fa4e4914 100644 --- a/crates/loam-cli/examples/soroban/ft/Cargo.toml +++ b/crates/loam-cli/examples/soroban/ft/Cargo.toml @@ -16,7 +16,7 @@ loam-subcontract-core = { workspace = true } loam-subcontract-ft = { workspace = true } -[dev_dependencies] +[dev-dependencies] loam-sdk = { workspace = true, features = ["soroban-sdk-testutils"] } [package.metadata.loam] diff --git a/crates/loam-cli/examples/soroban/status_message/Cargo.toml b/crates/loam-cli/examples/soroban/status_message/Cargo.toml index b92f38eb..cf6fb990 100644 --- a/crates/loam-cli/examples/soroban/status_message/Cargo.toml +++ b/crates/loam-cli/examples/soroban/status_message/Cargo.toml @@ -14,5 +14,5 @@ doctest = false loam-sdk = { workspace = true, features = ["loam-soroban-sdk"] } loam-subcontract-core = { workspace = true } -[dev_dependencies] +[dev-dependencies] loam-sdk = { workspace = true, features = ["soroban-sdk-testutils"] } From 8e6d9c91671fa8fac97b66899b059c0b46ea84f4 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Wed, 26 Jun 2024 18:02:22 -0400 Subject: [PATCH 02/31] Add NFT example --- examples/soroban/nft/Cargo.toml | 23 ++++++ examples/soroban/nft/src/lib.rs | 25 +++++++ examples/soroban/nft/src/nft.rs | 93 +++++++++++++++++++++++++ examples/soroban/nft/src/subcontract.rs | 24 +++++++ 4 files changed, 165 insertions(+) create mode 100644 examples/soroban/nft/Cargo.toml create mode 100644 examples/soroban/nft/src/lib.rs create mode 100644 examples/soroban/nft/src/nft.rs create mode 100644 examples/soroban/nft/src/subcontract.rs diff --git a/examples/soroban/nft/Cargo.toml b/examples/soroban/nft/Cargo.toml new file mode 100644 index 00000000..a3b43177 --- /dev/null +++ b/examples/soroban/nft/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "example-nft" +version = "0.0.0" +authors = ["Stellar Development Foundation "] +license = "Apache-2.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +loam-sdk = { workspace = true, features = ["loam-soroban-sdk"] } +loam-subcontract-core = { workspace = true } +loam-soroban-sdk = { workspace = true } + + +[dev-dependencies] +loam-sdk = { workspace = true, features = ["soroban-sdk-testutils"] } + +[package.metadata.loam] +contract = true diff --git a/examples/soroban/nft/src/lib.rs b/examples/soroban/nft/src/lib.rs new file mode 100644 index 00000000..b9f993e1 --- /dev/null +++ b/examples/soroban/nft/src/lib.rs @@ -0,0 +1,25 @@ +#![no_std] +use loam_sdk::soroban_contract; +use loam_subcontract_core::{admin::Admin, Core}; + +pub mod nft; +pub mod subcontract; + +use nft::MyNonFungibleToken; +use subcontract::{Initable, NonFungible}; + +pub struct Contract; + +impl Core for Contract { + type Impl = Admin; +} + +impl NonFungible for Contract { + type Impl = MyNonFungibleToken; +} + +impl Initable for Contract { + type Impl = MyNonFungibleToken; +} + +soroban_contract!(); diff --git a/examples/soroban/nft/src/nft.rs b/examples/soroban/nft/src/nft.rs new file mode 100644 index 00000000..2fd82405 --- /dev/null +++ b/examples/soroban/nft/src/nft.rs @@ -0,0 +1,93 @@ +use loam_sdk::{ + soroban_sdk::{self, contracttype, env, Address, Bytes, Lazy, Map}, + IntoKey, +}; +use loam_subcontract_core::Core; + +use crate::{ + subcontract::{IsInitable, IsNonFungible}, + Contract, +}; + +#[contracttype] +#[derive(IntoKey)] +pub struct MyNonFungibleToken { + admin: Address, + name: Bytes, + owners_to_nft_ids: Map, + nft_ids_to_owners: Map, + nft_ids_to_metadata: Map, +} + +impl MyNonFungibleToken { + pub fn new(admin: Address, name: Bytes) -> Self { + MyNonFungibleToken { + admin, + name, + owners_to_nft_ids: Map::new(env()), + nft_ids_to_owners: Map::new(env()), + nft_ids_to_metadata: Map::new(env()), + } + } +} + +impl Default for MyNonFungibleToken { + fn default() -> Self { + MyNonFungibleToken::new(env().current_contract_address(), Bytes::new(env())) + } +} + +impl IsInitable for MyNonFungibleToken { + fn nft_init(&mut self, admin: Address, name: Bytes) { + Contract::admin_get().unwrap().require_auth(); + MyNonFungibleToken::set_lazy(MyNonFungibleToken::new(admin, name)); + } +} + +impl IsNonFungible for MyNonFungibleToken { + // Mint a new NFT with the given ID, owner, and metadata + fn mint( + &mut self, + id: Bytes, //todo: change this to a u32? + owner: Address, + metadata: Bytes, + ) { + owner.require_auth(); + + // if the nft id is not already in the contract's storage we can add it + // todo: handle this more gracefully + if let Some(_metadata) = self.nft_ids_to_metadata.get(id.clone()) { + panic!("NFT with this ID already exists"); + } + + self.nft_ids_to_metadata.set(id.clone(), metadata); + self.nft_ids_to_owners.set(id.clone(), owner.clone()); + self.owners_to_nft_ids.set(owner, id); + } + + // Transfer the NFT from the current owner to the new owner + fn transfer(&mut self, id: Bytes, current_owner: Address, new_owner: Address) { + if let Some(owner_id) = self.nft_ids_to_owners.get(id.clone()) { + if owner_id != current_owner { + panic!("You are not the owner of this NFT"); + } + // remove the current owner + self.nft_ids_to_owners.remove(id.clone()); + self.owners_to_nft_ids.remove(current_owner); + + // add the new owner + self.nft_ids_to_owners.set(id.clone(), new_owner.clone()); + self.owners_to_nft_ids.set(new_owner, id); + } + } + + // Get the NFT from the contract's storage by id + fn get_nft(&self, id: Bytes) -> Option { + self.nft_ids_to_metadata.get(id) + } + + // Get the NFT from the contract's storage by owner id + fn get_owner(&self, id: Bytes) -> Option
{ + self.nft_ids_to_owners.get(id) + } +} diff --git a/examples/soroban/nft/src/subcontract.rs b/examples/soroban/nft/src/subcontract.rs new file mode 100644 index 00000000..d4f5a553 --- /dev/null +++ b/examples/soroban/nft/src/subcontract.rs @@ -0,0 +1,24 @@ +use loam_sdk::{ + soroban_sdk::{self, Address, Bytes, Lazy}, + subcontract, +}; + +#[subcontract] +pub trait IsNonFungible { + // Mint a new NFT with the given ID, owner, and metadata + fn mint(&mut self, id: Bytes, owner: Address, metadata: Bytes); + + // Transfer an NFT with the given ID from current_owner to new_owner + fn transfer(&mut self, id: Bytes, current_owner: Address, new_owner: Address); + + // Get the NFT with the given ID + fn get_nft(&self, id: Bytes) -> Option; + + // Find the owner of the NFT with the given ID + fn get_owner(&self, id: Bytes) -> Option
; +} + +#[subcontract] +pub trait IsInitable { + fn nft_init(&mut self, admin: Address, name: Bytes); +} From 9cf14ea5a9480d10aba680ee901b8871de530acc Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:07:57 -0400 Subject: [PATCH 03/31] Fix build errors --- examples/soroban/nft/src/lib.rs | 1 + examples/soroban/nft/src/subcontract.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/soroban/nft/src/lib.rs b/examples/soroban/nft/src/lib.rs index b9f993e1..ab38d917 100644 --- a/examples/soroban/nft/src/lib.rs +++ b/examples/soroban/nft/src/lib.rs @@ -1,6 +1,7 @@ #![no_std] use loam_sdk::soroban_contract; use loam_subcontract_core::{admin::Admin, Core}; +use loam_soroban_sdk::{Address, Bytes}; pub mod nft; pub mod subcontract; diff --git a/examples/soroban/nft/src/subcontract.rs b/examples/soroban/nft/src/subcontract.rs index d4f5a553..3f729ede 100644 --- a/examples/soroban/nft/src/subcontract.rs +++ b/examples/soroban/nft/src/subcontract.rs @@ -1,5 +1,5 @@ use loam_sdk::{ - soroban_sdk::{self, Address, Bytes, Lazy}, + soroban_sdk::{Address, Bytes, Lazy}, subcontract, }; From e1404c308b6a890ea3c5e9862e12398fd3ef40ea Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Thu, 27 Jun 2024 15:51:00 -0400 Subject: [PATCH 04/31] Change NFT ids to u32s --- examples/soroban/nft/src/nft.rs | 19 +++++++------------ examples/soroban/nft/src/subcontract.rs | 8 ++++---- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/examples/soroban/nft/src/nft.rs b/examples/soroban/nft/src/nft.rs index 2fd82405..314da5c1 100644 --- a/examples/soroban/nft/src/nft.rs +++ b/examples/soroban/nft/src/nft.rs @@ -14,9 +14,9 @@ use crate::{ pub struct MyNonFungibleToken { admin: Address, name: Bytes, - owners_to_nft_ids: Map, - nft_ids_to_owners: Map, - nft_ids_to_metadata: Map, + owners_to_nft_ids: Map, + nft_ids_to_owners: Map, + nft_ids_to_metadata: Map, } impl MyNonFungibleToken { @@ -46,12 +46,7 @@ impl IsInitable for MyNonFungibleToken { impl IsNonFungible for MyNonFungibleToken { // Mint a new NFT with the given ID, owner, and metadata - fn mint( - &mut self, - id: Bytes, //todo: change this to a u32? - owner: Address, - metadata: Bytes, - ) { + fn mint(&mut self, id: u32, owner: Address, metadata: Bytes) { owner.require_auth(); // if the nft id is not already in the contract's storage we can add it @@ -66,7 +61,7 @@ impl IsNonFungible for MyNonFungibleToken { } // Transfer the NFT from the current owner to the new owner - fn transfer(&mut self, id: Bytes, current_owner: Address, new_owner: Address) { + fn transfer(&mut self, id: u32, current_owner: Address, new_owner: Address) { if let Some(owner_id) = self.nft_ids_to_owners.get(id.clone()) { if owner_id != current_owner { panic!("You are not the owner of this NFT"); @@ -82,12 +77,12 @@ impl IsNonFungible for MyNonFungibleToken { } // Get the NFT from the contract's storage by id - fn get_nft(&self, id: Bytes) -> Option { + fn get_nft(&self, id: u32) -> Option { self.nft_ids_to_metadata.get(id) } // Get the NFT from the contract's storage by owner id - fn get_owner(&self, id: Bytes) -> Option
{ + fn get_owner(&self, id: u32) -> Option
{ self.nft_ids_to_owners.get(id) } } diff --git a/examples/soroban/nft/src/subcontract.rs b/examples/soroban/nft/src/subcontract.rs index 3f729ede..535c2e48 100644 --- a/examples/soroban/nft/src/subcontract.rs +++ b/examples/soroban/nft/src/subcontract.rs @@ -6,16 +6,16 @@ use loam_sdk::{ #[subcontract] pub trait IsNonFungible { // Mint a new NFT with the given ID, owner, and metadata - fn mint(&mut self, id: Bytes, owner: Address, metadata: Bytes); + fn mint(&mut self, id: u32, owner: Address, metadata: Bytes); // Transfer an NFT with the given ID from current_owner to new_owner - fn transfer(&mut self, id: Bytes, current_owner: Address, new_owner: Address); + fn transfer(&mut self, id: u32, current_owner: Address, new_owner: Address); // Get the NFT with the given ID - fn get_nft(&self, id: Bytes) -> Option; + fn get_nft(&self, id: u32) -> Option; // Find the owner of the NFT with the given ID - fn get_owner(&self, id: Bytes) -> Option
; + fn get_owner(&self, id: u32) -> Option
; } #[subcontract] From 4f311321483fe6600955adefd1557125ef75966f Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Thu, 27 Jun 2024 18:06:52 -0400 Subject: [PATCH 05/31] Refactor so that the nft id is generated in mint --- examples/soroban/nft/src/nft.rs | 27 ++++++++++++++++--------- examples/soroban/nft/src/subcontract.rs | 5 ++++- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/examples/soroban/nft/src/nft.rs b/examples/soroban/nft/src/nft.rs index 314da5c1..cf5c9bc2 100644 --- a/examples/soroban/nft/src/nft.rs +++ b/examples/soroban/nft/src/nft.rs @@ -14,6 +14,7 @@ use crate::{ pub struct MyNonFungibleToken { admin: Address, name: Bytes, + total_count: u32, owners_to_nft_ids: Map, nft_ids_to_owners: Map, nft_ids_to_metadata: Map, @@ -24,6 +25,7 @@ impl MyNonFungibleToken { MyNonFungibleToken { admin, name, + total_count: 0, owners_to_nft_ids: Map::new(env()), nft_ids_to_owners: Map::new(env()), nft_ids_to_metadata: Map::new(env()), @@ -45,19 +47,20 @@ impl IsInitable for MyNonFungibleToken { } impl IsNonFungible for MyNonFungibleToken { - // Mint a new NFT with the given ID, owner, and metadata - fn mint(&mut self, id: u32, owner: Address, metadata: Bytes) { + // Mint a new NFT with the given owner address and metadata, returning the id + fn mint(&mut self, owner: Address, metadata: Bytes) -> u32 { owner.require_auth(); - // if the nft id is not already in the contract's storage we can add it - // todo: handle this more gracefully - if let Some(_metadata) = self.nft_ids_to_metadata.get(id.clone()) { - panic!("NFT with this ID already exists"); - } + let current_count = self.total_count; + let new_id = current_count + 1; + + //todo: check that the metadata is unique + self.nft_ids_to_metadata.set(new_id.clone(), metadata); + self.nft_ids_to_owners.set(new_id.clone(), owner.clone()); + self.owners_to_nft_ids.set(owner, new_id); + self.total_count = new_id; - self.nft_ids_to_metadata.set(id.clone(), metadata); - self.nft_ids_to_owners.set(id.clone(), owner.clone()); - self.owners_to_nft_ids.set(owner, id); + new_id } // Transfer the NFT from the current owner to the new owner @@ -85,4 +88,8 @@ impl IsNonFungible for MyNonFungibleToken { fn get_owner(&self, id: u32) -> Option
{ self.nft_ids_to_owners.get(id) } + + fn get_total_count(&self) -> u32 { + self.total_count + } } diff --git a/examples/soroban/nft/src/subcontract.rs b/examples/soroban/nft/src/subcontract.rs index 535c2e48..acf9e937 100644 --- a/examples/soroban/nft/src/subcontract.rs +++ b/examples/soroban/nft/src/subcontract.rs @@ -6,7 +6,7 @@ use loam_sdk::{ #[subcontract] pub trait IsNonFungible { // Mint a new NFT with the given ID, owner, and metadata - fn mint(&mut self, id: u32, owner: Address, metadata: Bytes); + fn mint(&mut self, owner: Address, metadata: Bytes) -> u32; // Transfer an NFT with the given ID from current_owner to new_owner fn transfer(&mut self, id: u32, current_owner: Address, new_owner: Address); @@ -16,6 +16,9 @@ pub trait IsNonFungible { // Find the owner of the NFT with the given ID fn get_owner(&self, id: u32) -> Option
; + + // Get the total count of NFTs + fn get_total_count(&self) -> u32; } #[subcontract] From e647a55fc6679c807635c42db64e5513a293d9b7 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:22:18 -0400 Subject: [PATCH 06/31] Formatting & clippy --- examples/soroban/nft/src/lib.rs | 2 +- examples/soroban/nft/src/nft.rs | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/examples/soroban/nft/src/lib.rs b/examples/soroban/nft/src/lib.rs index ab38d917..01f50abc 100644 --- a/examples/soroban/nft/src/lib.rs +++ b/examples/soroban/nft/src/lib.rs @@ -1,7 +1,7 @@ #![no_std] use loam_sdk::soroban_contract; -use loam_subcontract_core::{admin::Admin, Core}; use loam_soroban_sdk::{Address, Bytes}; +use loam_subcontract_core::{admin::Admin, Core}; pub mod nft; pub mod subcontract; diff --git a/examples/soroban/nft/src/nft.rs b/examples/soroban/nft/src/nft.rs index cf5c9bc2..fbceea7e 100644 --- a/examples/soroban/nft/src/nft.rs +++ b/examples/soroban/nft/src/nft.rs @@ -21,6 +21,7 @@ pub struct MyNonFungibleToken { } impl MyNonFungibleToken { + #[must_use] pub fn new(admin: Address, name: Bytes) -> Self { MyNonFungibleToken { admin, @@ -55,8 +56,8 @@ impl IsNonFungible for MyNonFungibleToken { let new_id = current_count + 1; //todo: check that the metadata is unique - self.nft_ids_to_metadata.set(new_id.clone(), metadata); - self.nft_ids_to_owners.set(new_id.clone(), owner.clone()); + self.nft_ids_to_metadata.set(new_id, metadata); + self.nft_ids_to_owners.set(new_id, owner.clone()); self.owners_to_nft_ids.set(owner, new_id); self.total_count = new_id; @@ -65,16 +66,17 @@ impl IsNonFungible for MyNonFungibleToken { // Transfer the NFT from the current owner to the new owner fn transfer(&mut self, id: u32, current_owner: Address, new_owner: Address) { - if let Some(owner_id) = self.nft_ids_to_owners.get(id.clone()) { - if owner_id != current_owner { - panic!("You are not the owner of this NFT"); - } + if let Some(owner_id) = self.nft_ids_to_owners.get(id) { + assert!( + owner_id != current_owner, + "You are not the owner of this NFT" + ); // remove the current owner - self.nft_ids_to_owners.remove(id.clone()); + self.nft_ids_to_owners.remove(id); self.owners_to_nft_ids.remove(current_owner); // add the new owner - self.nft_ids_to_owners.set(id.clone(), new_owner.clone()); + self.nft_ids_to_owners.set(id, new_owner.clone()); self.owners_to_nft_ids.set(new_owner, id); } } From a1fb4ffb47eb2ba3fbac1d07da86c79755e747c0 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:57:16 -0400 Subject: [PATCH 07/31] Fully qualify soroban_sdk types in subcontract traits Apply suggestions from code review Co-authored-by: Willem Wyndham --- examples/soroban/nft/src/lib.rs | 1 - examples/soroban/nft/src/nft.rs | 2 ++ examples/soroban/nft/src/subcontract.rs | 28 +++++++++++++++++-------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/examples/soroban/nft/src/lib.rs b/examples/soroban/nft/src/lib.rs index 01f50abc..b9f993e1 100644 --- a/examples/soroban/nft/src/lib.rs +++ b/examples/soroban/nft/src/lib.rs @@ -1,6 +1,5 @@ #![no_std] use loam_sdk::soroban_contract; -use loam_soroban_sdk::{Address, Bytes}; use loam_subcontract_core::{admin::Admin, Core}; pub mod nft; diff --git a/examples/soroban/nft/src/nft.rs b/examples/soroban/nft/src/nft.rs index fbceea7e..81b8a3ff 100644 --- a/examples/soroban/nft/src/nft.rs +++ b/examples/soroban/nft/src/nft.rs @@ -23,6 +23,7 @@ pub struct MyNonFungibleToken { impl MyNonFungibleToken { #[must_use] pub fn new(admin: Address, name: Bytes) -> Self { + MyNonFungibleToken { admin, name, @@ -66,6 +67,7 @@ impl IsNonFungible for MyNonFungibleToken { // Transfer the NFT from the current owner to the new owner fn transfer(&mut self, id: u32, current_owner: Address, new_owner: Address) { + current_owner.require_auth(); if let Some(owner_id) = self.nft_ids_to_owners.get(id) { assert!( owner_id != current_owner, diff --git a/examples/soroban/nft/src/subcontract.rs b/examples/soroban/nft/src/subcontract.rs index acf9e937..e20a16fa 100644 --- a/examples/soroban/nft/src/subcontract.rs +++ b/examples/soroban/nft/src/subcontract.rs @@ -1,21 +1,27 @@ -use loam_sdk::{ - soroban_sdk::{Address, Bytes, Lazy}, - subcontract, -}; +use loam_sdk::{soroban_sdk::Lazy, subcontract}; #[subcontract] pub trait IsNonFungible { // Mint a new NFT with the given ID, owner, and metadata - fn mint(&mut self, owner: Address, metadata: Bytes) -> u32; + fn mint( + &mut self, + owner: loam_sdk::soroban_sdk::Address, + metadata: loam_sdk::soroban_sdk::Bytes, + ) -> u32; // Transfer an NFT with the given ID from current_owner to new_owner - fn transfer(&mut self, id: u32, current_owner: Address, new_owner: Address); + fn transfer( + &mut self, + id: u32, + current_owner: loam_sdk::soroban_sdk::Address, + new_owner: loam_sdk::soroban_sdk::Address, + ); // Get the NFT with the given ID - fn get_nft(&self, id: u32) -> Option; + fn get_nft(&self, id: u32) -> Option; // Find the owner of the NFT with the given ID - fn get_owner(&self, id: u32) -> Option
; + fn get_owner(&self, id: u32) -> Option; // Get the total count of NFTs fn get_total_count(&self) -> u32; @@ -23,5 +29,9 @@ pub trait IsNonFungible { #[subcontract] pub trait IsInitable { - fn nft_init(&mut self, admin: Address, name: Bytes); + fn nft_init( + &mut self, + admin: loam_sdk::soroban_sdk::Address, + name: loam_sdk::soroban_sdk::Bytes, + ); } From aabcdfa0275843a5a029a356aff4a5f726fdb835 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:27:50 -0400 Subject: [PATCH 08/31] Panic on transfer if the NFT id does not exist --- examples/soroban/nft/src/nft.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/soroban/nft/src/nft.rs b/examples/soroban/nft/src/nft.rs index 81b8a3ff..087f2025 100644 --- a/examples/soroban/nft/src/nft.rs +++ b/examples/soroban/nft/src/nft.rs @@ -68,6 +68,7 @@ impl IsNonFungible for MyNonFungibleToken { // Transfer the NFT from the current owner to the new owner fn transfer(&mut self, id: u32, current_owner: Address, new_owner: Address) { current_owner.require_auth(); + self.get_nft(id).expect("NFT does not exist"); if let Some(owner_id) = self.nft_ids_to_owners.get(id) { assert!( owner_id != current_owner, From 2c7fefde324db56c9decd4d161da8391d0b7b73b Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:42:37 -0400 Subject: [PATCH 09/31] Fix owner_id check --- examples/soroban/nft/src/nft.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/soroban/nft/src/nft.rs b/examples/soroban/nft/src/nft.rs index 087f2025..a8772a99 100644 --- a/examples/soroban/nft/src/nft.rs +++ b/examples/soroban/nft/src/nft.rs @@ -71,7 +71,7 @@ impl IsNonFungible for MyNonFungibleToken { self.get_nft(id).expect("NFT does not exist"); if let Some(owner_id) = self.nft_ids_to_owners.get(id) { assert!( - owner_id != current_owner, + owner_id == current_owner, "You are not the owner of this NFT" ); // remove the current owner From 1bad366a1d3d4b3b729b4b9315527b878d264560 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:02:27 -0400 Subject: [PATCH 10/31] Update examples/soroban/nft/src/nft.rs Co-authored-by: Willem Wyndham --- examples/soroban/nft/src/nft.rs | 2 +- examples/soroban/nft/src/subcontract.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/soroban/nft/src/nft.rs b/examples/soroban/nft/src/nft.rs index a8772a99..3d9d6902 100644 --- a/examples/soroban/nft/src/nft.rs +++ b/examples/soroban/nft/src/nft.rs @@ -42,7 +42,7 @@ impl Default for MyNonFungibleToken { } impl IsInitable for MyNonFungibleToken { - fn nft_init(&mut self, admin: Address, name: Bytes) { + fn nft_init(&self, admin: Address, name: Bytes) { Contract::admin_get().unwrap().require_auth(); MyNonFungibleToken::set_lazy(MyNonFungibleToken::new(admin, name)); } diff --git a/examples/soroban/nft/src/subcontract.rs b/examples/soroban/nft/src/subcontract.rs index e20a16fa..a850eed9 100644 --- a/examples/soroban/nft/src/subcontract.rs +++ b/examples/soroban/nft/src/subcontract.rs @@ -30,7 +30,7 @@ pub trait IsNonFungible { #[subcontract] pub trait IsInitable { fn nft_init( - &mut self, + &self, admin: loam_sdk::soroban_sdk::Address, name: loam_sdk::soroban_sdk::Bytes, ); From 432c91405f79427dd69db991c5c03c31b869c4e5 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:24:57 -0400 Subject: [PATCH 11/31] Cargo fmt updates --- examples/soroban/nft/src/nft.rs | 1 - examples/soroban/nft/src/subcontract.rs | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/examples/soroban/nft/src/nft.rs b/examples/soroban/nft/src/nft.rs index 3d9d6902..c4d93ad9 100644 --- a/examples/soroban/nft/src/nft.rs +++ b/examples/soroban/nft/src/nft.rs @@ -23,7 +23,6 @@ pub struct MyNonFungibleToken { impl MyNonFungibleToken { #[must_use] pub fn new(admin: Address, name: Bytes) -> Self { - MyNonFungibleToken { admin, name, diff --git a/examples/soroban/nft/src/subcontract.rs b/examples/soroban/nft/src/subcontract.rs index a850eed9..cc2371d0 100644 --- a/examples/soroban/nft/src/subcontract.rs +++ b/examples/soroban/nft/src/subcontract.rs @@ -29,9 +29,5 @@ pub trait IsNonFungible { #[subcontract] pub trait IsInitable { - fn nft_init( - &self, - admin: loam_sdk::soroban_sdk::Address, - name: loam_sdk::soroban_sdk::Bytes, - ); + fn nft_init(&self, admin: loam_sdk::soroban_sdk::Address, name: loam_sdk::soroban_sdk::Bytes); } From abe69e2a18e549ee8d6a5d67044df68bc9b66d00 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:27:45 -0400 Subject: [PATCH 12/31] Make ft_init fn accept &self instead of &mut self --- crates/loam-cli/examples/soroban/ft/src/ft.rs | 2 +- crates/loam-subcontract-ft/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/loam-cli/examples/soroban/ft/src/ft.rs b/crates/loam-cli/examples/soroban/ft/src/ft.rs index 0e614b88..93247ec6 100644 --- a/crates/loam-cli/examples/soroban/ft/src/ft.rs +++ b/crates/loam-cli/examples/soroban/ft/src/ft.rs @@ -48,7 +48,7 @@ impl Default for MyFungibleToken { } impl IsInitable for MyFungibleToken { - fn ft_init(&mut self, admin: Address, name: Bytes, symbol: Bytes, decimals: u32) { + fn ft_init(&self, admin: Address, name: Bytes, symbol: Bytes, decimals: u32) { Contract::admin_get().unwrap().require_auth(); MyFungibleToken::set_lazy(MyFungibleToken::new(admin, name, symbol, decimals)); } diff --git a/crates/loam-subcontract-ft/src/lib.rs b/crates/loam-subcontract-ft/src/lib.rs index 9ad57854..a4ca4cb8 100644 --- a/crates/loam-subcontract-ft/src/lib.rs +++ b/crates/loam-subcontract-ft/src/lib.rs @@ -90,7 +90,7 @@ pub trait IsFungible { pub trait IsInitable { /// Initialize ft Subcontract fn ft_init( - &mut self, + &self, admin: loam_sdk::soroban_sdk::Address, name: loam_sdk::soroban_sdk::Bytes, symbol: loam_sdk::soroban_sdk::Bytes, From 765fbf35dfa4082d5bfd8477e7c9ede75846b51e Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Wed, 3 Jul 2024 08:48:22 -0400 Subject: [PATCH 13/31] Update examples/soroban/nft/src/nft.rs Co-authored-by: Willem Wyndham --- examples/soroban/nft/src/nft.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/examples/soroban/nft/src/nft.rs b/examples/soroban/nft/src/nft.rs index c4d93ad9..9af4201f 100644 --- a/examples/soroban/nft/src/nft.rs +++ b/examples/soroban/nft/src/nft.rs @@ -67,20 +67,18 @@ impl IsNonFungible for MyNonFungibleToken { // Transfer the NFT from the current owner to the new owner fn transfer(&mut self, id: u32, current_owner: Address, new_owner: Address) { current_owner.require_auth(); - self.get_nft(id).expect("NFT does not exist"); - if let Some(owner_id) = self.nft_ids_to_owners.get(id) { - assert!( - owner_id == current_owner, - "You are not the owner of this NFT" - ); - // remove the current owner - self.nft_ids_to_owners.remove(id); - self.owners_to_nft_ids.remove(current_owner); + let owner_id = self.nft_ids_to_owners.get(id).expect("NFT does not exist"); + assert!( + owner_id == current_owner, + "You are not the owner of this NFT" + ); + // remove the current owner + self.nft_ids_to_owners.remove(id); + self.owners_to_nft_ids.remove(current_owner); - // add the new owner - self.nft_ids_to_owners.set(id, new_owner.clone()); - self.owners_to_nft_ids.set(new_owner, id); - } + // add the new owner + self.nft_ids_to_owners.set(id, new_owner.clone()); + self.owners_to_nft_ids.set(new_owner, id); } // Get the NFT from the contract's storage by id From 1aa5b11dc5599000486f8686ce7b2dba7b8353d6 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Wed, 3 Jul 2024 10:46:22 -0400 Subject: [PATCH 14/31] =?UTF-8?q?Fix=20owners=5Fto=5Fnft=5Fids=20behavior?= =?UTF-8?q?=20=F0=9F=99=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/soroban/nft/src/nft.rs | 56 +++++++++++++++++++++---- examples/soroban/nft/src/subcontract.rs | 6 +++ 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/examples/soroban/nft/src/nft.rs b/examples/soroban/nft/src/nft.rs index 9af4201f..973fd8ca 100644 --- a/examples/soroban/nft/src/nft.rs +++ b/examples/soroban/nft/src/nft.rs @@ -1,5 +1,5 @@ use loam_sdk::{ - soroban_sdk::{self, contracttype, env, Address, Bytes, Lazy, Map}, + soroban_sdk::{self, contracttype, env, Address, Bytes, Lazy, Map, Vec}, IntoKey, }; use loam_subcontract_core::Core; @@ -15,7 +15,7 @@ pub struct MyNonFungibleToken { admin: Address, name: Bytes, total_count: u32, - owners_to_nft_ids: Map, + owners_to_nft_ids: Map>, nft_ids_to_owners: Map, nft_ids_to_metadata: Map, } @@ -58,7 +58,14 @@ impl IsNonFungible for MyNonFungibleToken { //todo: check that the metadata is unique self.nft_ids_to_metadata.set(new_id, metadata); self.nft_ids_to_owners.set(new_id, owner.clone()); - self.owners_to_nft_ids.set(owner, new_id); + + let mut owner_collection = self + .owners_to_nft_ids + .get(owner.clone()) + .unwrap_or_else(|| Vec::new(env())); + owner_collection.push_back(new_id); + + self.owners_to_nft_ids.set(owner, owner_collection); self.total_count = new_id; new_id @@ -67,18 +74,47 @@ impl IsNonFungible for MyNonFungibleToken { // Transfer the NFT from the current owner to the new owner fn transfer(&mut self, id: u32, current_owner: Address, new_owner: Address) { current_owner.require_auth(); + // ensures that this Address has authorized invocation of the current contract + // during the on-chain execution the soroban host will perform the needed auth (verify the signatures) and ensure the replay prevention + + // what if current_owner is not the source account? + let owner_id = self.nft_ids_to_owners.get(id).expect("NFT does not exist"); assert!( owner_id == current_owner, "You are not the owner of this NFT" ); - // remove the current owner - self.nft_ids_to_owners.remove(id); - self.owners_to_nft_ids.remove(current_owner); - // add the new owner + // update the nft_ids_to_owners map with the new owner + self.nft_ids_to_owners.remove(id); self.nft_ids_to_owners.set(id, new_owner.clone()); - self.owners_to_nft_ids.set(new_owner, id); + + // remove the NFT id from the current owner's collection + let mut current_owner_collection = self + .owners_to_nft_ids + .get(current_owner.clone()) + .expect("Owner does not have a collection of NFTs"); + current_owner_collection.remove(id); + + if let Some(index) = current_owner_collection + .iter() + .position(|nft_id| nft_id == id) + { + current_owner_collection.remove(index.try_into().unwrap()); + } else { + panic!("NFT ID not found in owner's collection"); + } + + self.owners_to_nft_ids + .set(current_owner, current_owner_collection); + + // Add the NFT id to the new owner's collection + let mut new_owner_collection = self + .owners_to_nft_ids + .get(new_owner.clone()) + .unwrap_or_else(|| Vec::new(env())); + new_owner_collection.push_back(id); + self.owners_to_nft_ids.set(new_owner, new_owner_collection); } // Get the NFT from the contract's storage by id @@ -94,4 +130,8 @@ impl IsNonFungible for MyNonFungibleToken { fn get_total_count(&self) -> u32 { self.total_count } + + fn get_collection_by_owner(&self, owner: Address) -> Option> { + self.owners_to_nft_ids.get(owner) + } } diff --git a/examples/soroban/nft/src/subcontract.rs b/examples/soroban/nft/src/subcontract.rs index cc2371d0..9113bd28 100644 --- a/examples/soroban/nft/src/subcontract.rs +++ b/examples/soroban/nft/src/subcontract.rs @@ -25,6 +25,12 @@ pub trait IsNonFungible { // Get the total count of NFTs fn get_total_count(&self) -> u32; + + // Get all of the NFTs owned by the given address + fn get_collection_by_owner( + &self, + owner: loam_sdk::soroban_sdk::Address, + ) -> Option>; } #[subcontract] From b46b4c58df1b9f07bfd7b1e327fd5e4a76588a0e Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Mon, 15 Jul 2024 10:53:50 -0400 Subject: [PATCH 15/31] Move nft example to loam-cli dir --- {examples => crates/loam-cli/examples}/soroban/nft/Cargo.toml | 0 {examples => crates/loam-cli/examples}/soroban/nft/src/lib.rs | 0 {examples => crates/loam-cli/examples}/soroban/nft/src/nft.rs | 0 .../loam-cli/examples}/soroban/nft/src/subcontract.rs | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {examples => crates/loam-cli/examples}/soroban/nft/Cargo.toml (100%) rename {examples => crates/loam-cli/examples}/soroban/nft/src/lib.rs (100%) rename {examples => crates/loam-cli/examples}/soroban/nft/src/nft.rs (100%) rename {examples => crates/loam-cli/examples}/soroban/nft/src/subcontract.rs (100%) diff --git a/examples/soroban/nft/Cargo.toml b/crates/loam-cli/examples/soroban/nft/Cargo.toml similarity index 100% rename from examples/soroban/nft/Cargo.toml rename to crates/loam-cli/examples/soroban/nft/Cargo.toml diff --git a/examples/soroban/nft/src/lib.rs b/crates/loam-cli/examples/soroban/nft/src/lib.rs similarity index 100% rename from examples/soroban/nft/src/lib.rs rename to crates/loam-cli/examples/soroban/nft/src/lib.rs diff --git a/examples/soroban/nft/src/nft.rs b/crates/loam-cli/examples/soroban/nft/src/nft.rs similarity index 100% rename from examples/soroban/nft/src/nft.rs rename to crates/loam-cli/examples/soroban/nft/src/nft.rs diff --git a/examples/soroban/nft/src/subcontract.rs b/crates/loam-cli/examples/soroban/nft/src/subcontract.rs similarity index 100% rename from examples/soroban/nft/src/subcontract.rs rename to crates/loam-cli/examples/soroban/nft/src/subcontract.rs From b18d6d6fcb106b563e80db7934182ad427579777 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Mon, 15 Jul 2024 11:46:53 -0400 Subject: [PATCH 16/31] Use derive_contract for example nft --- .../loam-cli/examples/soroban/nft/src/lib.rs | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/crates/loam-cli/examples/soroban/nft/src/lib.rs b/crates/loam-cli/examples/soroban/nft/src/lib.rs index b9f993e1..1040ddd8 100644 --- a/crates/loam-cli/examples/soroban/nft/src/lib.rs +++ b/crates/loam-cli/examples/soroban/nft/src/lib.rs @@ -1,5 +1,5 @@ #![no_std] -use loam_sdk::soroban_contract; +use loam_sdk::derive_contract; use loam_subcontract_core::{admin::Admin, Core}; pub mod nft; @@ -8,18 +8,9 @@ pub mod subcontract; use nft::MyNonFungibleToken; use subcontract::{Initable, NonFungible}; +#[derive_contract( + Core(Admin), + NonFungible(MyNonFungibleToken), + Initable(MyNonFungibleToken) +)] pub struct Contract; - -impl Core for Contract { - type Impl = Admin; -} - -impl NonFungible for Contract { - type Impl = MyNonFungibleToken; -} - -impl Initable for Contract { - type Impl = MyNonFungibleToken; -} - -soroban_contract!(); From 959952ab442049f94b12f3448fb594eb9ee8f79f Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Mon, 15 Jul 2024 11:47:37 -0400 Subject: [PATCH 17/31] Refactor owners_to_nft_ids --- .../loam-cli/examples/soroban/nft/src/nft.rs | 35 +++++++------------ .../examples/soroban/nft/src/subcontract.rs | 2 +- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/crates/loam-cli/examples/soroban/nft/src/nft.rs b/crates/loam-cli/examples/soroban/nft/src/nft.rs index 973fd8ca..80d81f70 100644 --- a/crates/loam-cli/examples/soroban/nft/src/nft.rs +++ b/crates/loam-cli/examples/soroban/nft/src/nft.rs @@ -15,7 +15,7 @@ pub struct MyNonFungibleToken { admin: Address, name: Bytes, total_count: u32, - owners_to_nft_ids: Map>, + owners_to_nft_ids: Map>, // the owner's collection nft_ids_to_owners: Map, nft_ids_to_metadata: Map, } @@ -62,10 +62,10 @@ impl IsNonFungible for MyNonFungibleToken { let mut owner_collection = self .owners_to_nft_ids .get(owner.clone()) - .unwrap_or_else(|| Vec::new(env())); - owner_collection.push_back(new_id); - + .unwrap_or_else(|| Map::new(env())); + owner_collection.set(new_id, ()); self.owners_to_nft_ids.set(owner, owner_collection); + self.total_count = new_id; new_id @@ -74,11 +74,6 @@ impl IsNonFungible for MyNonFungibleToken { // Transfer the NFT from the current owner to the new owner fn transfer(&mut self, id: u32, current_owner: Address, new_owner: Address) { current_owner.require_auth(); - // ensures that this Address has authorized invocation of the current contract - // during the on-chain execution the soroban host will perform the needed auth (verify the signatures) and ensure the replay prevention - - // what if current_owner is not the source account? - let owner_id = self.nft_ids_to_owners.get(id).expect("NFT does not exist"); assert!( owner_id == current_owner, @@ -96,24 +91,15 @@ impl IsNonFungible for MyNonFungibleToken { .expect("Owner does not have a collection of NFTs"); current_owner_collection.remove(id); - if let Some(index) = current_owner_collection - .iter() - .position(|nft_id| nft_id == id) - { - current_owner_collection.remove(index.try_into().unwrap()); - } else { - panic!("NFT ID not found in owner's collection"); - } - self.owners_to_nft_ids .set(current_owner, current_owner_collection); - // Add the NFT id to the new owner's collection + // add the NFT id to the new owner's collection let mut new_owner_collection = self .owners_to_nft_ids .get(new_owner.clone()) - .unwrap_or_else(|| Vec::new(env())); - new_owner_collection.push_back(id); + .unwrap_or_else(|| Map::new(env())); + new_owner_collection.set(id, ()); self.owners_to_nft_ids.set(new_owner, new_owner_collection); } @@ -131,7 +117,10 @@ impl IsNonFungible for MyNonFungibleToken { self.total_count } - fn get_collection_by_owner(&self, owner: Address) -> Option> { - self.owners_to_nft_ids.get(owner) + fn get_collection_by_owner(&self, owner: Address) -> Vec { + self.owners_to_nft_ids + .get(owner) + .unwrap_or(Map::new(env())) + .keys() } } diff --git a/crates/loam-cli/examples/soroban/nft/src/subcontract.rs b/crates/loam-cli/examples/soroban/nft/src/subcontract.rs index 9113bd28..6cbd7dd1 100644 --- a/crates/loam-cli/examples/soroban/nft/src/subcontract.rs +++ b/crates/loam-cli/examples/soroban/nft/src/subcontract.rs @@ -30,7 +30,7 @@ pub trait IsNonFungible { fn get_collection_by_owner( &self, owner: loam_sdk::soroban_sdk::Address, - ) -> Option>; + ) -> loam_soroban_sdk::Vec; } #[subcontract] From 6e41c998121351dfb38b201c27e44d849ded801a Mon Sep 17 00:00:00 2001 From: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:19:40 -0400 Subject: [PATCH 18/31] build: 'loam build' changes Cargo.lock --- Cargo.lock | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 738f9ab7..1049f520 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1024,6 +1024,15 @@ dependencies = [ "loam-subcontract-ft", ] +[[package]] +name = "example-nft" +version = "0.0.0" +dependencies = [ + "loam-sdk", + "loam-soroban-sdk", + "loam-subcontract-core", +] + [[package]] name = "example-status-message" version = "0.0.0" From 95d21610ed5879134a9a73bb72e584f9615d3327 Mon Sep 17 00:00:00 2001 From: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:49:44 -0400 Subject: [PATCH 19/31] fix: use doc comments for subcontracts Now when I run this: ``` just build && \ stellar contract deploy --wasm target/loam/example_nft.wasm --source alice --network local --alias nft && \ stellar contract invoke --id nft -- --help ``` I get this output: ``` Commands: admin_get Get current admin admin_set Transfer to new admin Should be called in the same transaction as deploying the contract to ensure that a different account try to become admin redeploy Admin can redepoly the contract with given hash. mint Mint a new NFT with the given ID, owner, and metadata transfer Transfer an NFT with the given ID from current_owner to new_owner get_nft Get the NFT with the given ID get_owner Find the owner of the NFT with the given ID get_total_count Get the total count of NFTs get_collection_by_owner Get all of the NFTs owned by the given address nft_init Initialize the NFT contract with the given admin and name ``` --- .../examples/soroban/nft/src/subcontract.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/loam-cli/examples/soroban/nft/src/subcontract.rs b/crates/loam-cli/examples/soroban/nft/src/subcontract.rs index 6cbd7dd1..d49b2adb 100644 --- a/crates/loam-cli/examples/soroban/nft/src/subcontract.rs +++ b/crates/loam-cli/examples/soroban/nft/src/subcontract.rs @@ -2,14 +2,14 @@ use loam_sdk::{soroban_sdk::Lazy, subcontract}; #[subcontract] pub trait IsNonFungible { - // Mint a new NFT with the given ID, owner, and metadata + /// Mint a new NFT with the given ID, owner, and metadata fn mint( &mut self, owner: loam_sdk::soroban_sdk::Address, metadata: loam_sdk::soroban_sdk::Bytes, ) -> u32; - // Transfer an NFT with the given ID from current_owner to new_owner + /// Transfer an NFT with the given ID from current_owner to new_owner fn transfer( &mut self, id: u32, @@ -17,16 +17,16 @@ pub trait IsNonFungible { new_owner: loam_sdk::soroban_sdk::Address, ); - // Get the NFT with the given ID + /// Get the NFT with the given ID fn get_nft(&self, id: u32) -> Option; - // Find the owner of the NFT with the given ID + /// Find the owner of the NFT with the given ID fn get_owner(&self, id: u32) -> Option; - // Get the total count of NFTs + /// Get the total count of NFTs fn get_total_count(&self) -> u32; - // Get all of the NFTs owned by the given address + /// Get all of the NFTs owned by the given address fn get_collection_by_owner( &self, owner: loam_sdk::soroban_sdk::Address, @@ -35,5 +35,6 @@ pub trait IsNonFungible { #[subcontract] pub trait IsInitable { + /// Initialize the NFT contract with the given admin and name fn nft_init(&self, admin: loam_sdk::soroban_sdk::Address, name: loam_sdk::soroban_sdk::Bytes); } From ba7af1509a108903b64a29721fcf2f6068cc3ec3 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:12:19 -0400 Subject: [PATCH 20/31] Remove extraneous comments in nft.rs --- crates/loam-cli/examples/soroban/nft/src/nft.rs | 4 ---- crates/loam-cli/examples/soroban/nft/src/subcontract.rs | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/loam-cli/examples/soroban/nft/src/nft.rs b/crates/loam-cli/examples/soroban/nft/src/nft.rs index 80d81f70..94899d98 100644 --- a/crates/loam-cli/examples/soroban/nft/src/nft.rs +++ b/crates/loam-cli/examples/soroban/nft/src/nft.rs @@ -48,7 +48,6 @@ impl IsInitable for MyNonFungibleToken { } impl IsNonFungible for MyNonFungibleToken { - // Mint a new NFT with the given owner address and metadata, returning the id fn mint(&mut self, owner: Address, metadata: Bytes) -> u32 { owner.require_auth(); @@ -71,7 +70,6 @@ impl IsNonFungible for MyNonFungibleToken { new_id } - // Transfer the NFT from the current owner to the new owner fn transfer(&mut self, id: u32, current_owner: Address, new_owner: Address) { current_owner.require_auth(); let owner_id = self.nft_ids_to_owners.get(id).expect("NFT does not exist"); @@ -103,12 +101,10 @@ impl IsNonFungible for MyNonFungibleToken { self.owners_to_nft_ids.set(new_owner, new_owner_collection); } - // Get the NFT from the contract's storage by id fn get_nft(&self, id: u32) -> Option { self.nft_ids_to_metadata.get(id) } - // Get the NFT from the contract's storage by owner id fn get_owner(&self, id: u32) -> Option
{ self.nft_ids_to_owners.get(id) } diff --git a/crates/loam-cli/examples/soroban/nft/src/subcontract.rs b/crates/loam-cli/examples/soroban/nft/src/subcontract.rs index d49b2adb..dc75fd34 100644 --- a/crates/loam-cli/examples/soroban/nft/src/subcontract.rs +++ b/crates/loam-cli/examples/soroban/nft/src/subcontract.rs @@ -2,14 +2,14 @@ use loam_sdk::{soroban_sdk::Lazy, subcontract}; #[subcontract] pub trait IsNonFungible { - /// Mint a new NFT with the given ID, owner, and metadata + /// Mint a new NFT with the given owner and metadata fn mint( &mut self, owner: loam_sdk::soroban_sdk::Address, metadata: loam_sdk::soroban_sdk::Bytes, ) -> u32; - /// Transfer an NFT with the given ID from current_owner to new_owner + /// Transfer the NFT with the given ID from current_owner to new_owner fn transfer( &mut self, id: u32, From ccaa9aeebf57f900cc4cd2a0009351a8733d61b4 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:13:53 -0400 Subject: [PATCH 21/31] Remove loam-soroban-sdk from Cargo.toml as it is re-exported from loam-sdk Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com> --- Cargo.lock | 1 - crates/loam-cli/examples/soroban/nft/Cargo.toml | 1 - crates/loam-cli/examples/soroban/nft/src/subcontract.rs | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1049f520..2fc7bb0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1029,7 +1029,6 @@ name = "example-nft" version = "0.0.0" dependencies = [ "loam-sdk", - "loam-soroban-sdk", "loam-subcontract-core", ] diff --git a/crates/loam-cli/examples/soroban/nft/Cargo.toml b/crates/loam-cli/examples/soroban/nft/Cargo.toml index a3b43177..c94a6657 100644 --- a/crates/loam-cli/examples/soroban/nft/Cargo.toml +++ b/crates/loam-cli/examples/soroban/nft/Cargo.toml @@ -13,7 +13,6 @@ doctest = false [dependencies] loam-sdk = { workspace = true, features = ["loam-soroban-sdk"] } loam-subcontract-core = { workspace = true } -loam-soroban-sdk = { workspace = true } [dev-dependencies] diff --git a/crates/loam-cli/examples/soroban/nft/src/subcontract.rs b/crates/loam-cli/examples/soroban/nft/src/subcontract.rs index dc75fd34..bd08a62a 100644 --- a/crates/loam-cli/examples/soroban/nft/src/subcontract.rs +++ b/crates/loam-cli/examples/soroban/nft/src/subcontract.rs @@ -30,7 +30,7 @@ pub trait IsNonFungible { fn get_collection_by_owner( &self, owner: loam_sdk::soroban_sdk::Address, - ) -> loam_soroban_sdk::Vec; + ) -> loam_sdk::soroban_sdk::Vec; } #[subcontract] From d3acb1a7fb5d26581a37cad58b4e85d5d96d2fd6 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Tue, 16 Jul 2024 11:29:22 -0400 Subject: [PATCH 22/31] Require auth from the admin for minting --- crates/loam-cli/examples/soroban/nft/src/nft.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/loam-cli/examples/soroban/nft/src/nft.rs b/crates/loam-cli/examples/soroban/nft/src/nft.rs index 94899d98..43dabd90 100644 --- a/crates/loam-cli/examples/soroban/nft/src/nft.rs +++ b/crates/loam-cli/examples/soroban/nft/src/nft.rs @@ -2,12 +2,8 @@ use loam_sdk::{ soroban_sdk::{self, contracttype, env, Address, Bytes, Lazy, Map, Vec}, IntoKey, }; -use loam_subcontract_core::Core; -use crate::{ - subcontract::{IsInitable, IsNonFungible}, - Contract, -}; +use crate::subcontract::{IsInitable, IsNonFungible}; #[contracttype] #[derive(IntoKey)] @@ -42,14 +38,14 @@ impl Default for MyNonFungibleToken { impl IsInitable for MyNonFungibleToken { fn nft_init(&self, admin: Address, name: Bytes) { - Contract::admin_get().unwrap().require_auth(); + self.admin.require_auth(); MyNonFungibleToken::set_lazy(MyNonFungibleToken::new(admin, name)); } } impl IsNonFungible for MyNonFungibleToken { fn mint(&mut self, owner: Address, metadata: Bytes) -> u32 { - owner.require_auth(); + self.admin.require_auth(); let current_count = self.total_count; let new_id = current_count + 1; From cc22d8b9e410b1d2a5d8c9217e9b28716f5e61cb Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Tue, 16 Jul 2024 11:33:56 -0400 Subject: [PATCH 23/31] Bump total count limit --- .../loam-cli/examples/soroban/nft/src/nft.rs | 20 +++++++++---------- .../examples/soroban/nft/src/subcontract.rs | 12 +++++------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/loam-cli/examples/soroban/nft/src/nft.rs b/crates/loam-cli/examples/soroban/nft/src/nft.rs index 43dabd90..8ddaa25a 100644 --- a/crates/loam-cli/examples/soroban/nft/src/nft.rs +++ b/crates/loam-cli/examples/soroban/nft/src/nft.rs @@ -10,10 +10,10 @@ use crate::subcontract::{IsInitable, IsNonFungible}; pub struct MyNonFungibleToken { admin: Address, name: Bytes, - total_count: u32, - owners_to_nft_ids: Map>, // the owner's collection - nft_ids_to_owners: Map, - nft_ids_to_metadata: Map, + total_count: u128, + owners_to_nft_ids: Map>, // the owner's collection + nft_ids_to_owners: Map, + nft_ids_to_metadata: Map, } impl MyNonFungibleToken { @@ -44,7 +44,7 @@ impl IsInitable for MyNonFungibleToken { } impl IsNonFungible for MyNonFungibleToken { - fn mint(&mut self, owner: Address, metadata: Bytes) -> u32 { + fn mint(&mut self, owner: Address, metadata: Bytes) -> u128 { self.admin.require_auth(); let current_count = self.total_count; @@ -66,7 +66,7 @@ impl IsNonFungible for MyNonFungibleToken { new_id } - fn transfer(&mut self, id: u32, current_owner: Address, new_owner: Address) { + fn transfer(&mut self, id: u128, current_owner: Address, new_owner: Address) { current_owner.require_auth(); let owner_id = self.nft_ids_to_owners.get(id).expect("NFT does not exist"); assert!( @@ -97,19 +97,19 @@ impl IsNonFungible for MyNonFungibleToken { self.owners_to_nft_ids.set(new_owner, new_owner_collection); } - fn get_nft(&self, id: u32) -> Option { + fn get_nft(&self, id: u128) -> Option { self.nft_ids_to_metadata.get(id) } - fn get_owner(&self, id: u32) -> Option
{ + fn get_owner(&self, id: u128) -> Option
{ self.nft_ids_to_owners.get(id) } - fn get_total_count(&self) -> u32 { + fn get_total_count(&self) -> u128 { self.total_count } - fn get_collection_by_owner(&self, owner: Address) -> Vec { + fn get_collection_by_owner(&self, owner: Address) -> Vec { self.owners_to_nft_ids .get(owner) .unwrap_or(Map::new(env())) diff --git a/crates/loam-cli/examples/soroban/nft/src/subcontract.rs b/crates/loam-cli/examples/soroban/nft/src/subcontract.rs index bd08a62a..13da3d50 100644 --- a/crates/loam-cli/examples/soroban/nft/src/subcontract.rs +++ b/crates/loam-cli/examples/soroban/nft/src/subcontract.rs @@ -7,30 +7,30 @@ pub trait IsNonFungible { &mut self, owner: loam_sdk::soroban_sdk::Address, metadata: loam_sdk::soroban_sdk::Bytes, - ) -> u32; + ) -> u128; /// Transfer the NFT with the given ID from current_owner to new_owner fn transfer( &mut self, - id: u32, + id: u128, current_owner: loam_sdk::soroban_sdk::Address, new_owner: loam_sdk::soroban_sdk::Address, ); /// Get the NFT with the given ID - fn get_nft(&self, id: u32) -> Option; + fn get_nft(&self, id: u128) -> Option; /// Find the owner of the NFT with the given ID - fn get_owner(&self, id: u32) -> Option; + fn get_owner(&self, id: u128) -> Option; /// Get the total count of NFTs - fn get_total_count(&self) -> u32; + fn get_total_count(&self) -> u128; /// Get all of the NFTs owned by the given address fn get_collection_by_owner( &self, owner: loam_sdk::soroban_sdk::Address, - ) -> loam_sdk::soroban_sdk::Vec; + ) -> loam_sdk::soroban_sdk::Vec; } #[subcontract] From 553f4012f6f86ec1e846a10caac8c3998508c109 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Fri, 19 Jul 2024 16:06:01 -0400 Subject: [PATCH 24/31] Remove admin attribute from MyNonFungibleToken and use Contract::require_admin for authing protected fns --- crates/loam-cli/examples/soroban/nft/src/lib.rs | 8 ++++++++ crates/loam-cli/examples/soroban/nft/src/nft.rs | 16 +++++++--------- .../examples/soroban/nft/src/subcontract.rs | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/loam-cli/examples/soroban/nft/src/lib.rs b/crates/loam-cli/examples/soroban/nft/src/lib.rs index 1040ddd8..e3a22b67 100644 --- a/crates/loam-cli/examples/soroban/nft/src/lib.rs +++ b/crates/loam-cli/examples/soroban/nft/src/lib.rs @@ -14,3 +14,11 @@ use subcontract::{Initable, NonFungible}; Initable(MyNonFungibleToken) )] pub struct Contract; + +impl Contract { + pub(crate) fn require_auth() { + Contract::admin_get() + .expect("No admin! Call 'admin_set' first.") + .require_auth(); + } +} diff --git a/crates/loam-cli/examples/soroban/nft/src/nft.rs b/crates/loam-cli/examples/soroban/nft/src/nft.rs index 8ddaa25a..83e72140 100644 --- a/crates/loam-cli/examples/soroban/nft/src/nft.rs +++ b/crates/loam-cli/examples/soroban/nft/src/nft.rs @@ -3,12 +3,11 @@ use loam_sdk::{ IntoKey, }; -use crate::subcontract::{IsInitable, IsNonFungible}; +use crate::{subcontract::{IsInitable, IsNonFungible}, Contract}; #[contracttype] #[derive(IntoKey)] pub struct MyNonFungibleToken { - admin: Address, name: Bytes, total_count: u128, owners_to_nft_ids: Map>, // the owner's collection @@ -18,9 +17,8 @@ pub struct MyNonFungibleToken { impl MyNonFungibleToken { #[must_use] - pub fn new(admin: Address, name: Bytes) -> Self { + pub fn new(name: Bytes) -> Self { MyNonFungibleToken { - admin, name, total_count: 0, owners_to_nft_ids: Map::new(env()), @@ -32,20 +30,20 @@ impl MyNonFungibleToken { impl Default for MyNonFungibleToken { fn default() -> Self { - MyNonFungibleToken::new(env().current_contract_address(), Bytes::new(env())) + MyNonFungibleToken::new(Bytes::new(env())) } } impl IsInitable for MyNonFungibleToken { - fn nft_init(&self, admin: Address, name: Bytes) { - self.admin.require_auth(); - MyNonFungibleToken::set_lazy(MyNonFungibleToken::new(admin, name)); + fn nft_init(&self, name: Bytes) { + Contract::require_auth(); + MyNonFungibleToken::set_lazy(MyNonFungibleToken::new(name)); } } impl IsNonFungible for MyNonFungibleToken { fn mint(&mut self, owner: Address, metadata: Bytes) -> u128 { - self.admin.require_auth(); + Contract::require_auth(); let current_count = self.total_count; let new_id = current_count + 1; diff --git a/crates/loam-cli/examples/soroban/nft/src/subcontract.rs b/crates/loam-cli/examples/soroban/nft/src/subcontract.rs index 13da3d50..3dd9e628 100644 --- a/crates/loam-cli/examples/soroban/nft/src/subcontract.rs +++ b/crates/loam-cli/examples/soroban/nft/src/subcontract.rs @@ -36,5 +36,5 @@ pub trait IsNonFungible { #[subcontract] pub trait IsInitable { /// Initialize the NFT contract with the given admin and name - fn nft_init(&self, admin: loam_sdk::soroban_sdk::Address, name: loam_sdk::soroban_sdk::Bytes); + fn nft_init(&self, name: loam_sdk::soroban_sdk::Bytes); } From ec7df8129ff213efb8848e54ca5a31969dbfa0ca Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:46:42 -0400 Subject: [PATCH 25/31] WIP: add happy path tests for nft example --- .../loam-cli/examples/soroban/nft/src/lib.rs | 2 + .../loam-cli/examples/soroban/nft/src/test.rs | 111 ++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 crates/loam-cli/examples/soroban/nft/src/test.rs diff --git a/crates/loam-cli/examples/soroban/nft/src/lib.rs b/crates/loam-cli/examples/soroban/nft/src/lib.rs index e3a22b67..c9e7ec2f 100644 --- a/crates/loam-cli/examples/soroban/nft/src/lib.rs +++ b/crates/loam-cli/examples/soroban/nft/src/lib.rs @@ -22,3 +22,5 @@ impl Contract { .require_auth(); } } + +mod test; diff --git a/crates/loam-cli/examples/soroban/nft/src/test.rs b/crates/loam-cli/examples/soroban/nft/src/test.rs new file mode 100644 index 00000000..9cf390a6 --- /dev/null +++ b/crates/loam-cli/examples/soroban/nft/src/test.rs @@ -0,0 +1,111 @@ +#![cfg(test)] + +use super::{ + SorobanContract__ as SorobanContract, SorobanContract__Client as SorobanContractClient, +}; +use loam_sdk::soroban_sdk::{ + testutils::{Address as _, MockAuth, MockAuthInvoke}, + Address, Bytes, Env, IntoVal, +}; + +extern crate std; + +mod contract { + use loam_sdk::soroban_sdk; + + soroban_sdk::contractimport!(file = "../../../../../target/loam/example_nft.wasm"); +} + +fn init() -> (Env, SorobanContractClient<'static>, Address) { + let env = Env::default(); + let contract_id = env.register_contract(None, SorobanContract); + let client = SorobanContractClient::new(&env, &contract_id); + (env, client, contract_id) +} + +#[test] +fn test_nft() { + let (env, client, contract_id) = &init(); + + let admin = Address::generate(&env); + + // test admin_get and admin_set + match client.admin_get() { + Some(_) => assert!(false, "Admin already set"), + None => assert!(true), + } + client + .mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "admin_set", + args: (&admin,).into_val(env), + sub_invokes: &[], + }, + }]) + .admin_set(&admin); + + match client.admin_get() { + Some(a) => assert_eq!(a, admin), + None => assert!(false, "No admin set"), + } + + // test nft_init + let name = Bytes::from_slice(env, "nftexample".as_bytes()); + client + .mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "nft_init", + args: (name.clone(),).into_val(env), + sub_invokes: &[], + }, + }]) + .nft_init(&name); + + // test initial state + assert_eq!(client.get_total_count(), 0); + assert!(client.get_nft(&1).is_none()); + assert!(client.get_owner(&1).is_none()); + assert_eq!(client.get_collection_by_owner(&admin).len(), 0); + + // test mint & getter fns + let owner_1 = Address::generate(&env); + let metadata = Bytes::from_slice(env, "metadata".as_bytes()); + let nft_id = client + .mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "mint", + args: (owner_1.clone(), metadata.clone()).into_val(env), + sub_invokes: &[], + }, + }]) + .mint(&owner_1, &metadata); + assert_eq!(nft_id, 1); + assert_eq!(client.get_nft(&nft_id), Some(metadata)); + assert_eq!(client.get_owner(&nft_id), Some(owner_1.clone())); + assert_eq!(client.get_total_count(), 1); + assert_eq!(client.get_collection_by_owner(&owner_1).len(), 1); + + // test transfer + let owner_2 = Address::generate(&env); + client + .mock_auths(&[MockAuth { + address: &owner_1, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "transfer", + args: (nft_id.clone(), owner_1.clone(), owner_2.clone()).into_val(env), + sub_invokes: &[], + }, + }]) + .transfer(&nft_id, &owner_1, &owner_2); + assert_eq!(client.get_owner(&nft_id), Some(owner_2.clone())); + assert_eq!(client.get_total_count(), 1); + assert_eq!(client.get_collection_by_owner(&owner_1).len(), 0); + assert_eq!(client.get_collection_by_owner(&owner_2).len(), 1); +} From 7e7cfbf96051743cccb23557d48d28ac663c6b78 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Wed, 24 Jul 2024 11:58:02 -0400 Subject: [PATCH 26/31] Apply suggestions from code review Co-authored-by: Willem Wyndham --- crates/loam-cli/examples/soroban/nft/src/test.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/loam-cli/examples/soroban/nft/src/test.rs b/crates/loam-cli/examples/soroban/nft/src/test.rs index 9cf390a6..e590f2f2 100644 --- a/crates/loam-cli/examples/soroban/nft/src/test.rs +++ b/crates/loam-cli/examples/soroban/nft/src/test.rs @@ -30,10 +30,7 @@ fn test_nft() { let admin = Address::generate(&env); // test admin_get and admin_set - match client.admin_get() { - Some(_) => assert!(false, "Admin already set"), - None => assert!(true), - } + assert!(client.admin_get().is_none(), "Admin already set"); client .mock_auths(&[MockAuth { address: &admin, @@ -46,10 +43,7 @@ fn test_nft() { }]) .admin_set(&admin); - match client.admin_get() { - Some(a) => assert_eq!(a, admin), - None => assert!(false, "No admin set"), - } + assert!(matches!(client.admin_get(), Some(admin)), "No admin set"); // test nft_init let name = Bytes::from_slice(env, "nftexample".as_bytes()); From 5655fd83793f1ba4de01681d0180f4c545178f44 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Fri, 26 Jul 2024 09:54:21 -0400 Subject: [PATCH 27/31] Cleanup happy path test --- crates/loam-cli/examples/soroban/nft/src/test.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/loam-cli/examples/soroban/nft/src/test.rs b/crates/loam-cli/examples/soroban/nft/src/test.rs index e590f2f2..b9ba89a7 100644 --- a/crates/loam-cli/examples/soroban/nft/src/test.rs +++ b/crates/loam-cli/examples/soroban/nft/src/test.rs @@ -43,7 +43,7 @@ fn test_nft() { }]) .admin_set(&admin); - assert!(matches!(client.admin_get(), Some(admin)), "No admin set"); + assert_eq!(client.admin_get().unwrap(), admin); // test nft_init let name = Bytes::from_slice(env, "nftexample".as_bytes()); @@ -59,7 +59,6 @@ fn test_nft() { }]) .nft_init(&name); - // test initial state assert_eq!(client.get_total_count(), 0); assert!(client.get_nft(&1).is_none()); assert!(client.get_owner(&1).is_none()); From 3d83b38181bdcabf9aa179884197659c5962c588 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Fri, 26 Jul 2024 10:36:58 -0400 Subject: [PATCH 28/31] Add tests for error cases --- .../loam-cli/examples/soroban/nft/src/test.rs | 212 +++++++++++++++++- 1 file changed, 208 insertions(+), 4 deletions(-) diff --git a/crates/loam-cli/examples/soroban/nft/src/test.rs b/crates/loam-cli/examples/soroban/nft/src/test.rs index b9ba89a7..15b59a21 100644 --- a/crates/loam-cli/examples/soroban/nft/src/test.rs +++ b/crates/loam-cli/examples/soroban/nft/src/test.rs @@ -26,7 +26,6 @@ fn init() -> (Env, SorobanContractClient<'static>, Address) { #[test] fn test_nft() { let (env, client, contract_id) = &init(); - let admin = Address::generate(&env); // test admin_get and admin_set @@ -42,7 +41,6 @@ fn test_nft() { }, }]) .admin_set(&admin); - assert_eq!(client.admin_get().unwrap(), admin); // test nft_init @@ -65,7 +63,7 @@ fn test_nft() { assert_eq!(client.get_collection_by_owner(&admin).len(), 0); // test mint & getter fns - let owner_1 = Address::generate(&env); + let owner_1 = Address::generate(env); let metadata = Bytes::from_slice(env, "metadata".as_bytes()); let nft_id = client .mock_auths(&[MockAuth { @@ -85,7 +83,7 @@ fn test_nft() { assert_eq!(client.get_collection_by_owner(&owner_1).len(), 1); // test transfer - let owner_2 = Address::generate(&env); + let owner_2 = Address::generate(env); client .mock_auths(&[MockAuth { address: &owner_1, @@ -102,3 +100,209 @@ fn test_nft() { assert_eq!(client.get_collection_by_owner(&owner_1).len(), 0); assert_eq!(client.get_collection_by_owner(&owner_2).len(), 1); } + +#[test] +#[should_panic] +fn test_initializing_without_admin_set() { + let (env, client, contract_id) = &init(); + let admin = Address::generate(env); + // test nft_init + let name = Bytes::from_slice(env, "nftexample".as_bytes()); + client + .mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "nft_init", + args: (name.clone(),).into_val(env), + sub_invokes: &[], + }, + }]) + .nft_init(&name); +} + +#[test] +#[should_panic] +fn test_minting_by_non_admin() { + let (env, client, contract_id) = &init(); + let admin = Address::generate(&env); + + // set admin + assert!(client.admin_get().is_none(), "Admin already set"); + client + .mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "admin_set", + args: (&admin,).into_val(env), + sub_invokes: &[], + }, + }]) + .admin_set(&admin); + assert!(client.admin_get().is_some(), "Admin not set"); + + // nft_init + let name = Bytes::from_slice(env, "nftexample".as_bytes()); + client + .mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "nft_init", + args: (name.clone(),).into_val(env), + sub_invokes: &[], + }, + }]) + .nft_init(&name); + + assert_eq!(client.get_total_count(), 0); + + // try to mint from non-admin + let non_admin = Address::generate(env); + let metadata = Bytes::from_slice(env, "metadata".as_bytes()); + client + .mock_auths(&[MockAuth { + address: &non_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "mint", + args: (non_admin.clone(), metadata.clone()).into_val(env), + sub_invokes: &[], + }, + }]) + .mint(&non_admin, &metadata); +} + +#[test] +#[should_panic] +fn test_minting_without_contract_being_initialized() { + let (env, client, contract_id) = &init(); + let admin = Address::generate(&env); + + // set admin + assert!(client.admin_get().is_none(), "Admin already set"); + client + .mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "admin_set", + args: (&admin,).into_val(env), + sub_invokes: &[], + }, + }]) + .admin_set(&admin); + assert!(client.admin_get().is_some(), "Admin not set"); + + // try to mint + let non_admin = Address::generate(env); + let metadata = Bytes::from_slice(env, "metadata".as_bytes()); + client + .mock_auths(&[MockAuth { + address: &non_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "mint", + args: (non_admin.clone(), metadata.clone()).into_val(env), + sub_invokes: &[], + }, + }]) + .mint(&non_admin, &metadata); +} + +// Should this fail? Or is it okay that the getter methods work before initialization? +#[test] +fn test_getter_methods_before_initialization() { + let (env, client, contract_id) = &init(); + let admin = Address::generate(&env); + + // set admin + assert!(client.admin_get().is_none(), "Admin already set"); + client + .mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "admin_set", + args: (&admin,).into_val(env), + sub_invokes: &[], + }, + }]) + .admin_set(&admin); + assert!(client.admin_get().is_some(), "Admin not set"); + + assert_eq!(client.get_total_count(), 0); +} + +#[test] +#[should_panic] +fn test_transfer_by_non_owner() { + let (env, client, contract_id) = &init(); + let admin = Address::generate(&env); + + // set admin + assert!(client.admin_get().is_none(), "Admin already set"); + client + .mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "admin_set", + args: (&admin,).into_val(env), + sub_invokes: &[], + }, + }]) + .admin_set(&admin); + + assert_eq!(client.admin_get().unwrap(), admin); + + // nft_init + let name = Bytes::from_slice(env, "nftexample".as_bytes()); + client + .mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "nft_init", + args: (name.clone(),).into_val(env), + sub_invokes: &[], + }, + }]) + .nft_init(&name); + + assert_eq!(client.get_total_count(), 0); + assert!(client.get_nft(&1).is_none()); + assert!(client.get_owner(&1).is_none()); + assert_eq!(client.get_collection_by_owner(&admin).len(), 0); + + // mint nft to owner 1 + let owner_1 = Address::generate(env); + let metadata = Bytes::from_slice(env, "metadata".as_bytes()); + let nft_id = client + .mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "mint", + args: (owner_1.clone(), metadata.clone()).into_val(env), + sub_invokes: &[], + }, + }]) + .mint(&owner_1, &metadata); + assert_eq!(client.get_owner(&nft_id), Some(owner_1.clone())); + + // try to transfer nft by non-owner + let owner_2 = Address::generate(env); + client + .mock_auths(&[MockAuth { + address: &owner_2, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "transfer", + args: (nft_id.clone(), owner_2.clone(), owner_2.clone()).into_val(env), + sub_invokes: &[], + }, + }]) + .transfer(&nft_id, &owner_2, &owner_2); +} \ No newline at end of file From 3a6507ccfcbf73e5aee10a0971aaf5c429e1f493 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Fri, 26 Jul 2024 10:56:08 -0400 Subject: [PATCH 29/31] Ensure that panicks are happening for the right reasons --- .../loam-cli/examples/soroban/nft/src/test.rs | 93 +++++++++++++++++-- 1 file changed, 83 insertions(+), 10 deletions(-) diff --git a/crates/loam-cli/examples/soroban/nft/src/test.rs b/crates/loam-cli/examples/soroban/nft/src/test.rs index 15b59a21..854af3b0 100644 --- a/crates/loam-cli/examples/soroban/nft/src/test.rs +++ b/crates/loam-cli/examples/soroban/nft/src/test.rs @@ -102,7 +102,7 @@ fn test_nft() { } #[test] -#[should_panic] +#[should_panic(expected = "No admin! Call 'admin_set' first.")] fn test_initializing_without_admin_set() { let (env, client, contract_id) = &init(); let admin = Address::generate(env); @@ -122,7 +122,7 @@ fn test_initializing_without_admin_set() { } #[test] -#[should_panic] +#[should_panic(expected = "Unauthorized function call for address")] fn test_minting_by_non_admin() { let (env, client, contract_id) = &init(); let admin = Address::generate(&env); @@ -195,24 +195,24 @@ fn test_minting_without_contract_being_initialized() { .admin_set(&admin); assert!(client.admin_get().is_some(), "Admin not set"); - // try to mint - let non_admin = Address::generate(env); + // try to mint though the contract has not been initialized + let owner_1 = Address::generate(env); let metadata = Bytes::from_slice(env, "metadata".as_bytes()); client .mock_auths(&[MockAuth { - address: &non_admin, + address: &admin, invoke: &MockAuthInvoke { contract: &contract_id, fn_name: "mint", - args: (non_admin.clone(), metadata.clone()).into_val(env), + args: (owner_1.clone(), metadata.clone()).into_val(env), sub_invokes: &[], }, }]) - .mint(&non_admin, &metadata); + .mint(&owner_1, &metadata); } -// Should this fail? Or is it okay that the getter methods work before initialization? #[test] +#[should_panic] fn test_getter_methods_before_initialization() { let (env, client, contract_id) = &init(); let admin = Address::generate(&env); @@ -236,7 +236,7 @@ fn test_getter_methods_before_initialization() { } #[test] -#[should_panic] +#[should_panic(expected = "You are not the owner of this NFT")] fn test_transfer_by_non_owner() { let (env, client, contract_id) = &init(); let admin = Address::generate(&env); @@ -305,4 +305,77 @@ fn test_transfer_by_non_owner() { }, }]) .transfer(&nft_id, &owner_2, &owner_2); -} \ No newline at end of file +} + +#[test] +#[should_panic(expected = "NFT does not exist")] +fn test_transferring_a_non_existent_nft() { + let (env, client, contract_id) = &init(); + let admin = Address::generate(&env); + + // set admin + assert!(client.admin_get().is_none(), "Admin already set"); + client + .mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "admin_set", + args: (&admin,).into_val(env), + sub_invokes: &[], + }, + }]) + .admin_set(&admin); + assert_eq!(client.admin_get().unwrap(), admin); + + // nft_init + let name = Bytes::from_slice(env, "nftexample".as_bytes()); + client + .mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "nft_init", + args: (name.clone(),).into_val(env), + sub_invokes: &[], + }, + }]) + .nft_init(&name); + + // mint nft1 to owner 1 + let owner_1 = Address::generate(env); + let metadata = Bytes::from_slice(env, "metadata".as_bytes()); + let nft_id = client + .mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "mint", + args: (owner_1.clone(), metadata.clone()).into_val(env), + sub_invokes: &[], + }, + }]) + .mint(&owner_1, &metadata); + assert_eq!(nft_id, 1); + assert_eq!(client.get_owner(&nft_id), Some(owner_1.clone())); + + // try to transfer non_existing_nft_id to owner 2 + let owner_2 = Address::generate(env); + let non_existing_nft_id = 0; + client + .mock_auths(&[MockAuth { + address: &owner_1, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "transfer", + args: ( + non_existing_nft_id.clone(), + owner_1.clone(), + owner_2.clone(), + ) + .into_val(env), + sub_invokes: &[], + }, + }]) + .transfer(&non_existing_nft_id, &owner_1, &owner_2); +} From 63ed1f4f0e2a5e3eb264517e38d147fdca8af0f1 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Fri, 26 Jul 2024 11:48:53 -0400 Subject: [PATCH 30/31] Factor out some commonly use fns in nft test --- .../loam-cli/examples/soroban/nft/src/test.rs | 310 ++++++------------ 1 file changed, 96 insertions(+), 214 deletions(-) diff --git a/crates/loam-cli/examples/soroban/nft/src/test.rs b/crates/loam-cli/examples/soroban/nft/src/test.rs index 854af3b0..32cee3ad 100644 --- a/crates/loam-cli/examples/soroban/nft/src/test.rs +++ b/crates/loam-cli/examples/soroban/nft/src/test.rs @@ -23,28 +23,28 @@ fn init() -> (Env, SorobanContractClient<'static>, Address) { (env, client, contract_id) } -#[test] -fn test_nft() { - let (env, client, contract_id) = &init(); - let admin = Address::generate(&env); - - // test admin_get and admin_set - assert!(client.admin_get().is_none(), "Admin already set"); +fn set_admin(env: &Env, client: &SorobanContractClient, contract_id: &Address, admin: &Address) { client .mock_auths(&[MockAuth { - address: &admin, + address: admin, invoke: &MockAuthInvoke { - contract: &contract_id, + contract: contract_id, fn_name: "admin_set", - args: (&admin,).into_val(env), + args: (admin,).into_val(env), sub_invokes: &[], }, }]) - .admin_set(&admin); - assert_eq!(client.admin_get().unwrap(), admin); + .admin_set(admin); +} - // test nft_init - let name = Bytes::from_slice(env, "nftexample".as_bytes()); +fn init_nft_contract( + env: &Env, + client: &SorobanContractClient, + contract_id: &Address, + admin: &Address, + name: &str, +) { + let name = Bytes::from_slice(env, name.as_bytes()); client .mock_auths(&[MockAuth { address: &admin, @@ -56,45 +56,79 @@ fn test_nft() { }, }]) .nft_init(&name); +} - assert_eq!(client.get_total_count(), 0); - assert!(client.get_nft(&1).is_none()); - assert!(client.get_owner(&1).is_none()); - assert_eq!(client.get_collection_by_owner(&admin).len(), 0); - - // test mint & getter fns - let owner_1 = Address::generate(env); - let metadata = Bytes::from_slice(env, "metadata".as_bytes()); - let nft_id = client +fn mint_nft( + env: &Env, + client: &SorobanContractClient, + contract_id: &Address, + admin: &Address, + owner: &Address, + metadata: &Bytes, +) -> u128 { + client .mock_auths(&[MockAuth { address: &admin, invoke: &MockAuthInvoke { contract: &contract_id, fn_name: "mint", - args: (owner_1.clone(), metadata.clone()).into_val(env), + args: (owner.clone(), metadata.clone()).into_val(env), sub_invokes: &[], }, }]) - .mint(&owner_1, &metadata); - assert_eq!(nft_id, 1); - assert_eq!(client.get_nft(&nft_id), Some(metadata)); - assert_eq!(client.get_owner(&nft_id), Some(owner_1.clone())); - assert_eq!(client.get_total_count(), 1); - assert_eq!(client.get_collection_by_owner(&owner_1).len(), 1); + .mint(&owner, &metadata) +} - // test transfer - let owner_2 = Address::generate(env); +fn transfer_nft( + env: &Env, + client: &SorobanContractClient, + contract_id: &Address, + nft_id: u128, + owner: &Address, + new_owner: &Address, +) { client .mock_auths(&[MockAuth { - address: &owner_1, + address: &owner, invoke: &MockAuthInvoke { contract: &contract_id, fn_name: "transfer", - args: (nft_id.clone(), owner_1.clone(), owner_2.clone()).into_val(env), + args: (nft_id.clone(), owner.clone(), new_owner.clone()).into_val(env), sub_invokes: &[], }, }]) - .transfer(&nft_id, &owner_1, &owner_2); + .transfer(&nft_id, &owner, &new_owner); +} + +#[test] +fn test_nft() { + let (env, client, contract_id) = &init(); + let admin = Address::generate(&env); + + // test admin_get and admin_set + assert!(client.admin_get().is_none(), "Admin already set"); + set_admin(env, client, contract_id, &admin); + assert_eq!(client.admin_get().unwrap(), admin); + + init_nft_contract(env, client, contract_id, &admin, "nftexample"); + assert_eq!(client.get_total_count(), 0); + assert!(client.get_nft(&1).is_none()); + assert!(client.get_owner(&1).is_none()); + assert_eq!(client.get_collection_by_owner(&admin).len(), 0); + + // test mint & getter fns + let owner_1 = Address::generate(env); + let metadata = Bytes::from_slice(env, "metadata".as_bytes()); + let nft_id = mint_nft(env, client, contract_id, &admin, &owner_1, &metadata); + assert_eq!(nft_id, 1); + assert_eq!(client.get_nft(&nft_id), Some(metadata)); + assert_eq!(client.get_owner(&nft_id), Some(owner_1.clone())); + assert_eq!(client.get_total_count(), 1); + assert_eq!(client.get_collection_by_owner(&owner_1).len(), 1); + + // test transfer + let owner_2 = Address::generate(env); + transfer_nft(env, client, contract_id, nft_id, &owner_1, &owner_2); assert_eq!(client.get_owner(&nft_id), Some(owner_2.clone())); assert_eq!(client.get_total_count(), 1); assert_eq!(client.get_collection_by_owner(&owner_1).len(), 0); @@ -107,18 +141,7 @@ fn test_initializing_without_admin_set() { let (env, client, contract_id) = &init(); let admin = Address::generate(env); // test nft_init - let name = Bytes::from_slice(env, "nftexample".as_bytes()); - client - .mock_auths(&[MockAuth { - address: &admin, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "nft_init", - args: (name.clone(),).into_val(env), - sub_invokes: &[], - }, - }]) - .nft_init(&name); + init_nft_contract(env, client, contract_id, &admin, "nftexample"); } #[test] @@ -129,107 +152,48 @@ fn test_minting_by_non_admin() { // set admin assert!(client.admin_get().is_none(), "Admin already set"); - client - .mock_auths(&[MockAuth { - address: &admin, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "admin_set", - args: (&admin,).into_val(env), - sub_invokes: &[], - }, - }]) - .admin_set(&admin); + set_admin(env, client, contract_id, &admin); assert!(client.admin_get().is_some(), "Admin not set"); // nft_init - let name = Bytes::from_slice(env, "nftexample".as_bytes()); - client - .mock_auths(&[MockAuth { - address: &admin, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "nft_init", - args: (name.clone(),).into_val(env), - sub_invokes: &[], - }, - }]) - .nft_init(&name); + init_nft_contract(env, client, contract_id, &admin, "nftexample"); assert_eq!(client.get_total_count(), 0); // try to mint from non-admin let non_admin = Address::generate(env); let metadata = Bytes::from_slice(env, "metadata".as_bytes()); - client - .mock_auths(&[MockAuth { - address: &non_admin, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "mint", - args: (non_admin.clone(), metadata.clone()).into_val(env), - sub_invokes: &[], - }, - }]) - .mint(&non_admin, &metadata); + mint_nft(env, client, contract_id, &non_admin, &non_admin, &metadata); } #[test] #[should_panic] +#[ignore = "This should panic, but it's not"] fn test_minting_without_contract_being_initialized() { let (env, client, contract_id) = &init(); let admin = Address::generate(&env); // set admin assert!(client.admin_get().is_none(), "Admin already set"); - client - .mock_auths(&[MockAuth { - address: &admin, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "admin_set", - args: (&admin,).into_val(env), - sub_invokes: &[], - }, - }]) - .admin_set(&admin); + set_admin(env, client, contract_id, &admin); assert!(client.admin_get().is_some(), "Admin not set"); // try to mint though the contract has not been initialized let owner_1 = Address::generate(env); let metadata = Bytes::from_slice(env, "metadata".as_bytes()); - client - .mock_auths(&[MockAuth { - address: &admin, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "mint", - args: (owner_1.clone(), metadata.clone()).into_val(env), - sub_invokes: &[], - }, - }]) - .mint(&owner_1, &metadata); + mint_nft(env, client, contract_id, &admin, &owner_1, &metadata); } #[test] #[should_panic] +#[ignore = "This should panic, but it's not"] fn test_getter_methods_before_initialization() { let (env, client, contract_id) = &init(); let admin = Address::generate(&env); // set admin assert!(client.admin_get().is_none(), "Admin already set"); - client - .mock_auths(&[MockAuth { - address: &admin, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "admin_set", - args: (&admin,).into_val(env), - sub_invokes: &[], - }, - }]) - .admin_set(&admin); + set_admin(env, client, contract_id, &admin); assert!(client.admin_get().is_some(), "Admin not set"); assert_eq!(client.get_total_count(), 0); @@ -243,34 +207,11 @@ fn test_transfer_by_non_owner() { // set admin assert!(client.admin_get().is_none(), "Admin already set"); - client - .mock_auths(&[MockAuth { - address: &admin, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "admin_set", - args: (&admin,).into_val(env), - sub_invokes: &[], - }, - }]) - .admin_set(&admin); - - assert_eq!(client.admin_get().unwrap(), admin); + set_admin(env, client, contract_id, &admin); + assert!(client.admin_get().is_some(), "Admin not set"); // nft_init - let name = Bytes::from_slice(env, "nftexample".as_bytes()); - client - .mock_auths(&[MockAuth { - address: &admin, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "nft_init", - args: (name.clone(),).into_val(env), - sub_invokes: &[], - }, - }]) - .nft_init(&name); - + init_nft_contract(env, client, contract_id, &admin, "nftexample"); assert_eq!(client.get_total_count(), 0); assert!(client.get_nft(&1).is_none()); assert!(client.get_owner(&1).is_none()); @@ -279,32 +220,12 @@ fn test_transfer_by_non_owner() { // mint nft to owner 1 let owner_1 = Address::generate(env); let metadata = Bytes::from_slice(env, "metadata".as_bytes()); - let nft_id = client - .mock_auths(&[MockAuth { - address: &admin, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "mint", - args: (owner_1.clone(), metadata.clone()).into_val(env), - sub_invokes: &[], - }, - }]) - .mint(&owner_1, &metadata); + let nft_id = mint_nft(env, client, contract_id, &admin, &owner_1, &metadata); assert_eq!(client.get_owner(&nft_id), Some(owner_1.clone())); // try to transfer nft by non-owner - let owner_2 = Address::generate(env); - client - .mock_auths(&[MockAuth { - address: &owner_2, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "transfer", - args: (nft_id.clone(), owner_2.clone(), owner_2.clone()).into_val(env), - sub_invokes: &[], - }, - }]) - .transfer(&nft_id, &owner_2, &owner_2); + let non_owner = Address::generate(env); + transfer_nft(env, client, contract_id, nft_id, &non_owner, &non_owner); } #[test] @@ -315,67 +236,28 @@ fn test_transferring_a_non_existent_nft() { // set admin assert!(client.admin_get().is_none(), "Admin already set"); - client - .mock_auths(&[MockAuth { - address: &admin, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "admin_set", - args: (&admin,).into_val(env), - sub_invokes: &[], - }, - }]) - .admin_set(&admin); - assert_eq!(client.admin_get().unwrap(), admin); + set_admin(env, client, contract_id, &admin); + assert!(client.admin_get().is_some(), "Admin not set"); // nft_init - let name = Bytes::from_slice(env, "nftexample".as_bytes()); - client - .mock_auths(&[MockAuth { - address: &admin, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "nft_init", - args: (name.clone(),).into_val(env), - sub_invokes: &[], - }, - }]) - .nft_init(&name); + init_nft_contract(env, client, contract_id, &admin, "nftexample"); // mint nft1 to owner 1 let owner_1 = Address::generate(env); let metadata = Bytes::from_slice(env, "metadata".as_bytes()); - let nft_id = client - .mock_auths(&[MockAuth { - address: &admin, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "mint", - args: (owner_1.clone(), metadata.clone()).into_val(env), - sub_invokes: &[], - }, - }]) - .mint(&owner_1, &metadata); + let nft_id = mint_nft(env, client, contract_id, &admin, &owner_1, &metadata); assert_eq!(nft_id, 1); assert_eq!(client.get_owner(&nft_id), Some(owner_1.clone())); // try to transfer non_existing_nft_id to owner 2 let owner_2 = Address::generate(env); let non_existing_nft_id = 0; - client - .mock_auths(&[MockAuth { - address: &owner_1, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "transfer", - args: ( - non_existing_nft_id.clone(), - owner_1.clone(), - owner_2.clone(), - ) - .into_val(env), - sub_invokes: &[], - }, - }]) - .transfer(&non_existing_nft_id, &owner_1, &owner_2); + transfer_nft( + env, + client, + contract_id, + non_existing_nft_id, + &owner_1, + &owner_2, + ); } From dc9ed7c64c18bc0075a74fe9c954c67a684ec4cb Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:19:32 -0400 Subject: [PATCH 31/31] Add Metadata type for NFT example --- .../loam-cli/examples/soroban/nft/src/lib.rs | 1 + .../loam-cli/examples/soroban/nft/src/nft.rs | 8 ++--- .../examples/soroban/nft/src/subcontract.rs | 21 +++++++---- .../loam-cli/examples/soroban/nft/src/test.rs | 36 +++++++++++++++---- 4 files changed, 48 insertions(+), 18 deletions(-) diff --git a/crates/loam-cli/examples/soroban/nft/src/lib.rs b/crates/loam-cli/examples/soroban/nft/src/lib.rs index c9e7ec2f..89d0e9aa 100644 --- a/crates/loam-cli/examples/soroban/nft/src/lib.rs +++ b/crates/loam-cli/examples/soroban/nft/src/lib.rs @@ -7,6 +7,7 @@ pub mod subcontract; use nft::MyNonFungibleToken; use subcontract::{Initable, NonFungible}; +use crate::subcontract::Metadata; #[derive_contract( Core(Admin), diff --git a/crates/loam-cli/examples/soroban/nft/src/nft.rs b/crates/loam-cli/examples/soroban/nft/src/nft.rs index 83e72140..8bd65aca 100644 --- a/crates/loam-cli/examples/soroban/nft/src/nft.rs +++ b/crates/loam-cli/examples/soroban/nft/src/nft.rs @@ -3,7 +3,7 @@ use loam_sdk::{ IntoKey, }; -use crate::{subcontract::{IsInitable, IsNonFungible}, Contract}; +use crate::{subcontract::{IsInitable, IsNonFungible, Metadata}, Contract}; #[contracttype] #[derive(IntoKey)] @@ -12,7 +12,7 @@ pub struct MyNonFungibleToken { total_count: u128, owners_to_nft_ids: Map>, // the owner's collection nft_ids_to_owners: Map, - nft_ids_to_metadata: Map, + nft_ids_to_metadata: Map, } impl MyNonFungibleToken { @@ -42,7 +42,7 @@ impl IsInitable for MyNonFungibleToken { } impl IsNonFungible for MyNonFungibleToken { - fn mint(&mut self, owner: Address, metadata: Bytes) -> u128 { + fn mint(&mut self, owner: Address, metadata: Metadata) -> u128 { Contract::require_auth(); let current_count = self.total_count; @@ -95,7 +95,7 @@ impl IsNonFungible for MyNonFungibleToken { self.owners_to_nft_ids.set(new_owner, new_owner_collection); } - fn get_nft(&self, id: u128) -> Option { + fn get_nft(&self, id: u128) -> Option { self.nft_ids_to_metadata.get(id) } diff --git a/crates/loam-cli/examples/soroban/nft/src/subcontract.rs b/crates/loam-cli/examples/soroban/nft/src/subcontract.rs index 3dd9e628..62b9613c 100644 --- a/crates/loam-cli/examples/soroban/nft/src/subcontract.rs +++ b/crates/loam-cli/examples/soroban/nft/src/subcontract.rs @@ -1,13 +1,20 @@ -use loam_sdk::{soroban_sdk::Lazy, subcontract}; +use loam_sdk::{ + soroban_sdk::{self, contracttype, Lazy, String}, + subcontract, +}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub struct Metadata { + pub(crate) name: String, + pub(crate) description: String, + pub(crate) url: String, +} #[subcontract] pub trait IsNonFungible { /// Mint a new NFT with the given owner and metadata - fn mint( - &mut self, - owner: loam_sdk::soroban_sdk::Address, - metadata: loam_sdk::soroban_sdk::Bytes, - ) -> u128; + fn mint(&mut self, owner: loam_sdk::soroban_sdk::Address, metadata: Metadata) -> u128; /// Transfer the NFT with the given ID from current_owner to new_owner fn transfer( @@ -18,7 +25,7 @@ pub trait IsNonFungible { ); /// Get the NFT with the given ID - fn get_nft(&self, id: u128) -> Option; + fn get_nft(&self, id: u128) -> Option; /// Find the owner of the NFT with the given ID fn get_owner(&self, id: u128) -> Option; diff --git a/crates/loam-cli/examples/soroban/nft/src/test.rs b/crates/loam-cli/examples/soroban/nft/src/test.rs index 32cee3ad..aa0483a3 100644 --- a/crates/loam-cli/examples/soroban/nft/src/test.rs +++ b/crates/loam-cli/examples/soroban/nft/src/test.rs @@ -1,11 +1,13 @@ #![cfg(test)] +use crate::subcontract::Metadata; + use super::{ SorobanContract__ as SorobanContract, SorobanContract__Client as SorobanContractClient, }; use loam_sdk::soroban_sdk::{ testutils::{Address as _, MockAuth, MockAuthInvoke}, - Address, Bytes, Env, IntoVal, + Address, Bytes, Env, IntoVal, String, }; extern crate std; @@ -64,7 +66,7 @@ fn mint_nft( contract_id: &Address, admin: &Address, owner: &Address, - metadata: &Bytes, + metadata: &Metadata, ) -> u128 { client .mock_auths(&[MockAuth { @@ -118,7 +120,11 @@ fn test_nft() { // test mint & getter fns let owner_1 = Address::generate(env); - let metadata = Bytes::from_slice(env, "metadata".as_bytes()); + let metadata = Metadata { + name: String::from_str(env, "Nft Name"), + description: String::from_str(env, "description"), + url: String::from_str(env, "url"), + }; let nft_id = mint_nft(env, client, contract_id, &admin, &owner_1, &metadata); assert_eq!(nft_id, 1); assert_eq!(client.get_nft(&nft_id), Some(metadata)); @@ -162,7 +168,11 @@ fn test_minting_by_non_admin() { // try to mint from non-admin let non_admin = Address::generate(env); - let metadata = Bytes::from_slice(env, "metadata".as_bytes()); + let metadata = Metadata { + name: String::from_str(env, "Nft Name"), + description: String::from_str(env, "description"), + url: String::from_str(env, "url"), + }; mint_nft(env, client, contract_id, &non_admin, &non_admin, &metadata); } @@ -180,7 +190,11 @@ fn test_minting_without_contract_being_initialized() { // try to mint though the contract has not been initialized let owner_1 = Address::generate(env); - let metadata = Bytes::from_slice(env, "metadata".as_bytes()); + let metadata = Metadata { + name: String::from_str(env, "Nft Name"), + description: String::from_str(env, "description"), + url: String::from_str(env, "url"), + }; mint_nft(env, client, contract_id, &admin, &owner_1, &metadata); } @@ -219,7 +233,11 @@ fn test_transfer_by_non_owner() { // mint nft to owner 1 let owner_1 = Address::generate(env); - let metadata = Bytes::from_slice(env, "metadata".as_bytes()); + let metadata = Metadata { + name: String::from_str(env, "Nft Name"), + description: String::from_str(env, "description"), + url: String::from_str(env, "url"), + }; let nft_id = mint_nft(env, client, contract_id, &admin, &owner_1, &metadata); assert_eq!(client.get_owner(&nft_id), Some(owner_1.clone())); @@ -244,7 +262,11 @@ fn test_transferring_a_non_existent_nft() { // mint nft1 to owner 1 let owner_1 = Address::generate(env); - let metadata = Bytes::from_slice(env, "metadata".as_bytes()); + let metadata = Metadata { + name: String::from_str(env, "Nft Name"), + description: String::from_str(env, "description"), + url: String::from_str(env, "url"), + }; let nft_id = mint_nft(env, client, contract_id, &admin, &owner_1, &metadata); assert_eq!(nft_id, 1); assert_eq!(client.get_owner(&nft_id), Some(owner_1.clone()));