Repository: tari-project/tari
Pull request #7002: fix: payment id tari address usage
Author: login: SWvheerden
Created: 2025-04-30T06:27:57Z | Merged: 2025-04-30T07:13:53Z

Description
---
Fixes use of tari address with payment id inside

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
  - Improved error logging in transaction preparation, providing more detailed error information when sending service replies fails.

- **Refactor**
  - Removed support for payment IDs in single addresses, simplifying address handling and serialization.
  - Updated address size limits and validation logic for dual addresses, allowing for larger internal representations and more flexible size handling.
  - Cleaned up code to remove unnecessary arguments and streamline address creation across the wallet and address modules.

- **Tests**
  - Added comprehensive tests for dual addresses to ensure correct handling of maximum payment ID sizes and address conversions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/base_layer/common_types/src/tari_address/dual_address.rs b/base_layer/common_types/src/tari_address/dual_address.rs
index bd172030a77..71a7ca02a05 100644
--- a/base_layer/common_types/src/tari_address/dual_address.rs
+++ b/base_layer/common_types/src/tari_address/dual_address.rs
@@ -583,4 +583,66 @@ mod test {
             payment_id.as_slice()
         );
     }
+
+    #[test]
+    fn valid_max_payment_id() {
+        // Generate random public key
+        let mut rng = rand::thread_rng();
+        let view_key = CompressedPublicKey::from_secret_key(&PrivateKey::random(&mut rng));
+        let spend_key = CompressedPublicKey::from_secret_key(&PrivateKey::random(&mut rng));
+        let payment_id = vec![1u8; MAX_ENCRYPTED_DATA_SIZE + 1];
+
+        // Generate an emoji ID from the public key and ensure we recover it
+        let _emoji_id_from_public_key = DualAddress::new(
+            view_key.clone(),
+            spend_key.clone(),
+            Network::Esmeralda,
+            TariAddressFeatures::default(),
+            Some(payment_id.clone()),
+        )
+        .unwrap_err();
+        let payment_id = vec![1u8; MAX_ENCRYPTED_DATA_SIZE];
+
+        // Generate an emoji ID from the public key and ensure we recover it
+        let emoji_id_from_public_key = DualAddress::new(
+            view_key.clone(),
+            spend_key.clone(),
+            Network::Esmeralda,
+            TariAddressFeatures::default(),
+            Some(payment_id.clone()),
+        )
+        .unwrap();
+
+        assert_eq!(emoji_id_from_public_key.public_spend_key(), &spend_key);
+        assert_eq!(
+            emoji_id_from_public_key.payment_id_user_data.as_bytes(),
+            payment_id.as_slice()
+        );
+
+        // Check the size of the corresponding emoji string
+        let emoji_string = emoji_id_from_public_key.to_emoji_string();
+        assert_eq!(emoji_string.chars().count(), TARI_ADDRESS_INTERNAL_DUAL_SIZE + 256);
+
+        let features = emoji_id_from_public_key.features();
+        assert_eq!(features, TariAddressFeatures(7));
+        // Generate an emoji ID from the emoji string and ensure we recover it
+        let emoji_id_from_emoji_string = DualAddress::from_emoji_string(&emoji_string).unwrap();
+        assert_eq!(emoji_id_from_emoji_string.to_emoji_string(), emoji_string);
+
+        // Return to the original public keys for good measure
+        assert_eq!(emoji_id_from_emoji_string.public_spend_key(), &spend_key);
+        assert_eq!(
+            emoji_id_from_emoji_string.payment_id_user_data.as_bytes(),
+            payment_id.as_slice()
+        );
+        let bas58 = emoji_id_from_emoji_string.to_base58();
+        if bas58.len() > INTERNAL_DUAL_BASE58_MAX_SIZE {
+            dbg!(bas58.len());
+            panic!("Base58 is too long");
+        }
+        if bas58.len() < INTERNAL_DUAL_BASE58_MIN_SIZE {
+            dbg!(bas58.len());
+            panic!("Base58 is too short");
+        }
+    }
 }
diff --git a/base_layer/common_types/src/tari_address/mod.rs b/base_layer/common_types/src/tari_address/mod.rs
index bbf91aa1eac..ad3db97b8f1 100644
--- a/base_layer/common_types/src/tari_address/mod.rs
+++ b/base_layer/common_types/src/tari_address/mod.rs
@@ -44,7 +44,7 @@ use crate::{
 pub const TARI_ADDRESS_INTERNAL_DUAL_SIZE: usize = 67; // number of bytes used for the internal representation
 pub const TARI_ADDRESS_INTERNAL_SINGLE_SIZE: usize = 35; // number of bytes used for the internal representation
 const INTERNAL_DUAL_BASE58_MIN_SIZE: usize = 89; // number of bytes used for the internal representation
-const INTERNAL_DUAL_BASE58_MAX_SIZE: usize = 91; // number of bytes used for the internal representation
+const INTERNAL_DUAL_BASE58_MAX_SIZE: usize = 443; // number of bytes used for the internal representation
 const INTERNAL_SINGLE_MIN_BASE58_SIZE: usize = 45; // number of bytes used for the internal representation
 const INTERNAL_SINGLE_MAX_BASE58_SIZE: usize = 48; // number of bytes used for the internal representation
 const MAX_ENCRYPTED_DATA_SIZE: usize = 256; // max size of the payment_id_ bytes
@@ -128,6 +128,8 @@ pub enum TariAddressError {
     CreationError(String),
     #[error("Too large payment_id")]
     PaymentIdTooLarge,
+    #[error("Payment_id not supported on single addresses")]
+    PaymentIdNotSupported,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -159,13 +161,9 @@ impl TariAddress {
         spend_key: CompressedPublicKey,
         network: Network,
         features: TariAddressFeatures,
-        payment_id_user_data: Option<Vec<u8>>,
     ) -> Result<TariAddress, TariAddressError> {
         Ok(TariAddress::Single(Box::new(SingleAddress::new(
-            spend_key,
-            network,
-            features,
-            payment_id_user_data,
+            spend_key, network, features,
         )?)))
     }
 
@@ -223,7 +221,6 @@ impl TariAddress {
                 one.public_spend_key().clone(),
                 one.network(),
                 one.features().combine_features(two.features()),
-                None,
             ),
         }
     }
@@ -231,7 +228,13 @@ impl TariAddress {
     /// Gets the bytes size of the Tari Address
     pub fn get_size(&self) -> usize {
         match self {
-            TariAddress::Dual(_) => TARI_ADDRESS_INTERNAL_DUAL_SIZE,
+            TariAddress::Dual(v) => {
+                if v.features().contains(TariAddressFeatures::PAYMENT_ID) {
+                    v.to_vec().len()
+                } else {
+                    TARI_ADDRESS_INTERNAL_DUAL_SIZE
+                }
+            },
             TariAddress::Single(_) => TARI_ADDRESS_INTERNAL_SINGLE_SIZE,
         }
     }
@@ -239,7 +242,7 @@ impl TariAddress {
     pub fn get_payment_id_bytes(&self) -> Vec<u8> {
         match self {
             TariAddress::Dual(v) => v.get_payment_id_bytes(),
-            TariAddress::Single(v) => v.get_payment_id_bytes(),
+            TariAddress::Single(_) => vec![],
         }
     }
 
@@ -250,11 +253,7 @@ impl TariAddress {
                 address.add_payment_id(data)?;
                 Ok(TariAddress::Dual(address))
             },
-            TariAddress::Single(v) => {
-                let mut address = v.clone();
-                address.add_payment_id(data)?;
-                Ok(TariAddress::Single(address))
-            },
+            TariAddress::Single(_) => Err(TariAddressError::PaymentIdNotSupported),
         }
     }
 
@@ -337,7 +336,10 @@ impl TariAddress {
     /// Construct Tari Address from bytes
     pub fn from_bytes(bytes: &[u8]) -> Result<TariAddress, TariAddressError>
     where Self: Sized {
-        if !(bytes.len() == TARI_ADDRESS_INTERNAL_SINGLE_SIZE || bytes.len() == TARI_ADDRESS_INTERNAL_DUAL_SIZE) {
+        if !(bytes.len() == TARI_ADDRESS_INTERNAL_SINGLE_SIZE ||
+            (bytes.len() >= TARI_ADDRESS_INTERNAL_DUAL_SIZE &&
+                bytes.len() <= (TARI_ADDRESS_INTERNAL_DUAL_SIZE + MAX_ENCRYPTED_DATA_SIZE)))
+        {
             return Err(TariAddressError::InvalidSize);
         }
         if bytes.len() == TARI_ADDRESS_INTERNAL_SINGLE_SIZE {
@@ -356,12 +358,14 @@ impl TariAddress {
     }
 
     /// Construct Tari Address from hex
-    pub fn from_base58(hex_str: &str) -> Result<TariAddress, TariAddressError> {
-        if hex_str.len() < INTERNAL_SINGLE_MIN_BASE58_SIZE {
+    pub fn from_base58(bas58_str: &str) -> Result<TariAddress, TariAddressError> {
+        if bas58_str.len() < INTERNAL_SINGLE_MIN_BASE58_SIZE {
             return Err(TariAddressError::InvalidSize);
         }
 
-        let (first, rest) = hex_str.split_at_checked(2).ok_or(TariAddressError::InvalidCharacter)?;
+        let (first, rest) = bas58_str
+            .split_at_checked(2)
+            .ok_or(TariAddressError::InvalidCharacter)?;
         let (network, features) = first.split_at_checked(1).ok_or(TariAddressError::InvalidCharacter)?;
         let mut result = bs58::decode(network)
             .into_vec()
@@ -374,6 +378,7 @@ impl TariAddress {
             .map_err(|_| TariAddressError::CannotRecoverPublicKey)?;
         result.append(&mut features);
         result.append(&mut rest);
+
         Self::from_bytes(result.as_slice())
     }
 
@@ -534,7 +539,6 @@ mod test {
             public_key.clone(),
             Network::Esmeralda,
             TariAddressFeatures::create_interactive_only(),
-            None,
         )
         .unwrap();
         assert_eq!(emoji_id_from_public_key.public_spend_key(), &public_key);
@@ -561,7 +565,6 @@ mod test {
             public_key.clone(),
             Network::Esmeralda,
             TariAddressFeatures::create_one_sided_only(),
-            None,
         )
         .unwrap();
         assert_eq!(emoji_id_from_public_key.public_spend_key(), &public_key);
@@ -732,7 +735,6 @@ mod test {
             public_key.clone(),
             Network::Esmeralda,
             TariAddressFeatures::create_interactive_only(),
-            None,
         )
         .unwrap();
 
@@ -778,7 +780,6 @@ mod test {
             public_key.clone(),
             Network::Esmeralda,
             TariAddressFeatures::create_one_sided_only(),
-            None,
         )
         .unwrap();
 
diff --git a/base_layer/common_types/src/tari_address/single_address.rs b/base_layer/common_types/src/tari_address/single_address.rs
index 35fb0e753cc..c795c1d909f 100644
--- a/base_layer/common_types/src/tari_address/single_address.rs
+++ b/base_layer/common_types/src/tari_address/single_address.rs
@@ -25,7 +25,6 @@ use std::convert::TryFrom;
 use serde::{Deserialize, Serialize};
 use tari_common::configuration::Network;
 use tari_crypto::tari_utilities::ByteArray;
-use tari_max_size::MaxSizeBytes;
 use tari_utilities::hex::{from_hex, Hex};
 
 use crate::{
@@ -36,7 +35,6 @@ use crate::{
         TariAddressFeatures,
         INTERNAL_SINGLE_MAX_BASE58_SIZE,
         INTERNAL_SINGLE_MIN_BASE58_SIZE,
-        MAX_ENCRYPTED_DATA_SIZE,
         TARI_ADDRESS_INTERNAL_SINGLE_SIZE,
     },
     types::CompressedPublicKey,
@@ -47,7 +45,6 @@ pub struct SingleAddress {
     network: Network,
     features: TariAddressFeatures,
     public_spend_key: CompressedPublicKey,
-    payment_id_user_data: MaxSizeBytes<MAX_ENCRYPTED_DATA_SIZE>,
 }
 
 impl SingleAddress {
@@ -56,24 +53,11 @@ impl SingleAddress {
         spend_key: CompressedPublicKey,
         network: Network,
         features: TariAddressFeatures,
-        payment_id_user_data: Option<Vec<u8>>,
     ) -> Result<SingleAddress, TariAddressError> {
-        let mut features = features;
-        let payment_id_user_data = match payment_id_user_data {
-            Some(data) => {
-                if data.len() > MAX_ENCRYPTED_DATA_SIZE {
-                    return Err(TariAddressError::PaymentIdTooLarge);
-                }
-                features.set(TariAddressFeatures::PAYMENT_ID, true);
-                MaxSizeBytes::from_bytes_truncate(data)
-            },
-            None => MaxSizeBytes::empty(),
-        };
         Ok(Self {
             network,
             features,
             public_spend_key: spend_key,
-            payment_id_user_data,
         })
     }
 
@@ -82,25 +66,14 @@ impl SingleAddress {
         spend_key: CompressedPublicKey,
         network: Network,
     ) -> Result<SingleAddress, TariAddressError> {
-        Self::new(spend_key, network, TariAddressFeatures::create_interactive_only(), None)
-    }
-
-    pub fn add_payment_id(&mut self, data: Vec<u8>) -> Result<(), TariAddressError> {
-        if data.len() > MAX_ENCRYPTED_DATA_SIZE {
-            return Err(TariAddressError::PaymentIdTooLarge);
-        }
-        self.features.set(TariAddressFeatures::PAYMENT_ID, true);
-        self.payment_id_user_data = MaxSizeBytes::from_bytes_truncate(data);
-        Ok(())
+        Self::new(spend_key, network, TariAddressFeatures::create_interactive_only())
     }
 
     /// helper function to convert emojis to u8
     pub fn emoji_to_bytes(emoji: &str) -> Result<Vec<u8>, TariAddressError> {
         // The string must be the correct size, including the checksum
         let length = emoji.chars().count();
-        if !(TARI_ADDRESS_INTERNAL_SINGLE_SIZE..=TARI_ADDRESS_INTERNAL_SINGLE_SIZE + MAX_ENCRYPTED_DATA_SIZE)
-            .contains(&length)
-        {
+        if length != TARI_ADDRESS_INTERNAL_SINGLE_SIZE {
             return Err(TariAddressError::InvalidSize);
         }
 
@@ -122,10 +95,6 @@ impl SingleAddress {
         Self::from_bytes(&bytes)
     }
 
-    pub fn get_payment_id_bytes(&self) -> Vec<u8> {
-        self.payment_id_user_data.as_ref().to_vec()
-    }
-
     /// Gets the network from the Tari Address
     pub fn network(&self) -> Network {
         self.network
@@ -152,9 +121,7 @@ impl SingleAddress {
     pub fn from_bytes(bytes: &[u8]) -> Result<Self, TariAddressError>
     where Self: Sized {
         let length = bytes.len();
-        if !(TARI_ADDRESS_INTERNAL_SINGLE_SIZE..=TARI_ADDRESS_INTERNAL_SINGLE_SIZE + MAX_ENCRYPTED_DATA_SIZE)
-            .contains(&length)
-        {
+        if length != TARI_ADDRESS_INTERNAL_SINGLE_SIZE {
             return Err(TariAddressError::InvalidSize);
         }
         if validate_checksum(bytes).is_err() {
@@ -164,26 +131,22 @@ impl SingleAddress {
         let features = TariAddressFeatures::from_bits(bytes[1]).ok_or(TariAddressError::InvalidFeatures)?;
         let public_spend_key = CompressedPublicKey::from_canonical_bytes(&bytes[2..34])
             .map_err(|_| TariAddressError::CannotRecoverPublicKey)?;
-        let payment_id_user_data = MaxSizeBytes::from_bytes_truncate(&bytes[34..length - 1]);
         Ok(Self {
             network,
             features,
             public_spend_key,
-            payment_id_user_data,
         })
     }
 
     /// Convert Tari Address to bytes
     pub fn to_vec(&self) -> Vec<u8> {
-        let length = TARI_ADDRESS_INTERNAL_SINGLE_SIZE + self.payment_id_user_data.len();
-        let mut buf = vec![0; length];
+        let mut buf = [0u8; TARI_ADDRESS_INTERNAL_SINGLE_SIZE];
         buf[0] = self.network.as_byte();
         buf[1] = self.features.0;
         buf[2..34].copy_from_slice(self.public_spend_key.as_bytes());
-        buf[34..(length - 1)].copy_from_slice(self.payment_id_user_data.as_bytes());
-        let checksum = compute_checksum(&buf[0..(length - 1)]);
-        buf[length - 1] = checksum;
-        buf
+        let checksum = compute_checksum(&buf[0..34]);
+        buf[34] = checksum;
+        buf.to_vec()
     }
 
     /// Construct Tari Address from Base58
@@ -273,7 +236,6 @@ mod test {
             public_key.clone(),
             Network::Esmeralda,
             TariAddressFeatures::create_interactive_only(),
-            None,
         )
         .unwrap();
         assert_eq!(emoji_id_from_public_key.public_spend_key(), &public_key);
@@ -299,7 +261,6 @@ mod test {
             public_key.clone(),
             Network::Esmeralda,
             TariAddressFeatures::create_one_sided_only(),
-            None,
         )
         .unwrap();
         assert_eq!(emoji_id_from_public_key.public_spend_key(), &public_key);
@@ -362,7 +323,6 @@ mod test {
             public_key.clone(),
             Network::Esmeralda,
             TariAddressFeatures::create_interactive_only(),
-            None,
         )
         .unwrap();
 
@@ -399,7 +359,6 @@ mod test {
             public_key.clone(),
             Network::Esmeralda,
             TariAddressFeatures::create_one_sided_only(),
-            None,
         )
         .unwrap();
 
@@ -498,43 +457,4 @@ mod test {
         bytes[34] = checksum;
         assert_eq!(SingleAddress::from_bytes(&bytes), Err(TariAddressError::InvalidNetwork));
     }
-
-    #[test]
-    fn valid_payment_id() {
-        // Generate random public key
-        let mut rng = rand::thread_rng();
-        let spend_key = CompressedPublicKey::from_secret_key(&PrivateKey::random(&mut rng));
-
-        // Generate an emoji ID from the public key and ensure we recover it
-        let payment_id = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
-        let emoji_id_from_public_key = SingleAddress::new(
-            spend_key.clone(),
-            Network::Esmeralda,
-            TariAddressFeatures::default(),
-            Some(payment_id.clone()),
-        )
-        .unwrap();
-        assert_eq!(emoji_id_from_public_key.public_spend_key(), &spend_key);
-        assert_eq!(
-            emoji_id_from_public_key.payment_id_user_data.as_bytes(),
-            payment_id.as_slice()
-        );
-
-        // Check the size of the corresponding emoji string
-        let emoji_string = emoji_id_from_public_key.to_emoji_string();
-        assert_eq!(emoji_string.chars().count(), TARI_ADDRESS_INTERNAL_SINGLE_SIZE + 8);
-
-        let features = emoji_id_from_public_key.features();
-        assert_eq!(features, TariAddressFeatures(7));
-        // Generate an emoji ID from the emoji string and ensure we recover it
-        let emoji_id_from_emoji_string = SingleAddress::from_emoji_string(&emoji_string).unwrap();
-        assert_eq!(emoji_id_from_emoji_string.to_emoji_string(), emoji_string);
-
-        // Return to the original public keys for good measure
-        assert_eq!(emoji_id_from_emoji_string.public_spend_key(), &spend_key);
-        assert_eq!(
-            emoji_id_from_emoji_string.payment_id_user_data.as_bytes(),
-            payment_id.as_slice()
-        );
-    }
 }
diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs
index 9000613f889..21c7f74af76 100644
--- a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs
+++ b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs
@@ -235,8 +235,8 @@ where
             Ok(sp) => {
                 let _result = service_reply_channel
                     .send(Ok(TransactionServiceResponse::TransactionSent(self.id)))
-                    .inspect_err(|_| {
-                        warn!(target: LOG_TARGET, "Failed to send service reply");
+                    .inspect_err(|e| {
+                        warn!(target: LOG_TARGET, "Failed to send service reply: {:?}", e);
                     });
                 Ok(sp)
             },
@@ -244,8 +244,8 @@ where
                 let error_string = e.to_string();
                 let _size = service_reply_channel
                     .send(Err(TransactionServiceError::from(e)))
-                    .inspect_err(|_| {
-                        warn!(target: LOG_TARGET, "Failed to send service reply");
+                    .inspect_err(|e| {
+                        warn!(target: LOG_TARGET, "Failed to send service reply: {:?}", e);
                     });
                 Err(TransactionServiceProtocolError::new(
                     self.id,
diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs
index 57d7a3fa133..fbbcfd522d1 100644
--- a/base_layer/wallet/src/transaction_service/service.rs
+++ b/base_layer/wallet/src/transaction_service/service.rs
@@ -3036,7 +3036,6 @@ where
                     source_pubkey,
                     self.resources.interactive_tari_address.network(),
                     TariAddressFeatures::INTERACTIVE,
-                    None,
                 )?
             };
             let (tx_finalized_sender, tx_finalized_receiver) = mpsc::channel(100);


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>base_layer/common_types/src/tari_address/mod.rs (1)</summary><blockquote>

`360-380`: _⚠️ Potential issue_

**Missing upper-bound guard in `from_base58`**

`from_base58` validates only `INTERNAL_SINGLE_MIN_BASE58_SIZE`, leaving the door open to extremely large inputs that could allocate unbounded memory before failing later.

Add an upper bound consistent with the `Single`/`Dual` max constants:

```diff
-if bas58_str.len() < INTERNAL_SINGLE_MIN_BASE58_SIZE {
+if bas58_str.len() < INTERNAL_SINGLE_MIN_BASE58_SIZE ||
+   bas58_str.len() > INTERNAL_DUAL_BASE58_MAX_SIZE {
     return Err(TariAddressError::InvalidSize);
 }
```

Hardening this parser prevents denial-of-service scenarios via oversized strings.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs (2)</summary><blockquote>

`236-241`: **Avoid creating an unused `_result` binding – return value can be discarded directly**

The return value of `send(..)` is bound to `_result` and never used, resulting in an `unused variable` lint suppression.  
A more idiomatic pattern is to prefix the entire expression with `let _ =` (or simply drop the assignment) while still attaching the `inspect_err`:

```diff
-let _result = service_reply_channel
-    .send(Ok(TransactionServiceResponse::TransactionSent(self.id)))
-    .inspect_err(|e| {
-        warn!(target: LOG_TARGET, "Failed to send service reply: {:?}", e);
-    });
+let _ = service_reply_channel
+    .send(Ok(TransactionServiceResponse::TransactionSent(self.id)))
+    .inspect_err(|e| warn!(target: LOG_TARGET, "Failed to send service reply: {:?}", e));
```

This removes the needless variable and communicates the intent that the result is intentionally ignored.

---

`244-250`: **Same unused-variable pattern as above**

The error-path block repeats the pattern with `_size`.  Apply the same `let _ = …` idiom for consistency and to silence lint warnings without annotations.

</blockquote></details>
<details>
<summary>base_layer/common_types/src/tari_address/dual_address.rs (1)</summary><blockquote>

`640-647`: **Minor naming inconsistency: `bas58` vs `base58`**

`bas58` misses the “e” compared with the rest of the codebase (`base58`).  
Although this is only a local variable in a test, staying consistent helps grepability and reduces cognitive load.

```diff
-let bas58 = emoji_id_from_emoji_string.to_base58();
-if bas58.len() > INTERNAL_DUAL_BASE58_MAX_SIZE {
-    dbg!(bas58.len());
+let base58 = emoji_id_from_emoji_string.to_base58();
+if base58.len() > INTERNAL_DUAL_BASE58_MAX_SIZE {
+    dbg!(base58.len());
     panic!("Base58 is too long");
 }
-if bas58.len() < INTERNAL_DUAL_BASE58_MIN_SIZE {
-    dbg!(bas58.len());
+if base58.len() < INTERNAL_DUAL_BASE58_MIN_SIZE {
+    dbg!(base58.len());
     panic!("Base58 is too short");
 }
```

</blockquote></details>
<details>
<summary>base_layer/common_types/src/tari_address/mod.rs (1)</summary><blockquote>

`229-235`: **`get_size` allocates for every call when a payment-ID is present**

`v.to_vec()` clones the entire address just to obtain its length, which is already known:

```rust
if v.features().contains(TariAddressFeatures::PAYMENT_ID) {
    v.to_vec().len()
} …
```

Consider exposing (or caching) `DualAddress::len()` returning the computed length without an allocation:

```diff
-if v.features().contains(TariAddressFeatures::PAYMENT_ID) {
-    v.to_vec().len()
+if v.features().contains(TariAddressFeatures::PAYMENT_ID) {
+    TARI_ADDRESS_INTERNAL_DUAL_SIZE + v.get_payment_id_bytes().len()
```

This avoids needless heap work, especially in tight loops.

</blockquote></details>
<details>
<summary>base_layer/common_types/src/tari_address/single_address.rs (1)</summary><blockquote>

`70-71`: **Return type can be simplified – no fallible path**  
`Self::new` currently **cannot** return an error (it wraps the struct in `Ok` unconditionally).  
Because of this, `new_with_interactive_only` also has no fallible branch, yet it still returns `Result<SingleAddress, TariAddressError>`.  

Unless you foresee `Self::new` becoming fallible in the near future, you could simplify the API surface and drop the `Result` envelope here to make the call sites cleaner.

```diff
-pub fn new_with_interactive_only(
-    spend_key: CompressedPublicKey,
-    network: Network,
-) -> Result<SingleAddress, TariAddressError> {
-    Self::new(spend_key, network, TariAddressFeatures::create_interactive_only())
-}
+pub fn new_with_interactive_only(
+    spend_key: CompressedPublicKey,
+    network: Network,
+) -> SingleAddress {
+    // SAFETY: `Self::new` is infallible at present
+    Self::new(spend_key, network, TariAddressFeatures::create_interactive_only()).unwrap()
+}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bb5ceed0de9daac54827402a97ce07ad8084ff15 and 97613be0e89cd511f8deae06853500e7786cbee1.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `base_layer/common_types/src/tari_address/dual_address.rs` (1 hunks)
* `base_layer/common_types/src/tari_address/mod.rs` (7 hunks)
* `base_layer/common_types/src/tari_address/single_address.rs` (3 hunks)
* `base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs` (1 hunks)
* `base_layer/wallet/src/transaction_service/service.rs` (0 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* base_layer/wallet/src/transaction_service/service.rs

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (7)</summary>

* GitHub Check: test (mainnet, stagenet)
* GitHub Check: test (testnet, esmeralda)
* GitHub Check: test (nextnet, nextnet)
* GitHub Check: cargo check with stable
* GitHub Check: ci
* GitHub Check: Cucumber tests / FFI
* GitHub Check: Cucumber tests / Base Layer

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>base_layer/common_types/src/tari_address/mod.rs (1)</summary>

`159-165`: **`new_single_address` constructor no longer accepts a payment-ID – good, but update documentation**

The signature change looks correct.  
Ensure any rust-doc on `SingleAddress::new` & callers reflect the removal of the `payment_id_user_data` parameter to avoid stale examples.

</details>
<details>
<summary>base_layer/common_types/src/tari_address/single_address.rs (1)</summary>

`146-153`: **Checksum assembly LGTM**  

Buffer sizing, key copy and checksum placement are all correct and align with `from_bytes`.  
No issues spotted here.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T06:31:39Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>base_layer/common_types/src/tari_address/single_address.rs (2)</summary><blockquote>

`76-78`: **Improved size check for better precision**

The check has been modified to look for an exact size match rather than a range, which is more precise now that payment IDs are no longer supported in SingleAddress.

---

`124-126`: **Simplified size validation for SingleAddress**

The size check now looks for an exact match rather than using a range, which is clearer and more accurate since SingleAddress now has a fixed size without payment ID support.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>base_layer/common_types/src/tari_address/mod.rs (1)</summary><blockquote>

`360-369`: **Improved parameter naming and code formatting**

The parameter in `from_base58` has been renamed from `hex_str` to `bas58_str` for clarity, and the code formatting has been improved without changing functionality. Consider fixing the typo in the parameter name.


```diff
-pub fn from_base58(bas58_str: &str) -> Result<TariAddress, TariAddressError> {
+pub fn from_base58(base58_str: &str) -> Result<TariAddress, TariAddressError> {
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 97613be0e89cd511f8deae06853500e7786cbee1 and 4a0a9ac132423a4a7bc74c3444547dccc01dc2ef.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `base_layer/common_types/src/tari_address/dual_address.rs` (1 hunks)
* `base_layer/common_types/src/tari_address/mod.rs` (8 hunks)
* `base_layer/common_types/src/tari_address/single_address.rs` (3 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* base_layer/common_types/src/tari_address/dual_address.rs

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: test (testnet, esmeralda)
* GitHub Check: test (mainnet, stagenet)
* GitHub Check: test (nextnet, nextnet)
* GitHub Check: ci
* GitHub Check: cargo check with stable

</details>

<details>
<summary>🔇 Additional comments (9)</summary><blockquote>

<details>
<summary>base_layer/common_types/src/tari_address/single_address.rs (2)</summary>

`69-69`: **Constructor no longer sets payment ID feature**

The constructor no longer sets any payment ID feature, which aligns with the removal of payment ID support from SingleAddress.

---

`143-149`: **Fixed-size buffer allocation and simplified checksum calculation**

The serialization logic has been simplified to use a fixed-size buffer of exactly `TARI_ADDRESS_INTERNAL_SINGLE_SIZE` bytes, with the checksum calculated on the first 34 bytes and stored at index 34. This matches the removal of payment ID support and makes the code more straightforward.

</details>
<details>
<summary>base_layer/common_types/src/tari_address/mod.rs (7)</summary>

`47-47`: **Increased INTERNAL_DUAL_BASE58_MAX_SIZE to accommodate payment IDs**

The maximum Base58 size for dual addresses has been increased from 91 to 443 to properly accommodate the maximum possible size when including payment IDs.

---

`131-132`: **Added dedicated error variant for payment ID operations on single addresses**

A new dedicated error variant provides clearer feedback when attempting to use payment IDs with single addresses, which is no longer supported. This is a good improvement over the previous approach of returning a potentially misleading error.

---

`166-167`: **Removed payment ID parameter from SingleAddress constructor**

The `new_single_address` method no longer passes an optional payment ID to the SingleAddress constructor, aligning with the removal of payment ID support from SingleAddress.

---

`231-237`: **Improved dynamic size calculation for dual addresses**

The `get_size` method now correctly returns the actual serialized length for dual addresses with payment IDs, while still returning the fixed size for addresses without payment IDs. This provides more accurate size reporting.

---

`245-246`: **SingleAddress now returns empty payment ID data**

The `get_payment_id_bytes` method for single addresses now returns an empty vector instead of attempting to access non-existent payment ID data, which is consistent with the removal of payment ID support.

---

`256-257`: **Better error message when attempting to add payment ID to single address**

When attempting to create a payment ID address from a single address, the code now returns the more specific `PaymentIdNotSupported` error rather than potentially misleading errors. This matches the previous review suggestion.

---

`339-342`: **Updated byte length validation for dual addresses with payment IDs**

The `from_bytes` method now correctly accepts dual address byte arrays with lengths between the internal dual size and that size plus the maximum encrypted data size (256 bytes), supporting the increased size limit for payment IDs.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T06:42:29Z
user: login: hansieodendaal
body: utACK

Looks fine, just some suggestions for future maintenance, and `INTERNAL_DUAL_BASE58_MAX_SIZE` should be explained.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T06:57:54Z
user: login: SWvheerden
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T07:12:50Z
user: login: SWvheerden
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T07:13:26Z

Review comments:
diff_hunk: @@ -44,7 +44,7 @@ use crate::{
 pub const TARI_ADDRESS_INTERNAL_DUAL_SIZE: usize = 67; // number of bytes used for the internal representation
 pub const TARI_ADDRESS_INTERNAL_SINGLE_SIZE: usize = 35; // number of bytes used for the internal representation
 const INTERNAL_DUAL_BASE58_MIN_SIZE: usize = 89; // number of bytes used for the internal representation
-const INTERNAL_DUAL_BASE58_MAX_SIZE: usize = 91; // number of bytes used for the internal representation
+const INTERNAL_DUAL_BASE58_MAX_SIZE: usize = 441; // number of bytes used for the internal representation
 const INTERNAL_SINGLE_MIN_BASE58_SIZE: usize = 45; // number of bytes used for the internal representation
 const INTERNAL_SINGLE_MAX_BASE58_SIZE: usize = 48; // number of bytes used for the internal representation
 const MAX_ENCRYPTED_DATA_SIZE: usize = 256; // max size of the payment_id_ bytes
path: base_layer/common_types/src/tari_address/mod.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Re-evaluate `INTERNAL_DUAL_BASE58_MAX_SIZE` upper bound**

`INTERNAL_DUAL_BASE58_MAX_SIZE` increased to `441`, but a dual address with the maximum 323-byte inner payload (67 base + 256 payment-ID bytes) encodes to ≈ `ceil(323 × log256/log58)` = 442 base58 chars in the worst case.  
With some leading-byte combinations the current constant could under-approximate, causing `InvalidSize` for perfectly valid addresses.

Recommend bumping to 443 (or computing dynamically) and aligning the associated test boundary.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-30T06:31:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -223,23 +219,28 @@ impl TariAddress {
                 one.public_spend_key().clone(),
                 one.network(),
                 one.features().combine_features(two.features()),
-                None,
             ),
         }
     }
 
     /// Gets the bytes size of the Tari Address
     pub fn get_size(&self) -> usize {
         match self {
-            TariAddress::Dual(_) => TARI_ADDRESS_INTERNAL_DUAL_SIZE,
+            TariAddress::Dual(v) => {
+                if v.features().contains(TariAddressFeatures::PAYMENT_ID) {
+                    v.to_vec().len()
+                } else {
+                    TARI_ADDRESS_INTERNAL_DUAL_SIZE
+                }
+            },
             TariAddress::Single(_) => TARI_ADDRESS_INTERNAL_SINGLE_SIZE,
         }
     }
 
     pub fn get_payment_id_bytes(&self) -> Vec<u8> {
         match self {
             TariAddress::Dual(v) => v.get_payment_id_bytes(),
-            TariAddress::Single(v) => v.get_payment_id_bytes(),
+            TariAddress::Single(_) => vec![],
         }
     }
path: base_layer/common_types/src/tari_address/mod.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**`create_payment_id_address` returns misleading error for Single addresses**

Attempting to attach a payment-ID to a `Single` address yields `PaymentIdTooLarge`, which conflates “unsupported” with “size overflow”.

Introduce a dedicated variant (`PaymentIdNotSupported`) or reuse `InvalidFeatures` to convey intent, and prevent callers from misinterpreting the error.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-30T06:31:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,9 +124,7 @@ impl SingleAddress {
     pub fn from_bytes(bytes: &[u8]) -> Result<Self, TariAddressError>
     where Self: Sized {
         let length = bytes.len();
-        if !(TARI_ADDRESS_INTERNAL_SINGLE_SIZE..=TARI_ADDRESS_INTERNAL_SINGLE_SIZE + MAX_ENCRYPTED_DATA_SIZE)
-            .contains(&length)
-        {
+        if !(TARI_ADDRESS_INTERNAL_SINGLE_SIZE..=TARI_ADDRESS_INTERNAL_SINGLE_SIZE).contains(&length) {
             return Err(TariAddressError::InvalidSize);
path: base_layer/common_types/src/tari_address/single_address.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

_⚠️ Potential issue_

**Inefficient-to-read length check & hidden inconsistency**  

1. The range `TARI_ADDRESS_INTERNAL_SINGLE_SIZE..=TARI_ADDRESS_INTERNAL_SINGLE_SIZE` is equivalent to a single value.  
   Using it obscures intent and costs a reader a double-take.  
2. `emoji_to_bytes` (lines 75-81) still allows `TARI_ADDRESS_INTERNAL_SINGLE_SIZE + MAX_ENCRYPTED_DATA_SIZE`, but `from_bytes`
   rejects anything larger than the fixed size, so longer *emoji* strings pass the first check and then fail here.  
   That looks like an oversight introduced when payment-ID support was removed.

Fix both at once by tightening `emoji_to_bytes` and writing a clearer equality check here:

```diff
-// emoji_to_bytes length gate:
-if !(TARI_ADDRESS_INTERNAL_SINGLE_SIZE..=TARI_ADDRESS_INTERNAL_SINGLE_SIZE + MAX_ENCRYPTED_DATA_SIZE)
-    .contains(&length)
+if length != TARI_ADDRESS_INTERNAL_SINGLE_SIZE
 {
     return Err(TariAddressError::InvalidSize);
 }
```

```diff
-// from_bytes length gate:
-if !(TARI_ADDRESS_INTERNAL_SINGLE_SIZE..=TARI_ADDRESS_INTERNAL_SINGLE_SIZE).contains(&length) {
+if length != TARI_ADDRESS_INTERNAL_SINGLE_SIZE {
     return Err(TariAddressError::InvalidSize);
 }
```

This removes the silent mismatch and makes intent obvious.  




> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-30T06:31:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -583,4 +583,68 @@ mod test {
             payment_id.as_slice()
         );
     }
+
+    #[test]
+    fn valid_max_payment_id() {
+        // Generate random public key
+        let mut rng = rand::thread_rng();
+        let view_key = CompressedPublicKey::from_secret_key(&PrivateKey::random(&mut rng));
+        let spend_key = CompressedPublicKey::from_secret_key(&PrivateKey::random(&mut rng));
+        let payment_id = vec![1u8; MAX_ENCRYPTED_DATA_SIZE + 1];
path: base_layer/common_types/src/tari_address/dual_address.rs
user: login: hansieodendaal
body: MAX_ENCRYPTED_DATA_SIZE has a different value in encrypted_data.rs, suggest adding `MAX_PAYLOAD_SIZE: usize = 256` to encrypted_data.rs and use it here
```suggestion
        let payment_id = vec![1u8; MAX_PAYLOAD_SIZE + 1];
```
created_at: 2025-04-30T06:37:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -583,4 +583,68 @@ mod test {
             payment_id.as_slice()
         );
     }
+
+    #[test]
+    fn valid_max_payment_id() {
+        // Generate random public key
+        let mut rng = rand::thread_rng();
+        let view_key = CompressedPublicKey::from_secret_key(&PrivateKey::random(&mut rng));
+        let spend_key = CompressedPublicKey::from_secret_key(&PrivateKey::random(&mut rng));
+        let payment_id = vec![1u8; MAX_ENCRYPTED_DATA_SIZE + 1];
+
+        // Generate an emoji ID from the public key and ensure we recover it
+        let _emoji_id_from_public_key = DualAddress::new(
+            view_key.clone(),
+            spend_key.clone(),
+            Network::Esmeralda,
+            TariAddressFeatures::default(),
+            Some(payment_id.clone()),
+        )
+        .unwrap_err();
+        let spend_key = CompressedPublicKey::from_secret_key(&PrivateKey::random(&mut rng));
+
+        let payment_id = vec![1u8; MAX_ENCRYPTED_DATA_SIZE];
+
+        // Generate an emoji ID from the public key and ensure we recover it
+        let emoji_id_from_public_key = DualAddress::new(
+            view_key.clone(),
+            spend_key.clone(),
+            Network::Esmeralda,
+            TariAddressFeatures::default(),
+            Some(payment_id.clone()),
+        )
+        .unwrap();
+
+        assert_eq!(emoji_id_from_public_key.public_spend_key(), &spend_key);
+        assert_eq!(
+            emoji_id_from_public_key.payment_id_user_data.as_bytes(),
+            payment_id.as_slice()
+        );
+
+        // Check the size of the corresponding emoji string
+        let emoji_string = emoji_id_from_public_key.to_emoji_string();
+        assert_eq!(emoji_string.chars().count(), TARI_ADDRESS_INTERNAL_DUAL_SIZE + 256);
path: base_layer/common_types/src/tari_address/dual_address.rs
user: login: hansieodendaal
body: ```suggestion
        assert_eq!(emoji_string.chars().count(), TARI_ADDRESS_INTERNAL_DUAL_SIZE + MAX_PAYLOAD_SIZE);
```
created_at: 2025-04-30T06:38:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,7 +44,7 @@ use crate::{
 pub const TARI_ADDRESS_INTERNAL_DUAL_SIZE: usize = 67; // number of bytes used for the internal representation
 pub const TARI_ADDRESS_INTERNAL_SINGLE_SIZE: usize = 35; // number of bytes used for the internal representation
 const INTERNAL_DUAL_BASE58_MIN_SIZE: usize = 89; // number of bytes used for the internal representation
-const INTERNAL_DUAL_BASE58_MAX_SIZE: usize = 91; // number of bytes used for the internal representation
+const INTERNAL_DUAL_BASE58_MAX_SIZE: usize = 441; // number of bytes used for the internal representation
path: base_layer/common_types/src/tari_address/mod.rs
user: login: hansieodendaal
body: Where does this size come from?
441 - 91 = 350 ?
created_at: 2025-04-30T06:39:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,7 +44,7 @@ use crate::{
 pub const TARI_ADDRESS_INTERNAL_DUAL_SIZE: usize = 67; // number of bytes used for the internal representation
 pub const TARI_ADDRESS_INTERNAL_SINGLE_SIZE: usize = 35; // number of bytes used for the internal representation
 const INTERNAL_DUAL_BASE58_MIN_SIZE: usize = 89; // number of bytes used for the internal representation
-const INTERNAL_DUAL_BASE58_MAX_SIZE: usize = 91; // number of bytes used for the internal representation
+const INTERNAL_DUAL_BASE58_MAX_SIZE: usize = 443; // number of bytes used for the internal representation
path: base_layer/common_types/src/tari_address/mod.rs
user: login: hansieodendaal
body: Where does this size comes from?
443 - 91 = 352
created_at: 2025-04-30T06:42:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,7 +44,7 @@ use crate::{
 pub const TARI_ADDRESS_INTERNAL_DUAL_SIZE: usize = 67; // number of bytes used for the internal representation
 pub const TARI_ADDRESS_INTERNAL_SINGLE_SIZE: usize = 35; // number of bytes used for the internal representation
 const INTERNAL_DUAL_BASE58_MIN_SIZE: usize = 89; // number of bytes used for the internal representation
-const INTERNAL_DUAL_BASE58_MAX_SIZE: usize = 91; // number of bytes used for the internal representation
+const INTERNAL_DUAL_BASE58_MAX_SIZE: usize = 441; // number of bytes used for the internal representation
path: base_layer/common_types/src/tari_address/mod.rs
user: login: SWvheerden
body: its due to the encryption of the bytes that it does not get straight up translated. 
created_at: 2025-04-30T07:12:49Z
author_association: COLLABORATOR
diff_hunk: @@ -44,7 +44,7 @@ use crate::{
 pub const TARI_ADDRESS_INTERNAL_DUAL_SIZE: usize = 67; // number of bytes used for the internal representation
 pub const TARI_ADDRESS_INTERNAL_SINGLE_SIZE: usize = 35; // number of bytes used for the internal representation
 const INTERNAL_DUAL_BASE58_MIN_SIZE: usize = 89; // number of bytes used for the internal representation
-const INTERNAL_DUAL_BASE58_MAX_SIZE: usize = 91; // number of bytes used for the internal representation
+const INTERNAL_DUAL_BASE58_MAX_SIZE: usize = 443; // number of bytes used for the internal representation
path: base_layer/common_types/src/tari_address/mod.rs
user: login: SWvheerden
body: look at code rabbits math
created_at: 2025-04-30T07:13:26Z
author_association: COLLABORATOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-30T06:28:04Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update removes support for payment IDs from the `SingleAddress` type in the Tari addressing system, ensuring only `DualAddress` can carry payment ID data. The constructors, serialization, and deserialization logic for `SingleAddress` are simplified to fixed-size addresses without variable-length or optional payment ID fields. Corresponding method signatures and test cases are updated to reflect this change. Additionally, a new test is added to verify the maximum allowed payment ID size in `DualAddress`. Error handling and logging are slightly improved in the transaction send protocol. All code paths and method calls are adjusted to align with these changes.

## Changes

| File(s)                                                                                       | Change Summary                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
|-----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `base_layer/common_types/src/tari_address/single_address.rs`                                  | Removed all support for payment IDs in `SingleAddress`: deleted the field, related methods, and adjusted serialization/deserialization to fixed size. Updated constructor to exclude payment ID parameter.                                                                                              |
| `base_layer/common_types/src/tari_address/mod.rs`                                             | Increased dual address base58 max size, removed payment ID support from single addresses, updated method signatures, refined size/error handling for payment IDs, and adjusted serialization/deserialization logic. Updated tests and internal logic accordingly.                                        |
| `base_layer/common_types/src/tari_address/dual_address.rs`                                    | Added a test (`valid_max_payment_id`) to verify correct handling of the maximum allowed payment ID size in `DualAddress`, including serialization, deserialization, and string encoding/decoding.                                                                                                     |
| `base_layer/wallet/src/transaction_service/service.rs`                                        | Updated call to `TariAddress::new_single_address` to remove the now-nonexistent payment ID argument.                                                                                                                                                                                                                                         |
| `base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs`            | Improved error logging in the `prepare_transaction` method to include error details when sending a service reply fails.                                                                                                                                                                                                                       |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant TariAddress
    participant SingleAddress
    participant DualAddress

    User->>TariAddress: create new_single_address(spend_key, network, features)
    TariAddress->>SingleAddress: new(spend_key, network, features)
    SingleAddress-->>TariAddress: SingleAddress (no payment ID)

    User->>TariAddress: create dual address with payment ID
    TariAddress->>DualAddress: new(spend_key, view_key, network, features, payment_id)
    DualAddress-->>TariAddress: DualAddress (with payment ID)
```

## Possibly related PRs

- tari-project/tari#6997: Adds a test for the maximum payment ID size in `DualAddress`, directly related to these changes that enforce and test payment ID size limits and address structure.

## Poem

> A hop, a skip, a leap ahead,  
> SingleAddress leaves payment IDs in bed.  
> DualAddress takes the baton with pride,  
> Carrying payment IDs, none to hide.  
> Fixed-size and tidy, the code’s now neat—  
> In the warren of bytes, efficiency’s sweet!  
> 🐇✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=tari-project/tari&utm_content=7002):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-30T06:50:30Z
body: ## Test Results (CI)
0 tests   0 ✅  0s ⏱️
0 suites  0 💤
0 files    0 ❌

Results for commit 4a0a9ac1.

[test-results]:data:application/gzip;base64,H4sIADTIEWgC/1WMQQrDIBBFrxJcd2F0grSXKdNRYWgSi6Or0rs3pKHR3X/vw3uryHMQdRv0ZVBSufzB14yF03rgdhRp910qUS+e/OpERJ47EXJO+TC5rtLMNvfjs7ZzE9u5bVFaFi4bKECNV6TRGjAWAd2DHJAFgAmcJyI9ejIhqs8XBzhYH/sAAAA=

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-30T07:12:40Z
body: ## Test Results (Integration tests)
0 tests  ±0   0 ✅ ±0   0s ⏱️ ±0s
0 suites ±0   0 💤 ±0 
0 files   ±0   0 ❌ ±0 

Results for commit 4a0a9ac1. ± Comparison against base commit bb5ceed0.

[test-results]:data:application/gzip;base64,H4sIAGbNEWgC/1WMQQrDIBBFrxJcd2F0grSXKdNRYWgSi6Or0rs3pKHR3X/vw3uryHMQdRv0ZVBSufzB14yF03rgdhRp910qUS+e/OpERJ47EXJO+TC5rtLMNvfjs7ZzE9u5bVFaFi4bKECNV6TRGjAWAd2DHJAFgAmcJyI9ejIhqs8XBzhYH/sAAAA=

author_association: NONE