Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add interfaces & error for psp22 #297

Merged
merged 22 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 245 additions & 0 deletions pop-api/src/v0/fungibles/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
//! A set of errors for use in smart contracts that interact with the fungibles api. This includes errors compliant to standards.

use super::*;
chungquantin marked this conversation as resolved.
Show resolved Hide resolved
use ink::prelude::string::String;

/// Represents various errors related to fungible tokens.
///
/// The `FungiblesError` provides a detailed and specific set of error types that can occur when
/// interacting with fungible tokens. Each variant signifies a particular error
/// condition, facilitating precise error handling and debugging.
///
/// It is designed to be lightweight, including only the essential errors relevant to fungible token
/// operations. The `Other` variant serves as a catch-all for any unexpected errors. For more
/// detailed debugging, the `Other` variant can be converted into the richer `Error` type defined in
/// the primitives crate.
/// NOTE: The `FungiblesError` is WIP
#[derive(Debug, PartialEq, Eq)]
#[ink::scale_derive(Encode, Decode, TypeInfo)]
pub enum FungiblesError {
chungquantin marked this conversation as resolved.
Show resolved Hide resolved
/// An unspecified or unknown error occurred.
Other(StatusCode),
/// The token is not live; either frozen or being destroyed.
NotLive,
/// Not enough allowance to fulfill a request is available.
InsufficientAllowance,
/// Not enough balance to fulfill a request is available.
InsufficientBalance,
/// The token ID is already taken.
InUse,
/// Minimum balance should be non-zero.
MinBalanceZero,
/// The account to alter does not exist.
NoAccount,
/// The signing account has no permission to do the operation.
NoPermission,
/// The given token ID is unknown.
Unknown,
/// No balance for creation of tokens or fees.
// TODO: Originally `pallet_balances::Error::InsufficientBalance` but collides with the
// `InsufficientBalance` error that is used for `pallet_assets::Error::BalanceLow` to adhere
// to the standard. This deserves a second look.
NoBalance,
}

impl From<StatusCode> for FungiblesError {
/// Converts a `StatusCode` to a `FungiblesError`.
///
/// This conversion maps a `StatusCode`, returned by the runtime, to a more descriptive
/// `FungiblesError`. This provides better context and understanding of the error, allowing
/// developers to handle the most important errors effectively.
fn from(value: StatusCode) -> Self {
let encoded = value.0.to_le_bytes();
match encoded {
// Balances.
[_, BALANCES, 2, _] => FungiblesError::NoBalance,
// Assets.
[_, ASSETS, 0, _] => FungiblesError::NoAccount,
[_, ASSETS, 1, _] => FungiblesError::NoPermission,
[_, ASSETS, 2, _] => FungiblesError::Unknown,
[_, ASSETS, 3, _] => FungiblesError::InUse,
[_, ASSETS, 5, _] => FungiblesError::MinBalanceZero,
[_, ASSETS, 7, _] => FungiblesError::InsufficientAllowance,
[_, ASSETS, 10, _] => FungiblesError::NotLive,
_ => FungiblesError::Other(value),
}
}
}

/// The PSP22 error.
// TODO: Issue https://github.com/r0gue-io/pop-node/issues/298
#[derive(Debug, PartialEq, Eq)]
#[ink::scale_derive(Encode, Decode, TypeInfo)]
pub enum PSP22Error {
/// Custom error type for implementation-based errors.
Custom(String),
/// Returned when an account does not have enough tokens to complete the operation.
InsufficientBalance,
/// Returned if there is not enough allowance to complete the operation.
InsufficientAllowance,
/// Returned if recipient's address is zero.
ZeroRecipientAddress,
/// Returned if sender's address is zero.
ZeroSenderAddress,
/// Returned if a safe transfer check failed.
SafeTransferCheckFailed(String),
}

impl From<StatusCode> for PSP22Error {
chungquantin marked this conversation as resolved.
Show resolved Hide resolved
chungquantin marked this conversation as resolved.
Show resolved Hide resolved
/// Converts a `StatusCode` to a `PSP22Error`.
fn from(value: StatusCode) -> Self {
let encoded = value.0.to_le_bytes();
match encoded {
[_, ASSETS, 3, _] => PSP22Error::Custom(String::from("Unknown")),
[_, ASSETS, 7, _] => PSP22Error::InsufficientAllowance,
_ => PSP22Error::Custom(String::from("Other")),
chungquantin marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

#[cfg(test)]
mod tests {
use super::{FungiblesError, PSP22Error};
use crate::{
constants::{ASSETS, BALANCES},
primitives::{
ArithmeticError::*,
Error::{self, *},
TokenError::*,
TransactionalError::*,
},
StatusCode,
};
use ink::prelude::string::String;
use ink::scale::{Decode, Encode};

fn error_into_status_code(error: Error) -> StatusCode {
let mut encoded_error = error.encode();
encoded_error.resize(4, 0);
let value = u32::from_le_bytes(
encoded_error.try_into().expect("qed, resized to 4 bytes line above"),
);
value.into()
}

fn into_fungibles_error(error: Error) -> FungiblesError {
let status_code: StatusCode = error_into_status_code(error);
status_code.into()
}

fn into_psp22_error(error: Error) -> PSP22Error {
let status_code: StatusCode = error_into_status_code(error);
status_code.into()
}

// If we ever want to change the conversion from bytes to `u32`.
#[test]
fn status_code_vs_encoded() {
assert_eq!(u32::decode(&mut &[3u8, 10, 2, 0][..]).unwrap(), 133635u32);
assert_eq!(u32::decode(&mut &[3u8, 52, 0, 0][..]).unwrap(), 13315u32);
assert_eq!(u32::decode(&mut &[3u8, 52, 1, 0][..]).unwrap(), 78851u32);
assert_eq!(u32::decode(&mut &[3u8, 52, 2, 0][..]).unwrap(), 144387u32);
assert_eq!(u32::decode(&mut &[3u8, 52, 3, 0][..]).unwrap(), 209923u32);
assert_eq!(u32::decode(&mut &[3u8, 52, 5, 0][..]).unwrap(), 340995u32);
assert_eq!(u32::decode(&mut &[3u8, 52, 7, 0][..]).unwrap(), 472067u32);
assert_eq!(u32::decode(&mut &[3u8, 52, 10, 0][..]).unwrap(), 668675u32);
}

#[test]
fn converting_status_code_into_fungibles_error_works() {
let other_errors = vec![
Other,
CannotLookup,
BadOrigin,
// `ModuleError` other than assets module.
Module { index: 2, error: [5, 0] },
ConsumerRemaining,
NoProviders,
TooManyConsumers,
Token(OnlyProvider),
Arithmetic(Overflow),
Transactional(NoLayer),
Exhausted,
Corruption,
Unavailable,
RootNotAllowed,
Unknown { dispatch_error_index: 5, error_index: 5, error: 1 },
DecodingFailed,
];
for error in other_errors {
let status_code: StatusCode = error_into_status_code(error);
let fungibles_error: FungiblesError = status_code.into();
assert_eq!(fungibles_error, FungiblesError::Other(status_code))
}

assert_eq!(
into_fungibles_error(Module { index: BALANCES, error: [2, 0] }),
FungiblesError::NoBalance
);
assert_eq!(
into_fungibles_error(Module { index: ASSETS, error: [0, 0] }),
FungiblesError::NoAccount
);
assert_eq!(
into_fungibles_error(Module { index: ASSETS, error: [1, 0] }),
FungiblesError::NoPermission
);
assert_eq!(
into_fungibles_error(Module { index: ASSETS, error: [2, 0] }),
FungiblesError::Unknown
);
assert_eq!(
into_fungibles_error(Module { index: ASSETS, error: [3, 0] }),
FungiblesError::InUse
);
assert_eq!(
into_fungibles_error(Module { index: ASSETS, error: [5, 0] }),
FungiblesError::MinBalanceZero
);
assert_eq!(
into_fungibles_error(Module { index: ASSETS, error: [7, 0] }),
FungiblesError::InsufficientAllowance
);
assert_eq!(
into_fungibles_error(Module { index: ASSETS, error: [10, 0] }),
FungiblesError::NotLive
);
}

#[test]
fn converting_status_code_into_psp22_error_works() {
let other_errors = vec![
Other,
CannotLookup,
BadOrigin,
// `ModuleError` other than assets module.
Module { index: 2, error: [5, 0] },
ConsumerRemaining,
NoProviders,
TooManyConsumers,
Token(OnlyProvider),
Arithmetic(Overflow),
Transactional(NoLayer),
Exhausted,
Corruption,
Unavailable,
RootNotAllowed,
Unknown { dispatch_error_index: 5, error_index: 5, error: 1 },
DecodingFailed,
];
for error in other_errors {
let status_code: StatusCode = error_into_status_code(error);
let fungibles_error: PSP22Error = status_code.into();
assert_eq!(fungibles_error, PSP22Error::Custom(String::from("Other")))
}

assert_eq!(
into_psp22_error(Module { index: ASSETS, error: [3, 0] }),
PSP22Error::Custom(String::from("Unknown"))
);
assert_eq!(
into_psp22_error(Module { index: ASSETS, error: [7, 0] }),
PSP22Error::InsufficientAllowance
);
}
}
85 changes: 85 additions & 0 deletions pop-api/src/v0/fungibles/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! A set of events for use in smart contracts interacting with the fungibles API.
//!
//! The `Transfer` and `Approval` events conform to the PSP-22 standard. The other events
//! (`Create`, `StartDestroy`, `SetMetadata`, `ClearMetadata`) are provided for convenience.
//!
//! These events are not emitted by the API itself but can be used in your contracts to
//! track token operations. Be mindful of the costs associated with emitting events.
//!
//! For more details, refer to [ink! events](https://use.ink/basics/events).
chungquantin marked this conversation as resolved.
Show resolved Hide resolved

use super::*;

/// Event emitted when allowance by `owner` to `spender` changes.
// Differing style: event name abides by the PSP22 standard.
#[ink::event]
pub struct Approval {
chungquantin marked this conversation as resolved.
Show resolved Hide resolved
/// The owner providing the allowance.
#[ink(topic)]
pub owner: AccountId,
/// The beneficiary of the allowance.
#[ink(topic)]
pub spender: AccountId,
/// The new allowance amount.
pub value: u128,
}

/// Event emitted when transfer of tokens occurs.
// Differing style: event name abides by the PSP22 standard.
#[ink::event]
pub struct Transfer {
/// The source of the transfer. `None` when minting.
#[ink(topic)]
pub from: Option<AccountId>,
/// The recipient of the transfer. `None` when burning.
#[ink(topic)]
pub to: Option<AccountId>,
/// The amount transferred (or minted/burned).
pub value: u128,
}

/// Event emitted when a token is created.
#[ink::event]
pub struct Created {
/// The token identifier.
#[ink(topic)]
pub id: TokenId,
/// The creator of the token.
#[ink(topic)]
pub creator: AccountId,
/// The administrator of the token.
#[ink(topic)]
pub admin: AccountId,
}

/// Event emitted when a token is in the process of being destroyed.
#[ink::event]
pub struct DestroyStarted {
/// The token.
#[ink(topic)]
pub token: TokenId,
}

/// Event emitted when new metadata is set for a token.
#[ink::event]
pub struct MetadataSet {
/// The token.
#[ink(topic)]
pub token: TokenId,
/// The name of the token.
#[ink(topic)]
pub name: Vec<u8>,
/// The symbol of the token.
#[ink(topic)]
pub symbol: Vec<u8>,
/// The decimals of the token.
pub decimals: u8,
}

/// Event emitted when metadata is cleared for a token.
#[ink::event]
pub struct MetadataCleared {
/// The token.
#[ink(topic)]
pub token: TokenId,
}
Loading
Loading