diff --git a/README.md b/README.md index 4d120a025..763790237 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ want to disable default initialization, then you can prohibit it like this: ```rust impl Default for StatusMessage { fn default() -> Self { - near_sdk::env::panic_str("Contract should be initialized before the usage.") + near_sdk::env::panic_err(near_sdk::errors::ContractNotInitialized{}.into()) } } ``` @@ -154,7 +154,7 @@ pub fn my_method(&mut self) { pub fn my_method(&mut self ) { if near_sdk::env::current_account_id() != near_sdk::env::predecessor_account_id() { - near_sdk::env::panic_str("Method my_method is private"); + near_sdk::env::panic_err(near_sdk::errors::PrivateMethod::new("my_method")); } ... } diff --git a/examples/callback-results/src/lib.rs b/examples/callback-results/src/lib.rs index 1faa13e46..ca8fd6145 100644 --- a/examples/callback-results/src/lib.rs +++ b/examples/callback-results/src/lib.rs @@ -1,5 +1,6 @@ -use near_sdk::require; -use near_sdk::{env, near, Promise, PromiseError}; +use near_sdk::require_or_err; +use near_sdk::{env, near, BaseError, Promise, PromiseError}; +use near_sdk::errors::{InvalidArgument, UnexpectedFailure, InvalidPromiseReturn}; const A_VALUE: u8 = 8; @@ -25,24 +26,28 @@ impl Callback { /// Returns a static string if fail is false, return #[private] - pub fn b(fail: bool) -> &'static str { + pub fn b(fail: bool) -> Result<&'static str, BaseError> { if fail { - env::panic_str("failed within function b"); + return Err(UnexpectedFailure { + message: "Failed within function b".to_string(), + } + .into()); } - "Some string" + Ok("Some string") } /// Panics if value is 0, returns the value passed in otherwise. #[private] - pub fn c(value: u8) -> u8 { - require!(value > 0, "Value must be positive"); - value + pub fn c(value: u8) -> Result { + require_or_err!(value > 0, InvalidArgument::new("Value must be positive")); + Ok(value) } /// Panics if value is 0. #[private] - pub fn d(value: u8) { - require!(value > 0, "Value must be positive"); + pub fn d(value: u8) -> Result<(), InvalidArgument> { + require_or_err!(value > 0, InvalidArgument::new("Value must be positive")); + Ok(()) } /// Receives the callbacks from the other promises called. @@ -52,12 +57,12 @@ impl Callback { #[callback_result] b: Result, #[callback_result] c: Result, #[callback_result] d: Result<(), PromiseError>, - ) -> (bool, bool, bool) { - require!(a == A_VALUE, "Promise returned incorrect value"); + ) -> Result<(bool, bool, bool), BaseError> { + require_or_err!(a == A_VALUE, InvalidPromiseReturn::new("Promise returned incorrect value")); if let Ok(s) = b.as_ref() { - require!(s == "Some string"); + require_or_err!(s == "Some string"); } - (b.is_err(), c.is_err(), d.is_err()) + Ok((b.is_err(), c.is_err(), d.is_err())) } } diff --git a/examples/cross-contract-calls/low-level/src/lib.rs b/examples/cross-contract-calls/low-level/src/lib.rs index 0a5e1533b..ed362d2ac 100644 --- a/examples/cross-contract-calls/low-level/src/lib.rs +++ b/examples/cross-contract-calls/low-level/src/lib.rs @@ -1,5 +1,6 @@ use near_sdk::serde_json; use near_sdk::{env, near, require, Gas, NearToken, PromiseResult}; +use near_sdk::errors::PromiseFailed; // Prepaid gas for a single (not inclusive of recursion) `factorial` call. const FACTORIAL_CALL_GAS: Gas = Gas::from_tgas(20); @@ -45,7 +46,7 @@ impl CrossContract { require!(env::promise_results_count() == 1); let cur = match env::promise_result(0) { PromiseResult::Successful(x) => serde_json::from_slice::(&x).unwrap(), - _ => env::panic_str("Promise with index 0 failed"), + _ => env::panic_err(PromiseFailed::new(Some(0), None).into()), }; env::value_return(&serde_json::to_vec(&(cur * n)).unwrap()); } diff --git a/examples/error-handling/.cargo/config.toml b/examples/error-handling/.cargo/config.toml new file mode 100644 index 000000000..4a9f7c79c --- /dev/null +++ b/examples/error-handling/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.wasm32-unknown-unknown] +rustflags = ["-C", "link-arg=-s"] + +[build] +target-dir = "../../target" diff --git a/examples/error-handling/Cargo.toml b/examples/error-handling/Cargo.toml new file mode 100644 index 000000000..748f4d323 --- /dev/null +++ b/examples/error-handling/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "error-handling" +version = "0.1.0" +authors = ["Near Inc "] +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +near-sdk = { path = "../../near-sdk" } + +[dev-dependencies] +near-workspaces = { version = "0.9.0", default-features = false, features = ["install"] } +test-case = "2.0" +tokio = { version = "1.14", features = ["full"] } +anyhow = "1.0" + +[profile.release] +codegen-units = 1 +# Tell `rustc` to optimize for small code size. +opt-level = "z" +lto = true +debug = false +panic = "abort" diff --git a/examples/error-handling/README.md b/examples/error-handling/README.md new file mode 100644 index 000000000..04f601aab --- /dev/null +++ b/examples/error-handling/README.md @@ -0,0 +1,37 @@ +# error-handling + +cargo-near-new-project-description + +## How to Build Locally? + +Install [`cargo-near`](https://github.com/near/cargo-near) and run: + +```bash +cargo near build +``` + +## How to Test Locally? + +```bash +cargo test +``` + +## How to Deploy? + +Deployment is automated with GitHub Actions CI/CD pipeline. +To deploy manually, install [`cargo-near`](https://github.com/near/cargo-near) and run: + +```bash +cargo near deploy +``` + +## Useful Links + +- [cargo-near](https://github.com/near/cargo-near) - NEAR smart contract development toolkit for Rust +- [near CLI](https://near.cli.rs) - Interact with NEAR blockchain from command line +- [NEAR Rust SDK Documentation](https://docs.near.org/sdk/rust/introduction) +- [NEAR Documentation](https://docs.near.org) +- [NEAR StackOverflow](https://stackoverflow.com/questions/tagged/nearprotocol) +- [NEAR Discord](https://near.chat) +- [NEAR Telegram Developers Community Group](https://t.me/neardev) +- NEAR DevHub: [Telegram](https://t.me/neardevhub), [Twitter](https://twitter.com/neardevhub) diff --git a/examples/error-handling/build.sh b/examples/error-handling/build.sh new file mode 100755 index 000000000..8d0bd04ea --- /dev/null +++ b/examples/error-handling/build.sh @@ -0,0 +1,7 @@ +#!/bin/bash +TARGET="${CARGO_TARGET_DIR:-../../target}" +set -e +cd "$(dirname $0)" + +cargo build --all --target wasm32-unknown-unknown --release +cp $TARGET/wasm32-unknown-unknown/release/error_handling.wasm ./res/ diff --git a/examples/error-handling/src/lib.rs b/examples/error-handling/src/lib.rs new file mode 100644 index 000000000..d2519a716 --- /dev/null +++ b/examples/error-handling/src/lib.rs @@ -0,0 +1,155 @@ +// Find all our documentation at https://docs.near.org +use near_sdk::contract_error; +use near_sdk::near; +use near_sdk::BaseError; + +#[contract_error] +pub enum MyErrorEnum { + X, +} + +#[contract_error(sdk)] +pub struct MyErrorStruct { + x: u32, +} + +#[near(contract_state)] +#[derive(Default)] +pub struct Contract { + value: u32, +} + +#[near] +impl Contract { + #[init] + pub fn new() -> Self { + Self { value: 0 } + } + + // Examples of RPC response for function call: + // is_error = false + // --- Result ------------------------- + // 1 + // ------------------------------------ + // (changes value from 0 to 1) + // + // is_error = true + // Failed transaction + // Error: + // 0: Error: An error occurred during a `FunctionCall` Action, parameter is debug message. + // ExecutionError("Smart contract panicked: error in inc_handle_result") + // (does not change value) + #[handle_result] + pub fn inc_handle_result(&mut self, is_error: bool) -> Result { + self.value += 1; + if is_error { + Err("error in inc_handle_result") + } else { + Ok(self.value) + } + } + + // Examples of RPC response for function call: + // is_error = false + // --- Result ------------------------- + // 2 + // ------------------------------------ + // (changes value from 1 to 2) + // + // is_error = true + // Failed transaction + // Error: + // 0: Error: An error occurred during a `FunctionCall` Action, parameter is debug message. + // ExecutionError("Smart contract panicked: {\"error\":{\"error_type\":\"error_handling::MyErrorEnum\",\"value\":\"X\"}}") + // (changes value from 2 to 3) + #[persist_on_error] + pub fn inc_persist_on_err(&mut self, is_error: bool) -> Result { + self.value += 1; + if is_error { + Err(MyErrorEnum::X) + } else { + Ok(self.value) + } + } + + // Examples of RPC response for function call: + // is_error = false + // --- Result ------------------------- + // 4 + // ------------------------------------ + // (changes value from 3 to 4) + // + // is_error = true + // Failed transaction + // Error: + // 0: Error: An error occurred during a `FunctionCall` Action, parameter is debug message. + // ExecutionError("Smart contract panicked: {\"error\":{\"error_type\":\"error_handling::MyErrorStruct\",\"value\":{\"x\":5}}}") + // (does not change value) + pub fn inc_just_result(&mut self, is_error: bool) -> Result { + self.value += 1; + if is_error { + Err(MyErrorStruct { x: 5 }) + } else { + Ok(self.value) + } + } + + // Examples of RPC response for function call: + // is_error = false + // --- Result ------------------------- + // 5 + // ------------------------------------ + // (changes value from 4 to 5) + // + // is_error = true + // Failed transaction + // Error: + // 0: Error: An error occurred during a `FunctionCall` Action, parameter is debug message. + // ExecutionError("Smart contract panicked: Error") + // (does not change value) + pub fn inc_just_simple(&mut self, is_error: bool) -> u32 { + self.value += 1; + if is_error { + ::near_sdk::env::panic_str("Error"); + } else { + self.value + } + } + + // Examples of RPC response for function call: + // is_error = false + // --- Result ------------------------- + // 6 + // ------------------------------------ + // (changes value from 5 to 6) + // + // is_error = true + // Failed transaction + // Error: + // 0: Error: An error occurred during a `FunctionCall` Action, parameter is debug message. + // ExecutionError("Smart contract panicked: {\\\"error\\\":{\\\"cause\\\":{\\\"info\\\":{\\\"error\\\":{\\\"x\\\":5}},\\\"name\\\":\\\"near_sdk::utils::contract_error::BaseError\\\"},\\\"name\\\":\\\"CUSTOM_CONTRACT_ERROR\\\"}}") + // (does not change value) + pub fn inc_base_error(&mut self, is_error: bool) -> Result { + self.value += 1; + if is_error { + Err(MyErrorStruct { x: 5 }.into()) + } else { + Ok(self.value) + } + } + + // Does not compile as u64 is not marked with contract_error + // > the trait `ContractErrorTrait` is not implemented for `u64` + // pub fn inc_incorrect_result_type(&mut self, is_error: bool) -> Result { + // self.value += 1; + // if is_error { + // Err(0) + // } else { + // Ok(self.value) + // } + // } + + pub fn get_value(&self) -> u32 { + self.value + } +} diff --git a/examples/error-handling/tests/test_basics.rs b/examples/error-handling/tests/test_basics.rs new file mode 100644 index 000000000..44b14968d --- /dev/null +++ b/examples/error-handling/tests/test_basics.rs @@ -0,0 +1,59 @@ +use near_sdk::serde_json; +use near_workspaces::Contract; +use serde_json::json; + +async fn get_value(contract: &Contract) -> anyhow::Result { + let get_value: serde_json::Value = + contract.call("get_value").args_json(json!({})).view().await?.json()?; + + println!("get_value: {:?}", get_value); + + get_value.as_u64().ok_or_else(|| anyhow::anyhow!("get_value is not a u64")) +} + +async fn check_call( + contract: &Contract, + method: &str, + is_error: bool, + expected_value: u64, + expected_error: Option, +) { + let res = contract + .call(method) + .args_json(json!({ "is_error": is_error })) + .max_gas() + .transact() + .await + .unwrap(); + if is_error { + assert!(res.is_failure()); + if let Some(expected_error) = expected_error { + let string_error = + format!("{:?}", res.failures()[0].clone().into_result().unwrap_err()); + assert_eq!(string_error, expected_error); + } + } else { + assert!(res.is_success()); + } + assert_eq!(get_value(&contract).await.unwrap(), expected_value); +} + +#[tokio::test] +async fn test_error_handling() -> anyhow::Result<()> { + let worker = near_workspaces::sandbox().await?; + let contract = + worker.dev_deploy(&std::fs::read(format!("res/{}.wasm", "error_handling"))?).await?; + + check_call(&contract, "inc_handle_result", false, 1, None).await; + check_call(&contract, "inc_persist_on_err", false, 2, None).await; + check_call(&contract, "inc_just_result", false, 3, None).await; + check_call(&contract, "inc_just_simple", false, 4, None).await; + check_call(&contract, "inc_base_error", false, 5, None).await; + check_call(&contract, "inc_handle_result", true, 5, None).await; + check_call(&contract, "inc_persist_on_err", true, 6, Some("Error { repr: Custom { kind: Execution, error: ActionError(ActionError { index: Some(0), kind: FunctionCallError(ExecutionError(\"Smart contract panicked: {\\\"error\\\":{\\\"cause\\\":{\\\"info\\\":\\\"X\\\",\\\"name\\\":\\\"error_handling::MyErrorEnum\\\"},\\\"name\\\":\\\"CUSTOM_CONTRACT_ERROR\\\"}}\")) }) } }".to_string())).await; + check_call(&contract, "inc_just_result", true, 6, Some("Error { repr: Custom { kind: Execution, error: ActionError(ActionError { index: Some(0), kind: FunctionCallError(ExecutionError(\"Smart contract panicked: {\\\"error\\\":{\\\"cause\\\":{\\\"info\\\":{\\\"x\\\":5},\\\"name\\\":\\\"error_handling::MyErrorStruct\\\"},\\\"name\\\":\\\"SDK_CONTRACT_ERROR\\\"}}\")) }) } }".to_string())).await; + check_call(&contract, "inc_base_error", true, 6, Some("Error { repr: Custom { kind: Execution, error: ActionError(ActionError { index: Some(0), kind: FunctionCallError(ExecutionError(\"Smart contract panicked: {\\\"error\\\":{\\\"cause\\\":{\\\"info\\\":{\\\"error\\\":{\\\"x\\\":5}},\\\"name\\\":\\\"near_sdk::utils::contract_error::BaseError\\\"},\\\"name\\\":\\\"CUSTOM_CONTRACT_ERROR\\\"}}\")) }) } }".to_string())).await; + check_call(&contract, "inc_just_simple", true, 6, None).await; + + Ok(()) +} diff --git a/examples/factory-contract/low-level/src/lib.rs b/examples/factory-contract/low-level/src/lib.rs index d2bda474e..490f334ac 100644 --- a/examples/factory-contract/low-level/src/lib.rs +++ b/examples/factory-contract/low-level/src/lib.rs @@ -1,6 +1,8 @@ use near_sdk::json_types::U128; use near_sdk::serde_json; use near_sdk::{env, near, AccountId, Gas, NearToken, PromiseResult}; +use near_sdk::errors::PromiseFailed; +use near_sdk::BaseError; // Prepaid gas for making a single simple call. const SINGLE_CALL_GAS: Gas = Gas::from_tgas(20); @@ -57,7 +59,7 @@ impl FactoryContract { env::promise_return(promise1); } - pub fn get_result(&mut self, account_id: AccountId) { + pub fn get_result(&mut self, account_id: AccountId) -> Result<(), BaseError>{ match env::promise_result(0) { PromiseResult::Successful(_) => { env::promise_return(env::promise_create( @@ -67,8 +69,11 @@ impl FactoryContract { NearToken::from_near(0), SINGLE_CALL_GAS, )); + return Ok(()); } - _ => env::panic_str("Failed to set status"), + _ => { + return Err(PromiseFailed::new(Some(0), Some("Failed to set status")).into()); + }, }; } } diff --git a/examples/fungible-token/ft/src/lib.rs b/examples/fungible-token/ft/src/lib.rs index a6be56935..41e7822a9 100644 --- a/examples/fungible-token/ft/src/lib.rs +++ b/examples/fungible-token/ft/src/lib.rs @@ -27,8 +27,10 @@ use near_contract_standards::storage_management::{ use near_sdk::borsh::BorshSerialize; use near_sdk::collections::LazyOption; use near_sdk::json_types::U128; +use near_sdk::errors::ContractAlreadyInitialized; use near_sdk::{ - env, log, near, require, AccountId, BorshStorageKey, NearToken, PanicOnDefault, PromiseOrValue, + env, log, near, require, unwrap_or_err, AccountId, BaseError, BorshStorageKey, NearToken, + PanicOnDefault, PromiseOrValue, }; #[derive(PanicOnDefault)] @@ -72,14 +74,23 @@ impl Contract { /// the given fungible token metadata. #[init] pub fn new(owner_id: AccountId, total_supply: U128, metadata: FungibleTokenMetadata) -> Self { - require!(!env::state_exists(), "Already initialized"); - metadata.assert_valid(); + require!(!env::state_exists(), &String::from(ContractAlreadyInitialized {})); + let error = metadata.assert_valid(); + if let Err(e) = error { + env::panic_err(e.into()) + } let mut this = Self { token: FungibleToken::new(StorageKey::FungibleToken), metadata: LazyOption::new(StorageKey::Metadata, Some(&metadata)), }; - this.token.internal_register_account(&owner_id); - this.token.internal_deposit(&owner_id, total_supply.into()); + let register = this.token.internal_register_account(&owner_id); + if let Err(e) = register { + env::panic_err(e.into()) + } + let deposit = this.token.internal_deposit(&owner_id, total_supply.into()); + if let Err(e) = deposit { + env::panic_err(e.into()) + } near_contract_standards::fungible_token::events::FtMint { owner_id: &owner_id, @@ -95,7 +106,12 @@ impl Contract { #[near] impl FungibleTokenCore for Contract { #[payable] - fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option) { + fn ft_transfer( + &mut self, + receiver_id: AccountId, + amount: U128, + memo: Option, + ) -> Result<(), BaseError> { self.token.ft_transfer(receiver_id, amount, memo) } @@ -127,13 +143,16 @@ impl FungibleTokenResolver for Contract { sender_id: AccountId, receiver_id: AccountId, amount: U128, - ) -> U128 { - let (used_amount, burned_amount) = - self.token.internal_ft_resolve_transfer(&sender_id, receiver_id, amount); + ) -> Result { + let (used_amount, burned_amount) = unwrap_or_err!(self.token.internal_ft_resolve_transfer( + &sender_id, + receiver_id, + amount + )); if burned_amount > 0 { log!("Account @{} burned {}", sender_id, burned_amount); } - used_amount.into() + Ok(used_amount.into()) } } @@ -144,23 +163,25 @@ impl StorageManagement for Contract { &mut self, account_id: Option, registration_only: Option, - ) -> StorageBalance { + ) -> Result { self.token.storage_deposit(account_id, registration_only) } #[payable] - fn storage_withdraw(&mut self, amount: Option) -> StorageBalance { + fn storage_withdraw(&mut self, amount: Option) -> Result { self.token.storage_withdraw(amount) } #[payable] - fn storage_unregister(&mut self, force: Option) -> bool { + fn storage_unregister(&mut self, force: Option) -> Result { #[allow(unused_variables)] - if let Some((account_id, balance)) = self.token.internal_storage_unregister(force) { + if let Some((account_id, balance)) = + unwrap_or_err!(self.token.internal_storage_unregister(force)) + { log!("Closed @{} with {}", account_id, balance); - true + Ok(true) } else { - false + Ok(false) } } @@ -210,7 +231,7 @@ mod tests { } #[test] - #[should_panic(expected = "The contract is not initialized")] + #[should_panic(expected = "ContractNotInitialized")] fn test_default() { let context = get_context(accounts(1)); testing_env!(context.build()); @@ -228,7 +249,7 @@ mod tests { .predecessor_account_id(accounts(1)) .build()); // Paying for account registration, aka storage deposit - contract.storage_deposit(None, None); + let _ = contract.storage_deposit(None, None); testing_env!(context .storage_usage(env::storage_usage()) @@ -236,7 +257,7 @@ mod tests { .predecessor_account_id(accounts(2)) .build()); let transfer_amount = TOTAL_SUPPLY / 3; - contract.ft_transfer(accounts(1), transfer_amount.into(), None); + let _ = contract.ft_transfer(accounts(1), transfer_amount.into(), None); testing_env!(context .storage_usage(env::storage_usage()) diff --git a/examples/fungible-token/test-contract-defi/src/lib.rs b/examples/fungible-token/test-contract-defi/src/lib.rs index 83671a1e8..c13a943c9 100644 --- a/examples/fungible-token/test-contract-defi/src/lib.rs +++ b/examples/fungible-token/test-contract-defi/src/lib.rs @@ -3,6 +3,7 @@ Some hypothetical DeFi contract that will do smart things with the transferred t */ use near_contract_standards::fungible_token::{receiver::FungibleTokenReceiver, Balance}; use near_sdk::json_types::U128; +use near_sdk::errors::{ContractAlreadyInitialized, PermissionDenied}; use near_sdk::{env, log, near, require, AccountId, Gas, PanicOnDefault, PromiseOrValue}; const BASE_GAS: u64 = 5_000_000_000_000; @@ -25,7 +26,7 @@ trait ValueReturnTrait { impl DeFi { #[init] pub fn new(fungible_token_account_id: AccountId) -> Self { - require!(!env::state_exists(), "Already initialized"); + require!(!env::state_exists(), &String::from(ContractAlreadyInitialized {})); Self { fungible_token_account_id: fungible_token_account_id.into() } } } @@ -44,7 +45,9 @@ impl FungibleTokenReceiver for DeFi { // Verifying that we were called by fungible token contract that we expect. require!( env::predecessor_account_id() == self.fungible_token_account_id, - "Only supports the one fungible token contract" + &String::from(PermissionDenied::new(Some( + "Only supports the one fungible token contract" + ))) ); log!("in {} tokens from @{} ft_on_transfer, msg = {}", amount.0, sender_id, msg); match msg.as_str() { diff --git a/examples/fungible-token/tests/workspaces.rs b/examples/fungible-token/tests/workspaces.rs index 88ef49a08..5b2f931bb 100644 --- a/examples/fungible-token/tests/workspaces.rs +++ b/examples/fungible-token/tests/workspaces.rs @@ -55,7 +55,7 @@ async fn init( .await?; assert!(res.is_success()); - return Ok((ft_contract, alice, defi_contract)); + Ok((ft_contract, alice, defi_contract)) } #[tokio::test] diff --git a/examples/non-fungible-token/nft/src/lib.rs b/examples/non-fungible-token/nft/src/lib.rs index 33c0ce5fe..8518e6a18 100644 --- a/examples/non-fungible-token/nft/src/lib.rs +++ b/examples/non-fungible-token/nft/src/lib.rs @@ -27,6 +27,7 @@ use near_contract_standards::non_fungible_token::NonFungibleToken; use near_contract_standards::non_fungible_token::{Token, TokenId}; use near_sdk::collections::LazyOption; use near_sdk::json_types::U128; +use near_sdk::errors::ContractAlreadyInitialized; use near_sdk::{ env, near, require, AccountId, BorshStorageKey, PanicOnDefault, Promise, PromiseOrValue, }; @@ -73,8 +74,11 @@ impl Contract { #[init] pub fn new(owner_id: AccountId, metadata: NFTContractMetadata) -> Self { - require!(!env::state_exists(), "Already initialized"); - metadata.assert_valid(); + require!(!env::state_exists(), &String::from(ContractAlreadyInitialized {})); + let error = metadata.assert_valid(); + if let Err(e) = error { + env::panic_err(e.into()) + } Self { tokens: NonFungibleToken::new( StorageKey::NonFungibleToken, @@ -265,7 +269,7 @@ mod tests { } #[test] - #[should_panic(expected = "The contract is not initialized")] + #[should_panic(expected = "ContractNotInitialized")] fn test_default() { let context = get_context(accounts(1)); testing_env!(context.build()); diff --git a/examples/non-fungible-token/test-approval-receiver/src/lib.rs b/examples/non-fungible-token/test-approval-receiver/src/lib.rs index be476226e..d5872f02f 100644 --- a/examples/non-fungible-token/test-approval-receiver/src/lib.rs +++ b/examples/non-fungible-token/test-approval-receiver/src/lib.rs @@ -3,6 +3,7 @@ A stub contract that implements nft_on_approve for e2e testing nft_approve. */ use near_contract_standards::non_fungible_token::approval::NonFungibleTokenApprovalReceiver; use near_contract_standards::non_fungible_token::TokenId; +use near_sdk::errors::PermissionDenied; use near_sdk::{env, log, near, require, AccountId, Gas, PanicOnDefault, PromiseOrValue}; /// It is estimated that we need to attach 5 TGas for the code execution and 5 TGas for cross-contract call @@ -45,7 +46,7 @@ impl NonFungibleTokenApprovalReceiver for ApprovalReceiver { // Verifying that we were called by non-fungible token contract that we expect. require!( env::predecessor_account_id() == self.non_fungible_token_account_id, - "Only supports the one non-fungible token contract" + &String::from(PermissionDenied::new(Some("Only supports the one fungible token contract"))) ); log!( "in nft_on_approve; sender_id={}, previous_owner_id={}, token_id={}, msg={}", diff --git a/examples/non-fungible-token/test-token-receiver/src/lib.rs b/examples/non-fungible-token/test-token-receiver/src/lib.rs index 142d2c4b5..1856fb31f 100644 --- a/examples/non-fungible-token/test-token-receiver/src/lib.rs +++ b/examples/non-fungible-token/test-token-receiver/src/lib.rs @@ -4,6 +4,7 @@ A stub contract that implements nft_on_transfer for simulation testing nft_trans use near_contract_standards::non_fungible_token::core::NonFungibleTokenReceiver; use near_contract_standards::non_fungible_token::TokenId; use near_sdk::{env, log, near, require, AccountId, Gas, PanicOnDefault, PromiseOrValue}; +use near_sdk::errors::{InvalidArgument, PermissionDenied}; /// It is estimated that we need to attach 5 TGas for the code execution and 5 TGas for cross-contract call const GAS_FOR_NFT_ON_TRANSFER: Gas = Gas::from_tgas(10); @@ -46,7 +47,7 @@ impl NonFungibleTokenReceiver for TokenReceiver { // Verifying that we were called by non-fungible token contract that we expect. require!( env::predecessor_account_id() == self.non_fungible_token_account_id, - "Only supports the one non-fungible token contract" + &String::from(PermissionDenied::new(Some("Only supports the one fungible token contract"))) ); log!( "in nft_on_transfer; sender_id={}, previous_owner_id={}, token_id={}, msg={}", @@ -74,7 +75,7 @@ impl NonFungibleTokenReceiver for TokenReceiver { .ok_go(false) .into() } - _ => env::panic_str("unsupported msg"), + _ => env::panic_err(InvalidArgument::new("Unsupported msg")), } } } diff --git a/near-contract-standards/src/fungible_token/core.rs b/near-contract-standards/src/fungible_token/core.rs index defe0752c..f34bf5797 100644 --- a/near-contract-standards/src/fungible_token/core.rs +++ b/near-contract-standards/src/fungible_token/core.rs @@ -1,6 +1,7 @@ use near_sdk::ext_contract; use near_sdk::json_types::U128; use near_sdk::AccountId; +use near_sdk::BaseError; use near_sdk::PromiseOrValue; /// The core methods for a basic fungible token. Extension standards may be /// added in addition to this trait. @@ -8,7 +9,7 @@ use near_sdk::PromiseOrValue; /// # Examples /// /// ``` -/// use near_sdk::{near, PanicOnDefault, AccountId, PromiseOrValue}; +/// use near_sdk::{near, PanicOnDefault, AccountId, PromiseOrValue, BaseError}; /// use near_sdk::collections::LazyOption; /// use near_sdk::json_types::U128; /// use near_contract_standards::fungible_token::{FungibleToken, FungibleTokenCore}; @@ -24,7 +25,7 @@ use near_sdk::PromiseOrValue; /// #[near] /// impl FungibleTokenCore for Contract { /// #[payable] -/// fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option) { +/// fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option) -> Result<(), BaseError> { /// self.token.ft_transfer(receiver_id, amount, memo) /// } /// @@ -61,7 +62,12 @@ pub trait FungibleTokenCore { /// - `receiver_id` - the account ID of the receiver. /// - `amount` - the amount of tokens to transfer. Must be a positive number in decimal string representation. /// - `memo` - an optional string field in a free form to associate a memo with this transfer. - fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option); + fn ft_transfer( + &mut self, + receiver_id: AccountId, + amount: U128, + memo: Option, + ) -> Result<(), BaseError>; /// Transfers positive `amount` of tokens from the `env::predecessor_account_id` to `receiver_id` account. Then /// calls `ft_on_transfer` method on `receiver_id` contract and attaches a callback to resolve this transfer. diff --git a/near-contract-standards/src/fungible_token/core_impl.rs b/near-contract-standards/src/fungible_token/core_impl.rs index 3b3a5aae8..04c575469 100644 --- a/near-contract-standards/src/fungible_token/core_impl.rs +++ b/near-contract-standards/src/fungible_token/core_impl.rs @@ -3,17 +3,19 @@ use crate::fungible_token::events::{FtBurn, FtTransfer}; use crate::fungible_token::receiver::ext_ft_receiver; use crate::fungible_token::resolver::{ext_ft_resolver, FungibleTokenResolver}; use near_sdk::collections::LookupMap; +use near_sdk::errors::{ + InsufficientBalance, InsufficientGas, InvalidArgument, TotalSupplyOverflow, +}; use near_sdk::json_types::U128; use near_sdk::{ - assert_one_yocto, env, log, near, require, AccountId, Gas, IntoStorageKey, PromiseOrValue, - PromiseResult, StorageUsage, + assert_one_yocto, contract_error, env, log, near, require, unwrap_or_err, AccountId, Gas, + IntoStorageKey, PromiseOrValue, PromiseResult, StorageUsage, }; +use near_sdk::{require_or_err, BaseError}; const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas::from_tgas(5); const GAS_FOR_FT_TRANSFER_CALL: Gas = Gas::from_tgas(30); -const ERR_TOTAL_SUPPLY_OVERFLOW: &str = "Total supply overflow"; - pub type Balance = u128; /// Implementation of a FungibleToken standard. @@ -56,38 +58,67 @@ impl FungibleToken { self.accounts.remove(&tmp_account_id); } - pub fn internal_unwrap_balance_of(&self, account_id: &AccountId) -> Balance { + pub fn internal_unwrap_balance_of( + &self, + account_id: &AccountId, + ) -> Result { match self.accounts.get(account_id) { - Some(balance) => balance, - None => { - env::panic_str(format!("The account {} is not registered", &account_id).as_str()) - } + Some(balance) => Ok(balance), + None => Err(AccountNotRegistered::new(account_id.clone())), } } - pub fn internal_deposit(&mut self, account_id: &AccountId, amount: Balance) { - let balance = self.internal_unwrap_balance_of(account_id); + pub fn internal_deposit( + &mut self, + account_id: &AccountId, + amount: Balance, + ) -> Result<(), BaseError> { + let balance_result = self.internal_unwrap_balance_of(account_id); + let balance: u128; + if let Ok(unwrapped_balance) = balance_result { + balance = unwrapped_balance; + } else { + return Err(balance_result.unwrap_err().into()); + } if let Some(new_balance) = balance.checked_add(amount) { self.accounts.insert(account_id, &new_balance); - self.total_supply = self - .total_supply - .checked_add(amount) - .unwrap_or_else(|| env::panic_str(ERR_TOTAL_SUPPLY_OVERFLOW)); + let checked = self.total_supply.checked_add(amount); + match checked { + Some(new_total_supply) => { + self.total_supply = new_total_supply; + Ok(()) + } + None => Err(TotalSupplyOverflow {}.into()), + } } else { - env::panic_str("Balance overflow"); + Err(BalanceOverflow {}.into()) } } - pub fn internal_withdraw(&mut self, account_id: &AccountId, amount: Balance) { - let balance = self.internal_unwrap_balance_of(account_id); + pub fn internal_withdraw( + &mut self, + account_id: &AccountId, + amount: Balance, + ) -> Result<(), BaseError> { + let balance_result = self.internal_unwrap_balance_of(account_id); + let balance: u128; + if let Ok(unwrapped_balance) = balance_result { + balance = unwrapped_balance; + } else { + return Err(balance_result.unwrap_err().into()); + } if let Some(new_balance) = balance.checked_sub(amount) { self.accounts.insert(account_id, &new_balance); - self.total_supply = self - .total_supply - .checked_sub(amount) - .unwrap_or_else(|| env::panic_str(ERR_TOTAL_SUPPLY_OVERFLOW)); + let checked = self.total_supply.checked_sub(amount); + match checked { + Some(new_total_supply) => { + self.total_supply = new_total_supply; + Ok(()) + } + None => Err(TotalSupplyOverflow {}.into()), + } } else { - env::panic_str("The account doesn't have enough balance"); + Err(InsufficientBalance::new(None).into()) } } @@ -97,11 +128,11 @@ impl FungibleToken { receiver_id: &AccountId, amount: Balance, memo: Option, - ) { - require!(sender_id != receiver_id, "Sender and receiver should be different"); - require!(amount > 0, "The amount should be a positive number"); - self.internal_withdraw(sender_id, amount); - self.internal_deposit(receiver_id, amount); + ) -> Result<(), BaseError> { + require_or_err!(sender_id != receiver_id, ReceiverIsSender::new()); + require_or_err!(amount > 0, InvalidArgument::new("The amount should be a positive number")); + unwrap_or_err!(self.internal_withdraw(sender_id, amount)); + unwrap_or_err!(self.internal_deposit(receiver_id, amount)); FtTransfer { old_owner_id: sender_id, new_owner_id: receiver_id, @@ -109,21 +140,48 @@ impl FungibleToken { memo: memo.as_deref(), } .emit(); + Ok(()) } - pub fn internal_register_account(&mut self, account_id: &AccountId) { + pub fn internal_register_account( + &mut self, + account_id: &AccountId, + ) -> Result<(), AccountAlreadyRegistered> { if self.accounts.insert(account_id, &0).is_some() { - env::panic_str("The account is already registered"); + return Err(AccountAlreadyRegistered {}); } + Ok(()) + } +} + +#[contract_error] +pub struct ReceiverIsSender { + pub message: String, +} + +impl ReceiverIsSender { + pub fn new() -> Self { + Self { message: "The receiver should be different from the sender".to_string() } + } +} + +impl Default for ReceiverIsSender { + fn default() -> Self { + Self::new() } } impl FungibleTokenCore for FungibleToken { - fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option) { + fn ft_transfer( + &mut self, + receiver_id: AccountId, + amount: U128, + memo: Option, + ) -> Result<(), BaseError> { assert_one_yocto(); let sender_id = env::predecessor_account_id(); let amount: Balance = amount.into(); - self.internal_transfer(&sender_id, &receiver_id, amount, memo); + self.internal_transfer(&sender_id, &receiver_id, amount, memo) } fn ft_transfer_call( @@ -134,13 +192,14 @@ impl FungibleTokenCore for FungibleToken { msg: String, ) -> PromiseOrValue { assert_one_yocto(); - require!(env::prepaid_gas() > GAS_FOR_FT_TRANSFER_CALL, "More gas is required"); + require!(env::prepaid_gas() > GAS_FOR_FT_TRANSFER_CALL, &String::from(InsufficientGas {})); let sender_id = env::predecessor_account_id(); let amount: Balance = amount.into(); - self.internal_transfer(&sender_id, &receiver_id, amount, memo); + self.internal_transfer(&sender_id, &receiver_id, amount, memo) + .unwrap_or_else(|err| env::panic_err(err)); let receiver_gas = env::prepaid_gas() .checked_sub(GAS_FOR_FT_TRANSFER_CALL) - .unwrap_or_else(|| env::panic_str("Prepaid gas overflow")); + .unwrap_or_else(|| env::panic_err(InsufficientGas {}.into())); // Initiating receiver's call and the callback ext_ft_receiver::ext(receiver_id.clone()) .with_static_gas(receiver_gas) @@ -171,7 +230,7 @@ impl FungibleToken { sender_id: &AccountId, receiver_id: AccountId, amount: U128, - ) -> (u128, u128) { + ) -> Result<(u128, u128), BaseError> { let amount: Balance = amount.into(); // Get the unused amount from the `ft_on_transfer` call result. @@ -193,14 +252,17 @@ impl FungibleToken { if let Some(new_receiver_balance) = receiver_balance.checked_sub(refund_amount) { self.accounts.insert(&receiver_id, &new_receiver_balance); } else { - env::panic_str("The receiver account doesn't have enough balance"); + return Err(InsufficientBalance::new(Some( + "The receiver account doesn't have enough balance", + )) + .into()); } if let Some(sender_balance) = self.accounts.get(sender_id) { if let Some(new_sender_balance) = sender_balance.checked_add(refund_amount) { self.accounts.insert(sender_id, &new_sender_balance); } else { - env::panic_str("Sender balance overflow"); + return Err(InsufficientBalance::new(None).into()); } FtTransfer { @@ -210,28 +272,34 @@ impl FungibleToken { memo: Some("refund"), } .emit(); - let used_amount = amount - .checked_sub(refund_amount) - .unwrap_or_else(|| env::panic_str(ERR_TOTAL_SUPPLY_OVERFLOW)); - return (used_amount, 0); + let used_amount = amount.checked_sub(refund_amount); + let Some(used_amount) = used_amount else { + return Err(TotalSupplyOverflow {}.into()); + }; + return Ok((used_amount, 0)); } else { // Sender's account was deleted, so we need to burn tokens. - self.total_supply = self - .total_supply - .checked_sub(refund_amount) - .unwrap_or_else(|| env::panic_str(ERR_TOTAL_SUPPLY_OVERFLOW)); - log!("The account of the sender was deleted"); - FtBurn { - owner_id: &receiver_id, - amount: U128(refund_amount), - memo: Some("refund"), + let checked = self.total_supply.checked_sub(refund_amount); + match checked { + Some(new_total_supply) => { + self.total_supply = new_total_supply; + log!("The account of the sender was deleted"); + FtBurn { + owner_id: &receiver_id, + amount: U128(refund_amount), + memo: Some("refund"), + } + .emit(); + return Ok((amount, refund_amount)); + } + None => { + return Err(TotalSupplyOverflow {}.into()); + } } - .emit(); - return (amount, refund_amount); } } } - (amount, 0) + Ok((amount, 0)) } } @@ -241,7 +309,28 @@ impl FungibleTokenResolver for FungibleToken { sender_id: AccountId, receiver_id: AccountId, amount: U128, - ) -> U128 { - self.internal_ft_resolve_transfer(&sender_id, receiver_id, amount).0.into() + ) -> Result { + let transfer = self.internal_ft_resolve_transfer(&sender_id, receiver_id, amount); + match transfer { + Ok((used_amount, _)) => Ok(used_amount.into()), + Err(err) => Err(err), + } } } + +#[contract_error] +pub struct AccountNotRegistered { + account_id: AccountId, +} + +impl AccountNotRegistered { + pub fn new(account_id: AccountId) -> Self { + Self { account_id } + } +} + +#[contract_error] +pub struct AccountAlreadyRegistered {} + +#[contract_error] +pub struct BalanceOverflow {} diff --git a/near-contract-standards/src/fungible_token/metadata.rs b/near-contract-standards/src/fungible_token/metadata.rs index 1eecbbbf9..3100a9497 100644 --- a/near-contract-standards/src/fungible_token/metadata.rs +++ b/near-contract-standards/src/fungible_token/metadata.rs @@ -1,5 +1,6 @@ +use near_sdk::errors::InvalidHashLength; use near_sdk::json_types::Base64VecU8; -use near_sdk::{ext_contract, near, require}; +use near_sdk::{ext_contract, near, require_or_err, BaseError}; pub const FT_METADATA_SPEC: &str = "ft-1.0.0"; @@ -21,11 +22,12 @@ pub trait FungibleTokenMetadataProvider { } impl FungibleTokenMetadata { - pub fn assert_valid(&self) { - require!(self.spec == FT_METADATA_SPEC); - require!(self.reference.is_some() == self.reference_hash.is_some()); + pub fn assert_valid(&self) -> Result<(), BaseError> { + require_or_err!(self.spec == FT_METADATA_SPEC); + require_or_err!(self.reference.is_some() == self.reference_hash.is_some()); if let Some(reference_hash) = &self.reference_hash { - require!(reference_hash.0.len() == 32, "Hash has to be 32 bytes"); + require_or_err!(reference_hash.0.len() == 32, InvalidHashLength::new(32)); } + Ok(()) } } diff --git a/near-contract-standards/src/fungible_token/resolver.rs b/near-contract-standards/src/fungible_token/resolver.rs index ce58dad82..6a843c58d 100644 --- a/near-contract-standards/src/fungible_token/resolver.rs +++ b/near-contract-standards/src/fungible_token/resolver.rs @@ -1,11 +1,11 @@ -use near_sdk::{ext_contract, json_types::U128, AccountId}; +use near_sdk::{ext_contract, json_types::U128, AccountId, BaseError}; /// [`FungibleTokenResolver`] provides token transfer resolve functionality. /// /// # Examples /// /// ``` -/// use near_sdk::{near, PanicOnDefault, AccountId, log}; +/// use near_sdk::{near, PanicOnDefault, AccountId, log, unwrap_or_err, BaseError}; /// use near_sdk::collections::LazyOption; /// use near_sdk::json_types::U128; /// use near_contract_standards::fungible_token::{FungibleToken, FungibleTokenResolver}; @@ -26,13 +26,13 @@ use near_sdk::{ext_contract, json_types::U128, AccountId}; /// sender_id: AccountId, /// receiver_id: AccountId, /// amount: U128, -/// ) -> U128 { -/// let (used_amount, burned_amount) = -/// self.token.internal_ft_resolve_transfer(&sender_id, receiver_id, amount); +/// ) -> Result { +/// let (used_amount, burned_amount) = unwrap_or_err!( +/// self.token.internal_ft_resolve_transfer(&sender_id, receiver_id, amount)); /// if burned_amount > 0 { /// log!("Account @{} burned {}", sender_id, burned_amount); /// } -/// used_amount.into() +/// Ok(used_amount.into()) /// } /// } /// ``` @@ -44,5 +44,5 @@ pub trait FungibleTokenResolver { sender_id: AccountId, receiver_id: AccountId, amount: U128, - ) -> U128; + ) -> Result; } diff --git a/near-contract-standards/src/fungible_token/storage_impl.rs b/near-contract-standards/src/fungible_token/storage_impl.rs index b6ab31db0..7b88f4b35 100644 --- a/near-contract-standards/src/fungible_token/storage_impl.rs +++ b/near-contract-standards/src/fungible_token/storage_impl.rs @@ -1,6 +1,12 @@ use crate::fungible_token::{Balance, FungibleToken}; use crate::storage_management::{StorageBalance, StorageBalanceBounds, StorageManagement}; -use near_sdk::{assert_one_yocto, env, log, AccountId, NearToken, Promise}; +use near_sdk::errors::InsufficientBalance; +use near_sdk::{ + assert_one_yocto, contract_error, env, log, unwrap_or_err, AccountId, BaseError, NearToken, + Promise, +}; + +use super::core_impl::AccountNotRegistered; impl FungibleToken { /// Internal method that returns the Account ID and the balance in case the account was @@ -8,7 +14,7 @@ impl FungibleToken { pub fn internal_storage_unregister( &mut self, force: Option, - ) -> Option<(AccountId, Balance)> { + ) -> Result, BaseError> { assert_one_yocto(); let account_id = env::predecessor_account_id(); let force = force.unwrap_or(false); @@ -19,15 +25,13 @@ impl FungibleToken { Promise::new(account_id.clone()).transfer( self.storage_balance_bounds().min.saturating_add(NearToken::from_yoctonear(1)), ); - Some((account_id, balance)) + Ok(Some((account_id, balance))) } else { - env::panic_str( - "Can't unregister the account with the positive balance without force", - ) + Err(PositiveBalanceUnregistering::new().into()) } } else { log!("The account {} is not registered", &account_id); - None + Ok(None) } } @@ -43,6 +47,26 @@ impl FungibleToken { } } +#[contract_error] +pub struct PositiveBalanceUnregistering { + pub message: String, +} + +impl PositiveBalanceUnregistering { + pub fn new() -> Self { + Self { + message: "Can't unregister the account with the positive balance without force" + .to_string(), + } + } +} + +impl Default for PositiveBalanceUnregistering { + fn default() -> Self { + Self::new() + } +} + impl StorageManagement for FungibleToken { // `registration_only` doesn't affect the implementation for vanilla fungible token. #[allow(unused_variables)] @@ -50,7 +74,7 @@ impl StorageManagement for FungibleToken { &mut self, account_id: Option, registration_only: Option, - ) -> StorageBalance { + ) -> Result { let amount = env::attached_deposit(); let account_id = account_id.unwrap_or_else(env::predecessor_account_id); if self.accounts.contains_key(&account_id) { @@ -61,16 +85,19 @@ impl StorageManagement for FungibleToken { } else { let min_balance = self.storage_balance_bounds().min; if amount < min_balance { - env::panic_str("The attached deposit is less than the minimum storage balance"); + return Err(InsufficientBalance::new(Some( + "The attached deposit is less than the minimum storage balance", + )) + .into()); } - self.internal_register_account(&account_id); + unwrap_or_err!(self.internal_register_account(&account_id)); let refund = amount.saturating_sub(min_balance); if refund > NearToken::from_near(0) { Promise::new(env::predecessor_account_id()).transfer(refund); } } - self.internal_storage_balance_of(&account_id).unwrap() + Ok(self.internal_storage_balance_of(&account_id).unwrap()) } /// While storage_withdraw normally allows the caller to retrieve `available` balance, the basic @@ -79,25 +106,27 @@ impl StorageManagement for FungibleToken { /// * panics if `amount > 0` /// * never transfers Ⓝ to caller /// * returns a `storage_balance` struct if `amount` is 0 - fn storage_withdraw(&mut self, amount: Option) -> StorageBalance { + fn storage_withdraw(&mut self, amount: Option) -> Result { assert_one_yocto(); let predecessor_account_id = env::predecessor_account_id(); if let Some(storage_balance) = self.internal_storage_balance_of(&predecessor_account_id) { match amount { - Some(amount) if amount > NearToken::from_near(0) => { - env::panic_str("The amount is greater than the available storage balance"); - } - _ => storage_balance, + Some(amount) if amount > NearToken::from_near(0) => Err(InsufficientBalance::new( + Some("The amount is greater than the available storage balance"), + ) + .into()), + _ => Ok(storage_balance), } } else { - env::panic_str( - format!("The account {} is not registered", &predecessor_account_id).as_str(), - ); + Err(AccountNotRegistered::new(predecessor_account_id).into()) } } - fn storage_unregister(&mut self, force: Option) -> bool { - self.internal_storage_unregister(force).is_some() + fn storage_unregister(&mut self, force: Option) -> Result { + match self.internal_storage_unregister(force) { + Ok(unregistered) => Ok(unregistered.is_some()), + Err(err) => Err(err), + } } fn storage_balance_bounds(&self) -> StorageBalanceBounds { diff --git a/near-contract-standards/src/non_fungible_token/approval/approval_impl.rs b/near-contract-standards/src/non_fungible_token/approval/approval_impl.rs index 68aa841a3..4343bcecb 100644 --- a/near-contract-standards/src/non_fungible_token/approval/approval_impl.rs +++ b/near-contract-standards/src/non_fungible_token/approval/approval_impl.rs @@ -7,17 +7,39 @@ use crate::non_fungible_token::utils::{ assert_at_least_one_yocto, bytes_for_approved_account_id, refund_approved_account_ids, refund_approved_account_ids_iter, refund_deposit, }; +use crate::non_fungible_token::ApprovalNotSupported; use crate::non_fungible_token::NonFungibleToken; -use near_sdk::{assert_one_yocto, env, require, AccountId, Gas, Promise}; +use near_sdk::errors::PermissionDenied; +use near_sdk::{ + assert_one_yocto, contract_error, env, require_or_err, unwrap_or_err, AccountId, BaseError, + Gas, Promise, +}; const GAS_FOR_NFT_APPROVE: Gas = Gas::from_tgas(10); -fn expect_token_found(option: Option) -> T { - option.unwrap_or_else(|| env::panic_str("Token not found")) +#[contract_error] +pub struct TokenNotFound {} + +fn expect_token_found(option: Option) -> Result { + Ok(unwrap_or_err!(option, TokenNotFound {})) +} + +#[contract_error] +pub struct TokenNotApproved { + message: String, +} + +impl TokenNotApproved { + pub fn new(message: &str) -> Self { + Self { message: String::from(message) } + } } -fn expect_approval(option: Option) -> T { - option.unwrap_or_else(|| env::panic_str("next_approval_by_id must be set for approval ext")) +fn expect_approval(option: Option) -> Result { + Ok(unwrap_or_err!( + option, + TokenNotApproved::new("next_approval_by_id must be set for approval ext") + )) } impl NonFungibleTokenApproval for NonFungibleToken { @@ -26,18 +48,22 @@ impl NonFungibleTokenApproval for NonFungibleToken { token_id: TokenId, account_id: AccountId, msg: Option, - ) -> Option { - assert_at_least_one_yocto(); - let approvals_by_id = self - .approvals_by_id - .as_mut() - .unwrap_or_else(|| env::panic_str("NFT does not support Approval Management")); - - let owner_id = expect_token_found(self.owner_by_id.get(&token_id)); - - require!(env::predecessor_account_id() == owner_id, "Predecessor must be token owner."); - - let next_approval_id_by_id = expect_approval(self.next_approval_id_by_id.as_mut()); + ) -> Result, BaseError> { + unwrap_or_err!(assert_at_least_one_yocto()); + let approvals_by_id = unwrap_or_err!( + self.approvals_by_id.as_mut(), + ApprovalNotSupported::new("NFT does not support Approval Management") + ); + + let owner_id = unwrap_or_err!(expect_token_found(self.owner_by_id.get(&token_id))); + + require_or_err!( + env::predecessor_account_id() == owner_id, + PermissionDenied::new(Some("Predecessor must be token owner.")) + ); + + let next_approval_id_by_id = + unwrap_or_err!(expect_approval(self.next_approval_id_by_id.as_mut())); // update HashMap of approvals for this token let approved_account_ids = &mut approvals_by_id.get(&token_id).unwrap_or_default(); let approval_id: u64 = next_approval_id_by_id.get(&token_id).unwrap_or(1u64); @@ -54,26 +80,30 @@ impl NonFungibleTokenApproval for NonFungibleToken { // excess. let storage_used = if old_approval_id.is_none() { bytes_for_approved_account_id(&account_id) } else { 0 }; - refund_deposit(storage_used); + unwrap_or_err!(refund_deposit(storage_used)); // if given `msg`, schedule call to `nft_on_approve` and return it. Else, return None. - msg.map(|msg| { + Ok(msg.map(|msg| { ext_nft_approval_receiver::ext(account_id) .with_static_gas(env::prepaid_gas().saturating_sub(GAS_FOR_NFT_APPROVE)) .nft_on_approve(token_id, owner_id, approval_id, msg) - }) + })) } - fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId) { + fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId) -> Result<(), BaseError> { assert_one_yocto(); - let approvals_by_id = self.approvals_by_id.as_mut().unwrap_or_else(|| { - env::panic_str("NFT does not support Approval Management"); - }); + let approvals_by_id = unwrap_or_err!( + self.approvals_by_id.as_mut(), + ApprovalNotSupported::new("NFT does not support Approval Management") + ); - let owner_id = expect_token_found(self.owner_by_id.get(&token_id)); + let owner_id = unwrap_or_err!(expect_token_found(self.owner_by_id.get(&token_id))); let predecessor_account_id = env::predecessor_account_id(); - require!(predecessor_account_id == owner_id, "Predecessor must be token owner."); + require_or_err!( + predecessor_account_id == owner_id, + PermissionDenied::new(Some("Predecessor must be token owner.")) + ); // if token has no approvals, do nothing if let Some(approved_account_ids) = &mut approvals_by_id.get(&token_id) { @@ -92,18 +122,23 @@ impl NonFungibleTokenApproval for NonFungibleToken { } } } + Ok(()) } - fn nft_revoke_all(&mut self, token_id: TokenId) { + fn nft_revoke_all(&mut self, token_id: TokenId) -> Result<(), BaseError> { assert_one_yocto(); - let approvals_by_id = self.approvals_by_id.as_mut().unwrap_or_else(|| { - env::panic_str("NFT does not support Approval Management"); - }); + let approvals_by_id = unwrap_or_err!( + self.approvals_by_id.as_mut(), + ApprovalNotSupported::new("NFT does not support Approval Management") + ); - let owner_id = expect_token_found(self.owner_by_id.get(&token_id)); + let owner_id = unwrap_or_err!(expect_token_found(self.owner_by_id.get(&token_id))); let predecessor_account_id = env::predecessor_account_id(); - require!(predecessor_account_id == owner_id, "Predecessor must be token owner."); + require_or_err!( + predecessor_account_id == owner_id, + PermissionDenied::new(Some("Predecessor must be token owner.")) + ); // if token has no approvals, do nothing if let Some(approved_account_ids) = &mut approvals_by_id.get(&token_id) { @@ -112,6 +147,7 @@ impl NonFungibleTokenApproval for NonFungibleToken { // ...and remove whole HashMap of approvals approvals_by_id.remove(&token_id); } + Ok(()) } fn nft_is_approved( @@ -119,35 +155,35 @@ impl NonFungibleTokenApproval for NonFungibleToken { token_id: TokenId, approved_account_id: AccountId, approval_id: Option, - ) -> bool { - expect_token_found(self.owner_by_id.get(&token_id)); + ) -> Result { + unwrap_or_err!(expect_token_found(self.owner_by_id.get(&token_id))); let approvals_by_id = if let Some(a) = self.approvals_by_id.as_ref() { a } else { // contract does not support approval management - return false; + return Ok(false); }; let approved_account_ids = if let Some(ids) = approvals_by_id.get(&token_id) { ids } else { // token has no approvals - return false; + return Ok(false); }; let actual_approval_id = if let Some(id) = approved_account_ids.get(&approved_account_id) { id } else { // account not in approvals HashMap - return false; + return Ok(false); }; if let Some(given_approval_id) = approval_id { - &given_approval_id == actual_approval_id + Ok(&given_approval_id == actual_approval_id) } else { // account approved, no approval_id given - true + Ok(true) } } } diff --git a/near-contract-standards/src/non_fungible_token/approval/mod.rs b/near-contract-standards/src/non_fungible_token/approval/mod.rs index 002091268..6777b32f9 100644 --- a/near-contract-standards/src/non_fungible_token/approval/mod.rs +++ b/near-contract-standards/src/non_fungible_token/approval/mod.rs @@ -4,8 +4,7 @@ mod approval_receiver; pub use approval_receiver::*; use crate::non_fungible_token::token::TokenId; -use near_sdk::AccountId; -use near_sdk::Promise; +use near_sdk::{AccountId, BaseError, Promise}; /// Trait used when it's desired to have a non-fungible token that has a /// traditional escrow or approval system. This allows Alice to allow Bob @@ -21,7 +20,7 @@ use near_sdk::Promise; /// /// ``` /// use std::collections::HashMap; -/// use near_sdk::{PanicOnDefault, AccountId, PromiseOrValue, near, Promise}; +/// use near_sdk::{PanicOnDefault, AccountId, PromiseOrValue, near, Promise, BaseError}; /// use near_contract_standards::non_fungible_token::{TokenId, NonFungibleToken, NonFungibleTokenApproval}; /// /// #[near(contract_state)] @@ -33,22 +32,22 @@ use near_sdk::Promise; /// #[near] /// impl NonFungibleTokenApproval for Contract { /// #[payable] -/// fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option) -> Option { +/// fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option) -> Result, BaseError> { /// self.tokens.nft_approve(token_id, account_id, msg) /// } /// /// #[payable] -/// fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId) { -/// self.tokens.nft_revoke(token_id, account_id); +/// fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId) -> Result<(), BaseError> { +/// self.tokens.nft_revoke(token_id, account_id) /// } /// /// #[payable] -/// fn nft_revoke_all(&mut self, token_id: TokenId) { -/// self.tokens.nft_revoke_all(token_id); +/// fn nft_revoke_all(&mut self, token_id: TokenId) -> Result<(), BaseError> { +/// self.tokens.nft_revoke_all(token_id) /// /// } /// -/// fn nft_is_approved(&self, token_id: TokenId, approved_account_id: AccountId, approval_id: Option) -> bool { +/// fn nft_is_approved(&self, token_id: TokenId, approved_account_id: AccountId, approval_id: Option) -> Result { /// self.tokens.nft_is_approved(token_id, approved_account_id, approval_id) /// } /// } @@ -82,7 +81,7 @@ pub trait NonFungibleTokenApproval { token_id: TokenId, account_id: AccountId, msg: Option, - ) -> Option; + ) -> Result, BaseError>; /// Revoke an approved account for a specific token. /// @@ -96,7 +95,7 @@ pub trait NonFungibleTokenApproval { /// Arguments: /// * `token_id`: the token for which to revoke an approval /// * `account_id`: the account to remove from `approvals` - fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId); + fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId) -> Result<(), BaseError>; /// Revoke all approved accounts for a specific token. /// @@ -109,7 +108,7 @@ pub trait NonFungibleTokenApproval { /// /// Arguments: /// * `token_id`: the token with approvals to revoke - fn nft_revoke_all(&mut self, token_id: TokenId); + fn nft_revoke_all(&mut self, token_id: TokenId) -> Result<(), BaseError>; /// Check if a token is approved for transfer by a given account, optionally /// checking an approval_id @@ -127,5 +126,5 @@ pub trait NonFungibleTokenApproval { token_id: TokenId, approved_account_id: AccountId, approval_id: Option, - ) -> bool; + ) -> Result; } diff --git a/near-contract-standards/src/non_fungible_token/core/core_impl.rs b/near-contract-standards/src/non_fungible_token/core/core_impl.rs index 733daf8f1..83ca0401a 100644 --- a/near-contract-standards/src/non_fungible_token/core/core_impl.rs +++ b/near-contract-standards/src/non_fungible_token/core/core_impl.rs @@ -6,12 +6,14 @@ use crate::non_fungible_token::events::{NftMint, NftTransfer}; use crate::non_fungible_token::metadata::TokenMetadata; use crate::non_fungible_token::token::{Token, TokenId}; use crate::non_fungible_token::utils::{refund_approved_account_ids, refund_deposit_to_account}; +use crate::non_fungible_token::ApprovalNotSupported; use near_sdk::borsh::BorshSerialize; use near_sdk::collections::{LookupMap, TreeMap, UnorderedSet}; +use near_sdk::errors::{InsufficientGas, InvalidArgument, PermissionDenied}; use near_sdk::json_types::Base64VecU8; use near_sdk::{ - assert_one_yocto, env, near, require, AccountId, BorshStorageKey, Gas, IntoStorageKey, - PromiseOrValue, PromiseResult, StorageUsage, + assert_one_yocto, contract_error, env, near, require, require_or_err, unwrap_or_err, AccountId, + BaseError, BorshStorageKey, Gas, IntoStorageKey, PromiseOrValue, PromiseResult, StorageUsage, }; use std::collections::HashMap; use std::ops::Deref; @@ -165,21 +167,26 @@ impl NonFungibleToken { #[allow(clippy::ptr_arg)] token_id: &TokenId, from: &AccountId, to: &AccountId, - ) { + ) -> Result<(), BaseError> { // update owner self.owner_by_id.insert(token_id, to); // if using Enumeration standard, update old & new owner's token lists if let Some(tokens_per_owner) = &mut self.tokens_per_owner { // owner_tokens should always exist, so call `unwrap` without guard - let mut owner_tokens = tokens_per_owner.get(from).unwrap_or_else(|| { - env::panic_str("Unable to access tokens per owner in unguarded call.") - }); - owner_tokens.remove(token_id); - if owner_tokens.is_empty() { - tokens_per_owner.remove(from); - } else { - tokens_per_owner.insert(from, &owner_tokens); + let owner_tokens = tokens_per_owner.get(from); + match owner_tokens { + Some(mut owner_tokens) => { + owner_tokens.remove(token_id); + if owner_tokens.is_empty() { + tokens_per_owner.remove(from); + } else { + tokens_per_owner.insert(from, &owner_tokens); + } + } + None => { + return Err(PermissionDenied::new(Some("Owner does not have token")).into()); + } } let mut receiver_tokens = tokens_per_owner.get(to).unwrap_or_else(|| { @@ -189,7 +196,9 @@ impl NonFungibleToken { }); receiver_tokens.insert(token_id); tokens_per_owner.insert(to, &receiver_tokens); + return Ok(()); } + Ok(()) } /// Transfer from current owner to receiver_id, checking that sender is allowed to transfer. @@ -202,9 +211,8 @@ impl NonFungibleToken { #[allow(clippy::ptr_arg)] token_id: &TokenId, approval_id: Option, memo: Option, - ) -> (AccountId, Option>) { - let owner_id = - self.owner_by_id.get(token_id).unwrap_or_else(|| env::panic_str("Token not found")); + ) -> Result<(AccountId, Option>), BaseError> { + let owner_id = unwrap_or_err!(self.owner_by_id.get(token_id), TokenNotFound {}); // clear approvals, if using Approval Management extension // this will be rolled back by a panic if sending fails @@ -214,39 +222,43 @@ impl NonFungibleToken { // check if authorized let sender_id = if sender_id != &owner_id { // Panic if approval extension is NOT being used - let app_acc_ids = approved_account_ids - .as_ref() - .unwrap_or_else(|| env::panic_str("Approval extension is disabled")); + let app_acc_ids = unwrap_or_err!( + approved_account_ids.as_ref(), + ApprovalNotSupported::new("Approval extension is disabled") + ); // Approval extension is being used; get approval_id for sender. let actual_approval_id = app_acc_ids.get(sender_id); // Panic if sender not approved at all if actual_approval_id.is_none() { - env::panic_str("Sender not approved"); + return Err(PermissionDenied::new(Some("Sender not approved")).into()); } // If approval_id included, check that it matches - require!( + require_or_err!( approval_id.is_none() || actual_approval_id == approval_id.as_ref(), - format!( - "The actual approval_id {:?} is different from the given approval_id {:?}", - actual_approval_id, approval_id - ) + PermissionDenied::new(Some( + format!( + "The actual approval_id {:?} is different from the given approval_id {:?}", + actual_approval_id, approval_id + ) + .as_str() + )) ); Some(sender_id) } else { None }; - require!(&owner_id != receiver_id, "Current and next owner must differ"); + require_or_err!(&owner_id != receiver_id, ReceiverIsSender::new()); - self.internal_transfer_unguarded(token_id, &owner_id, receiver_id); + unwrap_or_err!(self.internal_transfer_unguarded(token_id, &owner_id, receiver_id)); NonFungibleToken::emit_transfer(&owner_id, receiver_id, token_id, sender_id, memo); // return previous owner & approvals - (owner_id, approved_account_ids) + Ok((owner_id, approved_account_ids)) } fn emit_transfer( @@ -283,7 +295,7 @@ impl NonFungibleToken { token_id: TokenId, token_owner_id: AccountId, token_metadata: Option, - ) -> Token { + ) -> Result { assert_eq!(env::predecessor_account_id(), self.owner_id, "Unauthorized"); self.internal_mint(token_id, token_owner_id, token_metadata) @@ -299,15 +311,22 @@ impl NonFungibleToken { token_id: TokenId, token_owner_id: AccountId, token_metadata: Option, - ) -> Token { + ) -> Result { let token = self.internal_mint_with_refund( token_id, token_owner_id, token_metadata, Some(env::predecessor_account_id()), ); - NftMint { owner_id: &token.owner_id, token_ids: &[&token.token_id], memo: None }.emit(); - token + + match token { + Ok(token) => { + NftMint { owner_id: &token.owner_id, token_ids: &[&token.token_id], memo: None } + .emit(); + Ok(token) + } + Err(err) => Err(err), + } } /// Mint a new token without checking: @@ -323,15 +342,15 @@ impl NonFungibleToken { token_owner_id: AccountId, token_metadata: Option, refund_id: Option, - ) -> Token { + ) -> Result { // Remember current storage usage if refund_id is Some let initial_storage_usage = refund_id.map(|account_id| (account_id, env::storage_usage())); if self.token_metadata_by_id.is_some() && token_metadata.is_none() { - env::panic_str("Must provide metadata"); + return Err(InvalidArgument::new("Must provide metadata").into()); } if self.owner_by_id.get(&token_id).is_some() { - env::panic_str("token_id must be unique"); + return Err(InvalidArgument::new("token_id must be unique").into()); } let owner_id: AccountId = token_owner_id; @@ -362,15 +381,35 @@ impl NonFungibleToken { if self.approvals_by_id.is_some() { Some(HashMap::new()) } else { None }; if let Some((id, storage_usage)) = initial_storage_usage { - refund_deposit_to_account(env::storage_usage() - storage_usage, id) + unwrap_or_err!(refund_deposit_to_account(env::storage_usage() - storage_usage, id)) } // Return any extra attached deposit not used for storage - Token { token_id, owner_id, metadata: token_metadata, approved_account_ids } + Ok(Token { token_id, owner_id, metadata: token_metadata, approved_account_ids }) + } +} + +#[contract_error] +pub struct ReceiverIsSender { + pub message: String, +} + +impl ReceiverIsSender { + pub fn new() -> Self { + Self { message: "Current and next owner must differ".to_string() } } } +impl Default for ReceiverIsSender { + fn default() -> Self { + Self::new() + } +} + +#[contract_error] +pub struct TokenNotFound {} + impl NonFungibleTokenCore for NonFungibleToken { fn nft_transfer( &mut self, @@ -378,10 +417,17 @@ impl NonFungibleTokenCore for NonFungibleToken { token_id: TokenId, approval_id: Option, memo: Option, - ) { + ) -> Result<(), BaseError> { assert_one_yocto(); let sender_id = env::predecessor_account_id(); - self.internal_transfer(&sender_id, &receiver_id, &token_id, approval_id, memo); + unwrap_or_err!(self.internal_transfer( + &sender_id, + &receiver_id, + &token_id, + approval_id, + memo + )); + Ok(()) } fn nft_transfer_call( @@ -393,10 +439,11 @@ impl NonFungibleTokenCore for NonFungibleToken { msg: String, ) -> PromiseOrValue { assert_one_yocto(); - require!(env::prepaid_gas() > GAS_FOR_NFT_TRANSFER_CALL, "More gas is required"); + require!(env::prepaid_gas() > GAS_FOR_NFT_TRANSFER_CALL, &String::from(InsufficientGas {})); let sender_id = env::predecessor_account_id(); - let (old_owner, old_approvals) = - self.internal_transfer(&sender_id, &receiver_id, &token_id, approval_id, memo); + let (old_owner, old_approvals) = self + .internal_transfer(&sender_id, &receiver_id, &token_id, approval_id, memo) + .unwrap_or_else(|err| env::panic_err(err)); // Initiating receiver's call and the callback ext_nft_receiver::ext(receiver_id.clone()) .with_static_gas(env::prepaid_gas().saturating_sub(GAS_FOR_NFT_TRANSFER_CALL)) @@ -428,7 +475,7 @@ impl NonFungibleTokenResolver for NonFungibleToken { receiver_id: AccountId, token_id: TokenId, approved_account_ids: Option>, - ) -> bool { + ) -> Result { // Get whether token should be returned let must_revert = match env::promise_result(0) { PromiseResult::Successful(value) => { @@ -443,7 +490,7 @@ impl NonFungibleTokenResolver for NonFungibleToken { // if call succeeded, return early if !must_revert { - return true; + return Ok(true); } // OTHERWISE, try to set owner back to previous_owner_id and restore approved_account_ids @@ -452,7 +499,7 @@ impl NonFungibleTokenResolver for NonFungibleToken { if let Some(current_owner) = self.owner_by_id.get(&token_id) { if current_owner != receiver_id { // The token is not owned by the receiver anymore. Can't return it. - return true; + return Ok(true); } } else { // The token was burned and doesn't exist anymore. @@ -460,10 +507,14 @@ impl NonFungibleTokenResolver for NonFungibleToken { if let Some(approved_account_ids) = approved_account_ids { refund_approved_account_ids(previous_owner_id, &approved_account_ids); } - return true; + return Ok(true); }; - self.internal_transfer_unguarded(&token_id, &receiver_id, &previous_owner_id); + unwrap_or_err!(self.internal_transfer_unguarded( + &token_id, + &receiver_id, + &previous_owner_id + )); // If using Approval Management extension, // 1. revert any approvals receiver already set, refunding storage costs @@ -477,6 +528,6 @@ impl NonFungibleTokenResolver for NonFungibleToken { } } NonFungibleToken::emit_transfer(&receiver_id, &previous_owner_id, &token_id, None, None); - false + Ok(false) } } diff --git a/near-contract-standards/src/non_fungible_token/core/mod.rs b/near-contract-standards/src/non_fungible_token/core/mod.rs index 4c5892bf3..7fcc42e16 100644 --- a/near-contract-standards/src/non_fungible_token/core/mod.rs +++ b/near-contract-standards/src/non_fungible_token/core/mod.rs @@ -10,6 +10,7 @@ pub use self::resolver::NonFungibleTokenResolver; use crate::non_fungible_token::token::{Token, TokenId}; use near_sdk::AccountId; +use near_sdk::BaseError; use near_sdk::PromiseOrValue; /// Used for all non-fungible tokens. The specification for the @@ -23,7 +24,7 @@ use near_sdk::PromiseOrValue; /// # Examples /// /// ``` -/// use near_sdk::{PanicOnDefault, AccountId, PromiseOrValue, near}; +/// use near_sdk::{PanicOnDefault, AccountId, PromiseOrValue, near, BaseError}; /// use near_contract_standards::non_fungible_token::{core::NonFungibleTokenCore, NonFungibleToken, TokenId, Token}; /// /// #[near(contract_state)] @@ -34,8 +35,8 @@ use near_sdk::PromiseOrValue; /// #[near] /// impl NonFungibleTokenCore for Contract { /// #[payable] -/// fn nft_transfer(&mut self, receiver_id: AccountId, token_id: TokenId, approval_id: Option, memo: Option) { -/// self.tokens.nft_transfer(receiver_id, token_id, approval_id, memo); +/// fn nft_transfer(&mut self, receiver_id: AccountId, token_id: TokenId, approval_id: Option, memo: Option) -> Result<(), BaseError> { +/// self.tokens.nft_transfer(receiver_id, token_id, approval_id, memo) /// } /// /// #[payable] @@ -78,7 +79,7 @@ pub trait NonFungibleTokenCore { token_id: TokenId, approval_id: Option, memo: Option, - ); + ) -> Result<(), BaseError>; /// Transfer token and call a method on a receiver contract. A successful /// workflow will end in a success execution outcome to the callback on the NFT diff --git a/near-contract-standards/src/non_fungible_token/core/resolver.rs b/near-contract-standards/src/non_fungible_token/core/resolver.rs index e43d409dc..b94dd85cd 100644 --- a/near-contract-standards/src/non_fungible_token/core/resolver.rs +++ b/near-contract-standards/src/non_fungible_token/core/resolver.rs @@ -1,5 +1,5 @@ use crate::non_fungible_token::token::TokenId; -use near_sdk::{ext_contract, AccountId}; +use near_sdk::{ext_contract, AccountId, BaseError}; use std::collections::HashMap; /// Used when an NFT is transferred using `nft_transfer_call`. This is the method that's called after `nft_on_transfer`. This trait is implemented on the NFT contract. @@ -8,7 +8,7 @@ use std::collections::HashMap; /// /// ``` /// use std::collections::HashMap; -/// use near_sdk::{PanicOnDefault, AccountId, PromiseOrValue, near}; +/// use near_sdk::{PanicOnDefault, AccountId, PromiseOrValue, near, BaseError}; /// use near_contract_standards::non_fungible_token::{NonFungibleToken, NonFungibleTokenResolver, TokenId}; /// /// #[near(contract_state)] @@ -19,7 +19,7 @@ use std::collections::HashMap; /// #[near] /// impl NonFungibleTokenResolver for Contract { /// #[private] -/// fn nft_resolve_transfer(&mut self, previous_owner_id: AccountId, receiver_id: AccountId, token_id: TokenId, approved_account_ids: Option>) -> bool { +/// fn nft_resolve_transfer(&mut self, previous_owner_id: AccountId, receiver_id: AccountId, token_id: TokenId, approved_account_ids: Option>) -> Result { /// self.tokens.nft_resolve_transfer(previous_owner_id, receiver_id, token_id, approved_account_ids) /// } /// } @@ -59,5 +59,5 @@ pub trait NonFungibleTokenResolver { receiver_id: AccountId, token_id: TokenId, approved_account_ids: Option>, - ) -> bool; + ) -> Result; } diff --git a/near-contract-standards/src/non_fungible_token/enumeration/enumeration_impl.rs b/near-contract-standards/src/non_fungible_token/enumeration/enumeration_impl.rs index 71ada42e2..04f352d2e 100644 --- a/near-contract-standards/src/non_fungible_token/enumeration/enumeration_impl.rs +++ b/near-contract-standards/src/non_fungible_token/enumeration/enumeration_impl.rs @@ -1,8 +1,9 @@ use super::NonFungibleTokenEnumeration; use crate::non_fungible_token::token::Token; use crate::non_fungible_token::NonFungibleToken; +use near_sdk::errors::{IndexOutOfBounds, InvalidArgument}; use near_sdk::json_types::U128; -use near_sdk::{env, require, AccountId}; +use near_sdk::{contract_error, require_or_err, unwrap_or_err, AccountId, BaseError}; type TokenId = String; @@ -26,36 +27,34 @@ impl NonFungibleTokenEnumeration for NonFungibleToken { (self.owner_by_id.len() as u128).into() } - fn nft_tokens(&self, from_index: Option, limit: Option) -> Vec { + fn nft_tokens( + &self, + from_index: Option, + limit: Option, + ) -> Result, BaseError> { // Get starting index, whether or not it was explicitly given. // Defaults to 0 based on the spec: // https://nomicon.io/Standards/NonFungibleToken/Enumeration.html#interface let start_index: u128 = from_index.map(From::from).unwrap_or_default(); - require!( - (self.owner_by_id.len() as u128) >= start_index, - "Out of bounds, please use a smaller from_index." - ); + require_or_err!((self.owner_by_id.len() as u128) >= start_index, IndexOutOfBounds {}); let limit = limit.map(|v| v as usize).unwrap_or(usize::MAX); - require!(limit != 0, "Cannot provide limit of 0."); - self.owner_by_id + require_or_err!(limit != 0, InvalidArgument::new("Cannot provide limit of 0.")); + Ok(self + .owner_by_id .iter() .skip(start_index as usize) .take(limit) .map(|(token_id, owner_id)| self.enum_get_token(owner_id, token_id)) - .collect() + .collect()) } - fn nft_supply_for_owner(&self, account_id: AccountId) -> U128 { - let tokens_per_owner = self.tokens_per_owner.as_ref().unwrap_or_else(|| { - env::panic_str( - "Could not find tokens_per_owner when calling a method on the \ - enumeration standard.", - ) - }); - tokens_per_owner + fn nft_supply_for_owner(&self, account_id: AccountId) -> Result { + let tokens_per_owner = + unwrap_or_err!(self.tokens_per_owner.as_ref(), TokensNotFound::new()); + Ok(tokens_per_owner .get(&account_id) .map(|account_tokens| U128::from(account_tokens.len() as u128)) - .unwrap_or(U128(0)) + .unwrap_or(U128(0))) } fn nft_tokens_for_owner( @@ -63,35 +62,49 @@ impl NonFungibleTokenEnumeration for NonFungibleToken { account_id: AccountId, from_index: Option, limit: Option, - ) -> Vec { - let tokens_per_owner = self.tokens_per_owner.as_ref().unwrap_or_else(|| { - env::panic_str( - "Could not find tokens_per_owner when calling a method on the \ - enumeration standard.", - ) - }); + ) -> Result, BaseError> { + let tokens_per_owner = + unwrap_or_err!(self.tokens_per_owner.as_ref(), TokensNotFound::new()); let token_set = if let Some(token_set) = tokens_per_owner.get(&account_id) { token_set } else { - return vec![]; + return Ok(vec![]); }; if token_set.is_empty() { - return vec![]; + return Ok(vec![]); } let limit = limit.map(|v| v as usize).unwrap_or(usize::MAX); - require!(limit != 0, "Cannot provide limit of 0."); + require_or_err!(limit != 0, InvalidArgument::new("Cannot provide limit of 0.")); let start_index: u128 = from_index.map(From::from).unwrap_or_default(); - require!( - token_set.len() as u128 > start_index, - "Out of bounds, please use a smaller from_index." - ); - token_set + require_or_err!(token_set.len() as u128 > start_index, IndexOutOfBounds {}); + Ok(token_set .iter() .skip(start_index as usize) .take(limit) .map(|token_id| self.enum_get_token(account_id.clone(), token_id)) - .collect() + .collect()) + } +} + +#[contract_error] +pub struct TokensNotFound { + pub message: String, +} + +impl TokensNotFound { + pub fn new() -> Self { + Self { + message: "Could not find tokens_per_owner when calling a method on the \ + enumeration standard." + .to_string(), + } + } +} + +impl Default for TokensNotFound { + fn default() -> Self { + Self::new() } } diff --git a/near-contract-standards/src/non_fungible_token/enumeration/mod.rs b/near-contract-standards/src/non_fungible_token/enumeration/mod.rs index ba8661551..3325446d5 100644 --- a/near-contract-standards/src/non_fungible_token/enumeration/mod.rs +++ b/near-contract-standards/src/non_fungible_token/enumeration/mod.rs @@ -2,7 +2,7 @@ mod enumeration_impl; use crate::non_fungible_token::token::Token; use near_sdk::json_types::U128; -use near_sdk::AccountId; +use near_sdk::{AccountId, BaseError}; /// Offers methods helpful in determining account ownership of NFTs and provides a way to page through NFTs per owner, determine total supply, etc. /// @@ -10,7 +10,7 @@ use near_sdk::AccountId; /// /// ``` /// use std::collections::HashMap; -/// use near_sdk::{PanicOnDefault, AccountId, PromiseOrValue, near, Promise}; +/// use near_sdk::{PanicOnDefault, AccountId, PromiseOrValue, near, Promise, BaseError}; /// use near_contract_standards::non_fungible_token::{NonFungibleToken, NonFungibleTokenEnumeration, TokenId, Token}; /// use near_sdk::json_types::U128; /// @@ -26,15 +26,15 @@ use near_sdk::AccountId; /// self.tokens.nft_total_supply() /// } /// -/// fn nft_tokens(&self, from_index: Option, limit: Option) -> Vec { +/// fn nft_tokens(&self, from_index: Option, limit: Option) -> Result, BaseError> { /// self.tokens.nft_tokens(from_index, limit) /// } /// -/// fn nft_supply_for_owner(&self, account_id: AccountId) -> U128 { +/// fn nft_supply_for_owner(&self, account_id: AccountId) -> Result { /// self.tokens.nft_supply_for_owner(account_id) /// } /// -/// fn nft_tokens_for_owner(&self, account_id: AccountId, from_index: Option, limit: Option) -> Vec { +/// fn nft_tokens_for_owner(&self, account_id: AccountId, from_index: Option, limit: Option) -> Result, BaseError> { /// self.tokens.nft_tokens_for_owner(account_id, from_index, limit) /// } /// } @@ -57,7 +57,7 @@ pub trait NonFungibleTokenEnumeration { &self, from_index: Option, // default: "0" limit: Option, // default: unlimited (could fail due to gas limit) - ) -> Vec; + ) -> Result, BaseError>; /// Get number of tokens owned by a given account /// @@ -67,7 +67,7 @@ pub trait NonFungibleTokenEnumeration { /// Returns the number of non-fungible tokens owned by given `account_id` as /// a string representing the value as an unsigned 128-bit integer to avoid JSON /// number limit of 2^53. - fn nft_supply_for_owner(&self, account_id: AccountId) -> U128; + fn nft_supply_for_owner(&self, account_id: AccountId) -> Result; /// Get list of all tokens owned by a given account /// @@ -83,5 +83,5 @@ pub trait NonFungibleTokenEnumeration { account_id: AccountId, from_index: Option, // default: "0" limit: Option, // default: unlimited (could fail due to gas limit) - ) -> Vec; + ) -> Result, BaseError>; } diff --git a/near-contract-standards/src/non_fungible_token/metadata.rs b/near-contract-standards/src/non_fungible_token/metadata.rs index d5dbc9ff3..a9a8ce7ad 100644 --- a/near-contract-standards/src/non_fungible_token/metadata.rs +++ b/near-contract-standards/src/non_fungible_token/metadata.rs @@ -1,5 +1,6 @@ +use near_sdk::errors::{InvalidContractState, InvalidHashLength}; use near_sdk::json_types::Base64VecU8; -use near_sdk::{near, require}; +use near_sdk::{contract_error, near, require_or_err, BaseError}; /// This spec can be treated like a version of the standard. pub const NFT_METADATA_SPEC: &str = "nft-1.0.0"; @@ -40,28 +41,44 @@ pub trait NonFungibleTokenMetadataProvider { } impl NFTContractMetadata { - pub fn assert_valid(&self) { - require!(self.spec == NFT_METADATA_SPEC, "Spec is not NFT metadata"); - require!( + pub fn assert_valid(&self) -> Result<(), BaseError> { + require_or_err!( + self.spec == NFT_METADATA_SPEC, + InvalidSpec::new("Spec is not NFT metadata") + ); + require_or_err!( self.reference.is_some() == self.reference_hash.is_some(), - "Reference and reference hash must be present" + InvalidContractState::new("Reference and reference hash must be present") ); if let Some(reference_hash) = &self.reference_hash { - require!(reference_hash.0.len() == 32, "Hash has to be 32 bytes"); + require_or_err!(reference_hash.0.len() == 32, InvalidHashLength::new(32)); } + Ok(()) } } impl TokenMetadata { - pub fn assert_valid(&self) { - require!(self.media.is_some() == self.media_hash.is_some()); + pub fn assert_valid(&self) -> Result<(), BaseError> { + require_or_err!(self.media.is_some() == self.media_hash.is_some()); if let Some(media_hash) = &self.media_hash { - require!(media_hash.0.len() == 32, "Media hash has to be 32 bytes"); + require_or_err!(media_hash.0.len() == 32, InvalidHashLength::new(32)); } - require!(self.reference.is_some() == self.reference_hash.is_some()); + require_or_err!(self.reference.is_some() == self.reference_hash.is_some()); if let Some(reference_hash) = &self.reference_hash { - require!(reference_hash.0.len() == 32, "Reference hash has to be 32 bytes"); + require_or_err!(reference_hash.0.len() == 32, InvalidHashLength::new(32)); } + Ok(()) + } +} + +#[contract_error] +pub struct InvalidSpec { + pub message: String, +} + +impl InvalidSpec { + pub fn new(message: &str) -> Self { + Self { message: message.to_string() } } } diff --git a/near-contract-standards/src/non_fungible_token/utils.rs b/near-contract-standards/src/non_fungible_token/utils.rs index bdee20b2e..8a118c953 100644 --- a/near-contract-standards/src/non_fungible_token/utils.rs +++ b/near-contract-standards/src/non_fungible_token/utils.rs @@ -1,4 +1,5 @@ -use near_sdk::{env, require, AccountId, NearToken, Promise}; +use near_sdk::errors::InsufficientBalance; +use near_sdk::{contract_error, env, require_or_err, AccountId, BaseError, NearToken, Promise}; use std::collections::HashMap; use std::mem::size_of; @@ -27,30 +28,48 @@ pub fn refund_approved_account_ids( refund_approved_account_ids_iter(account_id, approved_account_ids.keys()) } -pub fn refund_deposit_to_account(storage_used: u64, account_id: AccountId) { +pub fn refund_deposit_to_account( + storage_used: u64, + account_id: AccountId, +) -> Result<(), BaseError> { let required_cost = env::storage_byte_cost().saturating_mul(storage_used.into()); let attached_deposit = env::attached_deposit(); - require!( + require_or_err!( required_cost <= attached_deposit, - format!("Must attach {} yoctoNEAR to cover storage", required_cost) + InsufficientBalance::new(Some( + format!("Must attach {} yoctoNEAR to cover storage", required_cost).as_str() + )) ); let refund = attached_deposit.saturating_sub(required_cost); if refund.as_yoctonear() > 1 { Promise::new(account_id).transfer(refund); } + Ok(()) } /// Assumes that the precedecessor will be refunded -pub fn refund_deposit(storage_used: u64) { +pub fn refund_deposit(storage_used: u64) -> Result<(), BaseError> { refund_deposit_to_account(storage_used, env::predecessor_account_id()) } /// Assert that at least 1 yoctoNEAR was attached. -pub(crate) fn assert_at_least_one_yocto() { - require!( +pub(crate) fn assert_at_least_one_yocto() -> Result<(), BaseError> { + require_or_err!( env::attached_deposit() >= NearToken::from_yoctonear(1), - "Requires attached deposit of at least 1 yoctoNEAR" - ) + InsufficientBalance::new(Some("Requires attached deposit of at least 1 yoctoNEAR")) + ); + Ok(()) +} + +#[contract_error] +pub struct ApprovalNotSupported { + message: String, +} + +impl ApprovalNotSupported { + pub fn new(message: &str) -> Self { + Self { message: String::from(message) } + } } diff --git a/near-contract-standards/src/storage_management/mod.rs b/near-contract-standards/src/storage_management/mod.rs index 928fe7dfe..7eedbe240 100644 --- a/near-contract-standards/src/storage_management/mod.rs +++ b/near-contract-standards/src/storage_management/mod.rs @@ -1,4 +1,4 @@ -use near_sdk::{near, AccountId, NearToken}; +use near_sdk::{near, AccountId, BaseError, NearToken}; #[near(serializers=[borsh, json])] pub struct StorageBalance { @@ -21,7 +21,7 @@ pub struct StorageBalanceBounds { /// # Examples /// /// ``` -/// use near_sdk::{near, PanicOnDefault, AccountId, NearToken, log}; +/// use near_sdk::{near, PanicOnDefault, AccountId, NearToken, log, unwrap_or_err, BaseError}; /// use near_sdk::collections::LazyOption; /// use near_sdk::json_types::U128; /// use near_contract_standards::fungible_token::FungibleToken; @@ -44,23 +44,23 @@ pub struct StorageBalanceBounds { /// &mut self, /// account_id: Option, /// registration_only: Option, -/// ) -> StorageBalance { +/// ) -> Result { /// self.token.storage_deposit(account_id, registration_only) /// } /// /// #[payable] -/// fn storage_withdraw(&mut self, amount: Option) -> StorageBalance { +/// fn storage_withdraw(&mut self, amount: Option) -> Result { /// self.token.storage_withdraw(amount) /// } /// /// #[payable] -/// fn storage_unregister(&mut self, force: Option) -> bool { +/// fn storage_unregister(&mut self, force: Option) -> Result { /// #[allow(unused_variables)] -/// if let Some((account_id, balance)) = self.token.internal_storage_unregister(force) { +/// if let Some((account_id, balance)) = unwrap_or_err!(self.token.internal_storage_unregister(force)) { /// log!("Closed @{} with {}", account_id, balance); -/// true +/// Ok(true) /// } else { -/// false +/// Ok(false) /// } /// } /// @@ -82,7 +82,7 @@ pub trait StorageManagement { &mut self, account_id: Option, registration_only: Option, - ) -> StorageBalance; + ) -> Result; /// Withdraw specified amount of available Ⓝ for predecessor account. /// @@ -98,7 +98,7 @@ pub trait StorageManagement { /// function-call access-key call (UX wallet security) /// /// Returns the StorageBalance structure showing updated balances. - fn storage_withdraw(&mut self, amount: Option) -> StorageBalance; + fn storage_withdraw(&mut self, amount: Option) -> Result; /// Unregisters the predecessor account and returns the storage NEAR deposit back. /// @@ -111,7 +111,7 @@ pub trait StorageManagement { /// (UX wallet security) /// Returns `true` iff the account was unregistered. /// Returns `false` iff account was not registered before. - fn storage_unregister(&mut self, force: Option) -> bool; + fn storage_unregister(&mut self, force: Option) -> Result; fn storage_balance_bounds(&self) -> StorageBalanceBounds; diff --git a/near-contract-standards/src/upgrade/mod.rs b/near-contract-standards/src/upgrade/mod.rs index bfcbb9eb0..b7eb7449f 100644 --- a/near-contract-standards/src/upgrade/mod.rs +++ b/near-contract-standards/src/upgrade/mod.rs @@ -1,19 +1,26 @@ +use near_sdk::errors::{ContractUpgradeError, InvalidArgument, PermissionDenied}; use near_sdk::json_types::U64; -use near_sdk::{env, near, require, AccountId, Duration, Promise, Timestamp}; +use near_sdk::{ + env, near, require_or_err, unwrap_or_err, AccountId, BaseError, Duration, Promise, Timestamp, +}; type WrappedDuration = U64; pub trait Ownable { - fn assert_owner(&self) { - require!(env::predecessor_account_id() == self.get_owner(), "Owner must be predecessor"); + fn assert_owner(&self) -> Result<(), BaseError> { + require_or_err!( + env::predecessor_account_id() == self.get_owner(), + PermissionDenied::new(Some("Owner must be predecessor")) + ); + Ok(()) } fn get_owner(&self) -> AccountId; - fn set_owner(&mut self, owner: AccountId); + fn set_owner(&mut self, owner: AccountId) -> Result<(), BaseError>; } pub trait Upgradable { fn get_staging_duration(&self) -> WrappedDuration; - fn stage_code(&mut self, code: Vec, timestamp: Timestamp); + fn stage_code(&mut self, code: Vec, timestamp: Timestamp) -> Result<(), BaseError>; fn deploy_code(&mut self) -> Promise; /// Implement migration for the next version. @@ -42,9 +49,10 @@ impl Ownable for Upgrade { self.owner.clone() } - fn set_owner(&mut self, owner: AccountId) { - self.assert_owner(); + fn set_owner(&mut self, owner: AccountId) -> Result<(), BaseError> { + unwrap_or_err!(self.assert_owner()); self.owner = owner; + Ok(()) } } @@ -53,29 +61,34 @@ impl Upgradable for Upgrade { self.staging_duration.into() } - fn stage_code(&mut self, code: Vec, timestamp: Timestamp) { - self.assert_owner(); - require!( + fn stage_code(&mut self, code: Vec, timestamp: Timestamp) -> Result<(), BaseError> { + unwrap_or_err!(self.assert_owner()); + require_or_err!( env::block_timestamp() + self.staging_duration < timestamp, - "Timestamp must be later than staging duration" + InvalidArgument::new("Timestamp must be later than staging duration") ); // Writes directly into storage to avoid serialization penalty by using default struct. env::storage_write(b"upgrade", &code); self.staging_timestamp = timestamp; + Ok(()) } fn deploy_code(&mut self) -> Promise { if self.staging_timestamp < env::block_timestamp() { - env::panic_str( - format!( - "Deploy code too early: staging ends on {}", - self.staging_timestamp + self.staging_duration + env::panic_err( + ContractUpgradeError::new( + format!( + "Deploy code too early: staging ends on {}", + self.staging_timestamp + self.staging_duration + ) + .as_str(), ) - .as_str(), + .into(), ); } - let code = env::storage_read(b"upgrade") - .unwrap_or_else(|| env::panic_str("No upgrade code available")); + let code = env::storage_read(b"upgrade").unwrap_or_else(|| { + env::panic_err(ContractUpgradeError::new("No upgrade code available").into()) + }); env::storage_remove(b"upgrade"); Promise::new(env::current_account_id()).deploy_contract(code) } diff --git a/near-sdk-macros/src/core_impl/abi/abi_generator.rs b/near-sdk-macros/src/core_impl/abi/abi_generator.rs index 4089863ce..44080d40a 100644 --- a/near-sdk-macros/src/core_impl/abi/abi_generator.rs +++ b/near-sdk-macros/src/core_impl/abi/abi_generator.rs @@ -204,11 +204,16 @@ impl ImplItemMethodInfo { match &self.attr_signature_info.returns.kind { Default => quote! { ::std::option::Option::None }, General(ty) => self.abi_result_tokens_with_return_value(ty), - HandlesResult(ty) => { + HandlesResultExplicit(ty) => { // extract the `Ok` type from the result let ty = parse_quote! { <#ty as near_sdk::__private::ResultTypeExt>::Okay }; self.abi_result_tokens_with_return_value(&ty) } + HandlesResultImplicit(status_result) => { + let ty = &status_result.result_type; + let ty = parse_quote! { <#ty as near_sdk::__private::ResultTypeExt>::Okay }; + self.abi_result_tokens_with_return_value(&ty) + } } } diff --git a/near-sdk-macros/src/core_impl/code_generator/attr_sig_info.rs b/near-sdk-macros/src/core_impl/code_generator/attr_sig_info.rs index c6bbf4215..7838c8f04 100644 --- a/near-sdk-macros/src/core_impl/code_generator/attr_sig_info.rs +++ b/near-sdk-macros/src/core_impl/code_generator/attr_sig_info.rs @@ -222,11 +222,10 @@ impl AttrSigInfo { let ArgInfo { mutability, ident, ty, bindgen_ty, serializer_ty, .. } = arg; match &bindgen_ty { BindgenArgType::CallbackArg => { - let error_msg = format!("Callback computation {} was not successful", idx); let read_data = quote! { let data: ::std::vec::Vec = match ::near_sdk::env::promise_result(#idx) { ::near_sdk::PromiseResult::Successful(x) => x, - _ => ::near_sdk::env::panic_str(#error_msg) + _ => ::near_sdk::env::panic_err(::near_sdk::errors::CallbackComputationUnsuccessful::new(#idx).into()), }; }; let invocation = deserialize_data(serializer_ty); @@ -298,7 +297,7 @@ impl AttrSigInfo { |i| { let data: ::std::vec::Vec = match ::near_sdk::env::promise_result(i) { ::near_sdk::PromiseResult::Successful(x) => x, - _ => ::near_sdk::env::panic_str(&::std::format!("Callback computation {} was not successful", i)), + _ => ::near_sdk::env::panic_err(::near_sdk::errors::CallbackComputationUnsuccessful::new(i).into()), }; #invocation })); diff --git a/near-sdk-macros/src/core_impl/code_generator/impl_item_method_info.rs b/near-sdk-macros/src/core_impl/code_generator/impl_item_method_info.rs index fd5d507d2..ec03d1d9e 100644 --- a/near-sdk-macros/src/core_impl/code_generator/impl_item_method_info.rs +++ b/near-sdk-macros/src/core_impl/code_generator/impl_item_method_info.rs @@ -1,4 +1,5 @@ use crate::core_impl::info_extractor::{ImplItemMethodInfo, SerializerType}; +use crate::core_impl::utils; use crate::core_impl::{MethodKind, ReturnKind}; use proc_macro2::TokenStream as TokenStream2; use quote::quote; @@ -28,7 +29,8 @@ impl ImplItemMethodInfo { // here. ReturnKind::Default => self.void_return_body_tokens(), ReturnKind::General(_) => self.value_return_body_tokens(), - ReturnKind::HandlesResult { .. } => self.result_return_body_tokens(), + ReturnKind::HandlesResultExplicit { .. } => self.result_return_body_tokens(), + ReturnKind::HandlesResultImplicit { .. } => self.result_return_body_tokens(), }; quote! { @@ -84,6 +86,7 @@ impl ImplItemMethodInfo { let value_ser = self.value_ser_tokens(); let value_return = self.value_return_tokens(); let result_identifier = self.result_identifier(); + let handle_error = self.error_handling_tokens(); quote! { #contract_init @@ -94,8 +97,32 @@ impl ImplItemMethodInfo { #value_return #contract_ser } - ::std::result::Result::Err(err) => ::near_sdk::FunctionError::panic(&err) + ::std::result::Result::Err(err) => { + #handle_error + } + } + } + } + + fn error_handling_tokens(&self) -> TokenStream2 { + match &self.attr_signature_info.returns.kind { + ReturnKind::HandlesResultImplicit(status_result) => { + if status_result.persist_on_error { + let error_method_name = + quote::format_ident!("{}_error", self.attr_signature_info.ident); + let contract_ser = self.contract_ser_tokens(); + quote! { + #contract_ser + let promise = Contract::ext(::near_sdk::env::current_account_id()).#error_method_name(err).as_return(); + } + } else { + utils::standardized_error_panic_tokens() + } } + ReturnKind::HandlesResultExplicit { .. } => quote! { + ::near_sdk::FunctionError::panic(&err); + }, + _ => quote! {}, } } @@ -141,10 +168,10 @@ impl ImplItemMethodInfo { let reject_deposit_code = || { // If method is not payable, do a check to make sure that it doesn't consume deposit - let error = format!("Method {} doesn't accept deposit", self.attr_signature_info.ident); + let method_name = &self.attr_signature_info.ident.to_string(); quote! { if ::near_sdk::env::attached_deposit().as_yoctonear() != 0 { - ::near_sdk::env::panic_str(#error); + ::near_sdk::env::panic_err(::near_sdk::errors::DepositNotAccepted::new(#method_name).into()); } } }; @@ -172,10 +199,10 @@ impl ImplItemMethodInfo { fn private_check_tokens(&self) -> TokenStream2 { if self.attr_signature_info.is_private() { - let error = format!("Method {} is private", self.attr_signature_info.ident); + let method_name = &self.attr_signature_info.ident.to_string(); quote! { if ::near_sdk::env::current_account_id() != ::near_sdk::env::predecessor_account_id() { - ::near_sdk::env::panic_str(#error); + ::near_sdk::errors::PrivateMethod::new(#method_name); } } } else { @@ -195,7 +222,7 @@ impl ImplItemMethodInfo { if !init_method.ignores_state { quote! { if ::near_sdk::env::state_exists() { - ::near_sdk::env::panic_str("The contract has already been initialized"); + ::near_sdk::env::panic_err(::near_sdk::errors::ContractAlreadyInitialized{}.into()); } } } else { diff --git a/near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs b/near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs index 1dd3c3605..2c6888342 100644 --- a/near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +++ b/near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs @@ -1,16 +1,45 @@ use crate::core_impl::ext::generate_ext_function_wrappers; +use crate::core_impl::utils; +use crate::core_impl::ReturnKind; use crate::ItemImplInfo; use proc_macro2::TokenStream as TokenStream2; -use quote::ToTokens; +use quote::{format_ident, quote, ToTokens}; use syn::{spanned::Spanned, Ident}; impl ItemImplInfo { /// Generate the code that wraps pub fn wrapper_code(&self) -> TokenStream2 { let mut res = TokenStream2::new(); + let mut checks = quote! {}; for method in &self.methods { res.extend(method.method_wrapper()); + if let ReturnKind::HandlesResultImplicit { .. } = + method.attr_signature_info.returns.kind + { + let error_type = match &method.attr_signature_info.returns.original { + syn::ReturnType::Default => quote! { () }, + syn::ReturnType::Type(_, ty) => { + let error_type = utils::extract_error_type(ty); + quote! { #error_type } + } + }; + let method_name = &method.attr_signature_info.ident; + let check_trait_method_name = + format_ident!("assert_implements_my_trait{}", method_name); + + checks.extend(quote! { + fn #check_trait_method_name() { + let _ = near_sdk::check_contract_error_trait as fn(&#error_type); + } + }); + } } + let current_type = &self.ty; + res.extend(quote! { + impl #current_type { + #checks + } + }); res } @@ -23,13 +52,41 @@ impl ItemImplInfo { Err(e) => syn::Error::new(self.ty.span(), e).to_compile_error(), } } + + pub fn generate_error_methods(&self) -> TokenStream2 { + let mut error_methods = quote! {}; + + self.methods.iter().map(|m| &m.attr_signature_info).for_each(|method| { + let error_method_name = quote::format_ident!("{}_error", method.ident); + + if let ReturnKind::HandlesResultImplicit(status) = &method.returns.kind { + if status.persist_on_error { + let error_type = crate::get_error_type_from_status(status); + let panic_tokens = crate::standardized_error_panic_tokens(); + + let ty = self.ty.to_token_stream(); + + error_methods.extend(quote! { + #[near] + impl #ty { + pub fn #error_method_name(&self, err: #error_type) { + #panic_tokens + } + } + }); + } + } + }); + + error_methods + } } // Rustfmt removes comas. #[rustfmt::skip] #[cfg(test)] mod tests { - use syn::{Type, ImplItemFn, parse_quote}; - use crate::core_impl::info_extractor::ImplItemMethodInfo; + use syn::{parse_quote, ImplItemFn, Type}; + use crate::core_impl::info_extractor::{ImplItemMethodInfo, ItemImplInfo}; use crate::core_impl::utils::test_helpers::{local_insta_assert_snapshot, pretty_print_syn_str}; @@ -342,4 +399,40 @@ mod tests { let actual = method_info.method_wrapper(); local_insta_assert_snapshot!(pretty_print_syn_str(&actual).unwrap()); } + + #[test] + fn result_implicit() { + let impl_type: Type = syn::parse_str("Hello").unwrap(); + let mut method: ImplItemFn = parse_quote! { + pub fn method(&self) -> Result { } + }; + let method_info = ImplItemMethodInfo::new(&mut method, false, impl_type).unwrap().unwrap(); + let actual = method_info.method_wrapper(); + local_insta_assert_snapshot!(pretty_print_syn_str(&actual).unwrap()); + } + + #[test] + fn persist_on_error() { + let impl_type: Type = syn::parse_str("Hello").unwrap(); + let mut method: ImplItemFn = parse_quote! { + #[persist_on_error] + pub fn method(&mut self) -> Result { } + }; + let method_info = ImplItemMethodInfo::new(&mut method, false, impl_type).unwrap().unwrap(); + let actual = method_info.method_wrapper(); + local_insta_assert_snapshot!(pretty_print_syn_str(&actual).unwrap()); + } + + #[test] + fn generated_method_error() { + let mut impl_contract: syn::ItemImpl = parse_quote! { + impl Contract { + #[persist_on_error] + pub fn method(&mut self) -> Result { } + } + }; + let impl_contract_info = ItemImplInfo::new(&mut impl_contract).unwrap(); + let actual = impl_contract_info.generate_error_methods(); + local_insta_assert_snapshot!(pretty_print_syn_str(&actual).unwrap()); + } } diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/args_no_return_mut.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/args_no_return_mut.snap index c44db71cb..c61588662 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/args_no_return_mut.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/args_no_return_mut.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 155 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -7,7 +8,9 @@ expression: pretty_print_syn_str(&actual).unwrap() pub extern "C" fn method() { ::near_sdk::env::setup_panic_hook(); if ::near_sdk::env::attached_deposit().as_yoctonear() != 0 { - ::near_sdk::env::panic_str("Method method doesn't accept deposit"); + ::near_sdk::env::panic_err( + ::near_sdk::errors::DepositNotAccepted::new("method").into(), + ); } #[derive(::near_sdk::serde::Deserialize)] #[serde(crate = "::near_sdk::serde")] @@ -24,4 +27,3 @@ pub extern "C" fn method() { contract.method(k, m); ::near_sdk::env::state_write(&contract); } - diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/args_return_mut.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/args_return_mut.snap index 391c7056a..90c62d54a 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/args_return_mut.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/args_return_mut.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 165 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -7,7 +8,9 @@ expression: pretty_print_syn_str(&actual).unwrap() pub extern "C" fn method() { ::near_sdk::env::setup_panic_hook(); if ::near_sdk::env::attached_deposit().as_yoctonear() != 0 { - ::near_sdk::env::panic_str("Method method doesn't accept deposit"); + ::near_sdk::env::panic_err( + ::near_sdk::errors::DepositNotAccepted::new("method").into(), + ); } #[derive(::near_sdk::serde::Deserialize)] #[serde(crate = "::near_sdk::serde")] @@ -27,4 +30,3 @@ pub extern "C" fn method() { ::near_sdk::env::value_return(&result); ::near_sdk::env::state_write(&contract); } - diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/args_return_mut_borsh.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/args_return_mut_borsh.snap index d2aa37ba6..70066fe3b 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/args_return_mut_borsh.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/args_return_mut_borsh.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 299 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -7,7 +8,9 @@ expression: pretty_print_syn_str(&actual).unwrap() pub extern "C" fn method() { ::near_sdk::env::setup_panic_hook(); if ::near_sdk::env::attached_deposit().as_yoctonear() != 0 { - ::near_sdk::env::panic_str("Method method doesn't accept deposit"); + ::near_sdk::env::panic_err( + ::near_sdk::errors::DepositNotAccepted::new("method").into(), + ); } #[derive(::near_sdk::borsh::BorshDeserialize)] #[borsh(crate = "::near_sdk::borsh")] @@ -27,4 +30,3 @@ pub extern "C" fn method() { ::near_sdk::env::value_return(&result); ::near_sdk::env::state_write(&contract); } - diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args.snap index 2a38c958e..ef5eb4d43 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 205 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -8,7 +9,7 @@ pub extern "C" fn method() { ::near_sdk::env::setup_panic_hook(); if ::near_sdk::env::current_account_id() != ::near_sdk::env::predecessor_account_id() { - ::near_sdk::env::panic_str("Method method is private"); + ::near_sdk::errors::PrivateMethod::new("method"); } #[derive(::near_sdk::serde::Deserialize)] #[serde(crate = "::near_sdk::serde")] @@ -22,17 +23,24 @@ pub extern "C" fn method() { .expect("Failed to deserialize input from JSON."); let data: ::std::vec::Vec = match ::near_sdk::env::promise_result(0u64) { ::near_sdk::PromiseResult::Successful(x) => x, - _ => ::near_sdk::env::panic_str("Callback computation 0 was not successful"), + _ => { + ::near_sdk::env::panic_err( + ::near_sdk::errors::CallbackComputationUnsuccessful::new(0u64).into(), + ) + } }; let mut x: u64 = ::near_sdk::serde_json::from_slice(&data) .expect("Failed to deserialize callback using JSON"); let data: ::std::vec::Vec = match ::near_sdk::env::promise_result(1u64) { ::near_sdk::PromiseResult::Successful(x) => x, - _ => ::near_sdk::env::panic_str("Callback computation 1 was not successful"), + _ => { + ::near_sdk::env::panic_err( + ::near_sdk::errors::CallbackComputationUnsuccessful::new(1u64).into(), + ) + } }; let z: ::std::vec::Vec = ::near_sdk::serde_json::from_slice(&data) .expect("Failed to deserialize callback using JSON"); let contract: Hello = ::near_sdk::env::state_read().unwrap_or_default(); contract.method(&mut x, y, z); } - diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_mixed_serialization.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_mixed_serialization.snap index f67ff63f0..e303b1438 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_mixed_serialization.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_mixed_serialization.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 310 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -8,7 +9,7 @@ pub extern "C" fn method() { ::near_sdk::env::setup_panic_hook(); if ::near_sdk::env::current_account_id() != ::near_sdk::env::predecessor_account_id() { - ::near_sdk::env::panic_str("Method method is private"); + ::near_sdk::errors::PrivateMethod::new("method"); } #[derive(::near_sdk::borsh::BorshDeserialize)] #[borsh(crate = "::near_sdk::borsh")] @@ -22,17 +23,24 @@ pub extern "C" fn method() { .expect("Failed to deserialize input from Borsh."); let data: ::std::vec::Vec = match ::near_sdk::env::promise_result(0u64) { ::near_sdk::PromiseResult::Successful(x) => x, - _ => ::near_sdk::env::panic_str("Callback computation 0 was not successful"), + _ => { + ::near_sdk::env::panic_err( + ::near_sdk::errors::CallbackComputationUnsuccessful::new(0u64).into(), + ) + } }; let mut x: u64 = ::near_sdk::borsh::BorshDeserialize::try_from_slice(&data) .expect("Failed to deserialize callback using Borsh"); let data: ::std::vec::Vec = match ::near_sdk::env::promise_result(1u64) { ::near_sdk::PromiseResult::Successful(x) => x, - _ => ::near_sdk::env::panic_str("Callback computation 1 was not successful"), + _ => { + ::near_sdk::env::panic_err( + ::near_sdk::errors::CallbackComputationUnsuccessful::new(1u64).into(), + ) + } }; let z: ::std::vec::Vec = ::near_sdk::serde_json::from_slice(&data) .expect("Failed to deserialize callback using JSON"); let contract: Hello = ::near_sdk::env::state_read().unwrap_or_default(); contract.method(&mut x, y, z); } - diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_only.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_only.snap index 6be15f320..f1e8a504e 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_only.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_only.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 216 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -8,21 +9,28 @@ pub extern "C" fn method() { ::near_sdk::env::setup_panic_hook(); if ::near_sdk::env::current_account_id() != ::near_sdk::env::predecessor_account_id() { - ::near_sdk::env::panic_str("Method method is private"); + ::near_sdk::errors::PrivateMethod::new("method"); } let data: ::std::vec::Vec = match ::near_sdk::env::promise_result(0u64) { ::near_sdk::PromiseResult::Successful(x) => x, - _ => ::near_sdk::env::panic_str("Callback computation 0 was not successful"), + _ => { + ::near_sdk::env::panic_err( + ::near_sdk::errors::CallbackComputationUnsuccessful::new(0u64).into(), + ) + } }; let mut x: u64 = ::near_sdk::serde_json::from_slice(&data) .expect("Failed to deserialize callback using JSON"); let data: ::std::vec::Vec = match ::near_sdk::env::promise_result(1u64) { ::near_sdk::PromiseResult::Successful(x) => x, - _ => ::near_sdk::env::panic_str("Callback computation 1 was not successful"), + _ => { + ::near_sdk::env::panic_err( + ::near_sdk::errors::CallbackComputationUnsuccessful::new(1u64).into(), + ) + } }; let y: ::std::string::String = ::near_sdk::serde_json::from_slice(&data) .expect("Failed to deserialize callback using JSON"); let contract: Hello = ::near_sdk::env::state_read().unwrap_or_default(); contract.method(&mut x, y); } - diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_results.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_results.snap index f38a0e0f4..524deb45f 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_results.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_results.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 227 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -8,7 +9,7 @@ pub extern "C" fn method() { ::near_sdk::env::setup_panic_hook(); if ::near_sdk::env::current_account_id() != ::near_sdk::env::predecessor_account_id() { - ::near_sdk::env::panic_str("Method method is private"); + ::near_sdk::errors::PrivateMethod::new("method"); } let mut x: Result = match ::near_sdk::env::promise_result(0u64) { ::near_sdk::PromiseResult::Successful(data) => { @@ -37,4 +38,3 @@ pub extern "C" fn method() { let contract: Hello = ::near_sdk::env::state_read().unwrap_or_default(); contract.method(&mut x, y); } - diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_vec.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_vec.snap index e9e5deffb..115ed2eec 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_vec.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/callback_args_vec.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 238 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -8,7 +9,7 @@ pub extern "C" fn method() { ::near_sdk::env::setup_panic_hook(); if ::near_sdk::env::current_account_id() != ::near_sdk::env::predecessor_account_id() { - ::near_sdk::env::panic_str("Method method is private"); + ::near_sdk::errors::PrivateMethod::new("method"); } #[derive(::near_sdk::serde::Deserialize)] #[serde(crate = "::near_sdk::serde")] @@ -29,10 +30,9 @@ pub extern "C" fn method() { ) { ::near_sdk::PromiseResult::Successful(x) => x, _ => { - ::near_sdk::env::panic_str( - &::std::format!( - "Callback computation {} was not successful", i - ), + ::near_sdk::env::panic_err( + ::near_sdk::errors::CallbackComputationUnsuccessful::new(i) + .into(), ) } }; @@ -44,4 +44,3 @@ pub extern "C" fn method() { let contract: Hello = ::near_sdk::env::state_read().unwrap_or_default(); contract.method(x, y); } - diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/generated_method_error.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/generated_method_error.snap new file mode 100644 index 000000000..4ccc40af7 --- /dev/null +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/generated_method_error.snap @@ -0,0 +1,14 @@ +--- +source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 436 +expression: pretty_print_syn_str(&actual).unwrap() +--- +#[near] +impl Contract { + pub fn method_error( + &self, + err: as near_sdk::__private::ResultTypeExt>::Error, + ) { + ::near_sdk::env::panic_err(err.into()); + } +} diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_borsh.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_borsh.snap index f6e4c4db2..9e67ad62a 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_borsh.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_borsh.snap @@ -14,7 +14,9 @@ pub extern "C" fn method() { .expect("Failed to serialize the return value using Borsh."); ::near_sdk::env::value_return(&result); } - ::std::result::Result::Err(err) => ::near_sdk::FunctionError::panic(&err), + ::std::result::Result::Err(err) => { + ::near_sdk::FunctionError::panic(&err); + } } } diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_init.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_init.snap index 494fa004d..e9e1f1411 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_init.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_init.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 378 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -7,17 +8,24 @@ expression: pretty_print_syn_str(&actual).unwrap() pub extern "C" fn new() { ::near_sdk::env::setup_panic_hook(); if ::near_sdk::env::attached_deposit().as_yoctonear() != 0 { - ::near_sdk::env::panic_str("Method new doesn't accept deposit"); + ::near_sdk::env::panic_err( + ::near_sdk::errors::DepositNotAccepted::new("new").into(), + ); } if ::near_sdk::env::state_exists() { - ::near_sdk::env::panic_str("The contract has already been initialized"); + ::near_sdk::env::panic_err( + ::near_sdk::errors::ContractAlreadyInitialized { + } + .into(), + ); } let contract = Hello::new(); match contract { ::std::result::Result::Ok(contract) => { ::near_sdk::env::state_write(&contract); } - ::std::result::Result::Err(err) => ::near_sdk::FunctionError::panic(&err), + ::std::result::Result::Err(err) => { + ::near_sdk::FunctionError::panic(&err); + } } } - diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_init_ignore_state.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_init_ignore_state.snap index b6ba5f00b..c31f8e085 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_init_ignore_state.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_init_ignore_state.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 391 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -7,14 +8,17 @@ expression: pretty_print_syn_str(&actual).unwrap() pub extern "C" fn new() { ::near_sdk::env::setup_panic_hook(); if ::near_sdk::env::attached_deposit().as_yoctonear() != 0 { - ::near_sdk::env::panic_str("Method new doesn't accept deposit"); + ::near_sdk::env::panic_err( + ::near_sdk::errors::DepositNotAccepted::new("new").into(), + ); } let contract = Hello::new(); match contract { ::std::result::Result::Ok(contract) => { ::near_sdk::env::state_write(&contract); } - ::std::result::Result::Err(err) => ::near_sdk::FunctionError::panic(&err), + ::std::result::Result::Err(err) => { + ::near_sdk::FunctionError::panic(&err); + } } } - diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_json.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_json.snap index c613a8c21..e6be307bc 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_json.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_json.snap @@ -14,7 +14,9 @@ pub extern "C" fn method() { .expect("Failed to serialize the return value using JSON."); ::near_sdk::env::value_return(&result); } - ::std::result::Result::Err(err) => ::near_sdk::FunctionError::panic(&err), + ::std::result::Result::Err(err) => { + ::near_sdk::FunctionError::panic(&err); + } } } diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_mut.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_mut.snap index b7bff4894..71961f5ee 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_mut.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/handle_result_mut.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 352 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -7,7 +8,9 @@ expression: pretty_print_syn_str(&actual).unwrap() pub extern "C" fn method() { ::near_sdk::env::setup_panic_hook(); if ::near_sdk::env::attached_deposit().as_yoctonear() != 0 { - ::near_sdk::env::panic_str("Method method doesn't accept deposit"); + ::near_sdk::env::panic_err( + ::near_sdk::errors::DepositNotAccepted::new("method").into(), + ); } let mut contract: Hello = ::near_sdk::env::state_read().unwrap_or_default(); let result = contract.method(); @@ -18,7 +21,8 @@ pub extern "C" fn method() { ::near_sdk::env::value_return(&result); ::near_sdk::env::state_write(&contract); } - ::std::result::Result::Err(err) => ::near_sdk::FunctionError::panic(&err), + ::std::result::Result::Err(err) => { + ::near_sdk::FunctionError::panic(&err); + } } } - diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/init_ignore_state.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/init_ignore_state.snap index ce2b5a991..f5be1cfd0 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/init_ignore_state.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/init_ignore_state.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 274 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -7,7 +8,9 @@ expression: pretty_print_syn_str(&actual).unwrap() pub extern "C" fn method() { ::near_sdk::env::setup_panic_hook(); if ::near_sdk::env::attached_deposit().as_yoctonear() != 0 { - ::near_sdk::env::panic_str("Method method doesn't accept deposit"); + ::near_sdk::env::panic_err( + ::near_sdk::errors::DepositNotAccepted::new("method").into(), + ); } #[derive(::near_sdk::serde::Deserialize)] #[serde(crate = "::near_sdk::serde")] @@ -22,4 +25,3 @@ pub extern "C" fn method() { let contract = Hello::method(&mut k); ::near_sdk::env::state_write(&contract); } - diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/init_payable.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/init_payable.snap index e68d08b88..e618970b4 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/init_payable.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/init_payable.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 287 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -17,9 +18,12 @@ pub extern "C" fn method() { ) .expect("Failed to deserialize input from JSON."); if ::near_sdk::env::state_exists() { - ::near_sdk::env::panic_str("The contract has already been initialized"); + ::near_sdk::env::panic_err( + ::near_sdk::errors::ContractAlreadyInitialized { + } + .into(), + ); } let contract = Hello::method(&mut k); ::near_sdk::env::state_write(&contract); } - diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/no_args_no_return_mut.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/no_args_no_return_mut.snap index b0451a9fc..a280570b3 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/no_args_no_return_mut.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/no_args_no_return_mut.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 136 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -7,10 +8,11 @@ expression: pretty_print_syn_str(&actual).unwrap() pub extern "C" fn method() { ::near_sdk::env::setup_panic_hook(); if ::near_sdk::env::attached_deposit().as_yoctonear() != 0 { - ::near_sdk::env::panic_str("Method method doesn't accept deposit"); + ::near_sdk::env::panic_err( + ::near_sdk::errors::DepositNotAccepted::new("method").into(), + ); } let mut contract: Hello = ::near_sdk::env::state_read().unwrap_or_default(); contract.method(); ::near_sdk::env::state_write(&contract); } - diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/persist_on_error.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/persist_on_error.snap new file mode 100644 index 000000000..ea419b5ca --- /dev/null +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/persist_on_error.snap @@ -0,0 +1,31 @@ +--- +source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 423 +expression: pretty_print_syn_str(&actual).unwrap() +--- +#[cfg(target_arch = "wasm32")] +#[no_mangle] +pub extern "C" fn method() { + ::near_sdk::env::setup_panic_hook(); + if ::near_sdk::env::attached_deposit().as_yoctonear() != 0 { + ::near_sdk::env::panic_err( + ::near_sdk::errors::DepositNotAccepted::new("method").into(), + ); + } + let mut contract: Hello = ::near_sdk::env::state_read().unwrap_or_default(); + let result = contract.method(); + match result { + ::std::result::Result::Ok(result) => { + let result = ::near_sdk::serde_json::to_vec(&result) + .expect("Failed to serialize the return value using JSON."); + ::near_sdk::env::value_return(&result); + ::near_sdk::env::state_write(&contract); + } + ::std::result::Result::Err(err) => { + ::near_sdk::env::state_write(&contract); + let promise = Contract::ext(::near_sdk::env::current_account_id()) + .method_error(err) + .as_return(); + } + } +} diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/private_method.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/private_method.snap index 25ded1033..538b9cc9c 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/private_method.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/private_method.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 328 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -8,13 +9,14 @@ pub extern "C" fn private_method() { ::near_sdk::env::setup_panic_hook(); if ::near_sdk::env::current_account_id() != ::near_sdk::env::predecessor_account_id() { - ::near_sdk::env::panic_str("Method private_method is private"); + ::near_sdk::errors::PrivateMethod::new("private_method"); } if ::near_sdk::env::attached_deposit().as_yoctonear() != 0 { - ::near_sdk::env::panic_str("Method private_method doesn't accept deposit"); + ::near_sdk::env::panic_err( + ::near_sdk::errors::DepositNotAccepted::new("private_method").into(), + ); } let mut contract: Hello = ::near_sdk::env::state_read().unwrap_or_default(); contract.private_method(); ::near_sdk::env::state_write(&contract); } - diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/result_implicit.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/result_implicit.snap new file mode 100644 index 000000000..a62592869 --- /dev/null +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/result_implicit.snap @@ -0,0 +1,22 @@ +--- +source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 411 +expression: pretty_print_syn_str(&actual).unwrap() +--- +#[cfg(target_arch = "wasm32")] +#[no_mangle] +pub extern "C" fn method() { + ::near_sdk::env::setup_panic_hook(); + let contract: Hello = ::near_sdk::env::state_read().unwrap_or_default(); + let result = contract.method(); + match result { + ::std::result::Result::Ok(result) => { + let result = ::near_sdk::serde_json::to_vec(&result) + .expect("Failed to serialize the return value using JSON."); + ::near_sdk::env::value_return(&result); + } + ::std::result::Result::Err(err) => { + ::near_sdk::env::panic_err(err.into()); + } + } +} diff --git a/near-sdk-macros/src/core_impl/code_generator/snapshots/simple_init.snap b/near-sdk-macros/src/core_impl/code_generator/snapshots/simple_init.snap index d0d52935a..7f94914b3 100644 --- a/near-sdk-macros/src/core_impl/code_generator/snapshots/simple_init.snap +++ b/near-sdk-macros/src/core_impl/code_generator/snapshots/simple_init.snap @@ -1,5 +1,6 @@ --- source: near-sdk-macros/src/core_impl/code_generator/item_impl_info.rs +assertion_line: 250 expression: pretty_print_syn_str(&actual).unwrap() --- #[cfg(target_arch = "wasm32")] @@ -7,7 +8,9 @@ expression: pretty_print_syn_str(&actual).unwrap() pub extern "C" fn method() { ::near_sdk::env::setup_panic_hook(); if ::near_sdk::env::attached_deposit().as_yoctonear() != 0 { - ::near_sdk::env::panic_str("Method method doesn't accept deposit"); + ::near_sdk::env::panic_err( + ::near_sdk::errors::DepositNotAccepted::new("method").into(), + ); } #[derive(::near_sdk::serde::Deserialize)] #[serde(crate = "::near_sdk::serde")] @@ -20,9 +23,12 @@ pub extern "C" fn method() { ) .expect("Failed to deserialize input from JSON."); if ::near_sdk::env::state_exists() { - ::near_sdk::env::panic_str("The contract has already been initialized"); + ::near_sdk::env::panic_err( + ::near_sdk::errors::ContractAlreadyInitialized { + } + .into(), + ); } let contract = Hello::method(&mut k); ::near_sdk::env::state_write(&contract); } - diff --git a/near-sdk-macros/src/core_impl/info_extractor/attr_sig_info.rs b/near-sdk-macros/src/core_impl/info_extractor/attr_sig_info.rs index 772565295..c3527aece 100644 --- a/near-sdk-macros/src/core_impl/info_extractor/attr_sig_info.rs +++ b/near-sdk-macros/src/core_impl/info_extractor/attr_sig_info.rs @@ -135,6 +135,9 @@ impl AttrSigInfo { } visitor.visit_result_serializer_attr(attr, &serializer)?; } + "persist_on_error" => { + visitor.visit_persist_on_error_attr(attr)?; + } "handle_result" => { if let Some(value) = args.aliased { let handle_result = HandleResultAttr { check: value }; diff --git a/near-sdk-macros/src/core_impl/info_extractor/impl_item_method_info.rs b/near-sdk-macros/src/core_impl/info_extractor/impl_item_method_info.rs index e59adc5b3..2176329af 100644 --- a/near-sdk-macros/src/core_impl/info_extractor/impl_item_method_info.rs +++ b/near-sdk-macros/src/core_impl/info_extractor/impl_item_method_info.rs @@ -75,30 +75,6 @@ mod tests { assert_eq!(expected, actual.to_string()); } - #[test] - fn handle_result_without_marker() { - let impl_type: Type = syn::parse_str("Hello").unwrap(); - let mut method: ImplItemMethod = parse_quote! { - pub fn method(&self) -> Result { } - }; - let actual = ImplItemMethodInfo::new(&mut method, false, impl_type).map(|_| ()).unwrap_err(); - let expected = "Serializing Result has been deprecated. Consider marking your method with #[handle_result] if the second generic represents a panicable error or replacing Result with another two type sum enum otherwise. If you really want to keep the legacy behavior, mark the method with #[handle_result] and make it return Result, near_sdk::Abort>."; - assert_eq!(expected, actual.to_string()); - } - - #[test] - fn init_result_without_handle_result() { - let impl_type: Type = syn::parse_str("Hello").unwrap(); - let mut method: ImplItemMethod = parse_quote! { - #[init] - pub fn new() -> Result { } - }; - let actual = ImplItemMethodInfo::new(&mut method, false, impl_type).map(|_| ()).unwrap_err(); - let expected = "Serializing Result has been deprecated. Consider marking your method with #[handle_result] if the second generic represents a panicable error or replacing Result with another two type sum enum otherwise. If you really want to keep the legacy behavior, mark the method with #[handle_result] and make it return Result, near_sdk::Abort>."; - assert_eq!(expected, actual.to_string()); - } - - #[test] fn payable_self_by_value_fails() { let impl_type: Type = syn::parse_str("Hello").unwrap(); diff --git a/near-sdk-macros/src/core_impl/info_extractor/mod.rs b/near-sdk-macros/src/core_impl/info_extractor/mod.rs index b1793a43a..e473c864e 100644 --- a/near-sdk-macros/src/core_impl/info_extractor/mod.rs +++ b/near-sdk-macros/src/core_impl/info_extractor/mod.rs @@ -85,7 +85,82 @@ pub struct Returns { #[derive(Clone, PartialEq, Eq)] pub enum ReturnKind { + /// Return type is not specified. + /// + /// Functions default to `()` and closures default to type inference. + /// When the contract call happens: + /// - Contract struct is initialized + /// - The method is called + /// - Contract state is written if it is modifying method. + /// In case of panic, state is not written. + /// + /// # Example: + /// ```ignore + /// pub fn foo(&mut self); + /// ``` Default, + + /// Return type is specified. But it does not have any specifics. + /// + /// When the contract call happens, in addition to the Default: + /// - The return value is serialized and returned + /// + /// # Example: + /// ```ignore + /// pub fn foo(&mut self) -> u64; + /// ``` General(Type), - HandlesResult(Type), + + /// Return type is Result and the function is marked with #[handle_result]. + /// ErrType struct implements near_sdk::FunctionError. (i.e. used with #[derive(near_sdk::FunctionError)]) + /// + /// When the contract call happens, in addition to the General: + /// - In case Result value is Ok, the unwrapped object is returned + /// - In case Result value is Err, panic is called and state is not written. + /// + /// # Example: + /// ```ignore + /// #[handle_result] + /// pub fn foo(&mut self) -> Result; + /// ``` + HandlesResultExplicit(Type), + + /// Return type is Result and, the function is not marked with #[handle_result] and + /// ErrType struct implements near_sdk::ContractErrorTrait (i.e. used with #[near_sdk::contract_error]) + /// + /// When the contract call happens, in addition to General: + /// - In case Result value is Err, panic is called and state is not written. + /// As soon as ErrType implements ContractErrorTrait, it is returned as a well-defined structure. + /// You can see the structure in #[contract_error] documentation. + /// If the error struct does not implement ContractErrorTrait, the code should not compile. + /// - In case #[persist_on_error] is used on method, panic is not called. + /// And the contract state is written safely. + /// But the extra _error method is generated. + /// And this method is called in a new Promise. + /// This method effectively panics with structured error. + /// + /// # Example: + /// ```ignore + /// #[contract_error] + /// pub struct MyError; + /// + /// // if Ok() is returned, everything ok, otherwise panic with well-structured error + /// pub fn foo(&mut self) -> Result; + /// ``` + /// + /// ```ignore + /// // Write state safely anyway. + /// // if Ok() is returned, just return. Otherwise call new Promise which will panic with well-structured error. + /// #[persist_on_error] + /// pub fn foo(&mut self) -> Result; + /// ``` + /// + HandlesResultImplicit(StatusResult), +} +/// In other cases the code should not compile + +#[derive(Clone, PartialEq, Eq)] +pub struct StatusResult { + pub result_type: Type, + pub persist_on_error: bool, } diff --git a/near-sdk-macros/src/core_impl/info_extractor/visitor.rs b/near-sdk-macros/src/core_impl/info_extractor/visitor.rs index 1d02c1909..32548d0d1 100644 --- a/near-sdk-macros/src/core_impl/info_extractor/visitor.rs +++ b/near-sdk-macros/src/core_impl/info_extractor/visitor.rs @@ -14,6 +14,7 @@ struct ParsedData { handles_result: ResultHandling, is_payable: bool, is_private: bool, + persist_on_error: bool, ignores_state: bool, result_serializer: SerializerType, receiver: Option, @@ -41,6 +42,7 @@ impl Default for ParsedData { handles_result: Default::default(), is_payable: Default::default(), is_private: Default::default(), + persist_on_error: Default::default(), ignores_state: Default::default(), result_serializer: SerializerType::JSON, receiver: Default::default(), @@ -131,6 +133,11 @@ impl Visitor { if params.check { ResultHandling::NoCheck } else { ResultHandling::Check } } + pub fn visit_persist_on_error_attr(&mut self, _attr: &Attribute) -> syn::Result<()> { + self.parsed_data.persist_on_error = true; + Ok(()) + } + pub fn visit_receiver(&mut self, receiver: &Receiver) -> syn::Result<()> { use VisitorKind::*; @@ -163,7 +170,11 @@ impl Visitor { }, ReturnType::Type(_, typ) => Ok(Returns { original: self.return_type.clone(), - kind: parse_return_kind(typ, self.parsed_data.handles_result)?, + kind: parse_return_kind( + typ, + self.parsed_data.handles_result, + self.parsed_data.persist_on_error, + )?, }), } } @@ -209,26 +220,26 @@ fn is_view(sig: &Signature) -> bool { } } -fn parse_return_kind(typ: &Type, handles_result: ResultHandling) -> syn::Result { +fn parse_return_kind( + typ: &Type, + handles_result: ResultHandling, + persist_on_error: bool, +) -> syn::Result { match handles_result { - ResultHandling::NoCheck => Ok(ReturnKind::HandlesResult(typ.clone())), + ResultHandling::NoCheck => Ok(ReturnKind::HandlesResultExplicit(typ.clone())), ResultHandling::Check => { if !utils::type_is_result(typ) { Err(Error::new(typ.span(), "Function marked with #[handle_result] should return Result (where E implements FunctionError). If you're trying to use a type alias for `Result`, try `#[handle_result(aliased)]`.")) } else { - Ok(ReturnKind::HandlesResult(typ.clone())) + Ok(ReturnKind::HandlesResultExplicit(typ.clone())) } } ResultHandling::None => { if utils::type_is_result(typ) { - Err(Error::new( - typ.span(), - "Serializing Result has been deprecated. Consider marking your method \ - with #[handle_result] if the second generic represents a panicable error or \ - replacing Result with another two type sum enum otherwise. If you really want \ - to keep the legacy behavior, mark the method with #[handle_result] and make \ - it return Result, near_sdk::Abort>.", - )) + Ok(ReturnKind::HandlesResultImplicit(crate::StatusResult { + result_type: typ.clone(), + persist_on_error, + })) } else { Ok(ReturnKind::General(typ.clone())) } diff --git a/near-sdk-macros/src/core_impl/mod.rs b/near-sdk-macros/src/core_impl/mod.rs index 4f0dcd0c4..037fc6f1b 100644 --- a/near-sdk-macros/src/core_impl/mod.rs +++ b/near-sdk-macros/src/core_impl/mod.rs @@ -10,3 +10,4 @@ pub(crate) use contract_metadata::contract_source_metadata_const; pub(crate) use contract_metadata::ContractMetadata; pub(crate) use event::{get_event_version, near_events}; pub(crate) use info_extractor::*; +pub(crate) use utils::{get_error_type_from_status, standardized_error_panic_tokens}; diff --git a/near-sdk-macros/src/core_impl/utils/mod.rs b/near-sdk-macros/src/core_impl/utils/mod.rs index ab70364bf..3a3ec346c 100644 --- a/near-sdk-macros/src/core_impl/utils/mod.rs +++ b/near-sdk-macros/src/core_impl/utils/mod.rs @@ -1,3 +1,4 @@ +use crate::StatusResult; use proc_macro2::{Group, Span, TokenStream as TokenStream2, TokenTree}; use quote::quote; use syn::spanned::Spanned; @@ -47,6 +48,26 @@ pub(crate) fn extract_ok_type(ty: &Type) -> Option<&Type> { } } +pub(crate) fn extract_error_type(ty: &Type) -> Option<&Type> { + match ty { + Type::Path(type_path) if type_path.qself.is_none() && path_is_result(&type_path.path) => { + // Get the first segment of the path (there should be only one, in fact: "Result"): + let type_params = &type_path.path.segments.first()?.arguments; + // We are interested in the first angle-bracketed param responsible for Ok type (""): + let generic_arg = match type_params { + PathArguments::AngleBracketed(params) => Some(params.args.last()?), + _ => None, + }?; + // This argument must be a type: + match generic_arg { + GenericArgument::Type(ty) => Some(ty), + _ => None, + } + } + _ => None, + } +} + /// Checks whether the given path is literally "Vec". /// Note that it won't match a fully qualified name `std::vec::Vec` or a type alias like /// `type MyVec = Vec`. @@ -152,6 +173,18 @@ pub struct SanitizeSelfResult { pub self_occurrences: Vec, } +pub fn get_error_type_from_status(status_result: &StatusResult) -> syn::Type { + let ty = status_result.result_type.clone(); + syn::parse_quote! { <#ty as near_sdk::__private::ResultTypeExt>::Error } +} + +pub fn standardized_error_panic_tokens() -> TokenStream2 { + quote! { + // Initial error is wrapped into a struct to be able to serialize the type of it. + ::near_sdk::env::panic_err(err.into()); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/near-sdk-macros/src/lib.rs b/near-sdk-macros/src/lib.rs index 899efa4a5..a4ac89346 100644 --- a/near-sdk-macros/src/lib.rs +++ b/near-sdk-macros/src/lib.rs @@ -383,6 +383,7 @@ pub fn near_bindgen(attr: TokenStream, item: TokenStream) -> TokenStream { let abi_embedded = abi::embed(); #[cfg(not(feature = "__abi-embed-checked"))] let abi_embedded = quote! {}; + TokenStream::from(quote! { #input #ext_gen @@ -445,11 +446,14 @@ fn process_impl_block( // Add wrapper methods for ext call API let ext_generated_code = item_impl_info.generate_ext_wrapper_code(); + let error_methods = item_impl_info.generate_error_methods(); + Ok(TokenStream::from(quote! { #ext_generated_code #input #generated_code #abi_generated + #error_methods }) .into()) } @@ -768,7 +772,7 @@ pub fn derive_no_default(item: TokenStream) -> TokenStream { TokenStream::from(quote! { impl ::std::default::Default for #name { fn default() -> Self { - ::near_sdk::env::panic_str("The contract is not initialized"); + ::near_sdk::env::panic_err(::near_sdk::errors::ContractNotInitialized{}.into()); } } }) @@ -835,7 +839,7 @@ pub fn function_error(item: TokenStream) -> TokenStream { TokenStream::from(quote! { impl ::near_sdk::FunctionError for #name { fn panic(&self) -> ! { - ::near_sdk::env::panic_str(&::std::string::ToString::to_string(&self)) + ::near_sdk::env::panic_err(::near_sdk::errors::ContractError::new(&::std::string::ToString::to_string(&self)).into()) } } }) @@ -927,3 +931,150 @@ pub fn derive_event_attributes(item: TokenStream) -> TokenStream { ) } } + +/// This attribute macro is used on a struct or enum to generate the necessary code for an error +/// returned from a contract method call. +/// +/// # Example: +/// +/// For the error: +/// ```ignore +/// #[contract_error] +/// pub struct MyError { +/// field1: String, +/// field2: String, +/// } +/// ``` +/// +/// And the function: +/// ```ignore +/// pub fn my_method(&self) -> Result<(), MyError>; +/// ``` +/// +/// The error is serialized as a JSON object with the following structure: +/// ```ignore +/// { +/// "error": { +/// // this name can be "SDK_CONTRACT_ERROR" and "CUSTOM_CONTRACT_ERROR" +/// // To generate "SDK_CONTRACT_ERROR", use sdk attribute `#[contract_error(sdk)]`. +/// // Otherwise, it will generate "CUSTOM_CONTRACT_ERROR" +/// "name": "CUSTOM_CONTRACT_ERROR", +/// "cause": { +/// // this name is the name of error struct +/// "name": "MyError", +/// "info": { +/// /// fields of the error struct +/// "field1": "value1", +/// "field2": "value2" +/// } +/// } +/// } +/// ``` +/// +/// Note: you can assign any error defined like that to BaseError: +/// ```ignore +/// let base_error: BaseError = MyError { field1: "value1".to_string(), field2: "value2".to_string() }.into(); +/// ``` +/// +/// Use inside_nearsdk attribute (#[contract_error(inside_nearsdk)]) if the error struct is defined inside near-sdk. +/// Don't use if it is defined outside. +/// +/// Internally, it makes error struct to: +/// - implement `near_sdk::ContractErrorTrait` so that it becomes correct error +/// which can be returned from contract method with defined structure. +/// - implement `From for near_sdk::BaseError` as a polymorphic solution +/// - implement `From for String` to convert the error to a string + +#[derive(FromMeta)] +struct ContractErrorArgs { + sdk: Option, + inside_nearsdk: Option, +} +#[proc_macro_attribute] +pub fn contract_error(attr: TokenStream, item: TokenStream) -> TokenStream { + let meta_list = match NestedMeta::parse_meta_list(attr.into()) { + Ok(v) => v, + Err(e) => { + return TokenStream::from(Error::from(e).write_errors()); + } + }; + + let contract_error_args = match ContractErrorArgs::from_list(&meta_list) { + Ok(v) => v, + Err(e) => { + return TokenStream::from(e.write_errors()); + } + }; + + let input = syn::parse_macro_input!(item as syn::DeriveInput); + let ident = &input.ident; + + let error_type = if contract_error_args.sdk.unwrap_or(false) { + quote! {"SDK_CONTRACT_ERROR"} + } else { + quote! {"CUSTOM_CONTRACT_ERROR"} + }; + + let near_sdk_crate: proc_macro2::TokenStream; + let mut bool_inside_nearsdk_for_macro = quote! {false}; + + if contract_error_args.inside_nearsdk.unwrap_or(false) { + near_sdk_crate = quote! {crate}; + bool_inside_nearsdk_for_macro = quote! {true}; + } else { + near_sdk_crate = quote! {::near_sdk}; + }; + + let mut expanded = quote! { + #[#near_sdk_crate ::near(serializers=[json], inside_nearsdk=#bool_inside_nearsdk_for_macro)] + #input + + impl #near_sdk_crate ::ContractErrorTrait for #ident { + fn error_type(&self) -> &'static str { + #error_type + } + + fn wrap(&self) -> #near_sdk_crate ::serde_json::Value { + #[#near_sdk_crate ::near(inside_nearsdk=#bool_inside_nearsdk_for_macro, serializers = [json])] + struct ErrorWrapper { + name: String, + cause: ErrorCause, + } + + #[#near_sdk_crate ::near(inside_nearsdk=#bool_inside_nearsdk_for_macro, serializers = [json])] + struct ErrorCause { + name: String, + info: T + } + + #near_sdk_crate ::serde_json::json! { + { "error" : ErrorWrapper { + name: String::from(self.error_type()), + cause: ErrorCause { + name: std::any::type_name::<#ident>().to_string(), + info: self + } + } } + } + } + } + }; + if *ident != "BaseError" { + expanded.extend(quote! { + impl From<#ident> for String { + fn from(err: #ident) -> Self { + #near_sdk_crate ::serde_json::json!{#near_sdk_crate ::wrap_error(err)}.to_string() + } + } + + impl From<#ident> for #near_sdk_crate ::BaseError { + fn from(err: #ident) -> Self { + #near_sdk_crate ::BaseError{ + error: #near_sdk_crate ::wrap_error(err), + } + } + } + }); + } + TokenStream::from(expanded) +} diff --git a/near-sdk/compilation_tests/all.rs b/near-sdk/compilation_tests/all.rs index baad7f804..e6f9a622d 100644 --- a/near-sdk/compilation_tests/all.rs +++ b/near-sdk/compilation_tests/all.rs @@ -37,4 +37,5 @@ fn compilation_tests() { t.compile_fail("compilation_tests/contract_metadata_fn_name.rs"); t.pass("compilation_tests/contract_metadata_bindgen.rs"); t.pass("compilation_tests/types.rs"); + t.compile_fail("compilation_tests/error_handling_incorrect_result.rs"); } diff --git a/near-sdk/compilation_tests/error_handling_incorrect_result.rs b/near-sdk/compilation_tests/error_handling_incorrect_result.rs new file mode 100644 index 000000000..976ceb982 --- /dev/null +++ b/near-sdk/compilation_tests/error_handling_incorrect_result.rs @@ -0,0 +1,28 @@ +// Find all our documentation at https://docs.near.org +use near_sdk::contract_error; +use near_sdk::near; + +#[contract_error] +pub enum MyErrorEnum { + X, +} + +#[contract_error(sdk)] +pub struct MyErrorStruct { + x: u32, +} + +#[near(contract_state)] +#[derive(Default)] +pub struct Contract { + value: u32, +} + +#[near] +impl Contract { + pub fn inc_incorrect_result_type(&mut self) -> Result { + Err(0) + } +} + +fn main() {} diff --git a/near-sdk/compilation_tests/error_handling_incorrect_result.stderr b/near-sdk/compilation_tests/error_handling_incorrect_result.stderr new file mode 100644 index 000000000..1c1099ba9 --- /dev/null +++ b/near-sdk/compilation_tests/error_handling_incorrect_result.stderr @@ -0,0 +1,22 @@ +error[E0277]: the trait bound `u64: ContractErrorTrait` is not satisfied + --> compilation_tests/error_handling_incorrect_result.rs:21:1 + | +21 | #[near] + | ^^^^^^^ the trait `ContractErrorTrait` is not implemented for `u64` + | + = help: the following other types implement trait `ContractErrorTrait`: + MyErrorEnum + MyErrorStruct + InvalidArgument + InvalidContractState + near_sdk::errors::PermissionDenied + ContractUpgradeError + RequireFailed + PromiseFailed + and $N others +note: required by a bound in `check_contract_error_trait` + --> src/utils/contract_error.rs + | + | pub fn check_contract_error_trait(_: &T) {} + | ^^^^^^^^^^^^^^^^^^ required by this bound in `check_contract_error_trait` + = note: this error originates in the attribute macro `::near_sdk::near_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/near-sdk/compilation_tests/schema_derive_invalids.stderr b/near-sdk/compilation_tests/schema_derive_invalids.stderr index 07717bd18..52f705d4e 100644 --- a/near-sdk/compilation_tests/schema_derive_invalids.stderr +++ b/near-sdk/compilation_tests/schema_derive_invalids.stderr @@ -87,7 +87,7 @@ error[E0277]: the trait bound `Inner: JsonSchema` is not satisfied i128 and $N others note: required by a bound in `SchemaGenerator::subschema_for` - --> $CARGO/schemars-0.8.20/src/gen.rs + --> $CARGO/schemars-0.8.21/src/gen.rs | | pub fn subschema_for(&mut self) -> Schema { | ^^^^^^^^^^ required by this bound in `SchemaGenerator::subschema_for` diff --git a/near-sdk/src/collections/lazy_option.rs b/near-sdk/src/collections/lazy_option.rs index ad88b3835..23200f037 100644 --- a/near-sdk/src/collections/lazy_option.rs +++ b/near-sdk/src/collections/lazy_option.rs @@ -8,12 +8,10 @@ use std::marker::PhantomData; use borsh::{to_vec, BorshDeserialize, BorshSerialize}; use crate::env; +use crate::errors; use crate::IntoStorageKey; use near_sdk_macros::near; -const ERR_VALUE_SERIALIZATION: &str = "Cannot serialize value with Borsh"; -const ERR_VALUE_DESERIALIZATION: &str = "Cannot deserialize value with Borsh"; - /// An persistent lazy option, that stores a value in the storage. #[near(inside_nearsdk)] pub struct LazyOption { @@ -94,14 +92,14 @@ where fn serialize_value(value: &T) -> Vec { match to_vec(value) { Ok(x) => x, - Err(_) => env::panic_str(ERR_VALUE_SERIALIZATION), + Err(_) => env::panic_err(errors::BorshSerializeError::new("value").into()), } } fn deserialize_value(raw_value: &[u8]) -> T { match T::try_from_slice(raw_value) { Ok(x) => x, - Err(_) => env::panic_str(ERR_VALUE_DESERIALIZATION), + Err(_) => env::panic_err(errors::BorshDeserializeError::new("value").into()), } } diff --git a/near-sdk/src/collections/lookup_map.rs b/near-sdk/src/collections/lookup_map.rs index d699c76b0..fb71b8aab 100644 --- a/near-sdk/src/collections/lookup_map.rs +++ b/near-sdk/src/collections/lookup_map.rs @@ -6,13 +6,9 @@ use std::marker::PhantomData; use borsh::{to_vec, BorshDeserialize, BorshSerialize}; use crate::collections::append_slice; -use crate::{env, IntoStorageKey}; +use crate::{env, errors, IntoStorageKey}; use near_sdk_macros::near; -const ERR_KEY_SERIALIZATION: &str = "Cannot serialize key with Borsh"; -const ERR_VALUE_DESERIALIZATION: &str = "Cannot deserialize value with Borsh"; -const ERR_VALUE_SERIALIZATION: &str = "Cannot serialize value with Borsh"; - /// An non-iterable implementation of a map that stores its content directly on the trie. #[near(inside_nearsdk)] pub struct LookupMap { @@ -86,21 +82,21 @@ where fn serialize_key(key: &K) -> Vec { match to_vec(key) { Ok(x) => x, - Err(_) => env::panic_str(ERR_KEY_SERIALIZATION), + Err(_) => env::panic_err(errors::BorshSerializeError::new("key").into()), } } fn deserialize_value(raw_value: &[u8]) -> V { match V::try_from_slice(raw_value) { Ok(x) => x, - Err(_) => env::panic_str(ERR_VALUE_DESERIALIZATION), + Err(_) => env::panic_err(errors::BorshDeserializeError::new("value").into()), } } fn serialize_value(value: &V) -> Vec { match to_vec(value) { Ok(x) => x, - Err(_) => env::panic_str(ERR_VALUE_SERIALIZATION), + Err(_) => env::panic_err(errors::BorshSerializeError::new("value").into()), } } diff --git a/near-sdk/src/collections/lookup_set.rs b/near-sdk/src/collections/lookup_set.rs index f120485f9..79b9c5439 100644 --- a/near-sdk/src/collections/lookup_set.rs +++ b/near-sdk/src/collections/lookup_set.rs @@ -7,9 +7,7 @@ use borsh::{to_vec, BorshSerialize}; use near_sdk_macros::near; use crate::collections::append_slice; -use crate::{env, IntoStorageKey}; - -const ERR_ELEMENT_SERIALIZATION: &str = "Cannot serialize element with Borsh"; +use crate::{env, errors, IntoStorageKey}; /// A non-iterable implementation of a set that stores its content directly on the storage trie. /// @@ -71,7 +69,7 @@ where fn serialize_element(element: &T) -> Vec { match to_vec(element) { Ok(x) => x, - Err(_) => env::panic_str(ERR_ELEMENT_SERIALIZATION), + Err(_) => env::panic_err(errors::BorshSerializeError::new("element").into()), } } diff --git a/near-sdk/src/collections/tree_map.rs b/near-sdk/src/collections/tree_map.rs index efeda40b2..813d2db40 100644 --- a/near-sdk/src/collections/tree_map.rs +++ b/near-sdk/src/collections/tree_map.rs @@ -3,7 +3,7 @@ use std::ops::Bound; use crate::collections::LookupMap; use crate::collections::{append, Vector}; -use crate::{env, IntoStorageKey}; +use crate::{env, errors, IntoStorageKey}; use near_sdk_macros::near; /// TreeMap based on AVL-tree @@ -370,10 +370,18 @@ where /// ``` pub fn range(&self, r: (Bound, Bound)) -> impl Iterator + '_ { let (lo, hi) = match r { - (Bound::Included(a), Bound::Included(b)) if a > b => env::panic_str("Invalid range."), - (Bound::Excluded(a), Bound::Included(b)) if a > b => env::panic_str("Invalid range."), - (Bound::Included(a), Bound::Excluded(b)) if a > b => env::panic_str("Invalid range."), - (Bound::Excluded(a), Bound::Excluded(b)) if a >= b => env::panic_str("Invalid range."), + (Bound::Included(a), Bound::Included(b)) if a > b => { + env::panic_err(errors::InvalidTreeMapRange {}.into()) + } + (Bound::Excluded(a), Bound::Included(b)) if a > b => { + env::panic_err(errors::InvalidTreeMapRange {}.into()) + } + (Bound::Included(a), Bound::Excluded(b)) if a > b => { + env::panic_err(errors::InvalidTreeMapRange {}.into()) + } + (Bound::Excluded(a), Bound::Excluded(b)) if a >= b => { + env::panic_err(errors::InvalidTreeMapRange {}.into()) + } (lo, hi) => (lo, hi), }; @@ -1732,35 +1740,35 @@ mod tests { } #[test] - #[should_panic(expected = "Invalid range.")] + #[should_panic(expected = "InvalidTreeMapRange")] fn test_range_panics_same_excluded() { let map: TreeMap = TreeMap::new(next_trie_id()); let _ = map.range((Bound::Excluded(1), Bound::Excluded(1))); } #[test] - #[should_panic(expected = "Invalid range.")] + #[should_panic(expected = "InvalidTreeMapRange")] fn test_range_panics_non_overlap_incl_exlc() { let map: TreeMap = TreeMap::new(next_trie_id()); let _ = map.range((Bound::Included(2), Bound::Excluded(1))); } #[test] - #[should_panic(expected = "Invalid range.")] + #[should_panic(expected = "InvalidTreeMapRange")] fn test_range_panics_non_overlap_excl_incl() { let map: TreeMap = TreeMap::new(next_trie_id()); let _ = map.range((Bound::Excluded(2), Bound::Included(1))); } #[test] - #[should_panic(expected = "Invalid range.")] + #[should_panic(expected = "InvalidTreeMapRange")] fn test_range_panics_non_overlap_incl_incl() { let map: TreeMap = TreeMap::new(next_trie_id()); let _ = map.range((Bound::Included(2), Bound::Included(1))); } #[test] - #[should_panic(expected = "Invalid range.")] + #[should_panic(expected = "InvalidTreeMapRange")] fn test_range_panics_non_overlap_excl_excl() { let map: TreeMap = TreeMap::new(next_trie_id()); let _ = map.range((Bound::Excluded(2), Bound::Excluded(1))); diff --git a/near-sdk/src/collections/unordered_map/mod.rs b/near-sdk/src/collections/unordered_map/mod.rs index c59101bc0..34d2f543e 100644 --- a/near-sdk/src/collections/unordered_map/mod.rs +++ b/near-sdk/src/collections/unordered_map/mod.rs @@ -5,16 +5,11 @@ mod iter; pub use iter::Iter; use crate::collections::{append, append_slice, Vector}; -use crate::{env, IntoStorageKey}; +use crate::{env, errors, IntoStorageKey}; use borsh::{to_vec, BorshDeserialize, BorshSerialize}; use near_sdk_macros::near; use std::mem::size_of; -const ERR_INCONSISTENT_STATE: &str = "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; -const ERR_KEY_SERIALIZATION: &str = "Cannot serialize key with Borsh"; -const ERR_VALUE_DESERIALIZATION: &str = "Cannot deserialize value with Borsh"; -const ERR_VALUE_SERIALIZATION: &str = "Cannot serialize value with Borsh"; - /// An iterable implementation of a map that stores its content directly on the trie. #[near(inside_nearsdk)] pub struct UnorderedMap { @@ -53,7 +48,7 @@ impl UnorderedMap { let keys_len = self.keys.len(); let values_len = self.values.len(); if keys_len != values_len { - env::panic_str(ERR_INCONSISTENT_STATE) + env::panic_err(errors::InconsistentCollectionState::new().into()) } else { keys_len } @@ -64,7 +59,7 @@ impl UnorderedMap { let keys_is_empty = self.keys.is_empty(); let values_is_empty = self.values.is_empty(); if keys_is_empty != values_is_empty { - env::panic_str(ERR_INCONSISTENT_STATE) + env::panic_err(errors::InconsistentCollectionState::new().into()) } else { keys_is_empty } @@ -118,7 +113,7 @@ impl UnorderedMap { fn get_raw(&self, key_raw: &[u8]) -> Option> { self.get_index_raw(key_raw).map(|index| match self.values.get_raw(index) { Some(x) => x, - None => env::panic_str(ERR_INCONSISTENT_STATE), + None => env::panic_err(errors::InconsistentCollectionState::new().into()), }) } @@ -162,7 +157,7 @@ impl UnorderedMap { // element. let last_key_raw = match self.keys.get_raw(self.len() - 1) { Some(x) => x, - None => env::panic_str(ERR_INCONSISTENT_STATE), + None => env::panic_err(errors::InconsistentCollectionState::new().into()), }; env::storage_remove(&index_lookup); // If the removed element was the last element from keys, then we don't need to @@ -189,21 +184,21 @@ where fn serialize_key(key: &K) -> Vec { match to_vec(key) { Ok(x) => x, - Err(_) => env::panic_str(ERR_KEY_SERIALIZATION), + Err(_) => env::panic_err(errors::BorshSerializeError::new("key").into()), } } fn deserialize_value(raw_value: &[u8]) -> V { match V::try_from_slice(raw_value) { Ok(x) => x, - Err(_) => env::panic_str(ERR_VALUE_DESERIALIZATION), + Err(_) => env::panic_err(errors::BorshDeserializeError::new("value").into()), } } fn serialize_value(value: &V) -> Vec { match to_vec(value) { Ok(x) => x, - Err(_) => env::panic_str(ERR_VALUE_SERIALIZATION), + Err(_) => env::panic_err(errors::BorshSerializeError::new("value").into()), } } diff --git a/near-sdk/src/collections/unordered_set.rs b/near-sdk/src/collections/unordered_set.rs index 119cf089a..38189b421 100644 --- a/near-sdk/src/collections/unordered_set.rs +++ b/near-sdk/src/collections/unordered_set.rs @@ -1,14 +1,10 @@ //! A set implemented on a trie. Unlike `std::collections::HashSet` the elements in this set are not //! hashed but are instead serialized. use crate::collections::{append, append_slice, Vector}; -use crate::{env, IntoStorageKey}; +use crate::{env, errors, IntoStorageKey}; use borsh::{to_vec, BorshDeserialize, BorshSerialize}; use near_sdk_macros::near; use std::mem::size_of; - -const ERR_INCONSISTENT_STATE: &str = "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; -const ERR_ELEMENT_SERIALIZATION: &str = "Cannot serialize element with Borsh"; - /// An iterable implementation of a set that stores its content directly on the trie. #[near(inside_nearsdk)] pub struct UnorderedSet { @@ -117,7 +113,7 @@ impl UnorderedSet { // element. let last_element_raw = match self.elements.get_raw(self.len() - 1) { Some(x) => x, - None => env::panic_str(ERR_INCONSISTENT_STATE), + None => env::panic_err(errors::InconsistentCollectionState::new().into()), }; env::storage_remove(&index_lookup); // If the removed element was the last element from keys, then we don't need to @@ -144,7 +140,7 @@ where fn serialize_element(element: &T) -> Vec { match to_vec(element) { Ok(x) => x, - Err(_) => env::panic_str(ERR_ELEMENT_SERIALIZATION), + Err(_) => env::panic_err(errors::BorshSerializeError::new("element").into()), } } diff --git a/near-sdk/src/collections/vector.rs b/near-sdk/src/collections/vector.rs index 630d8250f..c8e1d0b96 100644 --- a/near-sdk/src/collections/vector.rs +++ b/near-sdk/src/collections/vector.rs @@ -8,15 +8,10 @@ use borsh::{to_vec, BorshDeserialize, BorshSerialize}; use near_sdk_macros::near; use crate::collections::append_slice; -use crate::{env, IntoStorageKey}; - -const ERR_INCONSISTENT_STATE: &str = "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; -const ERR_ELEMENT_DESERIALIZATION: &str = "Cannot deserialize element"; -const ERR_ELEMENT_SERIALIZATION: &str = "Cannot serialize element"; -const ERR_INDEX_OUT_OF_BOUNDS: &str = "Index out of bounds"; +use crate::{env, errors, IntoStorageKey}; fn expect_consistent_state(val: Option) -> T { - val.unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)) + val.unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())) } /// An iterable implementation of vector that stores its content on the trie. @@ -95,7 +90,7 @@ impl Vector { /// Panics if `index` is out of bounds. pub fn swap_remove_raw(&mut self, index: u64) -> Vec { if index >= self.len { - env::panic_str(ERR_INDEX_OUT_OF_BOUNDS) + env::panic_err(errors::IndexOutOfBounds {}.into()) } else if index + 1 == self.len { expect_consistent_state(self.pop_raw()) } else { @@ -104,7 +99,7 @@ impl Vector { if env::storage_write(&lookup_key, &raw_last_value) { expect_consistent_state(env::storage_get_evicted()) } else { - env::panic_str(ERR_INCONSISTENT_STATE) + env::panic_err(errors::InconsistentCollectionState::new().into()) } } } @@ -128,7 +123,7 @@ impl Vector { let raw_last_value = if env::storage_remove(&last_lookup_key) { expect_consistent_state(env::storage_get_evicted()) } else { - env::panic_str(ERR_INCONSISTENT_STATE) + env::panic_err(errors::InconsistentCollectionState::new().into()) }; Some(raw_last_value) } @@ -141,13 +136,13 @@ impl Vector { /// If `index` is out of bounds. pub fn replace_raw(&mut self, index: u64, raw_element: &[u8]) -> Vec { if index >= self.len { - env::panic_str(ERR_INDEX_OUT_OF_BOUNDS) + env::panic_err(errors::IndexOutOfBounds {}.into()) } else { let lookup_key = self.index_to_lookup_key(index); if env::storage_write(&lookup_key, raw_element) { expect_consistent_state(env::storage_get_evicted()) } else { - env::panic_str(ERR_INCONSISTENT_STATE); + env::panic_err(errors::InconsistentCollectionState::new().into()) } } } @@ -181,7 +176,8 @@ where T: BorshSerialize, { fn serialize_element(element: &T) -> Vec { - to_vec(element).unwrap_or_else(|_| env::panic_str(ERR_ELEMENT_SERIALIZATION)) + to_vec(element) + .unwrap_or_else(|_| env::panic_err(errors::BorshSerializeError::new("element").into())) } /// Appends an element to the back of the collection. @@ -203,8 +199,9 @@ where T: BorshDeserialize, { fn deserialize_element(raw_element: &[u8]) -> T { - T::try_from_slice(raw_element) - .unwrap_or_else(|_| env::panic_str(ERR_ELEMENT_DESERIALIZATION)) + T::try_from_slice(raw_element).unwrap_or_else(|_| { + env::panic_err(errors::BorshDeserializeError::new("element").into()) + }) } /// Returns the element by index or `None` if it is not present. diff --git a/near-sdk/src/environment/env.rs b/near-sdk/src/environment/env.rs index 901d0bc45..3729deac9 100644 --- a/near-sdk/src/environment/env.rs +++ b/near-sdk/src/environment/env.rs @@ -14,12 +14,9 @@ use crate::promise::Allowance; use crate::types::{ AccountId, BlockHeight, Gas, NearToken, PromiseIndex, PromiseResult, PublicKey, StorageUsage, }; -use crate::{CryptoHash, GasWeight, PromiseError}; +use crate::{errors, CryptoHash, GasWeight, PromiseError}; use near_sys as sys; -const REGISTER_EXPECTED_ERR: &str = - "Register was expected to have data because we just wrote it into it."; - /// Register used internally for atomic operations. This register is safe to use by the user, /// since it only needs to be untouched while methods of `Environment` execute, which is guaranteed /// guest code is not parallel. @@ -36,7 +33,7 @@ const MIN_ACCOUNT_ID_LEN: u64 = 2; const MAX_ACCOUNT_ID_LEN: u64 = 64; fn expect_register(option: Option) -> T { - option.unwrap_or_else(|| panic_str(REGISTER_EXPECTED_ERR)) + option.unwrap_or_else(|| panic_err(errors::RegisterEmpty::new().into())) } /// A simple macro helper to read blob value coming from host's method. @@ -462,7 +459,7 @@ pub fn alt_bn128_g1_multiexp(value: &[u8]) -> Vec { unsafe { sys::alt_bn128_g1_multiexp(value.len() as _, value.as_ptr() as _, ATOMIC_OP_REGISTER); }; - read_register(ATOMIC_OP_REGISTER).expect(REGISTER_EXPECTED_ERR) + expect_register(read_register(ATOMIC_OP_REGISTER)) } /// Compute alt_bn128 g1 sum. @@ -475,7 +472,7 @@ pub fn alt_bn128_g1_sum(value: &[u8]) -> Vec { unsafe { sys::alt_bn128_g1_sum(value.len() as _, value.as_ptr() as _, ATOMIC_OP_REGISTER); }; - read_register(ATOMIC_OP_REGISTER).expect(REGISTER_EXPECTED_ERR) + expect_register(read_register(ATOMIC_OP_REGISTER)) } /// Compute pairing check @@ -858,6 +855,10 @@ pub fn panic_str(message: &str) -> ! { unsafe { sys::panic_utf8(message.len() as _, message.as_ptr() as _) } } +pub fn panic_err(err: crate::BaseError) -> ! { + panic_str(serde_json::json!(err).to_string().as_str()) +} + /// Aborts the current contract execution without a custom message. /// To include a message, use [`panic_str`]. pub fn abort() -> ! { diff --git a/near-sdk/src/errors/custom.rs b/near-sdk/src/errors/custom.rs new file mode 100644 index 000000000..70d1559bc --- /dev/null +++ b/near-sdk/src/errors/custom.rs @@ -0,0 +1,123 @@ +use near_sdk_macros::contract_error; + +#[contract_error(inside_nearsdk)] +pub struct InvalidArgument { + pub message: String, +} + +impl InvalidArgument { + pub fn new(message: &str) -> Self { + Self { message: message.to_string() } + } +} + +#[contract_error(inside_nearsdk)] +pub struct InvalidContractState { + pub message: String, +} + +impl InvalidContractState { + pub fn new(message: &str) -> Self { + Self { message: message.to_string() } + } +} + +#[contract_error(inside_nearsdk)] +pub struct PermissionDenied { + message: Option, +} + +impl PermissionDenied { + pub fn new(message: Option<&str>) -> Self { + Self { message: message.map(|s| s.to_string()) } + } +} + +#[contract_error(inside_nearsdk)] +pub struct ContractUpgradeError { + pub message: String, +} + +impl ContractUpgradeError { + pub fn new(message: &str) -> Self { + Self { message: message.to_string() } + } +} +#[contract_error(inside_nearsdk)] +#[derive(Default)] +pub struct RequireFailed { + pub message: String, +} + +impl RequireFailed { + pub fn new() -> Self { + Self { message: "require! assertion failed".to_string() } + } +} + +#[contract_error(inside_nearsdk)] +pub struct PromiseFailed { + pub promise_index: Option, + pub message: Option, +} + +impl PromiseFailed { + pub fn new(promise_index: Option, message: Option<&str>) -> Self { + Self { promise_index, message: message.map(|s| s.to_string()) } + } +} + +#[contract_error(inside_nearsdk)] +pub struct InvalidPromiseReturn { + pub message: String, +} + +impl InvalidPromiseReturn { + pub fn new(message: &str) -> Self { + Self { message: message.to_string() } + } +} + +#[contract_error(inside_nearsdk)] +pub struct InsufficientBalance { + message: Option, +} + +impl InsufficientBalance { + pub fn new(message: Option<&str>) -> Self { + Self { message: message.map(|s| s.to_string()) } + } +} + +#[contract_error(inside_nearsdk)] +pub struct InsufficientGas {} + +#[contract_error(inside_nearsdk)] +pub struct TotalSupplyOverflow {} + +#[contract_error(inside_nearsdk)] +pub struct UnexpectedFailure { + pub message: String, +} + +#[contract_error(inside_nearsdk)] +pub struct ContractError { + pub message: String, +} + +impl ContractError { + pub fn new(message: &str) -> Self { + Self { message: message.to_string() } + } +} + +#[contract_error(inside_nearsdk)] +pub struct InvalidHashLength { + pub expected: usize, +} + +impl InvalidHashLength { + pub fn new(expected: usize) -> Self { + Self { expected } + } +} diff --git a/near-sdk/src/errors/mod.rs b/near-sdk/src/errors/mod.rs new file mode 100644 index 000000000..e583241da --- /dev/null +++ b/near-sdk/src/errors/mod.rs @@ -0,0 +1,5 @@ +mod custom; +mod sdk; + +pub use custom::*; +pub use sdk::*; diff --git a/near-sdk/src/errors/sdk.rs b/near-sdk/src/errors/sdk.rs new file mode 100644 index 000000000..e57a25add --- /dev/null +++ b/near-sdk/src/errors/sdk.rs @@ -0,0 +1,163 @@ +use near_sdk_macros::contract_error; + +#[contract_error(inside_nearsdk, sdk)] +pub struct DepositNotAccepted { + pub method: String, +} + +impl DepositNotAccepted { + pub fn new(method: &str) -> Self { + Self { method: method.to_string() } + } +} + +#[contract_error(inside_nearsdk, sdk)] +pub struct ContractNotInitialized {} + +#[contract_error(inside_nearsdk, sdk)] +pub struct ContractAlreadyInitialized {} + +#[contract_error(inside_nearsdk, sdk)] +pub struct CallbackComputationUnsuccessful { + pub index: u64, +} + +impl CallbackComputationUnsuccessful { + pub fn new(index: u64) -> Self { + Self { index } + } +} + +#[contract_error(inside_nearsdk, sdk)] +pub struct ActionInJointPromise { + pub message: String, +} + +impl ActionInJointPromise { + pub fn new() -> Self { + Self { message: "Cannot add action to a joint promise.".to_string() } + } +} + +impl Default for ActionInJointPromise { + fn default() -> Self { + Self::new() + } +} + +#[contract_error(inside_nearsdk, sdk)] +pub struct PromiseAlreadyScheduled { + pub message: String, +} + +impl PromiseAlreadyScheduled { + pub fn new() -> Self { + Self { + message: "Cannot callback promise which is already scheduled after another".to_string(), + } + } +} + +impl Default for PromiseAlreadyScheduled { + fn default() -> Self { + Self::new() + } +} + +#[contract_error(inside_nearsdk, sdk)] +pub struct CallbackJointPromise { + pub message: String, +} + +impl CallbackJointPromise { + pub fn new() -> Self { + Self { message: "Cannot callback joint promise".to_string() } + } +} + +impl Default for CallbackJointPromise { + fn default() -> Self { + Self::new() + } +} + +#[contract_error(inside_nearsdk, sdk)] +pub struct PrivateMethod { + pub method_name: String, +} + +impl PrivateMethod { + pub fn new(method_name: &str) -> Self { + Self { method_name: method_name.to_string() } + } +} + +#[contract_error(inside_nearsdk, sdk)] +pub struct BorshSerializeError { + subject: String, +} + +impl BorshSerializeError { + pub fn new(subject: &str) -> Self { + Self { subject: subject.to_string() } + } +} + +#[contract_error(inside_nearsdk, sdk)] +pub struct BorshDeserializeError { + subject: String, +} + +impl BorshDeserializeError { + pub fn new(subject: &str) -> Self { + Self { subject: subject.to_string() } + } +} + +#[contract_error(inside_nearsdk, sdk)] +pub struct InvalidTreeMapRange {} + +#[contract_error(inside_nearsdk, sdk)] +pub struct InconsistentCollectionState { + pub message: String, +} + +impl InconsistentCollectionState { + pub fn new() -> Self { + Self { + message: "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?".to_string() + } + } +} + +impl Default for InconsistentCollectionState { + fn default() -> Self { + Self::new() + } +} + +#[contract_error(inside_nearsdk, sdk)] +pub struct IndexOutOfBounds {} + +#[contract_error(inside_nearsdk, sdk)] +pub struct KeyNotFound {} + +#[contract_error(inside_nearsdk, sdk)] +pub struct RegisterEmpty { + pub message: String, +} + +impl RegisterEmpty { + pub fn new() -> Self { + Self { + message: "Register was expected to have data because we just wrote it into it." + .to_string(), + } + } +} + +impl Default for RegisterEmpty { + fn default() -> Self { + Self::new() + } +} diff --git a/near-sdk/src/lib.rs b/near-sdk/src/lib.rs index 45df0dbcd..e70e8a436 100644 --- a/near-sdk/src/lib.rs +++ b/near-sdk/src/lib.rs @@ -9,8 +9,8 @@ extern crate quickcheck; pub use near_sdk_macros::{ - ext_contract, near, near_bindgen, BorshStorageKey, EventMetadata, FunctionError, NearSchema, - PanicOnDefault, + contract_error, ext_contract, near, near_bindgen, BorshStorageKey, EventMetadata, + FunctionError, NearSchema, PanicOnDefault, }; pub mod store; @@ -36,6 +36,8 @@ pub mod json_types; mod types; pub use crate::types::*; +pub mod errors; + #[cfg(all(feature = "unit-testing", not(target_arch = "wasm32")))] pub use environment::mock; #[cfg(all(feature = "unit-testing", not(target_arch = "wasm32")))] diff --git a/near-sdk/src/private/mod.rs b/near-sdk/src/private/mod.rs index c74520618..46f5e0a22 100644 --- a/near-sdk/src/private/mod.rs +++ b/near-sdk/src/private/mod.rs @@ -5,10 +5,9 @@ pub use near_abi::{ AbiBorshParameter, AbiFunction, AbiFunctionKind, AbiFunctionModifier, AbiJsonParameter, AbiParameters, AbiType, }; -#[cfg(feature = "abi")] + mod result_type_ext; -#[cfg(feature = "abi")] pub use result_type_ext::ResultTypeExt; use crate::IntoStorageKey; diff --git a/near-sdk/src/promise.rs b/near-sdk/src/promise.rs index 56f03ec33..6670b3759 100644 --- a/near-sdk/src/promise.rs +++ b/near-sdk/src/promise.rs @@ -8,7 +8,7 @@ use std::num::NonZeroU128; use std::rc::Rc; use crate::env::migrate_to_allowance; -use crate::{AccountId, Gas, GasWeight, NearToken, PromiseIndex, PublicKey}; +use crate::{errors, AccountId, Gas, GasWeight, NearToken, PromiseIndex, PublicKey}; /// Allow an access key to spend either an unlimited or limited amount of gas // This wrapper prevents incorrect construction @@ -265,7 +265,7 @@ impl Promise { match &self.subtype { PromiseSubtype::Single(x) => x.actions.borrow_mut().push(action), PromiseSubtype::Joint(_) => { - crate::env::panic_str("Cannot add action to a joint promise.") + crate::env::panic_err(errors::ActionInJointPromise::new().into()) } } self @@ -452,13 +452,13 @@ impl Promise { PromiseSubtype::Single(x) => { let mut after = x.after.borrow_mut(); if after.is_some() { - crate::env::panic_str( - "Cannot callback promise which is already scheduled after another", - ); + crate::env::panic_err(errors::PromiseAlreadyScheduled::new().into()); } *after = Some(self) } - PromiseSubtype::Joint(_) => crate::env::panic_str("Cannot callback joint promise."), + PromiseSubtype::Joint(_) => { + crate::env::panic_err(errors::CallbackJointPromise::new().into()) + } } other } diff --git a/near-sdk/src/store/free_list/iter.rs b/near-sdk/src/store/free_list/iter.rs index c8208bfa8..b952b9886 100644 --- a/near-sdk/src/store/free_list/iter.rs +++ b/near-sdk/src/store/free_list/iter.rs @@ -2,8 +2,8 @@ use std::iter::FusedIterator; use borsh::{BorshDeserialize, BorshSerialize}; -use super::{FreeList, Slot, ERR_INCONSISTENT_STATE}; -use crate::{env, store::vec}; +use super::{FreeList, Slot}; +use crate::{env, errors, store::vec}; impl<'a, T> IntoIterator for &'a FreeList where @@ -30,7 +30,9 @@ where } fn decrement_count(count: &mut u32) { - *count = count.checked_sub(1).unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)); + *count = count + .checked_sub(1) + .unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())); } /// An iterator over elements in the storage bucket. This only yields the occupied entries. @@ -73,7 +75,7 @@ where None => { // This should never be hit, because if 0 occupied elements, should have // returned before the loop - env::panic_str(ERR_INCONSISTENT_STATE) + env::panic_err(errors::InconsistentCollectionState::new().into()) } } } @@ -110,7 +112,7 @@ where None => { // This should never be hit, because if 0 occupied elements, should have // returned before the loop - env::panic_str(ERR_INCONSISTENT_STATE) + env::panic_err(errors::InconsistentCollectionState::new().into()) } } } @@ -157,7 +159,7 @@ where None => { // This should never be hit, because if 0 occupied elements, should have // returned before the loop - env::panic_str(ERR_INCONSISTENT_STATE) + env::panic_err(errors::InconsistentCollectionState::new().into()) } } } @@ -194,7 +196,7 @@ where None => { // This should never be hit, because if 0 occupied elements, should have // returned before the loop - env::panic_str(ERR_INCONSISTENT_STATE) + env::panic_err(errors::InconsistentCollectionState::new().into()) } } } diff --git a/near-sdk/src/store/free_list/mod.rs b/near-sdk/src/store/free_list/mod.rs index 10cf36442..b8e0993f4 100644 --- a/near-sdk/src/store/free_list/mod.rs +++ b/near-sdk/src/store/free_list/mod.rs @@ -1,8 +1,8 @@ mod iter; pub use self::iter::{Drain, Iter, IterMut}; -use super::{Vector, ERR_INCONSISTENT_STATE}; -use crate::{env, IntoStorageKey}; +use super::Vector; +use crate::{env, errors, IntoStorageKey}; use near_sdk_macros::{near, NearSchema}; use borsh::{BorshDeserialize, BorshSerialize}; @@ -153,7 +153,7 @@ where // Update pointer on bucket to this next index self.first_free = next_index; } else { - env::panic_str(ERR_INCONSISTENT_STATE) + env::panic_err(errors::InconsistentCollectionState::new().into()) } } else { // No vacant cells, push and return index of pushed element @@ -256,7 +256,7 @@ where self.elements.swap(curr_free_index.0, occupied_index); } else { //Could not find an occupied slot to fill the free slot - env::panic_str(ERR_INCONSISTENT_STATE) + env::panic_err(errors::InconsistentCollectionState::new().into()) } } @@ -271,7 +271,7 @@ where Some(Slot::Empty { next_free }) => *next_free, Some(Slot::Occupied(_)) => { //The free list chain should not have an occupied slot - env::panic_str(ERR_INCONSISTENT_STATE) + env::panic_err(errors::InconsistentCollectionState::new().into()) } _ => None, }; diff --git a/near-sdk/src/store/index_map.rs b/near-sdk/src/store/index_map.rs index 2a37bc287..b3db53969 100644 --- a/near-sdk/src/store/index_map.rs +++ b/near-sdk/src/store/index_map.rs @@ -5,10 +5,7 @@ use near_sdk_macros::near; use once_cell::unsync::OnceCell; use crate::utils::StableMap; -use crate::{env, CacheEntry, EntryState, IntoStorageKey}; - -const ERR_ELEMENT_DESERIALIZATION: &str = "Cannot deserialize element"; -const ERR_ELEMENT_SERIALIZATION: &str = "Cannot serialize element"; +use crate::{env, errors, CacheEntry, EntryState, IntoStorageKey}; #[near(inside_nearsdk)] pub(crate) struct IndexMap @@ -59,8 +56,9 @@ where match v.value().as_ref() { Some(modified) => { buf.clear(); - BorshSerialize::serialize(modified, &mut buf) - .unwrap_or_else(|_| env::panic_str(ERR_ELEMENT_SERIALIZATION)); + BorshSerialize::serialize(modified, &mut buf).unwrap_or_else(|_| { + env::panic_err(errors::BorshSerializeError::new("element").into()) + }); env::storage_write(&key_buf, &buf); } None => { @@ -95,8 +93,9 @@ where T: BorshSerialize + BorshDeserialize, { fn deserialize_element(raw_element: &[u8]) -> T { - T::try_from_slice(raw_element) - .unwrap_or_else(|_| env::panic_str(ERR_ELEMENT_DESERIALIZATION)) + T::try_from_slice(raw_element).unwrap_or_else(|_| { + env::panic_err(errors::BorshDeserializeError::new("element").into()) + }) } /// Returns the element by index or `None` if it is not present. diff --git a/near-sdk/src/store/iterable_map/entry.rs b/near-sdk/src/store/iterable_map/entry.rs index ec859897f..11410c7b0 100644 --- a/near-sdk/src/store/iterable_map/entry.rs +++ b/near-sdk/src/store/iterable_map/entry.rs @@ -2,8 +2,9 @@ use crate::env; use borsh::{BorshDeserialize, BorshSerialize}; use super::ValueAndIndex; +use crate::errors; use crate::store::key::ToKey; -use crate::store::{IterableMap, LookupMap, Vector, ERR_INCONSISTENT_STATE}; +use crate::store::{IterableMap, LookupMap, Vector}; /// A view into a single entry in the map, which can be vacant or occupied. pub enum Entry<'a, K: 'a, V: 'a, H: 'a> @@ -239,8 +240,10 @@ where K: BorshDeserialize + Ord + Clone, V: BorshDeserialize, { - let old_value = - self.values.remove(&self.key).unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)); + let old_value = self + .values + .remove(&self.key) + .unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())); let last_index = self.keys.len() - 1; self.keys.swap_remove(old_value.key_index); @@ -265,7 +268,11 @@ where /// } /// ``` pub fn get(&self) -> &V { - &self.values.get(&self.key).unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)).value + &self + .values + .get(&self.key) + .unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())) + .value } /// Gets a mutable reference to the value in the entry. @@ -299,7 +306,7 @@ where &mut self .values .get_mut(&self.key) - .unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)) + .unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())) .value } @@ -330,7 +337,7 @@ where &mut self .values .get_mut(&self.key) - .unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)) + .unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())) .value } diff --git a/near-sdk/src/store/iterable_map/impls.rs b/near-sdk/src/store/iterable_map/impls.rs index ccea007f5..3371fba06 100644 --- a/near-sdk/src/store/iterable_map/impls.rs +++ b/near-sdk/src/store/iterable_map/impls.rs @@ -2,8 +2,8 @@ use std::borrow::Borrow; use borsh::{BorshDeserialize, BorshSerialize}; -use super::{IterableMap, ToKey, ERR_NOT_EXIST}; -use crate::env; +use super::{IterableMap, ToKey}; +use crate::{env, errors}; impl Extend<(K, V)> for IterableMap where @@ -36,6 +36,6 @@ where /// /// Panics if the key does not exist in the map fn index(&self, index: &Q) -> &Self::Output { - self.get(index).unwrap_or_else(|| env::panic_str(ERR_NOT_EXIST)) + self.get(index).unwrap_or_else(|| env::panic_err(errors::KeyNotFound {}.into())) } } diff --git a/near-sdk/src/store/iterable_map/iter.rs b/near-sdk/src/store/iterable_map/iter.rs index 549e8df2b..a5660f79b 100644 --- a/near-sdk/src/store/iterable_map/iter.rs +++ b/near-sdk/src/store/iterable_map/iter.rs @@ -2,9 +2,9 @@ use std::iter::FusedIterator; use borsh::{BorshDeserialize, BorshSerialize}; -use super::{IterableMap, LookupMap, ToKey, ValueAndIndex, ERR_INCONSISTENT_STATE}; -use crate::env; +use super::{IterableMap, LookupMap, ToKey, ValueAndIndex}; use crate::store::vec; +use crate::{env, errors}; impl<'a, K, V, H> IntoIterator for &'a IterableMap where @@ -74,7 +74,10 @@ where fn nth(&mut self, n: usize) -> Option { let key = self.keys.nth(n)?; - let entry = self.values.get(key).unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)); + let entry = self + .values + .get(key) + .unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())); Some((key, &entry.value)) } @@ -115,7 +118,10 @@ where fn nth_back(&mut self, n: usize) -> Option { let key = self.keys.nth_back(n)?; - let entry = self.values.get(key).unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)); + let entry = self + .values + .get(key) + .unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())); Some((key, &entry.value)) } @@ -150,8 +156,10 @@ where K: Clone, V: BorshDeserialize, { - let entry = - self.values.get_mut(key).unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)); + let entry = self + .values + .get_mut(key) + .unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())); //* SAFETY: The lifetime can be swapped here because we can assert that the iterator //* will only give out one mutable reference for every individual key in the bucket //* during the iteration, and there is no overlap. This operates under the @@ -465,7 +473,7 @@ where let value = self .values .remove(&key) - .unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)) + .unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())) .value; (key, value) diff --git a/near-sdk/src/store/iterable_map/mod.rs b/near-sdk/src/store/iterable_map/mod.rs index f1642a2ff..9f4429247 100644 --- a/near-sdk/src/store/iterable_map/mod.rs +++ b/near-sdk/src/store/iterable_map/mod.rs @@ -13,13 +13,13 @@ use borsh::{BorshDeserialize, BorshSerialize}; use near_sdk_macros::near; use crate::store::key::{Sha256, ToKey}; -use crate::{env, IntoStorageKey}; +use crate::{env, errors, IntoStorageKey}; use crate::store::Vector; pub use entry::{Entry, OccupiedEntry, VacantEntry}; pub use self::iter::{Drain, Iter, IterMut, Keys, Values, ValuesMut}; -use super::{LookupMap, ERR_INCONSISTENT_STATE, ERR_NOT_EXIST}; +use super::LookupMap; /// A lazily loaded storage map that stores its content directly on the storage trie. /// This structure is similar to [`near_sdk::store::LookupMap`](crate::store::LookupMap), except @@ -601,11 +601,12 @@ where x if x == last_index => {} // Otherwise update it's index. _ => { - let swapped_key = - keys.get(key_index).unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)); - let value = values - .get_mut(swapped_key) - .unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)); + let swapped_key = keys.get(key_index).unwrap_or_else(|| { + env::panic_err(errors::InconsistentCollectionState::new().into()) + }); + let value = values.get_mut(swapped_key).unwrap_or_else(|| { + env::panic_err(errors::InconsistentCollectionState::new().into()) + }); value.key_index = key_index; } } diff --git a/near-sdk/src/store/iterable_set/mod.rs b/near-sdk/src/store/iterable_set/mod.rs index e5612f7e3..ef581500c 100644 --- a/near-sdk/src/store/iterable_set/mod.rs +++ b/near-sdk/src/store/iterable_set/mod.rs @@ -5,10 +5,10 @@ mod impls; mod iter; pub use self::iter::{Difference, Drain, Intersection, Iter, SymmetricDifference, Union}; -use super::{LookupMap, ERR_INCONSISTENT_STATE}; +use super::LookupMap; use crate::store::key::{Sha256, ToKey}; use crate::store::Vector; -use crate::{env, IntoStorageKey}; +use crate::{env, errors, IntoStorageKey}; use borsh::{BorshDeserialize, BorshSerialize}; use std::borrow::Borrow; use std::fmt; @@ -509,10 +509,9 @@ where x if x == last_index => {} // Otherwise update it's index. _ => { - let element = self - .elements - .get(element_index) - .unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)); + let element = self.elements.get(element_index).unwrap_or_else(|| { + env::panic_err(errors::InconsistentCollectionState::new().into()) + }); self.index.set(element.clone(), Some(element_index)); } } diff --git a/near-sdk/src/store/lazy/mod.rs b/near-sdk/src/store/lazy/mod.rs index 0bd6f5f63..9a72b07f0 100644 --- a/near-sdk/src/store/lazy/mod.rs +++ b/near-sdk/src/store/lazy/mod.rs @@ -11,21 +11,15 @@ use once_cell::unsync::OnceCell; use near_sdk_macros::near; -use crate::env; -use crate::store::ERR_INCONSISTENT_STATE; use crate::utils::{CacheEntry, EntryState}; -use crate::IntoStorageKey; - -const ERR_VALUE_SERIALIZATION: &str = "Cannot serialize value with Borsh"; -const ERR_VALUE_DESERIALIZATION: &str = "Cannot deserialize value with Borsh"; -const ERR_NOT_FOUND: &str = "No value found for the given key"; +use crate::{env, errors, IntoStorageKey}; fn expect_key_exists(val: Option) -> T { - val.unwrap_or_else(|| env::panic_str(ERR_NOT_FOUND)) + val.unwrap_or_else(|| env::panic_err(errors::KeyNotFound {}.into())) } fn expect_consistent_state(val: Option) -> T { - val.unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)) + val.unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())) } pub(crate) fn load_and_deserialize(key: &[u8]) -> CacheEntry @@ -33,8 +27,8 @@ where T: BorshDeserialize, { let bytes = expect_key_exists(env::storage_read(key)); - let val = - T::try_from_slice(&bytes).unwrap_or_else(|_| env::panic_str(ERR_VALUE_DESERIALIZATION)); + let val = T::try_from_slice(&bytes) + .unwrap_or_else(|_| env::panic_err(errors::BorshDeserializeError::new("value").into())); CacheEntry::new_cached(Some(val)) } @@ -42,7 +36,8 @@ pub(crate) fn serialize_and_store(key: &[u8], value: &T) where T: BorshSerialize, { - let serialized = to_vec(value).unwrap_or_else(|_| env::panic_str(ERR_VALUE_SERIALIZATION)); + let serialized = to_vec(value) + .unwrap_or_else(|_| env::panic_err(errors::BorshSerializeError::new("value").into())); env::storage_write(key, &serialized); } @@ -97,9 +92,11 @@ where if let Some(v) = self.cache.get_mut() { *v.value_mut() = Some(value); } else { - self.cache - .set(CacheEntry::new_modified(Some(value))) - .unwrap_or_else(|_| env::panic_str("cache is checked to not be filled above")) + self.cache.set(CacheEntry::new_modified(Some(value))).unwrap_or_else(|_| { + env::panic_err( + errors::ContractError::new("cache is checked to not be filled above").into(), + ) + }) } } diff --git a/near-sdk/src/store/lookup_map/impls.rs b/near-sdk/src/store/lookup_map/impls.rs index d2e8276b8..3865492a7 100644 --- a/near-sdk/src/store/lookup_map/impls.rs +++ b/near-sdk/src/store/lookup_map/impls.rs @@ -2,8 +2,8 @@ use std::borrow::Borrow; use borsh::{BorshDeserialize, BorshSerialize}; -use super::{LookupMap, ToKey, ERR_NOT_EXIST}; -use crate::env; +use super::{LookupMap, ToKey}; +use crate::{env, errors}; impl Extend<(K, V)> for LookupMap where @@ -37,6 +37,6 @@ where /// /// Panics if the key does not exist in the map fn index(&self, index: &Q) -> &Self::Output { - self.get(index).unwrap_or_else(|| env::panic_str(ERR_NOT_EXIST)) + self.get(index).unwrap_or_else(|| env::panic_err(errors::KeyNotFound {}.into())) } } diff --git a/near-sdk/src/store/lookup_map/mod.rs b/near-sdk/src/store/lookup_map/mod.rs index 012d603c6..a65e00e9a 100644 --- a/near-sdk/src/store/lookup_map/mod.rs +++ b/near-sdk/src/store/lookup_map/mod.rs @@ -8,16 +8,12 @@ use borsh::{BorshDeserialize, BorshSerialize}; use near_sdk_macros::near; use once_cell::unsync::OnceCell; -use super::ERR_NOT_EXIST; use crate::store::key::{Identity, ToKey}; use crate::utils::{EntryState, StableMap}; -use crate::{env, CacheEntry, IntoStorageKey}; +use crate::{env, errors, CacheEntry, IntoStorageKey}; pub use entry::{Entry, OccupiedEntry, VacantEntry}; -const ERR_ELEMENT_DESERIALIZATION: &str = "Cannot deserialize element"; -const ERR_ELEMENT_SERIALIZATION: &str = "Cannot serialize element"; - /// A non-iterable, lazily loaded storage map that stores its content directly on the storage trie. /// /// This map stores the values under a hash of the map's `prefix` and [`BorshSerialize`] of the key @@ -210,7 +206,9 @@ where H: ToKey, { fn deserialize_element(bytes: &[u8]) -> V { - V::try_from_slice(bytes).unwrap_or_else(|_| env::panic_str(ERR_ELEMENT_DESERIALIZATION)) + V::try_from_slice(bytes).unwrap_or_else(|_| { + env::panic_err(errors::BorshDeserializeError::new("element").into()) + }) } fn load_element(prefix: &[u8], key: &Q) -> (H::KeyType, Option) @@ -441,8 +439,9 @@ where match val.value().as_ref() { Some(modified) => { buf.clear(); - BorshSerialize::serialize(modified, &mut buf) - .unwrap_or_else(|_| env::panic_str(ERR_ELEMENT_SERIALIZATION)); + BorshSerialize::serialize(modified, &mut buf).unwrap_or_else(|_| { + env::panic_err(errors::BorshSerializeError::new("element").into()) + }); env::storage_write(key.as_ref(), &buf); } None => { diff --git a/near-sdk/src/store/mod.rs b/near-sdk/src/store/mod.rs index 0874a2df0..77715fa92 100644 --- a/near-sdk/src/store/mod.rs +++ b/near-sdk/src/store/mod.rs @@ -102,9 +102,3 @@ pub(crate) use self::free_list::FreeList; /// Storage key hash function types and trait to override map hash functions. pub mod key; - -pub(crate) const ERR_INCONSISTENT_STATE: &str = - "The collection is in an inconsistent state. Did previous smart \ - contract execution terminate unexpectedly?"; - -pub(crate) const ERR_NOT_EXIST: &str = "Key does not exist in map"; diff --git a/near-sdk/src/store/tree_map/impls.rs b/near-sdk/src/store/tree_map/impls.rs index 43567d461..4b4abcc69 100644 --- a/near-sdk/src/store/tree_map/impls.rs +++ b/near-sdk/src/store/tree_map/impls.rs @@ -2,8 +2,8 @@ use std::borrow::Borrow; use borsh::{BorshDeserialize, BorshSerialize}; -use crate::env; -use crate::store::{key::ToKey, TreeMap, ERR_NOT_EXIST}; +use crate::store::{key::ToKey, TreeMap}; +use crate::{env, errors}; impl Extend<(K, V)> for TreeMap where @@ -37,6 +37,6 @@ where /// /// Panics if the key does not exist in the map fn index(&self, index: &Q) -> &Self::Output { - self.get(index).unwrap_or_else(|| env::panic_str(ERR_NOT_EXIST)) + self.get(index).unwrap_or_else(|| env::panic_err(errors::KeyNotFound {}.into())) } } diff --git a/near-sdk/src/store/unordered_map/impls.rs b/near-sdk/src/store/unordered_map/impls.rs index 32f5a6553..fb0ae1bbd 100644 --- a/near-sdk/src/store/unordered_map/impls.rs +++ b/near-sdk/src/store/unordered_map/impls.rs @@ -2,8 +2,8 @@ use std::borrow::Borrow; use borsh::{BorshDeserialize, BorshSerialize}; -use super::{ToKey, UnorderedMap, ERR_NOT_EXIST}; -use crate::env; +use super::{ToKey, UnorderedMap}; +use crate::{env, errors}; impl Extend<(K, V)> for UnorderedMap where @@ -37,6 +37,6 @@ where /// /// Panics if the key does not exist in the map fn index(&self, index: &Q) -> &Self::Output { - self.get(index).unwrap_or_else(|| env::panic_str(ERR_NOT_EXIST)) + self.get(index).unwrap_or_else(|| env::panic_err(errors::KeyNotFound {}.into())) } } diff --git a/near-sdk/src/store/unordered_map/iter.rs b/near-sdk/src/store/unordered_map/iter.rs index 42199ea5d..eceeab35a 100644 --- a/near-sdk/src/store/unordered_map/iter.rs +++ b/near-sdk/src/store/unordered_map/iter.rs @@ -2,8 +2,8 @@ use std::iter::FusedIterator; use borsh::{BorshDeserialize, BorshSerialize}; -use super::{LookupMap, ToKey, UnorderedMap, ValueAndIndex, ERR_INCONSISTENT_STATE}; -use crate::{env, store::free_list}; +use super::{LookupMap, ToKey, UnorderedMap, ValueAndIndex}; +use crate::{env, errors, store::free_list}; impl<'a, K, V, H> IntoIterator for &'a UnorderedMap where @@ -73,7 +73,10 @@ where fn nth(&mut self, n: usize) -> Option { let key = self.keys.nth(n)?; - let entry = self.values.get(key).unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)); + let entry = self + .values + .get(key) + .unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())); Some((key, &entry.value)) } @@ -114,7 +117,10 @@ where fn nth_back(&mut self, n: usize) -> Option { let key = self.keys.nth_back(n)?; - let entry = self.values.get(key).unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)); + let entry = self + .values + .get(key) + .unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())); Some((key, &entry.value)) } @@ -149,8 +155,10 @@ where K: Clone, V: BorshDeserialize, { - let entry = - self.values.get_mut(key).unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)); + let entry = self + .values + .get_mut(key) + .unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())); //* SAFETY: The lifetime can be swapped here because we can assert that the iterator //* will only give out one mutable reference for every individual key in the bucket //* during the iteration, and there is no overlap. This operates under the @@ -464,7 +472,7 @@ where let value = self .values .remove(&key) - .unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)) + .unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())) .value; (key, value) diff --git a/near-sdk/src/store/unordered_map/mod.rs b/near-sdk/src/store/unordered_map/mod.rs index e0633b79d..a5d0f925a 100644 --- a/near-sdk/src/store/unordered_map/mod.rs +++ b/near-sdk/src/store/unordered_map/mod.rs @@ -13,13 +13,13 @@ use borsh::{BorshDeserialize, BorshSerialize}; use near_sdk_macros::near; use crate::store::key::{Sha256, ToKey}; -use crate::{env, IntoStorageKey}; +use crate::{env, errors, IntoStorageKey}; pub use entry::{Entry, OccupiedEntry, VacantEntry}; pub use self::iter::{Drain, Iter, IterMut, Keys, Values, ValuesMut}; use super::free_list::FreeListIndex; -use super::{FreeList, LookupMap, ERR_INCONSISTENT_STATE, ERR_NOT_EXIST}; +use super::{FreeList, LookupMap}; /// A lazily loaded storage map that stores its content directly on the storage trie. /// This structure is similar to [`near_sdk::store::LookupMap`](crate::store::LookupMap), except @@ -593,7 +593,7 @@ where let key = self .keys .remove(old_value.key_index) - .unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)); + .unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())); // Return removed value Some((key, old_value.value)) diff --git a/near-sdk/src/store/unordered_set/mod.rs b/near-sdk/src/store/unordered_set/mod.rs index e5a32b72f..fa94e62a5 100644 --- a/near-sdk/src/store/unordered_set/mod.rs +++ b/near-sdk/src/store/unordered_set/mod.rs @@ -5,10 +5,10 @@ mod impls; mod iter; pub use self::iter::{Difference, Drain, Intersection, Iter, SymmetricDifference, Union}; -use super::{FreeList, LookupMap, ERR_INCONSISTENT_STATE}; +use super::{FreeList, LookupMap}; use crate::store::free_list::FreeListIndex; use crate::store::key::{Sha256, ToKey}; -use crate::{env, IntoStorageKey}; +use crate::{env, errors, IntoStorageKey}; use borsh::{BorshDeserialize, BorshSerialize}; use std::borrow::Borrow; use std::fmt; @@ -514,9 +514,9 @@ where { match self.index.remove(value) { Some(element_index) => { - self.elements - .remove(element_index) - .unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)); + self.elements.remove(element_index).unwrap_or_else(|| { + env::panic_err(errors::InconsistentCollectionState::new().into()) + }); true } None => false, diff --git a/near-sdk/src/store/vec/impls.rs b/near-sdk/src/store/vec/impls.rs index c02999ae0..708ce444f 100644 --- a/near-sdk/src/store/vec/impls.rs +++ b/near-sdk/src/store/vec/impls.rs @@ -1,8 +1,8 @@ use borsh::{BorshDeserialize, BorshSerialize}; use super::iter::{Iter, IterMut}; -use super::{Vector, ERR_INDEX_OUT_OF_BOUNDS}; -use crate::env; +use super::Vector; +use crate::{env, errors}; impl Drop for Vector where @@ -58,7 +58,7 @@ where type Output = T; fn index(&self, index: u32) -> &Self::Output { - self.get(index).unwrap_or_else(|| env::panic_str(ERR_INDEX_OUT_OF_BOUNDS)) + self.get(index).unwrap_or_else(|| env::panic_err(errors::IndexOutOfBounds {}.into())) } } @@ -67,6 +67,6 @@ where T: BorshSerialize + BorshDeserialize, { fn index_mut(&mut self, index: u32) -> &mut Self::Output { - self.get_mut(index).unwrap_or_else(|| env::panic_str(ERR_INDEX_OUT_OF_BOUNDS)) + self.get_mut(index).unwrap_or_else(|| env::panic_err(errors::IndexOutOfBounds {}.into())) } } diff --git a/near-sdk/src/store/vec/iter.rs b/near-sdk/src/store/vec/iter.rs index ba39a1de2..b18cd2a96 100644 --- a/near-sdk/src/store/vec/iter.rs +++ b/near-sdk/src/store/vec/iter.rs @@ -1,8 +1,8 @@ use borsh::{BorshDeserialize, BorshSerialize}; use core::{iter::FusedIterator, ops::Range}; -use super::{Vector, ERR_INDEX_OUT_OF_BOUNDS}; -use crate::env; +use super::Vector; +use crate::{env, errors}; /// An iterator over references to each element in the stored vector. #[derive(Debug)] @@ -51,7 +51,9 @@ where fn nth(&mut self, n: usize) -> Option { let idx = self.range.nth(n)?; - Some(self.vec.get(idx).unwrap_or_else(|| env::panic_str(ERR_INDEX_OUT_OF_BOUNDS))) + Some( + self.vec.get(idx).unwrap_or_else(|| env::panic_err(errors::IndexOutOfBounds {}.into())), + ) } } @@ -68,7 +70,9 @@ where fn nth_back(&mut self, n: usize) -> Option { let idx = self.range.nth_back(n)?; - Some(self.vec.get(idx).unwrap_or_else(|| env::panic_str(ERR_INDEX_OUT_OF_BOUNDS))) + Some( + self.vec.get(idx).unwrap_or_else(|| env::panic_err(errors::IndexOutOfBounds {}.into())), + ) } } @@ -136,7 +140,9 @@ where fn nth(&mut self, n: usize) -> Option { let idx = self.range.nth(n)?; - Some(self.get_mut(idx).unwrap_or_else(|| env::panic_str(ERR_INDEX_OUT_OF_BOUNDS))) + Some( + self.get_mut(idx).unwrap_or_else(|| env::panic_err(errors::IndexOutOfBounds {}.into())), + ) } } @@ -153,7 +159,9 @@ where fn nth_back(&mut self, n: usize) -> Option { let idx = self.range.nth_back(n)?; - Some(self.get_mut(idx).unwrap_or_else(|| env::panic_str(ERR_INDEX_OUT_OF_BOUNDS))) + Some( + self.get_mut(idx).unwrap_or_else(|| env::panic_err(errors::IndexOutOfBounds {}.into())), + ) } } diff --git a/near-sdk/src/store/vec/mod.rs b/near-sdk/src/store/vec/mod.rs index 8703d7ee4..e38ed1c79 100644 --- a/near-sdk/src/store/vec/mod.rs +++ b/near-sdk/src/store/vec/mod.rs @@ -64,15 +64,12 @@ use borsh::{BorshDeserialize, BorshSerialize}; use near_sdk_macros::NearSchema; pub use self::iter::{Drain, Iter, IterMut}; -use super::ERR_INCONSISTENT_STATE; -use crate::{env, IntoStorageKey}; +use crate::{env, errors, IntoStorageKey}; use super::IndexMap; -const ERR_INDEX_OUT_OF_BOUNDS: &str = "Index out of bounds"; - fn expect_consistent_state(val: Option) -> T { - val.unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE)) + val.unwrap_or_else(|| env::panic_err(errors::InconsistentCollectionState::new().into())) } /// An iterable implementation of vector that stores its content on the trie. This implementation @@ -252,7 +249,7 @@ where /// ``` pub fn set(&mut self, index: u32, value: T) { if index >= self.len() { - env::panic_str(ERR_INDEX_OUT_OF_BOUNDS); + env::panic_err(errors::IndexOutOfBounds {}.into()); } self.values.set(index, Some(value)); @@ -276,8 +273,10 @@ where /// ``` pub fn push(&mut self, element: T) { let last_idx = self.len(); - self.len = - self.len.checked_add(1).unwrap_or_else(|| env::panic_str(ERR_INDEX_OUT_OF_BOUNDS)); + self.len = self + .len + .checked_add(1) + .unwrap_or_else(|| env::panic_err(errors::IndexOutOfBounds {}.into())); self.set(last_idx, element) } } @@ -333,7 +332,7 @@ where pub(crate) fn swap(&mut self, a: u32, b: u32) { if a >= self.len() || b >= self.len() { - env::panic_str(ERR_INDEX_OUT_OF_BOUNDS); + env::panic_err(errors::IndexOutOfBounds {}.into()); } self.values.swap(a, b); @@ -363,7 +362,7 @@ where /// ``` pub fn swap_remove(&mut self, index: u32) -> T { if self.is_empty() { - env::panic_str(ERR_INDEX_OUT_OF_BOUNDS); + env::panic_err(errors::IndexOutOfBounds {}.into()); } self.swap(index, self.len() - 1); @@ -410,7 +409,7 @@ where /// ``` pub fn replace(&mut self, index: u32, element: T) -> T { if index >= self.len { - env::panic_str(ERR_INDEX_OUT_OF_BOUNDS); + env::panic_err(errors::IndexOutOfBounds {}.into()); } self.values.insert(index, element).unwrap() } @@ -488,17 +487,17 @@ where R: RangeBounds, { let start = match range.start_bound() { - Bound::Excluded(i) => { - i.checked_add(1).unwrap_or_else(|| env::panic_str(ERR_INDEX_OUT_OF_BOUNDS)) - } + Bound::Excluded(i) => i + .checked_add(1) + .unwrap_or_else(|| env::panic_err(errors::IndexOutOfBounds {}.into())), Bound::Included(i) => *i, Bound::Unbounded => 0, }; let end = match range.end_bound() { Bound::Excluded(i) => *i, - Bound::Included(i) => { - i.checked_add(1).unwrap_or_else(|| env::panic_str(ERR_INDEX_OUT_OF_BOUNDS)) - } + Bound::Included(i) => i + .checked_add(1) + .unwrap_or_else(|| env::panic_err(errors::IndexOutOfBounds {}.into())), Bound::Unbounded => self.len(), }; diff --git a/near-sdk/src/types/error.rs b/near-sdk/src/types/error.rs index b72be55cd..42e1ed7fe 100644 --- a/near-sdk/src/types/error.rs +++ b/near-sdk/src/types/error.rs @@ -1,3 +1,5 @@ +use crate::errors; + /// Enables contract runtime to panic with the given type. Any error type used in conjunction /// with `#[handle_result]` has to implement this trait. /// @@ -29,7 +31,7 @@ where T: AsRef, { fn panic(&self) -> ! { - crate::env::panic_str(self.as_ref()) + crate::env::panic_err(errors::ContractError::new(self.as_ref()).into()) } } diff --git a/near-sdk/src/utils/contract_error.rs b/near-sdk/src/utils/contract_error.rs new file mode 100644 index 000000000..efed83910 --- /dev/null +++ b/near-sdk/src/utils/contract_error.rs @@ -0,0 +1,22 @@ +pub trait ContractErrorTrait { + fn error_type(&self) -> &'static str; + fn wrap(&self) -> serde_json::Value; +} + +pub fn check_contract_error_trait(_: &T) {} + +#[crate::contract_error(inside_nearsdk)] +pub struct BaseError { + #[serde(flatten)] + pub error: serde_json::Value, +} + +impl From for String { + fn from(value: BaseError) -> Self { + value.error.to_string() + } +} + +pub fn wrap_error(error: T) -> serde_json::Value { + error.wrap() +} diff --git a/near-sdk/src/utils/mod.rs b/near-sdk/src/utils/mod.rs index ce77c721a..355b8613b 100644 --- a/near-sdk/src/utils/mod.rs +++ b/near-sdk/src/utils/mod.rs @@ -6,6 +6,8 @@ mod stable_map; pub(crate) use self::stable_map::StableMap; mod cache_entry; pub(crate) use cache_entry::{CacheEntry, EntryState}; +mod contract_error; +pub use contract_error::{check_contract_error_trait, wrap_error, BaseError, ContractErrorTrait}; use crate::{env, NearToken, PromiseResult}; @@ -47,6 +49,8 @@ macro_rules! log { /// This macro can be used similarly to [`assert!`] but will reduce code size by not including /// file and rust specific data in the panic message. /// +/// Panics with near_sdk::errors::RequireFailed unless error message provided +/// /// # Examples /// /// ```no_run @@ -64,7 +68,7 @@ macro_rules! require { if cfg!(debug_assertions) { assert!($cond) } else if !$cond { - $crate::env::panic_str("require! assertion failed"); + $crate::env::panic_err(::near_sdk::errors::RequireFailed::new().into()); } }; ($cond:expr, $message:expr $(,)?) => { @@ -78,6 +82,91 @@ macro_rules! require { }; } +/// Helper macro to create assertions that will return an error. +/// +/// This macro can be used similarly to [`require!`] but will return an error instead of panicking. +/// +/// Returns Err(near_sdk::errors::RequireFailed) unless error message provided +/// +/// # Examples +/// +/// ```no_run +/// use near_sdk::require_or_err; +/// use near_sdk::BaseError; +/// use near_sdk::errors::ContractError; +/// +/// # fn f() -> Result<(), BaseError> { +/// let a = 2; +/// require_or_err!(a > 0); +/// require_or_err!("test" != "other", ContractError::new("Some custom error message if false")); +/// Ok(()) +/// # } +/// ``` +#[macro_export] +macro_rules! require_or_err { + ($cond:expr $(,)?) => { + if !$cond { + return Err(::near_sdk::errors::RequireFailed::new().into()); + } + }; + ($cond:expr, $err:expr $(,)?) => { + if !$cond { + return Err($err.into()); + } + }; +} + +/// Helper macro to unwrap an Option or Result, returning an error if None or Err. +/// +/// - If you have an option you would like to unwrap, you use unwrap_or_err! on it and +/// provide an error that will be returned from the function in case the option value is None +/// +/// - If you have a result you would like to unwrap, you use unwrap_or_err! on it and +/// the error will be returned from the function in case the result is an Err +/// +/// # Examples +/// +/// ```no_run +/// use near_sdk::unwrap_or_err; +/// use near_sdk::errors::ContractError; +/// +/// # fn method() -> Result { +/// +/// let option_some: Option = Some(5); +/// let option_none: Option = None; +/// +/// let result_ok: Result = Ok(5); +/// let result_err: Result = Err(ContractError::new("Some error")); +/// +/// let option_success: u64 = unwrap_or_err!(option_some, ContractError::new("Some error")); // option_success == 5 +/// let option_error: u64 = unwrap_or_err!(option_none, ContractError::new("Some error")); // error is returned from main +/// +/// let result_success: u64 = unwrap_or_err!(result_ok); // result_success == 5 +/// let result_error: u64 = unwrap_or_err!(result_err); // error is returned from main +/// +/// Ok(0) +/// # } +///``` +#[macro_export] +macro_rules! unwrap_or_err { + ( $exp:expr, $err:expr ) => { + match $exp { + Some(x) => x, + None => { + return Err($err.into()); + } + } + }; + ( $exp:expr ) => { + match $exp { + Ok(x) => x, + Err(err) => { + return Err(err.into()); + } + } + }; +} + /// Assert that predecessor_account_id == current_account_id, meaning contract called itself. pub fn assert_self() { require!(env::predecessor_account_id() == env::current_account_id(), "Method is private");