From 0651fb18dc4dd035243afecb40b1226ec3f16710 Mon Sep 17 00:00:00 2001 From: garikbesson Date: Mon, 30 Dec 2024 15:14:51 +0100 Subject: [PATCH] check royaties don't exceed 100% while minting --- integration-tests/src/tests.rs | 296 ++++++++++++++++++++++++++++----- nft-series/src/series.rs | 10 ++ 2 files changed, 264 insertions(+), 42 deletions(-) diff --git a/integration-tests/src/tests.rs b/integration-tests/src/tests.rs index 3021568..c10d6e7 100644 --- a/integration-tests/src/tests.rs +++ b/integration-tests/src/tests.rs @@ -53,9 +53,17 @@ async fn main() -> Result<(), Box> { test_nft_mint_call(&owner, &alice, &nft_contract).await?; test_nft_approve_call(&bob, &nft_contract, &market_contract).await?; test_sell_nft_listed_on_marketplace(&alice, &nft_contract, &market_contract, &bob).await?; - test_transfer_nft_when_listed_on_marketplace(&alice, &bob, &charlie, &nft_contract, &market_contract).await?; + test_transfer_nft_when_listed_on_marketplace( + &alice, + &bob, + &charlie, + &nft_contract, + &market_contract, + ) + .await?; test_approval_revoke(&alice, &bob, &nft_contract, &market_contract).await?; test_reselling_and_royalties(&alice, &bob, &charlie, &nft_contract, &market_contract).await?; + test_royalties_exceeding_100_percents(&alice, &nft_contract, &market_contract).await?; Ok(()) } @@ -99,7 +107,8 @@ async fn test_nft_mint_call( }, }); - let _ = user.call(contract.id(), "nft_mint") + let _ = user + .call(contract.id(), "nft_mint") .args_json(request_payload) .deposit(NearToken::from_millinear(80)) .transact() @@ -113,15 +122,15 @@ async fn test_nft_mint_call( .json()?; let expected = json!([ - { + { "approved_account_ids": {}, "royalty": {}, "token_id": "1", "owner_id": user.id(), "metadata": { - "expires_at": serde_json::Value::Null, - "extra": serde_json::Value::Null, - "issued_at": serde_json::Value::Null, + "expires_at": serde_json::Value::Null, + "extra": serde_json::Value::Null, + "issued_at": serde_json::Value::Null, "copies": serde_json::Value::Null, "media_hash": serde_json::Value::Null, "reference": serde_json::Value::Null, @@ -159,7 +168,7 @@ async fn test_nft_approve_call( .transact() .await? .json()?; - + assert_eq!(result, true); println!(" Passed ✅ test_nft_approve_call"); Ok(()) @@ -176,21 +185,53 @@ async fn test_sell_nft_listed_on_marketplace( let sale_price: NearToken = NearToken::from_yoctonear(10000000000000000000000000); helpers::mint_nft(seller, nft_contract, token_id).await?; - helpers::pay_for_storage(seller, market_contract, NearToken::from_yoctonear(1000000000000000000000000)).await?; + helpers::pay_for_storage( + seller, + market_contract, + NearToken::from_yoctonear(1000000000000000000000000), + ) + .await?; helpers::approve_nft(market_contract, seller, nft_contract, token_id).await?; - helpers::place_nft_for_sale(seller, market_contract, nft_contract, token_id, approval_id, &sale_price).await?; + helpers::place_nft_for_sale( + seller, + market_contract, + nft_contract, + token_id, + approval_id, + &sale_price, + ) + .await?; let before_seller_balance: NearToken = helpers::get_user_balance(seller).await; let before_buyer_balance: NearToken = helpers::get_user_balance(buyer).await; - helpers::purchase_listed_nft(buyer, market_contract, nft_contract, token_id, sale_price).await?; + helpers::purchase_listed_nft(buyer, market_contract, nft_contract, token_id, sale_price) + .await?; let after_seller_balance: NearToken = helpers::get_user_balance(seller).await; let after_buyer_balance: NearToken = helpers::get_user_balance(buyer).await; - let dp = 1; // being exact requires keeping track of gas usage - assert_eq!(helpers::round_to_near_dp(after_seller_balance.as_yoctonear(), dp), helpers::round_to_near_dp(before_seller_balance.saturating_add(sale_price).as_yoctonear(), dp), "seller did not receive the sale price"); - assert_eq!(helpers::round_to_near_dp(after_buyer_balance.as_yoctonear(), dp), helpers::round_to_near_dp(before_buyer_balance.saturating_sub(sale_price).as_yoctonear(), dp), "buyer did not send the sale price"); + let dp = 1; // being exact requires keeping track of gas usage + assert_eq!( + helpers::round_to_near_dp(after_seller_balance.as_yoctonear(), dp), + helpers::round_to_near_dp( + before_seller_balance + .saturating_add(sale_price) + .as_yoctonear(), + dp + ), + "seller did not receive the sale price" + ); + assert_eq!( + helpers::round_to_near_dp(after_buyer_balance.as_yoctonear(), dp), + helpers::round_to_near_dp( + before_buyer_balance + .saturating_sub(sale_price) + .as_yoctonear(), + dp + ), + "buyer did not send the sale price" + ); println!(" Passed ✅ test_sell_nft_listed_on_marketplace"); Ok(()) @@ -210,26 +251,53 @@ async fn test_transfer_nft_when_listed_on_marketplace( helpers::mint_nft(seller, nft_contract, token_id).await?; helpers::pay_for_storage(seller, market_contract, NearToken::from_millinear(10)).await?; helpers::approve_nft(market_contract, seller, nft_contract, token_id).await?; - helpers::place_nft_for_sale(seller, market_contract, nft_contract, token_id, approval_id, &sale_price).await?; + helpers::place_nft_for_sale( + seller, + market_contract, + nft_contract, + token_id, + approval_id, + &sale_price, + ) + .await?; helpers::transfer_nft(seller, first_buyer, nft_contract, token_id).await?; // attempt purchase NFT let before_seller_balance: NearToken = helpers::get_user_balance(seller).await; let before_buyer_balance: NearToken = helpers::get_user_balance(second_buyer).await; - helpers::purchase_listed_nft(second_buyer, market_contract, nft_contract, token_id, sale_price).await?; + helpers::purchase_listed_nft( + second_buyer, + market_contract, + nft_contract, + token_id, + sale_price, + ) + .await?; let after_seller_balance: NearToken = helpers::get_user_balance(seller).await; let after_buyer_balance: NearToken = helpers::get_user_balance(second_buyer).await; // assert owner remains first_buyer let token_info: serde_json::Value = helpers::get_nft_token_info(nft_contract, token_id).await?; let owner_id: String = token_info["owner_id"].as_str().unwrap().to_string(); - assert_eq!(owner_id, first_buyer.id().to_string(), "token owner is not first_buyer"); + assert_eq!( + owner_id, + first_buyer.id().to_string(), + "token owner is not first_buyer" + ); // assert balances remain equal - let dp = 1; // being exact requires keeping track of gas usage - assert_eq!(helpers::round_to_near_dp(after_seller_balance.as_yoctonear(), dp), helpers::round_to_near_dp(before_seller_balance.as_yoctonear(), dp), "seller balance changed"); - assert_eq!(helpers::round_to_near_dp(after_buyer_balance.as_yoctonear(), dp), helpers::round_to_near_dp(before_buyer_balance.as_yoctonear(), dp), "buyer balance changed"); + let dp = 1; // being exact requires keeping track of gas usage + assert_eq!( + helpers::round_to_near_dp(after_seller_balance.as_yoctonear(), dp), + helpers::round_to_near_dp(before_seller_balance.as_yoctonear(), dp), + "seller balance changed" + ); + assert_eq!( + helpers::round_to_near_dp(after_buyer_balance.as_yoctonear(), dp), + helpers::round_to_near_dp(before_buyer_balance.as_yoctonear(), dp), + "buyer balance changed" + ); println!(" Passed ✅ test_transfer_nft_when_listed_on_marketplace"); @@ -248,14 +316,23 @@ async fn test_approval_revoke( helpers::mint_nft(first_user, nft_contract, token_id).await?; helpers::pay_for_storage(first_user, market_contract, NearToken::from_millinear(10)).await?; - helpers::place_nft_for_sale(first_user, market_contract, nft_contract, token_id, approval_id, &sale_price).await?; + helpers::place_nft_for_sale( + first_user, + market_contract, + nft_contract, + token_id, + approval_id, + &sale_price, + ) + .await?; // nft_revoke market_contract call let revoke_payload = json!({ "account_id": market_contract.id(), "token_id": token_id, }); - let _ = first_user.call(nft_contract.id(), "nft_revoke") + let _ = first_user + .call(nft_contract.id(), "nft_revoke") .args_json(revoke_payload) .deposit(helpers::ONE_YOCTO_NEAR) .transact() @@ -265,20 +342,37 @@ async fn test_approval_revoke( let before_seller_balance: NearToken = helpers::get_user_balance(first_user).await; let before_buyer_balance: NearToken = helpers::get_user_balance(second_user).await; helpers::purchase_listed_nft( - second_user, market_contract, nft_contract, token_id, sale_price - ).await?; + second_user, + market_contract, + nft_contract, + token_id, + sale_price, + ) + .await?; let after_seller_balance: NearToken = helpers::get_user_balance(first_user).await; let after_buyer_balance: NearToken = helpers::get_user_balance(second_user).await; // assert owner remains first_user let token_info: serde_json::Value = helpers::get_nft_token_info(nft_contract, token_id).await?; let owner_id: String = token_info["owner_id"].as_str().unwrap().to_string(); - assert_eq!(owner_id, first_user.id().to_string(), "token owner is not first_user"); + assert_eq!( + owner_id, + first_user.id().to_string(), + "token owner is not first_user" + ); // assert balances unchanged - let dp = 1; // being exact requires keeping track of gas usage - assert_eq!(helpers::round_to_near_dp(after_seller_balance.as_yoctonear(), dp), helpers::round_to_near_dp(before_seller_balance.as_yoctonear(), dp), "seller balance changed"); - assert_eq!(helpers::round_to_near_dp(after_buyer_balance.as_yoctonear(), dp), helpers::round_to_near_dp(before_buyer_balance.as_yoctonear(), dp), "buyer balance changed"); + let dp = 1; // being exact requires keeping track of gas usage + assert_eq!( + helpers::round_to_near_dp(after_seller_balance.as_yoctonear(), dp), + helpers::round_to_near_dp(before_seller_balance.as_yoctonear(), dp), + "seller balance changed" + ); + assert_eq!( + helpers::round_to_near_dp(after_buyer_balance.as_yoctonear(), dp), + helpers::round_to_near_dp(before_buyer_balance.as_yoctonear(), dp), + "buyer balance changed" + ); println!(" Passed ✅ test_approval_revoke"); Ok(()) @@ -293,7 +387,7 @@ async fn test_reselling_and_royalties( ) -> Result<(), Box> { let token_id = "7"; let approval_id = 0; - let sale_price: NearToken = NearToken::from_near(1); // 1 NEAR in yoctoNEAR + let sale_price: NearToken = NearToken::from_near(1); // 1 NEAR in yoctoNEAR // mint with royalties let request_payload = json!({ @@ -308,7 +402,8 @@ async fn test_reselling_and_royalties( user.id().to_string(): 2000 as u128 } }); - let _ = user.call(nft_contract.id(), "nft_mint") + let _ = user + .call(nft_contract.id(), "nft_mint") .args_json(request_payload) .deposit(NearToken::from_yoctonear(helpers::DEFAULT_DEPOSIT)) .transact() @@ -316,37 +411,89 @@ async fn test_reselling_and_royalties( helpers::pay_for_storage(user, market_contract, NearToken::from_millinear(10)).await?; helpers::approve_nft(market_contract, user, nft_contract, token_id).await?; - helpers::place_nft_for_sale(user, market_contract, nft_contract, token_id, approval_id, &sale_price).await?; + helpers::place_nft_for_sale( + user, + market_contract, + nft_contract, + token_id, + approval_id, + &sale_price, + ) + .await?; // first_buyer purchases NFT let mut before_seller_balance: NearToken = helpers::get_user_balance(user).await; let mut before_buyer_balance: NearToken = helpers::get_user_balance(first_buyer).await; - helpers::purchase_listed_nft(first_buyer, market_contract, nft_contract, token_id, sale_price).await?; + helpers::purchase_listed_nft( + first_buyer, + market_contract, + nft_contract, + token_id, + sale_price, + ) + .await?; let mut after_seller_balance: NearToken = helpers::get_user_balance(user).await; let mut after_buyer_balance: NearToken = helpers::get_user_balance(first_buyer).await; // assert owner becomes first_buyer let token_info: serde_json::Value = helpers::get_nft_token_info(nft_contract, token_id).await?; let owner_id: String = token_info["owner_id"].as_str().unwrap().to_string(); - assert_eq!(owner_id, first_buyer.id().to_string(), "token owner is not first_buyer"); + assert_eq!( + owner_id, + first_buyer.id().to_string(), + "token owner is not first_buyer" + ); // assert balances changed - let dp = 1; // being exact requires keeping track of gas usage - assert_eq!(helpers::round_to_near_dp(after_seller_balance.as_yoctonear(), dp), helpers::round_to_near_dp(before_seller_balance.saturating_add(sale_price).as_yoctonear(), dp), "seller balance unchanged"); - assert_eq!(helpers::round_to_near_dp(after_buyer_balance.as_yoctonear(), dp), helpers::round_to_near_dp(before_buyer_balance.saturating_sub(sale_price).as_yoctonear(), dp), "buyer balance unchanged"); + let dp = 1; // being exact requires keeping track of gas usage + assert_eq!( + helpers::round_to_near_dp(after_seller_balance.as_yoctonear(), dp), + helpers::round_to_near_dp( + before_seller_balance + .saturating_add(sale_price) + .as_yoctonear(), + dp + ), + "seller balance unchanged" + ); + assert_eq!( + helpers::round_to_near_dp(after_buyer_balance.as_yoctonear(), dp), + helpers::round_to_near_dp( + before_buyer_balance + .saturating_sub(sale_price) + .as_yoctonear(), + dp + ), + "buyer balance unchanged" + ); // first buyer lists nft for sale let approval_id = 1; helpers::pay_for_storage(first_buyer, market_contract, NearToken::from_millinear(10)).await?; helpers::approve_nft(market_contract, first_buyer, nft_contract, token_id).await?; - helpers::place_nft_for_sale(first_buyer, market_contract, nft_contract, token_id, approval_id, &sale_price).await?; + helpers::place_nft_for_sale( + first_buyer, + market_contract, + nft_contract, + token_id, + approval_id, + &sale_price, + ) + .await?; // second_buyer purchases NFT - let resale_price = sale_price.saturating_mul(5); // 15 NEAR + let resale_price = sale_price.saturating_mul(5); // 15 NEAR before_seller_balance = helpers::get_user_balance(first_buyer).await; before_buyer_balance = helpers::get_user_balance(second_buyer).await; let before_user_balance: NearToken = helpers::get_user_balance(user).await; - helpers::purchase_listed_nft(second_buyer, market_contract, nft_contract, token_id, resale_price).await?; + helpers::purchase_listed_nft( + second_buyer, + market_contract, + nft_contract, + token_id, + resale_price, + ) + .await?; let after_user_balance: NearToken = helpers::get_user_balance(user).await; after_seller_balance = helpers::get_user_balance(first_buyer).await; after_buyer_balance = helpers::get_user_balance(second_buyer).await; @@ -354,14 +501,79 @@ async fn test_reselling_and_royalties( // assert owner changes to second_buyer let token_info: serde_json::Value = helpers::get_nft_token_info(nft_contract, token_id).await?; let owner_id: String = token_info["owner_id"].as_str().unwrap().to_string(); - assert_eq!(owner_id, second_buyer.id().to_string(), "token owner is not second_buyer"); + assert_eq!( + owner_id, + second_buyer.id().to_string(), + "token owner is not second_buyer" + ); // assert balances changed let royalty_fee = resale_price.saturating_div(5); - assert_eq!(helpers::round_to_near_dp(after_seller_balance.as_yoctonear(), dp), helpers::round_to_near_dp(before_seller_balance.saturating_add(resale_price).saturating_sub(royalty_fee).as_yoctonear(), dp), "seller balance unchanged"); - assert_eq!(helpers::round_to_near_dp(after_buyer_balance.as_yoctonear(), dp), helpers::round_to_near_dp(before_buyer_balance.saturating_sub(resale_price).as_yoctonear(), dp), "buyer balance unchanged"); - assert_eq!(helpers::round_to_near_dp(after_user_balance.as_yoctonear(), dp), helpers::round_to_near_dp(before_user_balance.saturating_add(royalty_fee).as_yoctonear(), dp), "user balance unchanged"); + assert_eq!( + helpers::round_to_near_dp(after_seller_balance.as_yoctonear(), dp), + helpers::round_to_near_dp( + before_seller_balance + .saturating_add(resale_price) + .saturating_sub(royalty_fee) + .as_yoctonear(), + dp + ), + "seller balance unchanged" + ); + assert_eq!( + helpers::round_to_near_dp(after_buyer_balance.as_yoctonear(), dp), + helpers::round_to_near_dp( + before_buyer_balance + .saturating_sub(resale_price) + .as_yoctonear(), + dp + ), + "buyer balance unchanged" + ); + assert_eq!( + helpers::round_to_near_dp(after_user_balance.as_yoctonear(), dp), + helpers::round_to_near_dp( + before_user_balance + .saturating_add(royalty_fee) + .as_yoctonear(), + dp + ), + "user balance unchanged" + ); println!(" Passed ✅ test_reselling_and_royalties"); Ok(()) } + +async fn test_royalties_exceeding_100_percents( + user: &Account, + nft_contract: &Contract, + market_contract: &Contract, +) -> Result<(), Box> { + let token_id = "7"; + + // mint with royalties + let request_payload = json!({ + "token_id": token_id, + "receiver_id": user.id(), + "metadata": { + "title": "Grumpy Cat", + "description": "Not amused.", + "media": "https://www.adamsdrafting.com/wp-content/uploads/2018/06/More-Grumpy-Cat.jpg" + }, + "perpetual_royalties": { + user.id().to_string(): 5000 as u128, + market_contract.id().to_string(): 6000 as u128 + } + }); + let minting_result = user + .call(nft_contract.id(), "nft_mint") + .args_json(request_payload) + .deposit(NearToken::from_yoctonear(helpers::DEFAULT_DEPOSIT)) + .transact() + .await?; + assert!(minting_result.is_failure()); + + println!(" Passed ✅ test_royalties_exceeding_100_percents"); + Ok(()) +} diff --git a/nft-series/src/series.rs b/nft-series/src/series.rs index 6b82395..0b119c2 100644 --- a/nft-series/src/series.rs +++ b/nft-series/src/series.rs @@ -25,6 +25,16 @@ impl Contract { "only approved creators can add a type" ); + // Check that the total royalty amount does not exceed 100% + if !royalty.is_none() { + let mut total_royalty = 0; + + for (_, v) in royalty.clone().unwrap().iter() { + total_royalty += *v; + } + require!(total_royalty <= 100, "total royalty can't exceed 100%"); + } + // Insert the series and ensure it doesn't already exist require!( self.series_by_id