diff --git a/concordium-cis2/CHANGELOG.md b/concordium-cis2/CHANGELOG.md index 2482b216..984e9fff 100644 --- a/concordium-cis2/CHANGELOG.md +++ b/concordium-cis2/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased changes - Bump MSRV to 1.72 +- Add `FromStr` implementations for `TokenId` types. +- Add a `serde` feature that derives `serde::Serialize` and `serde::Deserialize` for `TokenId` types, `TokenAmount` types, `OnReceivingCis2DataParams`, `OnReceivingCis2Params`, `AdditionalData`, and `Receiver`. - Fix `SchemaType` implementation of `OnReceivingCis2DataParams` so that it matches `Serial` and `Deserial` implementations. ## concordium-cis2 6.1.0 (2024-02-22) @@ -28,7 +30,6 @@ call into other cis2 compatible smart contracts in a type safe way. - Bump concordium-std to version 8. - ## concordium-cis2 4.0.0 (2023-06-16) - Bump concordium-std to version 7. diff --git a/concordium-cis2/Cargo.toml b/concordium-cis2/Cargo.toml index 340883e0..f1cb34bd 100644 --- a/concordium-cis2/Cargo.toml +++ b/concordium-cis2/Cargo.toml @@ -19,10 +19,27 @@ default-features = false version = "0.11" default-features = false +[dependencies.serde] +version = "1.0" +features = ["alloc", "derive"] +optional = true +default-features = false + +[dependencies.concordium-contracts-common] +path = "../concordium-rust-sdk/concordium-base/smart-contracts/contracts-common/concordium-contracts-common" +version = "*" +optional = true +default-features = false + [features] default = ["std"] std = ["concordium-std/std"] u256_amount = [] +serde = [ + "dep:serde", + "concordium-contracts-common/derive-serde", + "primitive-types/impl-serde", +] [lib] crate-type = ["rlib"] @@ -36,5 +53,5 @@ opt-level = "s" # exist on that platform. targets = ["wasm32-unknown-unknown"] # Default features to build documentation for. -features = ["std", "u256_amount"] +features = ["std", "u256_amount", "serde"] rustc-args = ["--cfg", "docsrs"] diff --git a/concordium-cis2/src/lib.rs b/concordium-cis2/src/lib.rs index 75dbad3d..56d88d04 100644 --- a/concordium-cis2/src/lib.rs +++ b/concordium-cis2/src/lib.rs @@ -28,9 +28,11 @@ //! //! # Features //! -//! This crate has features `std` and `u256_amount`. The former one is default. -//! When `u256_amount` feature is enabled the type [`TokenAmountU256`] is -//! defined and implements the [`IsTokenAmount`] interface. +//! This crate has features `std`, `u256_amount`, and `serde`. The first one is +//! default. When the `u256_amount` feature is enabled the type +//! [`TokenAmountU256`] is defined and implements the [`IsTokenAmount`] +//! interface. The `serde` features derives `serde::Serialize` and +//! `serde::Deserialize` for a variety of types. #![cfg_attr(not(feature = "std"), no_std)] mod cis2_client; @@ -40,9 +42,11 @@ use concordium_std::{collections::BTreeMap, schema::SchemaType, *}; // Re-export for backward compatibility. pub use concordium_std::MetadataUrl; #[cfg(not(feature = "std"))] -use core::{fmt, ops}; +use core::{fmt, ops, str::FromStr}; +#[cfg(feature = "serde")] +use serde::{Deserialize as SerdeDeserialize, Serialize as SerdeSerialize}; #[cfg(feature = "std")] -use std::{fmt, ops}; +use std::{fmt, ops, str::FromStr}; use convert::TryFrom; @@ -101,6 +105,11 @@ pub trait IsTokenAmount: Serialize + schema::SchemaType {} /// unless the bytes have some significant meaning, it is most likely better to /// use a smaller fixed size token ID such as `TokenIdU8`. #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Serialize)] +#[cfg_attr( + feature = "serde", + derive(SerdeSerialize, SerdeDeserialize), + serde(into = "String", try_from = "String") +)] pub struct TokenIdVec(#[concordium(size_length = 1)] pub Vec); impl IsTokenId for TokenIdVec {} @@ -119,6 +128,37 @@ impl fmt::Display for TokenIdVec { } } +#[cfg(feature = "serde")] +impl From for String { + fn from(id: TokenIdVec) -> Self { id.to_string() } +} + +#[cfg(feature = "serde")] +impl TryFrom for TokenIdVec { + type Error = ParseError; + + fn try_from(s: String) -> Result { s.parse() } +} + +/// Parse the token ID from a hex string +impl FromStr for TokenIdVec { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + if s.len() % 2 != 0 || !s.is_ascii() { + return Err(ParseError {}); + } + + let mut id = Vec::with_capacity(s.len() / 2); + for i in (0..s.len()).step_by(2) { + let byte = u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| ParseError {})?; + id.push(byte); + } + + Ok(Self(id)) + } +} + /// Token Identifier, which combined with the address of the contract instance, /// forms the unique identifier of a token type. /// @@ -132,6 +172,11 @@ impl fmt::Display for TokenIdVec { /// For fixed sized token IDs with integer representations see `TokenIdU8`, /// `TokenIdU16`, `TokenIdU32` and `TokenIdU64`. #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Copy, Clone)] +#[cfg_attr( + feature = "serde", + derive(SerdeSerialize, SerdeDeserialize), + serde(into = "String", try_from = "String") +)] pub struct TokenIdFixed(pub [u8; N]); impl IsTokenId for TokenIdFixed {} @@ -179,6 +224,25 @@ impl fmt::Display for TokenIdFixed { } } +#[cfg(feature = "serde")] +impl From> for String { + fn from(id: TokenIdFixed) -> Self { id.to_string() } +} + +#[cfg(feature = "serde")] +impl TryFrom for TokenIdFixed { + type Error = ParseError; + + fn try_from(s: String) -> Result { s.parse() } +} + +/// Parse the token ID from a hex string +impl FromStr for TokenIdFixed { + type Err = ParseError; + + fn from_str(s: &str) -> Result { parse_bytes_exact(s).map(Self) } +} + /// Token Identifier, which combined with the address of the contract instance, /// forms the unique identifier of a token type. /// @@ -190,6 +254,11 @@ impl fmt::Display for TokenIdFixed { /// token ID space is fixed to 8 bytes and some token IDs cannot be represented. /// For a more general token ID type see `TokenIdVec`. #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Copy, Clone)] +#[cfg_attr( + feature = "serde", + derive(SerdeSerialize, SerdeDeserialize), + serde(into = "String", try_from = "String") +)] pub struct TokenIdU64(pub u64); impl IsTokenId for TokenIdU64 {} @@ -235,6 +304,28 @@ impl fmt::Display for TokenIdU64 { } } +#[cfg(feature = "serde")] +impl From for String { + fn from(id: TokenIdU64) -> Self { id.to_string() } +} + +#[cfg(feature = "serde")] +impl TryFrom for TokenIdU64 { + type Error = ParseError; + + fn try_from(s: String) -> Result { s.parse() } +} + +/// Parse the token ID from a hex string +impl FromStr for TokenIdU64 { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let bytes = parse_bytes_exact(s)?; + Ok(Self(u64::from_le_bytes(bytes))) + } +} + /// Token Identifier, which combined with the address of the contract instance, /// forms the unique identifier of a token type. /// @@ -246,6 +337,11 @@ impl fmt::Display for TokenIdU64 { /// token ID space is fixed to 4 bytes and some token IDs cannot be represented. /// For a more general token ID type see `TokenIdVec`. #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Copy, Clone)] +#[cfg_attr( + feature = "serde", + derive(SerdeSerialize, SerdeDeserialize), + serde(into = "String", try_from = "String") +)] pub struct TokenIdU32(pub u32); impl IsTokenId for TokenIdU32 {} @@ -291,6 +387,28 @@ impl fmt::Display for TokenIdU32 { } } +#[cfg(feature = "serde")] +impl From for String { + fn from(id: TokenIdU32) -> Self { id.to_string() } +} + +#[cfg(feature = "serde")] +impl TryFrom for TokenIdU32 { + type Error = ParseError; + + fn try_from(s: String) -> Result { s.parse() } +} + +/// Parse the token ID from a hex string +impl FromStr for TokenIdU32 { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let bytes = parse_bytes_exact(s)?; + Ok(Self(u32::from_le_bytes(bytes))) + } +} + /// Token Identifier, which combined with the address of the contract instance, /// forms the unique identifier of a token type. /// @@ -302,6 +420,11 @@ impl fmt::Display for TokenIdU32 { /// token ID space is fixed to 2 bytes and some token IDs cannot be represented. /// For a more general token ID type see `TokenIdVec`. #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Copy, Clone)] +#[cfg_attr( + feature = "serde", + derive(SerdeSerialize, SerdeDeserialize), + serde(into = "String", try_from = "String") +)] pub struct TokenIdU16(pub u16); impl IsTokenId for TokenIdU16 {} @@ -347,6 +470,28 @@ impl fmt::Display for TokenIdU16 { } } +#[cfg(feature = "serde")] +impl From for String { + fn from(id: TokenIdU16) -> Self { id.to_string() } +} + +#[cfg(feature = "serde")] +impl TryFrom for TokenIdU16 { + type Error = ParseError; + + fn try_from(s: String) -> Result { s.parse() } +} + +/// Parse the token ID from a hex string +impl FromStr for TokenIdU16 { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let bytes = parse_bytes_exact(s)?; + Ok(Self(u16::from_le_bytes(bytes))) + } +} + /// Token Identifier, which combined with the address of the contract instance, /// forms the unique identifier of a token type. /// @@ -358,6 +503,11 @@ impl fmt::Display for TokenIdU16 { /// token ID space is fixed to 1 byte and some token IDs cannot be represented. /// For a more general token ID type see `TokenIdVec`. #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Copy, Clone)] +#[cfg_attr( + feature = "serde", + derive(SerdeSerialize, SerdeDeserialize), + serde(into = "String", try_from = "String") +)] pub struct TokenIdU8(pub u8); impl IsTokenId for TokenIdU8 {} @@ -403,6 +553,42 @@ impl fmt::Display for TokenIdU8 { } } +#[cfg(feature = "serde")] +impl From for String { + fn from(id: TokenIdU8) -> Self { id.to_string() } +} + +#[cfg(feature = "serde")] +impl TryFrom for TokenIdU8 { + type Error = ParseError; + + fn try_from(s: String) -> Result { s.parse() } +} + +/// Parse the token ID from a hex string +impl FromStr for TokenIdU8 { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let bytes = parse_bytes_exact::<1>(s)?; + Ok(Self(bytes[0])) + } +} + +/// Parses a hex string as a little endian array of bytes of length `N`. +fn parse_bytes_exact(s: &str) -> Result<[u8; N], ParseError> { + if s.len() != 2 * N || !s.is_ascii() { + return Err(ParseError {}); + } + + let mut bytes = [0; N]; + for (i, place) in bytes.iter_mut().enumerate() { + *place = u8::from_str_radix(&s[(2 * i)..(2 * i + 2)], 16).map_err(|_| ParseError {})?; + } + + Ok(bytes) +} + /// Token Identifier, which combined with the address of the contract instance, /// forms the unique identifier of a token type. /// @@ -414,6 +600,11 @@ impl fmt::Display for TokenIdU8 { /// token ID can be represented with this type and other token IDs cannot be /// represented. For a more general token ID type see `TokenIdVec`. #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Copy, Clone)] +#[cfg_attr( + feature = "serde", + derive(SerdeSerialize, SerdeDeserialize), + serde(into = "String", try_from = "String") +)] pub struct TokenIdUnit(); impl IsTokenId for TokenIdUnit {} @@ -440,9 +631,28 @@ impl Deserial for TokenIdUnit { } } +#[cfg(feature = "serde")] +impl From for String { + fn from(_id: TokenIdUnit) -> Self { String::from("") } +} + +#[cfg(feature = "serde")] +impl TryFrom for TokenIdUnit { + type Error = ParseError; + + fn try_from(s: String) -> Result { + if s == "" { + Ok(Self()) + } else { + Err(ParseError {}) + } + } +} + macro_rules! token_amount_wrapper { ($name:ident, $wrapped:ty) => { #[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Default)] + #[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] #[repr(transparent)] pub struct $name(pub $wrapped); @@ -559,8 +769,9 @@ mod u256_token { use super::*; use primitive_types::U256; #[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Default)] - #[repr(transparent)] + #[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] #[cfg_attr(docsrs, cfg(feature = "u256_amount"))] + #[repr(transparent)] pub struct TokenAmountU256(pub U256); impl ops::Add for TokenAmountU256 { @@ -1014,6 +1225,7 @@ where // specification, the order of the variants and the order of their fields // cannot be changed. #[derive(Debug, Serialize, Clone, SchemaType)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] pub enum Receiver { /// The receiver is an account address. Account( @@ -1053,6 +1265,7 @@ impl From for Receiver { /// Additional information to include with a transfer. #[derive(Debug, Serialize, Clone)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] #[concordium(transparent)] pub struct AdditionalData(#[concordium(size_length = 2)] Vec); @@ -1078,6 +1291,7 @@ impl AsRef<[u8]> for AdditionalData { // Note: For the serialization to be derived according to the CIS2 // specification, the order of the fields cannot be changed. #[derive(Debug, Serialize, Clone, SchemaType)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] pub struct Transfer { /// The ID of the token being transferred. pub token_id: T, @@ -1094,6 +1308,7 @@ pub struct Transfer { /// The parameter type for the contract function `transfer`. #[derive(Debug, Serialize, Clone, SchemaType)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] #[concordium(transparent)] pub struct TransferParams( #[concordium(size_length = 2)] pub Vec>, @@ -1111,6 +1326,7 @@ impl AsRef<[Transfer]> for TransferParams< // Note: For the serialization to be derived according to the CIS2 // specification, the order of the variants cannot be changed. #[derive(Debug, Serialize, Clone, Copy, SchemaType, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] pub enum OperatorUpdate { /// Remove the operator. Remove, @@ -1122,6 +1338,7 @@ pub enum OperatorUpdate { // Note: For the serialization to be derived according to the CIS2 // specification, the order of the fields cannot be changed. #[derive(Debug, Serialize, Clone, SchemaType, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] pub struct UpdateOperator { /// The update for this operator. pub update: OperatorUpdate, @@ -1133,6 +1350,7 @@ pub struct UpdateOperator { /// The parameter type for the contract function `updateOperator`. #[derive(Debug, Serialize, Clone, SchemaType)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] #[concordium(transparent)] pub struct UpdateOperatorParams(#[concordium(size_length = 2)] pub Vec); @@ -1239,6 +1457,7 @@ impl AsRef<[MetadataUrl]> for TokenMetadataQueryResponse { // Note: For the serialization to be derived according to the CIS2 // specification, the order of the fields cannot be changed. #[derive(Debug, Serialize, SchemaType)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] pub struct OnReceivingCis2Params { /// The ID of the token received. pub token_id: T, @@ -1255,6 +1474,7 @@ pub struct OnReceivingCis2Params { // Note: For the serialization to be derived according to the CIS2 // specification, the order of the fields cannot be changed. #[derive(Debug)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] pub struct OnReceivingCis2DataParams { /// The ID of the token received. pub token_id: T, @@ -1570,4 +1790,22 @@ mod test { let amount: TokenAmountU8 = from_bytes(&[255, 0b00000001]).expect("Failed to parse bytes"); assert_eq!(amount, TokenAmountU8::from(u8::MAX)) } + + #[test] + fn token_id_vec_from_str_test() { + let id = TokenIdVec(vec![1, 2, 3, 255]); + assert_eq!(id.to_string().parse(), Ok(id)) + } + + #[test] + fn parse_bytes_exact_test() { + // the hex string "deadBEEF" corresponds to `0xDEADBEEF_u32.to_be_bytes()`, + // since the strings are written in little endian order, i.e. "de" is the first + // byte, "ad" is the second, etc. + assert_eq!(parse_bytes_exact::<4>("deadBEEF"), Ok(0xDEADBEEF_u32.to_be_bytes())); + // odd number of characters fails + assert!(parse_bytes_exact::<3>("deadBEE").is_err()); + // invalid character fails + assert!(parse_bytes_exact::<4>("deadBEEK").is_err()); + } } diff --git a/concordium-rust-sdk b/concordium-rust-sdk index 97ac9c3d..68529664 160000 --- a/concordium-rust-sdk +++ b/concordium-rust-sdk @@ -1 +1 @@ -Subproject commit 97ac9c3dcc366354ce0d7ee955606056640d9942 +Subproject commit 68529664f395eec287057deb220751a91f41cd98 diff --git a/examples/cis5-smart-contract-wallet/Cargo.toml b/examples/cis5-smart-contract-wallet/Cargo.toml index 135d3ea0..fcf7b607 100644 --- a/examples/cis5-smart-contract-wallet/Cargo.toml +++ b/examples/cis5-smart-contract-wallet/Cargo.toml @@ -9,20 +9,30 @@ license = "MPL-2.0" default = ["std", "bump_alloc"] std = ["concordium-std/std", "concordium-cis2/std"] bump_alloc = ["concordium-std/bump_alloc"] +serde = [ + "concordium-contracts-common/derive-serde", + "concordium-cis2/serde", + "dep:serde", +] [dependencies] -concordium-std = {path = "../../concordium-std", default-features = false} -concordium-cis2 = {path = "../../concordium-cis2", default-features = false, features=[ - "u256_amount"]} +concordium-std = { path = "../../concordium-std", default-features = false } +concordium-cis2 = { path = "../../concordium-cis2", default-features = false, features = [ + "u256_amount", +] } +serde = { version = "1.0", optional = true, default-features = false, features = [ + "derive", +] } +concordium-contracts-common = "*" [dev-dependencies] -concordium-smart-contract-testing = {path = "../../contract-testing"} -cis2-multi = {path = "../cis2-multi"} -ed25519-dalek = { version = "2.0", features = ["rand_core"] } +concordium-smart-contract-testing = { path = "../../contract-testing" } +cis2-multi = { path = "../cis2-multi" } +ed25519-dalek = { version = "2.0", features = ["rand_core"] } rand = "0.8" [lib] -crate-type=["cdylib", "rlib"] +crate-type = ["cdylib", "rlib"] [profile.release] codegen-units = 1 diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index f70236ba..c99940c3 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -41,6 +41,8 @@ //! (third-party) to do so. use concordium_cis2::{self as cis2, *}; use concordium_std::*; +#[cfg(feature = "serde")] +use serde::{Deserialize as SerdeDeserialize, Serialize as SerdeSerialize}; // The testnet genesis hash is: // 0x4221332d34e1694168c2a0c0b3fd0f273809612cb13d000d5c2e00e85f50f796 @@ -522,6 +524,7 @@ impl SigningAmount for TokenAmount {} /// The token amount signed in the message. #[derive(Serialize, Clone, SchemaType)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] pub struct TokenAmount { /// The token amount signed in the message. pub token_amount: ContractTokenAmount, @@ -533,6 +536,7 @@ pub struct TokenAmount { /// A single withdrawal of CCD or some amount of tokens. #[derive(Serialize, Clone, SchemaType)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] pub struct Withdraw { /// The address receiving the CCD or tokens being withdrawn. pub to: Receiver, @@ -544,6 +548,7 @@ pub struct Withdraw { /// The withdraw message that is signed by the signer. #[derive(Serialize, Clone, SchemaType)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] pub struct WithdrawMessage { /// The entry_point that the signature is intended for. pub entry_point: OwnedEntrypointName, @@ -568,6 +573,7 @@ impl IsMessage for WithdrawMessage { /// A batch of withdrawals signed by a signer. #[derive(Serialize, SchemaType)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] pub struct WithdrawBatch { /// The signer public key. pub signer: PublicKeyEd25519, @@ -580,6 +586,7 @@ pub struct WithdrawBatch { /// The parameter type for the contract functions /// `withdrawCcd/withdrawCis2Tokens`. #[derive(Serialize, SchemaType)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] #[concordium(transparent)] #[repr(transparent)] pub struct WithdrawParameter { @@ -950,6 +957,7 @@ fn withdraw_cis2_tokens( /// A single transfer of CCD or some amount of tokens. #[derive(Serialize, Clone, SchemaType)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] pub struct Transfer { /// The public key receiving the tokens being transferred. pub to: PublicKeyEd25519, @@ -959,6 +967,7 @@ pub struct Transfer { /// The transfer message that is signed by the signer. #[derive(Serialize, Clone, SchemaType)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] pub struct TransferMessage { /// The entry_point that the signature is intended for. pub entry_point: OwnedEntrypointName, @@ -983,6 +992,7 @@ impl IsMessage for TransferMessage { /// A batch of transfers signed by a signer. #[derive(Serialize, SchemaType)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] pub struct TransferBatch { /// The signer public key. pub signer: PublicKeyEd25519, @@ -995,6 +1005,7 @@ pub struct TransferBatch { /// The parameter type for the contract functions /// `transferCcd/transferCis2Tokens`. #[derive(Serialize, SchemaType)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] #[concordium(transparent)] #[repr(transparent)] pub struct TransferParameter {