From c8e2e2cca87efefb8a0c0f005f6ec80bf928ba92 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:57:08 +1000 Subject: [PATCH 1/7] add token muxed extension --- soroban-sdk/src/testutils/arbitrary.rs | 35 ++++++++ soroban-sdk/src/token.rs | 2 + soroban-sdk/src/token/muxed_ext.rs | 110 +++++++++++++++++++++++++ soroban-token-sdk/src/event.rs | 26 +++++- 4 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 soroban-sdk/src/token/muxed_ext.rs diff --git a/soroban-sdk/src/testutils/arbitrary.rs b/soroban-sdk/src/testutils/arbitrary.rs index a48b03a7e..0cd896ce3 100644 --- a/soroban-sdk/src/testutils/arbitrary.rs +++ b/soroban-sdk/src/testutils/arbitrary.rs @@ -682,6 +682,41 @@ mod objects { } } +/// Implementations for imaginary types, that don't really exist outside of the +/// Rust SDK and are merely mapped to and from other types. +mod imaginary { + use super::api::*; + use crate::token::muxed_ext::Mux; + use crate::ConversionError; + use crate::{BytesN, Env, String, TryFromVal}; + use arbitrary::Arbitrary; + use std::string::String as RustString; + + #[derive(Arbitrary, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] + pub enum ArbitraryMux { + None, + Id(u64), + Text(RustString), + Hash([u8; 32]), + } + + impl SorobanArbitrary for Mux { + type Prototype = ArbitraryMux; + } + + impl TryFromVal for Mux { + type Error = ConversionError; + fn try_from_val(env: &Env, v: &ArbitraryMux) -> Result { + match v { + ArbitraryMux::None => Ok(Mux::None), + ArbitraryMux::Id(id) => Ok(Mux::Id(*id)), + ArbitraryMux::Text(text) => Ok(Mux::Text(String::from_str(env, &text))), + ArbitraryMux::Hash(hash) => Ok(Mux::Hash(BytesN::from_array(env, &hash))), + } + } + } +} + /// Implementations of `soroban_sdk::testutils::arbitrary::api` for tuples of Soroban types. /// /// The implementation is similar to objects, but macroized. diff --git a/soroban-sdk/src/token.rs b/soroban-sdk/src/token.rs index 16c1050a6..57e2ff963 100644 --- a/soroban-sdk/src/token.rs +++ b/soroban-sdk/src/token.rs @@ -7,6 +7,8 @@ //! Use [`TokenClient`] for calling token contracts such as the Stellar Asset //! Contract. +pub mod muxed_ext; + use crate::{contractclient, contractspecfn, Address, Env, String}; // The interface below was copied from diff --git a/soroban-sdk/src/token/muxed_ext.rs b/soroban-sdk/src/token/muxed_ext.rs new file mode 100644 index 000000000..bf31247c6 --- /dev/null +++ b/soroban-sdk/src/token/muxed_ext.rs @@ -0,0 +1,110 @@ +use crate::{ + contractclient, contractspecfn, Address, BytesN, ConversionError, Env, String, TryFromVal, + TryIntoVal, Val, +}; +use core::fmt::Debug; + +/// Extension interface for Token contracts that implement the transfer_muxed +/// extension, such as the Stellar Asset Contract. +/// +/// Defined by [SEP-??]. +/// +/// [SEP-??]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-00??.md +/// +/// Tokens allow holders of the token to transfer tokens to other addresses, and +/// the transfer_muxed extension allows a token to be transferred with an +/// accompanying muxed ID for both the from and to addresses. The muxed IDs are +/// emitted in respective events. +/// +/// Tokens implementing the extension expose a single function for doing so: +/// - [`transfer_muxed`][Self::transfer_muxed] +#[contractspecfn(name = "super::StellarAssetSpec", export = false)] +#[contractclient(crate_path = "crate", name = "TokenMuxedExtTransferClient")] +pub trait TokenMuxedExtInterface { + /// Transfer `amount` from `from` to `to`. + /// + /// Passess through the `from_id` and `to_id` to the event. + /// + /// # Arguments + /// + /// * `from` - The address holding the balance of tokens which will be + /// withdrawn from. + /// * `from_mux` - The muxed ID of the sender to be emitted in the event. + /// * `to` - The address which will receive the transferred tokens. + /// * `to_mux` - The muxed ID of the receiver to be emitted in the event. + /// * `amount` - The amount of tokens to be transferred. + /// + /// # Events + /// + /// Emits an event with topics `["transfer_muxed", from: Address, to: Address], + /// data = {amount: i128, from_mux: Mux, to_mux: Mux}` + fn transfer_muxed( + env: Env, + from: Address, + from_mux: Mux, + to: Address, + to_mux: Mux, + amount: i128, + ); +} + +/// Mux is a value that off-chain identifies a sub-identifier of an Address +/// on-chain. +/// +/// A mux is also commonly referred to as a memo. +/// +/// A mux may be a void (none), an ID (64-bit number), a String, or a 32-byte +/// Hash. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Mux { + None, + Id(u64), + Text(String), + Hash(BytesN<32>), +} + +impl TryFromVal for Mux { + type Error = ConversionError; + + fn try_from_val(env: &Env, v: &Val) -> Result { + if v.is_void() { + Ok(Self::None) + } else if let Ok(v) = v.try_into_val(env) { + Ok(Self::Id(v)) + } else if let Ok(v) = v.try_into_val(env) { + Ok(Self::Text(v)) + } else if let Ok(v) = v.try_into_val(env) { + Ok(Self::Hash(v)) + } else { + Err(ConversionError) + } + } +} + +impl TryFromVal for Val { + type Error = ConversionError; + + fn try_from_val(env: &Env, v: &Mux) -> Result { + match v { + Mux::None => Ok(Val::VOID.to_val()), + Mux::Id(v) => v.try_into_val(env).map_err(|_| ConversionError), + Mux::Text(v) => v.try_into_val(env).map_err(|_| ConversionError), + Mux::Hash(v) => v.try_into_val(env).map_err(|_| ConversionError), + } + } +} + +#[cfg(not(target_family = "wasm"))] +use crate::env::internal::xdr::ScVal; + +#[cfg(not(target_family = "wasm"))] +impl From<&Mux> for ScVal { + fn from(v: &Mux) -> Self { + match v { + Mux::None => ScVal::Void, + Mux::Id(v) => ScVal::U64(*v), + Mux::Text(v) => v.into(), + Mux::Hash(v) => v.into(), + } + } +} diff --git a/soroban-token-sdk/src/event.rs b/soroban-token-sdk/src/event.rs index 804cd5ab8..97006ac1c 100644 --- a/soroban-token-sdk/src/event.rs +++ b/soroban-token-sdk/src/event.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{symbol_short, Address, Env, Symbol}; +use soroban_sdk::{contracttype, symbol_short, token::muxed_ext::Mux, Address, Env, Symbol}; pub struct Events { env: Env, @@ -22,6 +22,23 @@ impl Events { self.env.events().publish(topics, amount); } + pub fn transfer_muxed( + &self, + from: Address, + from_mux: Mux, + to: Address, + to_mux: Mux, + amount: i128, + ) { + let topics = (Symbol::new(&self.env, "transfer_muxed"), from, to); + let data = TransferMuxedData { + amount, + from_mux, + to_mux, + }; + self.env.events().publish(topics, data); + } + pub fn mint(&self, admin: Address, to: Address, amount: i128) { let topics = (symbol_short!("mint"), admin, to); self.env.events().publish(topics, amount); @@ -47,3 +64,10 @@ impl Events { self.env.events().publish(topics, amount); } } + +#[contracttype] +struct TransferMuxedData { + amount: i128, + from_mux: Mux, + to_mux: Mux, +} From 728ebb6452229ab393ada400d15e68d93f4281e3 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:34:47 +1000 Subject: [PATCH 2/7] assume bytes to scval conversion never errors (cherry picked from commit 638dac856fbaab7d317243b7a14321e3af4692b1) --- soroban-sdk/src/bytes.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/soroban-sdk/src/bytes.rs b/soroban-sdk/src/bytes.rs index b956eb9d5..854233e41 100644 --- a/soroban-sdk/src/bytes.rs +++ b/soroban-sdk/src/bytes.rs @@ -1076,18 +1076,21 @@ impl From<&BytesN> for Bytes { } #[cfg(not(target_family = "wasm"))] -impl TryFrom<&BytesN> for ScVal { - type Error = ConversionError; - fn try_from(v: &BytesN) -> Result { - Ok(ScVal::try_from_val(&v.0.env, &v.0.obj.to_val())?) +impl From<&BytesN> for ScVal { + fn from(v: &BytesN) -> Self { + // This conversion occurs only in test utilities, and theoretically all + // values should convert to an ScVal because the Env won't let the host + // type to exist otherwise, unwrapping. Even if there are edge cases + // that don't, this is a trade off for a better test developer + // experience. + ScVal::try_from_val(&v.0.env, &v.0.obj.to_val()).unwrap() } } #[cfg(not(target_family = "wasm"))] -impl TryFrom> for ScVal { - type Error = ConversionError; - fn try_from(v: BytesN) -> Result { - (&v).try_into() +impl From> for ScVal { + fn from(v: BytesN) -> Self { + (&v).into() } } From 803c5dfc34ce97e839e7161c5c1cdc1c245b8a81 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:57:21 +1000 Subject: [PATCH 3/7] add convenience conversions --- soroban-sdk/src/token/muxed_ext.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/soroban-sdk/src/token/muxed_ext.rs b/soroban-sdk/src/token/muxed_ext.rs index bf31247c6..305ccbc30 100644 --- a/soroban-sdk/src/token/muxed_ext.rs +++ b/soroban-sdk/src/token/muxed_ext.rs @@ -63,6 +63,30 @@ pub enum Mux { Hash(BytesN<32>), } +impl From<()> for Mux { + fn from(_: ()) -> Self { + Self::None + } +} + +impl From for Mux { + fn from(v: u64) -> Self { + Self::Id(v) + } +} + +impl From for Mux { + fn from(v: String) -> Self { + Self::Text(v) + } +} + +impl From> for Mux { + fn from(v: BytesN<32>) -> Self { + Self::Hash(v) + } +} + impl TryFromVal for Mux { type Error = ConversionError; From 51a46a2526d4a4906782632856fb6a76a5c1a8e4 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Feb 2025 07:31:23 +1000 Subject: [PATCH 4/7] simplify token mux to id-only mode --- soroban-sdk/src/token/muxed_ext.rs | 102 +---------------------------- soroban-token-sdk/src/event.rs | 12 ++-- 2 files changed, 9 insertions(+), 105 deletions(-) diff --git a/soroban-sdk/src/token/muxed_ext.rs b/soroban-sdk/src/token/muxed_ext.rs index 305ccbc30..a43411deb 100644 --- a/soroban-sdk/src/token/muxed_ext.rs +++ b/soroban-sdk/src/token/muxed_ext.rs @@ -1,8 +1,4 @@ -use crate::{ - contractclient, contractspecfn, Address, BytesN, ConversionError, Env, String, TryFromVal, - TryIntoVal, Val, -}; -use core::fmt::Debug; +use crate::{contractclient, contractspecfn, Address, Env}; /// Extension interface for Token contracts that implement the transfer_muxed /// extension, such as the Stellar Asset Contract. @@ -37,98 +33,6 @@ pub trait TokenMuxedExtInterface { /// # Events /// /// Emits an event with topics `["transfer_muxed", from: Address, to: Address], - /// data = {amount: i128, from_mux: Mux, to_mux: Mux}` - fn transfer_muxed( - env: Env, - from: Address, - from_mux: Mux, - to: Address, - to_mux: Mux, - amount: i128, - ); -} - -/// Mux is a value that off-chain identifies a sub-identifier of an Address -/// on-chain. -/// -/// A mux is also commonly referred to as a memo. -/// -/// A mux may be a void (none), an ID (64-bit number), a String, or a 32-byte -/// Hash. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum Mux { - None, - Id(u64), - Text(String), - Hash(BytesN<32>), -} - -impl From<()> for Mux { - fn from(_: ()) -> Self { - Self::None - } -} - -impl From for Mux { - fn from(v: u64) -> Self { - Self::Id(v) - } -} - -impl From for Mux { - fn from(v: String) -> Self { - Self::Text(v) - } -} - -impl From> for Mux { - fn from(v: BytesN<32>) -> Self { - Self::Hash(v) - } -} - -impl TryFromVal for Mux { - type Error = ConversionError; - - fn try_from_val(env: &Env, v: &Val) -> Result { - if v.is_void() { - Ok(Self::None) - } else if let Ok(v) = v.try_into_val(env) { - Ok(Self::Id(v)) - } else if let Ok(v) = v.try_into_val(env) { - Ok(Self::Text(v)) - } else if let Ok(v) = v.try_into_val(env) { - Ok(Self::Hash(v)) - } else { - Err(ConversionError) - } - } -} - -impl TryFromVal for Val { - type Error = ConversionError; - - fn try_from_val(env: &Env, v: &Mux) -> Result { - match v { - Mux::None => Ok(Val::VOID.to_val()), - Mux::Id(v) => v.try_into_val(env).map_err(|_| ConversionError), - Mux::Text(v) => v.try_into_val(env).map_err(|_| ConversionError), - Mux::Hash(v) => v.try_into_val(env).map_err(|_| ConversionError), - } - } -} - -#[cfg(not(target_family = "wasm"))] -use crate::env::internal::xdr::ScVal; - -#[cfg(not(target_family = "wasm"))] -impl From<&Mux> for ScVal { - fn from(v: &Mux) -> Self { - match v { - Mux::None => ScVal::Void, - Mux::Id(v) => ScVal::U64(*v), - Mux::Text(v) => v.into(), - Mux::Hash(v) => v.into(), - } - } + /// data = {amount: i128, from_id: u64, to_id: u64}` + fn transfer_muxed(env: Env, from: Address, from_id: u64, to: Address, to_id: u64, amount: i128); } diff --git a/soroban-token-sdk/src/event.rs b/soroban-token-sdk/src/event.rs index 97006ac1c..937e9c483 100644 --- a/soroban-token-sdk/src/event.rs +++ b/soroban-token-sdk/src/event.rs @@ -25,16 +25,16 @@ impl Events { pub fn transfer_muxed( &self, from: Address, - from_mux: Mux, + from_id: u64, to: Address, - to_mux: Mux, + to_id: u64, amount: i128, ) { let topics = (Symbol::new(&self.env, "transfer_muxed"), from, to); let data = TransferMuxedData { amount, - from_mux, - to_mux, + from_id, + to_id, }; self.env.events().publish(topics, data); } @@ -68,6 +68,6 @@ impl Events { #[contracttype] struct TransferMuxedData { amount: i128, - from_mux: Mux, - to_mux: Mux, + from_id: u64, + to_id: u64, } From 5ddc8b0088bd3f14820a438a506780b391b4f315 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Feb 2025 07:34:15 +1000 Subject: [PATCH 5/7] remove token muxed extension import --- soroban-token-sdk/src/event.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soroban-token-sdk/src/event.rs b/soroban-token-sdk/src/event.rs index 937e9c483..c69aeea6e 100644 --- a/soroban-token-sdk/src/event.rs +++ b/soroban-token-sdk/src/event.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contracttype, symbol_short, token::muxed_ext::Mux, Address, Env, Symbol}; +use soroban_sdk::{contracttype, symbol_short, Address, Env, Symbol}; pub struct Events { env: Env, From 8bd5b78608f7149246d5e5f0b3ef005e9b776968 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Feb 2025 07:34:41 +1000 Subject: [PATCH 6/7] remove arbitrary impl for muxed account extension --- soroban-sdk/src/testutils/arbitrary.rs | 35 -------------------------- 1 file changed, 35 deletions(-) diff --git a/soroban-sdk/src/testutils/arbitrary.rs b/soroban-sdk/src/testutils/arbitrary.rs index 0cd896ce3..a48b03a7e 100644 --- a/soroban-sdk/src/testutils/arbitrary.rs +++ b/soroban-sdk/src/testutils/arbitrary.rs @@ -682,41 +682,6 @@ mod objects { } } -/// Implementations for imaginary types, that don't really exist outside of the -/// Rust SDK and are merely mapped to and from other types. -mod imaginary { - use super::api::*; - use crate::token::muxed_ext::Mux; - use crate::ConversionError; - use crate::{BytesN, Env, String, TryFromVal}; - use arbitrary::Arbitrary; - use std::string::String as RustString; - - #[derive(Arbitrary, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] - pub enum ArbitraryMux { - None, - Id(u64), - Text(RustString), - Hash([u8; 32]), - } - - impl SorobanArbitrary for Mux { - type Prototype = ArbitraryMux; - } - - impl TryFromVal for Mux { - type Error = ConversionError; - fn try_from_val(env: &Env, v: &ArbitraryMux) -> Result { - match v { - ArbitraryMux::None => Ok(Mux::None), - ArbitraryMux::Id(id) => Ok(Mux::Id(*id)), - ArbitraryMux::Text(text) => Ok(Mux::Text(String::from_str(env, &text))), - ArbitraryMux::Hash(hash) => Ok(Mux::Hash(BytesN::from_array(env, &hash))), - } - } - } -} - /// Implementations of `soroban_sdk::testutils::arbitrary::api` for tuples of Soroban types. /// /// The implementation is similar to objects, but macroized. From a4aae373abd49f6addd0352278a3c558974efa22 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Feb 2025 08:50:47 +1000 Subject: [PATCH 7/7] emit transfer event --- soroban-token-sdk/src/event.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/soroban-token-sdk/src/event.rs b/soroban-token-sdk/src/event.rs index c69aeea6e..9980d5381 100644 --- a/soroban-token-sdk/src/event.rs +++ b/soroban-token-sdk/src/event.rs @@ -19,7 +19,8 @@ impl Events { pub fn transfer(&self, from: Address, to: Address, amount: i128) { let topics = (symbol_short!("transfer"), from, to); - self.env.events().publish(topics, amount); + let data = TransferData { amount }; + self.env.events().publish(topics, data); } pub fn transfer_muxed( @@ -30,7 +31,7 @@ impl Events { to_id: u64, amount: i128, ) { - let topics = (Symbol::new(&self.env, "transfer_muxed"), from, to); + let topics = (Symbol::new(&self.env, "transfer"), from, to); let data = TransferMuxedData { amount, from_id, @@ -65,6 +66,11 @@ impl Events { } } +#[contracttype] +struct TransferData { + amount: i128, +} + #[contracttype] struct TransferMuxedData { amount: i128,