Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/rs-dpp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ dash-sdk-features = [
"data-contract-json-conversion",
"identity-value-conversion",
"state-transition-value-conversion",
"state-transition-json-conversion",
"state-transition-signing",
"client",
"platform-value-cbor",
Expand Down
2 changes: 2 additions & 0 deletions packages/rs-dpp/src/address_funds/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod fee_strategy;
mod platform_address;
#[cfg(feature = "state-transition-serde-conversion")]
pub mod platform_address_map_serde;
mod witness;
mod witness_verification_operations;

Expand Down
58 changes: 58 additions & 0 deletions packages/rs-dpp/src/address_funds/platform_address_map_serde.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/// Custom serde serialization for BTreeMap<PlatformAddress, V>
/// Converts PlatformAddress keys to strings using Display when serializing to JSON
use crate::address_funds::PlatformAddress;
use serde::de::{MapAccess, Visitor};
use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::BTreeMap;
use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;

pub fn serialize<S, V>(map: &BTreeMap<PlatformAddress, V>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
V: Serialize,
{
let mut ser_map = serializer.serialize_map(Some(map.len()))?;
for (k, v) in map {
// Convert PlatformAddress to string using Display
ser_map.serialize_entry(&k.to_string(), v)?;
}
ser_map.end()
}

pub fn deserialize<'de, D, V>(deserializer: D) -> Result<BTreeMap<PlatformAddress, V>, D::Error>
where
D: Deserializer<'de>,
V: Deserialize<'de>,
{
struct MapVisitor<V> {
marker: PhantomData<V>,
}

impl<'de, V: Deserialize<'de>> Visitor<'de> for MapVisitor<V> {
type Value = BTreeMap<PlatformAddress, V>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a map with PlatformAddress keys")
}

fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut result = BTreeMap::new();
while let Some((key_str, value)) = map.next_entry::<String, V>()? {
let key = PlatformAddress::from_str(&key_str)
.map_err(|e| serde::de::Error::custom(format!("{}", e)))?;
result.insert(key, value);
}
Ok(result)
}
}

deserializer.deserialize_map(MapVisitor {
marker: PhantomData,
})
}
31 changes: 31 additions & 0 deletions packages/rs-dpp/src/state_transition/json_conversion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use crate::state_transition::{
JsonStateTransitionSerializationOptions, StateTransition, StateTransitionJsonConvert,
};
use crate::ProtocolError;
use serde_json::Value as JsonValue;

#[cfg(feature = "state-transition-json-conversion")]
impl StateTransitionJsonConvert<'_> for StateTransition {
fn to_json(
&self,
options: JsonStateTransitionSerializationOptions,
) -> Result<JsonValue, ProtocolError> {
match self {
StateTransition::DataContractCreate(st) => st.to_json(options),
StateTransition::DataContractUpdate(st) => st.to_json(options),
StateTransition::Batch(st) => st.to_json(options),
StateTransition::IdentityCreate(st) => st.to_json(options),
StateTransition::IdentityTopUp(st) => st.to_json(options),
StateTransition::IdentityCreditWithdrawal(st) => st.to_json(options),
StateTransition::IdentityUpdate(st) => st.to_json(options),
StateTransition::IdentityCreditTransfer(st) => st.to_json(options),
StateTransition::MasternodeVote(st) => st.to_json(options),
StateTransition::IdentityCreditTransferToAddresses(st) => st.to_json(options),
StateTransition::IdentityCreateFromAddresses(st) => st.to_json(options),
StateTransition::IdentityTopUpFromAddresses(st) => st.to_json(options),
StateTransition::AddressFundsTransfer(st) => st.to_json(options),
StateTransition::AddressFundingFromAssetLock(st) => st.to_json(options),
StateTransition::AddressCreditWithdrawal(st) => st.to_json(options),
}
}
}
26 changes: 26 additions & 0 deletions packages/rs-dpp/src/state_transition/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,16 @@ pub mod errors;
use crate::util::hash::ripemd160_sha256;
use crate::util::hash::{hash_double_to_vec, hash_single};

#[cfg(feature = "state-transition-json-conversion")]
mod json_conversion;
pub mod proof_result;
mod serialization;
pub mod state_transitions;
#[cfg(test)]
mod test_json_serde_integration;
mod traits;
#[cfg(feature = "state-transition-value-conversion")]
mod value_conversion;

// pub mod state_transition_fee;

Expand Down Expand Up @@ -383,6 +389,26 @@ impl OptionallyAssetLockProved for StateTransition {
}
}

impl StateTransitionFieldTypes for StateTransition {
fn signature_property_paths() -> Vec<&'static str> {
// The top-level enum doesn't have fixed signature paths
// Each variant has its own specific paths
vec![]
}

fn identifiers_property_paths() -> Vec<&'static str> {
// The top-level enum doesn't have fixed identifier paths
// Each variant has its own specific paths
vec![]
}

fn binary_property_paths() -> Vec<&'static str> {
// The top-level enum doesn't have fixed binary paths
// Each variant has its own specific paths
vec![]
}
}

/// The state transition signing options
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
pub struct StateTransitionSigningOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ use crate::{identity::core_script::CoreScript, withdrawal::Pooling, ProtocolErro
)]
#[derive(Default)]
pub struct AddressCreditWithdrawalTransitionV0 {
#[cfg_attr(
feature = "state-transition-serde-conversion",
serde(with = "crate::address_funds::platform_address_map_serde")
)]
pub inputs: BTreeMap<PlatformAddress, (AddressNonce, Credits)>,
/// Optional output for change
pub output: Option<(PlatformAddress, Credits)>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,20 @@ mod property_names {
pub struct AddressFundingFromAssetLockTransitionV0 {
pub asset_lock_proof: AssetLockProof,
/// Inputs from existing platform addresses (optional, for combining funds)
#[cfg_attr(
feature = "state-transition-serde-conversion",
serde(with = "crate::address_funds::platform_address_map_serde")
)]
pub inputs: BTreeMap<PlatformAddress, (AddressNonce, Credits)>,
/// Outputs to fund platform addresses.
/// - `Some(credits)` = explicit amount to send to this address
/// - `None` = this address receives everything remaining after explicit outputs and fees
/// Exactly one output must be `None` to receive the remainder
/// (ensures full asset lock consumption).
#[cfg_attr(
feature = "state-transition-serde-conversion",
serde(with = "crate::address_funds::platform_address_map_serde")
)]
pub outputs: BTreeMap<PlatformAddress, Option<Credits>>,
pub fee_strategy: AddressFundsFeeStrategy,
pub user_fee_increase: UserFeeIncrease,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,15 @@ use serde::{Deserialize, Serialize};
#[platform_serialize(unversioned)]
#[derive(Default)]
pub struct AddressFundsTransferTransitionV0 {
#[cfg_attr(
feature = "state-transition-serde-conversion",
serde(with = "crate::address_funds::platform_address_map_serde")
)]
pub inputs: BTreeMap<PlatformAddress, (AddressNonce, Credits)>,
#[cfg_attr(
feature = "state-transition-serde-conversion",
serde(with = "crate::address_funds::platform_address_map_serde")
)]
pub outputs: BTreeMap<PlatformAddress, Credits>,
pub fee_strategy: AddressFundsFeeStrategy,
pub user_fee_increase: UserFeeIncrease,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ pub struct IdentityCreateFromAddressesTransitionV0 {
// When signing, we don't sign the signatures for keys
#[platform_signable(into = "Vec<IdentityPublicKeyInCreationSignable>")]
pub public_keys: Vec<IdentityPublicKeyInCreation>,
#[cfg_attr(
feature = "state-transition-serde-conversion",
serde(with = "crate::address_funds::platform_address_map_serde")
)]
pub inputs: BTreeMap<PlatformAddress, (AddressNonce, Credits)>,
/// Optional output to send remaining credits to an address
pub output: Option<(PlatformAddress, Credits)>,
Expand All @@ -56,6 +60,10 @@ pub struct IdentityCreateFromAddressesTransitionV0 {
struct IdentityCreateFromAddressesTransitionV0Inner {
// Own ST fields
public_keys: Vec<IdentityPublicKeyInCreation>,
#[cfg_attr(
feature = "state-transition-serde-conversion",
serde(with = "crate::address_funds::platform_address_map_serde")
)]
inputs: BTreeMap<PlatformAddress, (AddressNonce, Credits)>,
output: Option<(PlatformAddress, Credits)>,
fee_strategy: AddressFundsFeeStrategy,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ use serde::{Deserialize, Serialize};
pub struct IdentityCreditTransferToAddressesTransitionV0 {
// Own ST fields
pub identity_id: Identifier,
#[cfg_attr(
feature = "state-transition-serde-conversion",
serde(with = "crate::address_funds::platform_address_map_serde")
)]
pub recipient_addresses: BTreeMap<PlatformAddress, Credits>,
pub nonce: IdentityNonce,
pub user_fee_increase: UserFeeIncrease,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ use crate::ProtocolError;
)]
#[derive(Default)]
pub struct IdentityTopUpFromAddressesTransitionV0 {
#[cfg_attr(
feature = "state-transition-serde-conversion",
serde(with = "crate::address_funds::platform_address_map_serde")
)]
pub inputs: BTreeMap<PlatformAddress, (AddressNonce, Credits)>,
/// Optional output to send remaining credits to an address
pub output: Option<(PlatformAddress, Credits)>,
Expand Down
125 changes: 125 additions & 0 deletions packages/rs-dpp/src/state_transition/test_json_serde_integration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#[cfg(all(
test,
feature = "state-transition-serde-conversion",
feature = "state-transition-json-conversion"
))]
mod test_serde_json_serialization {
use crate::address_funds::PlatformAddress;
use crate::state_transition::address_funds_transfer_transition::v0::AddressFundsTransferTransitionV0;
use crate::state_transition::address_funds_transfer_transition::AddressFundsTransferTransition;
use crate::state_transition::{
JsonStateTransitionSerializationOptions, StateTransition, StateTransitionJsonConvert,
};
use std::collections::BTreeMap;

/// Recursively search a JSON value for a given key name.
fn find_key_recursive<'a>(
val: &'a serde_json::Value,
key: &str,
) -> Option<&'a serde_json::Value> {
match val {
serde_json::Value::Object(map) => {
if let Some(v) = map.get(key) {
return Some(v);
}
for v in map.values() {
if let Some(found) = find_key_recursive(v, key) {
return Some(found);
}
}
None
}
serde_json::Value::Array(arr) => {
for v in arr {
if let Some(found) = find_key_recursive(v, key) {
return Some(found);
}
}
None
}
_ => None,
}
}

/// Helper: build an AddressFundsTransfer StateTransition with known addresses.
fn make_address_funds_transfer() -> StateTransition {
let mut inputs = BTreeMap::new();
let input_address = PlatformAddress::P2pkh([1u8; 20]);
inputs.insert(input_address, (1u32, 1000u64));

let mut outputs = BTreeMap::new();
let output_address = PlatformAddress::P2pkh([2u8; 20]);
outputs.insert(output_address, 900u64);

let v0 = AddressFundsTransferTransitionV0 {
inputs,
outputs,
user_fee_increase: 0,
fee_strategy: Default::default(),
input_witnesses: vec![],
};

StateTransition::AddressFundsTransfer(AddressFundsTransferTransition::V0(v0))
}

/// Given an AddressFundsTransfer variant with BTreeMap<PlatformAddress, _> fields,
/// When calling to_json() on the inner variant directly,
/// Then serialization succeeds without "key must be a string" errors.
#[test]
fn variant_to_json_succeeds() {
let st = make_address_funds_transfer();
if let StateTransition::AddressFundsTransfer(ref inner) = st {
inner
.to_json(JsonStateTransitionSerializationOptions::default())
.expect("variant to_json should succeed");
}
}

/// Given an AddressFundsTransfer wrapped in the top-level StateTransition enum,
/// When calling to_json() on the enum and serde_json::to_string_pretty(),
/// Then both serialization paths succeed and PlatformAddress map keys are
/// rendered as human-readable strings (e.g. "P2PKH(hex...)"), not complex objects.
#[test]
fn enum_to_json_succeeds_and_serde_roundtrips() {
let st = make_address_funds_transfer();

// StateTransition::to_json() must succeed (the whole point of the fix).
let json = st
.to_json(JsonStateTransitionSerializationOptions::default())
.expect("StateTransition::to_json should succeed");

// serde_json must also work now (custom map-key serializer).
let serde_json_str = serde_json::to_string_pretty(&st)
.expect("serde_json::to_string_pretty should succeed after custom serde fix");

// The to_json output must contain string keys for PlatformAddress maps.
let inputs = json.get("inputs").expect("JSON must have 'inputs' field");
assert!(
inputs.is_object(),
"inputs must be a JSON object (string keys)"
);
for key in inputs.as_object().unwrap().keys() {
assert!(
key.starts_with("P2PKH(") || key.starts_with("P2SH("),
"map key must be a PlatformAddress Display string, got: {key}"
);
}

// The serde output must also have string keys for PlatformAddress maps.
// The exact nesting depends on enum repr; find "inputs" anywhere in the tree.
let serde_val: serde_json::Value =
serde_json::from_str(&serde_json_str).expect("re-parse must succeed");
let inputs_field = find_key_recursive(&serde_val, "inputs")
.expect("serde JSON must contain an 'inputs' field somewhere");
assert!(
inputs_field.is_object(),
"serde inputs must be a JSON object with string keys"
);
for key in inputs_field.as_object().unwrap().keys() {
assert!(
key.starts_with("P2PKH(") || key.starts_with("P2SH("),
"serde map key must be a PlatformAddress Display string, got: {key}"
);
}
}
}
Loading
Loading