feat(dpp): unify JSON/Value conversion traits + comprehensive round-trip tests#3573
Draft
feat(dpp): unify JSON/Value conversion traits + comprehensive round-trip tests#3573
Conversation
Adds JsonConvertible / ValueConvertible impls (canonical traits in
packages/rs-dpp/src/serialization/serialization_traits.rs) to the
domain types catalogued in docs/json-value-conversion-inventory.md.
This is the unification first pass — round-trip correctness, tagged-
enum tag preservation, and integer-precision tests are deferred to the
second pass per the plan. Some impls may produce broken JSON or fail
round-trip until pass 2 fixes them; that's expected.
Coverage:
- Symmetrize V-only and J-only types (15+1).
- Add J+V to types missing both: top priorities (DataContract,
StateTransition, BatchTransition, Document, AssetLockProof,
AddressCreditWithdrawalTransition, Pooling) plus 22 batch transitions
and 19 leaf serde types.
Skipped: types without serde derives, lifetime-param refs, and the
wasm-dpp legacy crate per minimum-touch policy.
Approach: derive(JsonConvertible/ValueConvertible) where the type
already opts into the json_safe_fields macro ecosystem; empty manual
impl X {} (§6 escape hatch) elsewhere to bypass the JsonSafeFields
cascade. Both paths use the trait's default serde-delegating methods.
Adds planning docs:
- docs/json-value-conversion-inventory.md — structural inventory.
- docs/json-value-unification-plan.md — phased plan with critical
findings and per-mechanism deprecation decisions.
cargo check -p dpp passes with --features=json-conversion,value-conversion,serde-conversion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates the unification plan with: - Progress table tracking the 5 passes (1 done, 2 in progress). - Phase B/C status updated: ~80 types now have canonical impls. - Skip-list rationale for types we deliberately did NOT migrate (no serde derives, lifetime params, internal indirection). - Section 11 "Lessons learned from pass 1" — the JsonSafeFields cascade, BTreeMap-of-enum-keys serde helpers, what shipped in the 481 commits we pulled, test-fixture pattern, sandbox/sccache/gpg gotchas. - Reference to pass-1 commit 9f23d67. Companion doc gets a status banner pointing back to the plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ity) Adding empty impl JsonConvertible/ValueConvertible for DataContract in pass 1 collided with the existing DataContractJsonConversionMethodsV0:: to_json(&self, &PlatformVersion) at every call site that passes a PlatformVersion — Rust E0034 (multiple applicable items in scope). Per the unification plan §3.11 step 10, DataContract is KEEP-AS-EXCEPTION (version-aware serde via DataContractInSerializationFormat). The proper unification path renames the legacy methods to *_versioned first, then the canonical traits can layer on. That's a follow-up. For now, leave a comment in data_contract/mod.rs explaining the absence and pointing readers at DataContractInSerializationFormat (which DOES have the canonical traits) when they need a JSON shape. cargo test -p dpp --features=json-conversion,value-conversion,serde-conversion --lib json_convertible_tests now passes (10/10 — the 5 address-transition round-trip + tag-preservation tests from pass 1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds json_round_trip + value_round_trip tests for 11 types covered by the pass-1 unification commit (9f23d67). All 28 tests in the new modules pass; no regressions in the existing 3432 dpp lib tests. Types covered: - Identity, IdentityV0, IdentityPublicKey - AddressCreditWithdrawalTransition - TokenContractInfo, TokenPaymentInfo - Document - Pooling - GroupStateTransitionInfo Types skipped with TODO (V0 inner lacks Default): - AssetLockValue (AssetLockValueV0) - GroupAction (GroupActionV0 has GroupActionEvent field with no Default) Pass-2 work continues: more types to follow, then bug discovery (StateTransition untagged, ExtendedDocument bug, Critical-1 / -2 / -4). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds round-trip tests for TokenEmergencyAction, GasFeesPaidBy, and YesNoAbstainVoteChoice — all flat enums with derive(Default). Also marks TokenMarketplaceRules and other types whose V0 lacks Default with TODO(unification pass 2) comments — they need explicit fixtures. 34 json_convertible_tests pass, no regressions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…DistributionType (pass 2) DocumentPatch has Default and J+V impls — round-trips cleanly. TokenDistributionType has Default but the J+V impls are on its variants (TokenDistributionTypeWithResolvedRecipient, TokenDistributionInfo), neither of which has Default — left as TODO for explicit fixture. 36/36 json_convertible_tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…perty assertions Per user direction, every J/V test must: 1. Use a NON-DEFAULT fixture (distinguishable values per field). 2. Round-trip via to_json/from_json (and to_object/from_object). 3. Assert each field of the recovered value individually — catches silent field drops, type narrowing, and PartialEq quirks that whole-struct equality can miss. IdentityCreateFromAddressesTransition is the canonical example — fixture has 6 non-default fields including a 2-entry inputs map with both P2PKH+P2SH addresses, a populated public key, two witness types, custom fee strategy, and non-zero user_fee_increase. All three tests pass (json_round_trip, value_round_trip, format_version_tag). Plan §8 updated with the new mandatory convention and rationale. Existing tests with Default fixtures are now legacy and will be upgraded as we revisit each type in pass 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sferToAddresses tests Apply the new mandatory convention (non-default fixture + per-property assertions + round-trip) to two more address transitions. Both fixtures use distinguishable values for every field (identity_id, recipient_addresses, nonce, signature, fee strategy, witnesses, etc.) so the per-property assertions actually exercise data preservation. 3/5 address transitions now on the new convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upgrade AddressFundingFromAssetLockTransition, AddressFundsTransferTransition,
and AddressCreditWithdrawalTransition tests to non-default fixture +
per-property assertions per the new convention.
Bug surfaced: AddressFundingFromAssetLockTransition.value_round_trip
fails — `OutPoint` inside `ChainAssetLockProof` cannot deserialize from
`platform_value::Value::Map` ("invalid type: map, expected an OutPoint").
JSON round-trip works fine. Marked the value test #[ignore] with the
reason and logged in plan §10b for pass-3 fix.
5/5 address transitions now on the new convention. 46 json_convertible_tests
pass, 3 ignored (1 OutPoint bug + 2 StateTransition untagged-enum known
failures).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…erty assertions Replaces the legacy Identity::default() fixture with one that has: - id: Identifier::new([0x42; 32]) - balance: 1_000_000 - revision: 7 - public_keys: BTreeMap with 2 distinct entries Per-property assertions check id, balance, revision, and public_keys count. Removes the duplicate empty-fixture test module that was leftover. 401 dpp lib tests pass (filtered to identity::identity). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… tests Apply non-default fixture + per-property assertion convention to: - IdentityPublicKey (8 distinguishable fields incl. disabled_at, contract_bounds) - TokenContractInfo (contract_id + token_contract_position; note: untagged enum) - Pooling (test all 3 variants — Never/IfAvailable/Standard) 48 json_convertible_tests pass, 3 ignored (1 OutPoint bug, 2 StateTransition). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces single-Default-fixture tests for unit enums with each_variant() pattern that exercises all variants in turn. This is the per-property-assertion equivalent for unit-only enums where each discriminant is the only "field". Upgrades: - TokenEmergencyAction (Pause, Resume) - GasFeesPaidBy (DocumentOwner, ContractOwner, PreferContractOwner) - YesNoAbstainVoteChoice (YES, NO, ABSTAIN) 48 json_convertible_tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply non-default fixture + per-property assertion convention to: - GroupStateTransitionInfo (group_contract_position=5, action_id=[0x33;32], action_is_proposer=true) - DocumentPatch (id=[0x77;32], 2 properties, revision=3, updated_at=1.7T) 48 json_convertible_tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…per-property 5-field fixture with all Option fields populated and gas_fees_paid_by set to a non-default variant. Per-property assertion verifies each field preserves through round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er-property 5-field fixture (owner_id, transitions, user_fee_increase, signature_public_key_id, signature) with distinguishable values. transitions vec is empty since DocumentTransition sub-types are tested in their own modules. Per-property assertion verifies each field preserves through round-trip. 49 json_convertible_tests pass, 3 ignored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rk list Updates the plan with: - Pass-2 status table — 17/~80 types upgraded, 1 bug surfaced. - Explicit list of types still on Default fixtures or without tests. - Cost estimate: ~10-15 hours of focused work to finish pass 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds basic round-trip + format version tag tests for: - IdentityCreateTransition (json/value tests #[ignore]: V0::default() has structurally invalid asset_lock_proof — needs explicit fixture) - IdentityTopUpTransition - IdentityCreditTransferTransition - MasternodeVoteTransition - IdentityPublicKeyInCreation - IdentityUpdateTransition - IdentityCreditWithdrawalTransition DataContractCreateTransition and DataContractUpdateTransition skipped: their V0 inners lack Default — needs explicit fixtures (TODO). 68 json_convertible_tests pass, 5 ignored (3 prior + 2 new IdentityCreateTransition pending real fixture). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds basic round-trip tests using Default fixture for: - BlockInfo (struct with Default) - Vote (manual Default impl) - VotePoll (manual Default impl) - ResourceVoteChoice (derived Default with #[default] variant) - InstantAssetLockProof (manual Default impl) Marks 6 types as TODO (no Default — needs explicit fixture): - ContractBoundSpecification, ChainAssetLockProof, - ExtendedBlockInfo, ExtendedEpochInfo, FinalizedEpochInfo, - IdentityTokenInfo, TokenStatus. 78 json_convertible_tests pass, 5 ignored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces TODOs with hand-built fixtures for:
- IdentityTokenInfo (frozen=true)
- TokenStatus (paused=true)
- ExtendedEpochInfo (6 fields, distinguishable values)
- FinalizedEpochInfo (12 fields incl. block_proposers map)
- ExtendedBlockInfo (8 fields incl. signature [u8;96])
Bug surfaced: ExtendedBlockInfo value_round_trip fails on signature
field round-trip via platform_value::Value ("Invalid symbol 17"). JSON
works. Marked #[ignore] and logged in plan §10b.
87 conversion tests pass, 6 ignored (3 prior + 1 new bug + 2 needs-fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AssetLockValue uses AssetLockValue::new() factory (V0 fields are pub(super), can't be set directly). ChainAssetLockProof uses OutPoint::from_str factory; value test ignored due to known OutPoint round-trip bug. 90 conversion tests pass, 7 ignored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…IndexInformation)
…ourceVotePoll + ContestedDocumentVotePollWinnerInfo 102 conversion tests pass, 7 ignored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ansition Both use fully-qualified trait syntax to disambiguate from legacy StateTransitionValueConvert::to_object/to_json methods on the same type — known E0034 ambiguity per plan §3.11. 106 conversion tests pass, 7 ignored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DocumentReplaceTransition, DocumentTransferTransition, DocumentPurchaseTransition, DocumentUpdatePriceTransition — all use fully-qualified trait syntax to disambiguate from legacy methods. 116 conversion tests pass, 7 ignored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nMint 122 conversion tests pass, 7 ignored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…troyFrozenFunds 128 tests pass, 7 ignored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y/Claim/DirectPurchase/SetPrice) 136 conversion tests pass, 7 ignored. All 17 of 19 batch sub-transitions now tested (only TokenConfigUpdate remaining — needs TokenConfigurationChangeItem fixture). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the post-round-trip per-property `assert_v0_fields(&recovered)`
helper with **literal wire-shape assertions** using `serde_json::json!`
and `platform_value::platform_value!`. Every test now reads as a complete
spec for the type's serialized form — a reader sees exactly what JSON /
Value bytes the type produces.
The new pattern, applied uniformly:
```rust
let json = original.to_json().unwrap();
assert_eq!(json, json!({"$formatVersion": "0", "field": <literal>, ...}));
let recovered = T::from_json(json).unwrap();
assert_eq!(original, recovered);
let value = original.to_object().unwrap();
assert_eq!(value, platform_value!({"$formatVersion": "0", "field": <literal_with_suffix>, ...}));
let recovered = T::from_object(value).unwrap();
assert_eq!(original, recovered);
```
Two key benefits over the old structural-only `assert_eq!(original, recovered)`:
1. **Locks in the literal wire shape** — key names, encodings, types.
Catches bugs in the serialize side that PartialEq+structural assert_eq
would silently miss because they're symmetric (a serialize bug that
always outputs zero would be matched by a deserialize bug that always
reads zero).
2. **Locks in sized integer variants** in the Value path. Numeric literals
in `platform_value!` carry their Rust type via explicit suffixes
(`7u16`, `0u8`, `1_000_000u64`); a bare `7` would produce
`Value::I32(7)` and the assertion would fail — that's the catch.
Anywhere a sized integer field has its size erased on the JSON wire
(because JSON has only one number type), the JSON-side assertion has
a comment pointing the reader to the value-path assertion that locks
the typed variant.
Tier handling:
- Tier 1+2 (most files): full inline `json!`/`platform_value!` literals.
- Tier 3 (`extended_document`, `token_configuration`): envelope-only —
inline shape would be 200+ lines for the embedded `DataContract` /
schemas. We assert wrapper-specific keys and trust the nested types'
own per-type round-trip tests for inner content.
- Tier 4 (`shield_from_asset_lock_transition`, `identity_create/topup`):
envelope-only on the non-deterministic `instant_asset_lock_proof_fixture`
field (random bytes per call); deterministic siblings get full literal
assertions.
Surprises documented inline at the assertion site:
- `OutPoint` — JSON uses `"<txid>:<vout>"` string (dashcore string-impl);
Value uses `{txid: Bytes32, vout: U32}` typed map.
- `BTreeMap<PlatformAddress, ...>` — serializes as array of
`{address, nonce, amount}` objects (custom serde helper), not as a
JSON map.
- `Validator` — externally-tagged enum (no `#[serde(tag)]`), so wire
shape is `{"V0": {...}}` (uppercase) with snake_case fields.
- `token_set_price_for_direct_purchase_transition` — stale
`serde(rename = "issuedToIdentityId")` on the `price` field
(copy-paste from mint transition); documented as the actual wire key.
- `Vec<u8>` renders as `Value::Array([U8(...), ...])`, not `Value::Bytes`
— typed differently from `Bytes32` / `BinaryData` wrappers.
Bonus: the `address_funds/witness.rs` AddressWitness tests had a bare
`each_variant` round-trip pattern (missed by the initial 49-file grep
that targeted `fn json_round_trip()`); upgraded to per-variant tests
with full wire-shape assertions and base64-encoded `BinaryData`
literals matching the actual serialized form.
Test counts: 3625 passing, 8 ignored (same 6 unrelated
recursive_schema_validator + 2 StateTransition umbrella). 1036 platform-value
passing. Net `#[test]` change: +2 (from witness 2-→4 split). The slight
overall count drop is from a few `each_variant` patterns being
consolidated into single iterating tests with stronger assertions.
49 files changed:
- data_contract: config, group, change_control_rules, 7 associated_token
types
- voting: contender, contested_document_resource_vote_poll, resource_vote,
contested_document_vote_poll_winner_info
- identity transitions: 7 transitions
- shielded transitions: 5 transitions
- batch_transition sub-types: 17 (6 document, 11 token)
- core_types: validator
- document: Document, ExtendedDocument
- state_transition: top-level umbrella, proof_result, asset_lock_proof
- address_funds: witness
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second wave of the wire-shape assertion rollout — covers the 35 round-trip
tests the initial 49-file sweep missed (their files lived outside the
explicit batch lists). Same pattern as commit `8b198eb3ce`: replace the
post-round-trip `assert_v0_fields(&recovered)` per-property helper with
literal wire-shape assertions using `serde_json::json!` and
`platform_value::platform_value!`, and split each-variant tests into
per-variant tests.
Files (35) — block / data_contract / identity / tokens / voting /
state_transition / asset_lock / chain_asset_lock_proof / withdrawal /
group / document_patch / array / storage_requirements:
- 12 block / data_contract / asset_lock / identity-misc / group /
document_patch (batch 4)
- 13 state_transition / token transitions / DataContract create&update
(batch 5; envelope-only Tier 3 on the two DataContract transitions)
- 10 identity / chain_asset_lock_proof / tokens / voting / withdrawal /
validator_set (batch 6; BLS keys interpolated via `to_value(&pk)` to
avoid 96-char hex literals dominating the assertion)
Special handling locked in:
- `data_contract_create/update_transition` keep their `assert_v0_fields`
helper because the JSON test still uses
`normalize_integer_variants_for_json_round_trip` (the Critical-1
serde_json single-Number-type limitation). Value-side moved to
envelope-only — embedded `DataContractInSerializationFormat` would
inline to hundreds of lines. Both kept as documented exceptions.
- `validator_set` interpolates BLS pubkeys via the seeded-RNG fixture
values; structural shape (externally-tagged "V0", snake_case fields,
ProTxHash-keyed BTreeMap) is asserted inline. `bls_pubkey_serde`
unit tests independently cover BLS round-trip.
- `chain_asset_lock_proof` documents the OutPoint dual-shape inline:
HR emits `"<txid>:<vout>"` string; non-HR emits `{txid: Bytes32,
vout: U32}` struct. Includes a sanity check pinning the Bitcoin-
reversed-byte-order Txid convention.
Surprising wire shapes documented inline:
- `KeyType::ECDSA_HASH160 = 2`, `Purpose::TRANSFER = 3` (`#[repr(u8)]`
+ `Serialize_repr`).
- 20-byte `BinaryData` → `Value::Bytes20` (typed sized-bytes variant),
not generic `Value::Bytes`.
- `BTreeMap<Identifier, u64>` keys become base58 strings in JSON HR via
`json_safe_identifier_u64_map`; non-HR keeps `Value::Identifier` keys.
- `ArrayItemType` is untagged: unit variants serialize as bare strings
(`"Integer"`); tuple variants as `{"String": [3, 50]}`.
- `GasFeesPaidBy` and `Validator` keep PascalCase (no `rename_all`)
inside otherwise camelCase contexts.
- `data_contract_update_transition` field name is `$identity-contract-nonce`
(kebab-case + dollar prefix), not camelCase.
- `document_base_transition` is externally-tagged (no `serde(tag)`),
so V0 wraps as `{"V0": {...}}`.
- `tokens/contract_info` is `serde(untagged)` — emits flat object with
no `$formatVersion` discriminator.
Test counts: 3641 passing (+16 vs `8b198eb3ce`), 8 ignored (unchanged).
The +16 is from each-variant tests being split into per-variant tests
(more granular coverage). 1036 platform-value tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nContractInfo
Per the wasm-dpp2 CONVENTIONS doc (`packages/wasm-dpp2/CONVENTIONS.md`,
"Versioning" section): every versioned protocol enum should use
`#[serde(tag = "$formatVersion")]` with each variant renamed to its
version string. Two top-level versioned enums weren't following this:
- `AssetLockValue` had no `serde(tag)` at all → wire shape was
`{"V0": {...}}` (default externally-tagged form).
- `TokenContractInfo` was deliberately `serde(untagged)` → wire shape
was a flat object with no version tag, looking superficially correct
but carrying no version information at all.
Both are top-level types not embedded behind `serde(flatten)` anywhere,
so adding the tag is a clean fix. Wire shape changes from:
{"V0": {...inner fields...}} (old AssetLockValue)
{...inner fields...} (old TokenContractInfo)
to the canonical:
{"$formatVersion": "0", ...inner fields...}
Tests updated to assert the new wire shape.
Out of scope here:
- Validator / ValidatorSet are also externally-tagged but contain
dashcore hash newtypes (ProTxHash, PubkeyHash, QuorumHash) whose
serde uses two disjoint visitors — adding `tag = "$formatVersion"`
routes deserialization through serde's `ContentDeserializer` which
always reports `is_human_readable: true`, breaking the bytes path
(same root cause as the OutPoint/Txid issue fixed in commit
09c0a2b with the local `outpoint_serde` wrapper). Fixing this
properly needs the same dual-shape-visitor wrapper applied to all
the dashcore hash newtypes used in ValidatorSet — a larger refactor
out of scope for a convention sweep. Left for future work.
- The 17 inner Document/Token sub-transitions (DocumentBaseTransition,
TokenBaseTransition, etc.) are flattened into parent transitions
via `serde(flatten)`. Adding a tag would clash with the parent's
own `$formatVersion` (the same parent/child collision pattern fixed
in ExtendedDocument, commit 95554c8). Per user direction, deferred.
dpp lib: 3641 passing (unchanged), 8 ignored (unchanged). Wire shape now
matches the documented convention for the two affected types.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the wasm-dpp2 CONVENTIONS.md "Versioning" section: every versioned
protocol enum should use `#[serde(tag = "$formatVersion")]`. `Validator`
and `ValidatorSet` were the last two top-level versioned enums still
defaulting to externally-tagged `{"V0": {...}}` form.
Wire shape changes from:
{"V0": {"pro_tx_hash": "...", ...}}
to the canonical:
{"$formatVersion": "0", "pro_tx_hash": "...", ...}
JSON-side tests pass — dashcore hash newtypes (`ProTxHash`, `PubkeyHash`,
`QuorumHash`) deserialize cleanly from hex strings on the HR path.
Value-side tests are `#[ignore]`'d pending dashcore PR #708
(dashpay/rust-dashcore#708) — the dashcore hash
newtypes need dual-shape visitors so they round-trip through serde's
`ContentDeserializer`, which always reports `is_human_readable: true`
even when wrapping bytes from a non-HR source like `platform_value::Value`.
This is the same root cause as the OutPoint/Txid bug fixed locally in
commit 09c0a2b; ProTxHash/PubkeyHash trip the same wire on
`tag = "$formatVersion"` deserialization through ContentDeserializer.
Once that PR lands and we bump the dashcore dependency, drop the
`#[ignore]`s on the two value tests.
Note: `ValidatorSetV0::members` is `BTreeMap<ProTxHash, ValidatorV0>`
(not `BTreeMap<ProTxHash, Validator>`), so members are the bare V0
struct on the wire without their own `$formatVersion` tag — the test
documents this inline.
dpp lib: 3639 passing, 10 ignored (+2 from the new value-path
`#[ignore]`s, otherwise unchanged).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…or ignores PR #708 only fixed the `serde_struct_human_string_impl!` macro (used by `OutPoint`), not the `hashes::serde_macros::SerdeHash` macro family used by `ProTxHash`/`PubkeyHash`/`QuorumHash`. They have the same kind of bug (string-only HR visitor → fails through `ContentDeserializer`) but live in a different macro and need their own fix. Update the `#[ignore]` notes on the two value-side tests to: - Remove the misleading PR #708 link. - Spell out the actual error (`HexVisitor::visit_str` sees the 32-byte buffer interpreted as 32 chars instead of the expected 64-char hex form, hence "bad hex string length 32 (expected 64)"). - Frame as a "follow-up dashcore PR" pending in the same family. No code changes — just clarifies what we're actually waiting on. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e PR dashcore PR #729 (dashpay/rust-dashcore#729) is the companion to #708 — same `ContentDeserializer` HR-quirk root cause, but for the separate `hashes::serde_macros::SerdeHash` macro family that generates `Txid` / `BlockHash` / `ProTxHash` / `PubkeyHash` / `QuorumHash` etc. (vs. #708 which fixed `OutPoint` via `serde_struct_human_string_impl!`). Update the two `#[ignore]` notes on `Validator::value_round_trip` and `ValidatorSet::value_round_trip` to reference #729 instead of the vague "follow-up PR" phrasing. When #729 lands and we bump dashcore, drop the `#[ignore]`s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tagged)
The umbrella `StateTransition` enum was `#[serde(untagged)]`, which made
deserialize ambiguous — serde tries each of the 20 variants in order until
one matches structurally. The two umbrella round-trip tests had been
`#[ignore]`'d for that reason since pass 2.
Apply `#[serde(tag = "type", rename_all = "camelCase")]` matching the
existing codebase convention for **semantically-different-variant**
enums:
- `AssetLockProof` (Instant/Chain) — `tag = "type", rename_all = "camelCase"`
- `ContractBoundSpecification` — same
- `ActionEvent` — `tag = "type", content = "data", rename_all = "camelCase"`
This is distinct from `tag = "$formatVersion"` which is used for **versioning**
(V0/V1 of the same logical type) — `StateTransition` discriminates between
20 different transition kinds, not versions of one transition.
Wire shape changes from:
{<inner-variant fields directly>} (old, ambiguous)
to:
{"type": "dataContractCreate", ...} (canonical, self-describing)
The `type` key doesn't collide with any inner enum's `$formatVersion`
(different namespace) or with inner serde fields named `type` (umbrella
tag is resolved before serde descends into the variant body).
Verified non-breaking for all observed downstream consumers:
- `PlatformSerialize` binary path (used by gRPC, GroveDB, proof-verifier,
rs-sdk) is unchanged — only JSON/Value paths see the new shape.
- `wasm-dpp2`'s `StateTransitionWasm` only exposes
`toBytes/fromBytes/toHex/fromHex/toBase64/fromBase64` — no `toJSON()`
or `toObject()` over the umbrella, so JS callers go through inner-variant
wrappers (already using `tag = "$formatVersion"`).
- `cargo check -p drive -p drive-abci -p dash-sdk -p wasm-dpp2` all pass.
- Zero `StateTransition::to_json` / `to_object` / `from_json` / `from_object`
call sites across rs-drive / rs-drive-abci / rs-sdk / dapi-grpc.
Tests updated: the two previously-`#[ignore]`'d umbrella round-trip tests
in `state_transition::mod::json_convertible_tests` are now active and
asserting the new wire shape.
dpp lib: 3641 passing (+2 from unignored umbrella tests), 8 ignored
(was 10).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…atch The umbrella `StateTransition` enum has 20 variants but the previous test exercised only one (IdentityCreateFromAddresses). With the `tag = "type", rename_all = "camelCase"` change in commit 4fcb3d4 each variant's tag dispatch is its own potential failure mode (e.g. an inner field named `type` clashing with the umbrella tag, or a serde rename resolving unexpectedly). Cover all 20. Approach: expose each inner transition's existing `json_convertible_tests::fixture()` as `pub(crate)` so the umbrella can reuse it instead of duplicating fixture construction. Each fixture already has the per-type wire-shape coverage; the umbrella test layer just asserts the tag dispatch boundary: 1. JSON wire emits `{"type": "<expected camelCase tag>", ...}`. 2. Round-trip preserves variant (via `std::mem::discriminant`). 3. Round-trip preserves structural equality (via PartialEq). 4. Same three assertions on the Value path. Helper: `assert_umbrella_round_trip(stx, expected_tag)` for the 18 variants where bit-exact equality holds, and `assert_umbrella_round_trip_lossy_json_int_variants` for `DataContractCreate`/`DataContractUpdate` which embed a `DataContract` whose `document_schemas` carry sized-int variants (`U32`/`I32`) that JSON's single Number type cannot preserve. The lossy helper applies the existing `normalize_integer_variants_for_json_round_trip` to both sides before comparing — same Critical-1 mitigation used for the per-type tests in commit 7397c73. The Value path keeps its strict bit-exact assertion (platform_value preserves sized ints). Test count delta: +18 (20 new umbrella tests replace the previous 2). Files changed: - `state_transition/mod.rs` — replace 2-test umbrella with 20-test per-variant module + helpers. - 20 inner-transition `mod.rs` files — `mod json_convertible_tests` and `fn fixture()` made `pub(crate)`. dpp lib: 3659 passing (+18), 8 ignored (unchanged). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply tag = "type" to DocumentTransition / TokenTransition / BatchedTransition
umbrella enums (was externally-tagged); migrate 17 leaf transition wrappers
from externally-tagged {"V0": {...}} to tag = "$formatVersion"; switch
TokenBaseTransition + DocumentBaseTransition to tag = "$baseFormatVersion"
(kept serde(flatten)'d into leaves) so the full transition wire shape becomes
flat with both discriminators at the top level.
DocumentCreateTransitionV0 / DocumentReplaceTransitionV0 carry manual
Deserialize impls because their serde(flatten) data: BTreeMap<String, Value>
catchall would otherwise steal the base's discriminator + struct fields.
Each impl reads the wire into a BTreeMap, peels off a BASE_FIELD_NAMES const
set into the base, then routes remaining keys to data. Auto-derive Serialize
retained on both. In-source warning notes that adding a base field requires
updating BASE_FIELD_NAMES in both impls.
Add round-trip tests for 10 leaf JsonConvertible/ValueConvertible types
that lacked them: PlatformAddress, GroupActionStatus, GroupActionEvent,
TokenEvent, TokenPricingSchedule, SerializedAction, TokenTransferTransition,
DistributionFunction, RewardDistributionType, StoredAssetLockInfo. Add
per-variant umbrella round-trip tests for DocumentTransition (6),
TokenTransition (11), BatchedTransition (2) reusing pub(crate) leaf fixtures.
Refresh docs/json-value-unification-plan.md with the test count, tag-key
conventions table, and the catchall-collision rationale for the manual
Deserialize impls.
3621 -> 3716 dpp lib tests passing (+95), 8 ignored (unchanged).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Apply `#[json_safe_fields]` to `DocumentBaseTransitionV0`,
`DocumentBaseTransitionV1`, `TokenPaymentInfoV0`, and
`DocumentCreateTransitionV0`.
- `entropy: [u8; 32]` now serializes as base64 string in JSON HR (was
array of numbers) — matches shielded-transition byte-field convention.
- `identity_contract_nonce: u64` (= IdentityNonce), token amounts, and
token payment info u64s now go through `json_safe_u64` /
`json_safe_option_u64`: native u64 in non-HR; large values stringify
in JSON HR to avoid JS Number precision loss.
- Add `JsonSafeFields` impls for `DocumentBaseTransition`,
`TokenPaymentInfo`, `GasFeesPaidBy`.
- Add `json_safe_option_string_u64_tuple` helper module to protect the u64
inside `Option<(String, Credits)>` (the tuple shape can't be auto-routed
by the macro). Apply it via `serde(with = ...)` on
`DocumentCreateTransitionV0::prefunded_voting_balance`.
- Update `DocumentCreateTransitionV0`'s manual `Deserialize` impl to handle
the three entropy wire shapes after `serde_bytes` injection: `Value::Text`
(base64-decoded), `Value::Bytes32` (direct), `Value::Bytes` (length-checked
conversion).
- Update the `document_create_transition` test wire shape to expect the new
base64 entropy / u64-stringification / `Value::Bytes32` shape.
Note: this commit also includes whitespace-only changes from running
`cargo fmt --all` across the workspace (a stray side-effect of the format
pass). The behavioral changes are confined to the files listed above.
3716 -> 3716 dpp lib tests passing, 8 ignored (unchanged).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comprehensive audit found 11 V0 structs with u64-alias fields lacking JS-safe wire-shape protection. Apply `#[json_safe_fields]` to: - TokenBaseTransitionV0 (identity_contract_nonce: IdentityNonce) - TokenBurnTransitionV0 (burn_amount: u64) - TokenMintTransitionV0 (amount: u64) - TokenDirectPurchaseTransitionV0 (token_count, total_agreed_price) - DocumentReplaceTransitionV0 (revision) - DocumentUpdatePriceTransitionV0 (revision, price) - DocumentTransferTransitionV0 (revision) - DocumentPurchaseTransitionV0 (revision, price) - DocumentPatch (revision, updated_at) Add JsonSafeFields impls for the wrapper enums: - TokenBaseTransition (wraps json_safe_fields-annotated V0) - GroupStateTransitionInfo (only has u16/Identifier/bool fields) TokenTransferTransitionV0 cannot use the macro because its `Option<SharedEncryptedNote>` and `Option<PrivateEncryptedNote>` fields are tuples containing `Vec<u8>` that can't be auto-routed by the macro and would also need a custom serde helper to base64-encode the byte payload. Apply `serde(with = "json_safe_u64")` directly to the `amount` field instead — surgical fix; the encrypted-note shape stays as before. Update the manual Deserialize on DocumentReplaceTransitionV0 to accept both numeric and string forms for `$revision` (matching the new json_safe_u64 stringification of large values) — the existing test fixtures use small values that stay numeric, but production-scale revisions could exceed MAX_SAFE_INTEGER. Skipped: Epoch::key (`[u8; 2]` — small enough that JSON-array shape is fine; not a real wire-shape concern). Out of scope: TokenTransferTransitionV0's encrypted-note tuples need a dedicated serde helper for `(u32, u32, Vec<u8>)` to produce base64 in JSON HR. Tracked as future work in source comments. 3716 -> 3716 dpp lib tests passing, 8 ignored (unchanged). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pted-note tuple alias Register `SharedEncryptedNote` and `PrivateEncryptedNote` (both `(u32, u32, Vec<u8>)` shape) as known type aliases in the `json_safe_fields` proc macro. New `Option<encrypted-note-alias>` field type is auto-routed to a new `json_safe_option_encrypted_note` serde helper that: - HR (JSON): emits `[u32, u32, "<base64>"]` — base64-encoded `Vec<u8>` - non-HR (platform_value, bincode): emits `[u32, u32, <raw bytes>]` - Deserialize accepts both via `deserialize_any` With the alias registered, `TokenTransferTransitionV0` can now use the plain `#[json_safe_fields]` attribute — the macro injects `json_safe_u64` on `amount: u64`, `json_safe_option_encrypted_note` on `shared_encrypted_note` and `private_encrypted_note`, and skips the flatten on `base`. Replaces the per-field `serde(with = "json_safe_u64")` workaround from the previous commit. Pattern: when a new tuple-shaped alias appears that can't be auto-routed by serde, add the alias name to `ENCRYPTED_NOTE_ALIASES` (or extend the match in `serde_with_suffix_for_type`) and provide a matching helper module — same convention as `U64_ALIASES`. 3716 -> 3716 dpp lib tests passing, 8 ignored (unchanged). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the wasm-dpp2 convention, every serde-injected discriminator key carries
a \$ prefix so it never collides with user-data field names. Migrate the
transition umbrellas:
- BatchedTransition: tag = "type", content = "data" (adjacent) -> tag = "\$transition"
(internal). Drops the "data" wrapper. Wire shape is now flat:
{"\$transition": "document", "\$action": "create", "\$formatVersion": "0", ...}.
- DocumentTransition: tag = "type" -> tag = "\$action". Cannot use \$type
because the flattened DocumentBaseTransition exposes document_type_name as
\$type (long-standing DPP convention). Variant names (create/replace/
delete/transfer/updatePrice/purchase) read naturally as actions; matches
the existing PROPERTY_ACTION = "\$action" constant.
- TokenTransition: tag = "type" -> tag = "\$action". Symmetric with
DocumentTransition so consumers discriminate the inner shape with the
same key regardless of \$transition value.
- StateTransition: tag = "type" -> tag = "\$type". Outermost umbrella with
no flatten path that could collide; \$type fits naturally for transition
*kinds* (Batch, IdentityCreate, DataContractCreate, ...).
Net wire-shape result: every system field uses a \$ prefix —
\$transition / \$action / \$type / \$formatVersion / \$baseFormatVersion /
\$id / \$dataContractId / etc. User-data fields (camelCase) never collide
with system fields.
Update umbrella round-trip test helpers to assert on the new keys.
3716 -> 3716 dpp lib tests passing, 8 ignored (unchanged).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply the wire-shape convention rule from this session:
- Discriminator key uses `$` prefix only when the same wire-shape level
has other `$`-prefixed fields. Plain `type` otherwise.
- Sum types should be internally tagged (no `data` wrapper) where the
variant shape allows it.
Vote (wraps ResourceVote which uses `tag = "$formatVersion"`):
Inner has `$formatVersion` at the same flattened level → use `$type`
internal tagging. Wire shape:
{"$type": "resourceVote", "$formatVersion": "0", "votePoll": {...}, "resourceVoteChoice": {...}}
VotePoll (wraps ContestedDocumentResourceVotePoll, plain camelCase fields):
No `$`-prefixed fields at the flattened level → use plain `type`
internal tagging. Wire shape:
{"type": "contestedDocumentResourceVotePoll", "contractId": ..., "documentTypeName": ..., ...}
GroupActionEvent (wraps TokenEvent which uses `tag = "type", content = "data"`):
Inner exposes `type`/`data` only (no `$`-prefixed fields). The rule
prescribes plain `type` internal tagging — but `type` collides with
TokenEvent's `type`, and TokenEvent is consensus-binary-locked
(cannot rename). Adjacent tagging is the only rule-consistent shape
here; reverted to original `tag = "type", content = "data"`.
Update the four affected test wire shapes (resource_vote, masternode_vote
inside json_round_trip + value_round_trip).
Decisions remaining:
- ResourceVoteChoice + ContestedDocumentVotePollWinnerInfo — both have
tuple variants of `Identifier` (which serializes as base58 string, not
a struct), so internal tagging fails natively. Will need either custom
Serialize/Deserialize or struct-variant refactor — separate decisions.
3716 -> 3716 dpp lib tests passing, 8 ignored (unchanged).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously kept adjacent (`tag = "type", content = "data"`) because plain
`type` collides with the locked `TokenEvent` discriminator and `\$type`
would violate the rule (no other `\$`-fields at the wire level).
Use `kind` instead: distinct from the inner `TokenEvent`'s `type`,
plain prefix per the rule, and reads naturally ("the kind is
tokenEvent"). Drops the `data` wrapper. Wire shape:
{"kind": "tokenEvent", "type": "mint", "data": [...]}
3716 -> 3716 dpp lib tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nnerInfo via custom serde impls
Both enums have a tuple variant wrapping `Identifier` (`TowardsIdentity` /
`WonByIdentity`). `Identifier` serializes as a base58 string (not a map),
so serde's auto-derive can't internal-tag — the convention prescribes
custom Serialize / Deserialize emitting a flat shape (precedents:
`AddressFundsFeeStrategyStep`, `AddressWitness`).
Wire-shape change:
- `ResourceVoteChoice::TowardsIdentity(id)`:
before {"type": "towardsIdentity", "data": "<base58>"}
after {"type": "towardsIdentity", "identity": "<base58>"}
- `ContestedDocumentVotePollWinnerInfo::WonByIdentity(id)`:
before {"type": "wonByIdentity", "data": "<base58>"}
after {"type": "wonByIdentity", "identity": "<base58>"}
- Unit variants (`Abstain` / `Lock` / `NoWinner` / `Locked`) keep their
`{"type": "..."}` shape.
The synthesized field name is `identity` (matches the struct-variant form
that serde would have emitted if these had been refactored to
`TowardsIdentity { identity: Identifier }`). Bincode `Encode`/`Decode`
derives are untouched — consensus binary path unchanged.
Update test wire-shape assertions in `resource_vote`,
`masternode_vote_transition`, and `contested_document_vote_poll_winner_info`.
3716 -> 3716 dpp lib tests passing, 8 ignored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Custom Serialize / Deserialize that emits internally-tagged flat shape
with named fields per variant. The earlier "skip TokenEvent because of
consensus" was incorrect — bincode (Encode/Decode) and serde
(Serialize/Deserialize) are independent paths in this codebase
(per CONVENTIONS.md). Reshaping serde for ergonomics is safe as long
as the bincode derives stay in place, which they do here.
Wire-shape change (JSON / platform_value only):
- Mint: {type, data: [amount, recipient, note]}
-> {type, amount, recipient, publicNote}
- Burn: {type, data: [amount, from, note]}
-> {type, amount, burnFromIdentifier, publicNote}
- Freeze / Unfreeze: {type, data: [frozen, note]}
-> {type, frozenIdentifier, publicNote}
- DestroyFrozenFunds: {type, data: [frozen, amount, note]}
-> {type, frozenIdentifier, amount, publicNote}
- Transfer: {type, data: [recipient, note, shared, private, amount]}
-> {type, recipient, publicNote, sharedEncryptedNote,
privateEncryptedNote, amount}
- Claim: {type, data: [distributionType, amount, note]}
-> {type, distributionType, amount, publicNote}
- EmergencyAction: {type, data: [action, note]}
-> {type, action, publicNote}
- ConfigUpdate: {type, data: [change, note]}
-> {type, configurationChange, publicNote}
- ChangePriceForDirectPurchase: {type, data: [schedule, note]}
-> {type, pricingSchedule, publicNote}
- DirectPurchase: {type, data: [amount, credits]}
-> {type, amount, credits}
u64 fields (TokenAmount / Credits) route through json_safe_u64
(stringify above MAX_SAFE_INTEGER in JSON HR).
sharedEncryptedNote / privateEncryptedNote (Option<(u32,u32,Vec<u8>)>)
route through json_safe_option_encrypted_note (base64 the inner Vec<u8>
in JSON HR).
Bincode Encode/Decode unchanged — consensus binary path unaffected.
Side effect: GroupActionEvent's wire shape becomes
{kind: "tokenEvent", type: "mint", amount: ..., recipient: ..., ...}
(the inner TokenEvent's flat shape merges with the outer `kind` tag).
Update test wire-shape assertions in token_event and action_event.
3716 -> 3716 dpp lib tests passing, 8 ignored (unchanged).
Maintenance trap (called out in source): adding a new TokenEvent
variant requires updating both Serialize and Deserialize blocks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… test The comment described the pre-flatten state of VotePoll / ResourceVoteChoice (adjacent tagging with "data" wrapper). Both have moved to flat shapes this session — VotePoll uses internal tagging on "type", ResourceVoteChoice uses a custom Serialize/Deserialize emitting a synthesized "identity" field. 3716 -> 3716 dpp lib tests passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Status pinned to commit 91b16e4 (2026-05-06). Updates: - Add passes 2.7 (tag-shape sweep) and 2.8 (json_safe_fields rollout) with status entries. - Add per-commit table rows for each step of the convention sweep: fe92868, d14ce1a, 017c308, cd56289, 38d1388, f11fdb5, c36f93b, 2674d95, 71d2e75, 91b16e4. - Rewrite the Tag-key conventions section to reflect the final state: - Codify the core rule: internal tagging only (no `data` wrapper); `$`-prefix discriminator only when wire-shape level has other `$`-prefixed fields. - Drop the `tag = "type", content = "data"` row entirely — zero remaining usages in rs-dpp. - Add `$type` / `$action` / `$transition` / `kind` rows with examples. - Document the custom serde impl precedents (TokenEvent, ResourceVoteChoice, ContestedDocumentVotePollWinnerInfo) for tuple-variant enums where auto-derive can't internal-tag. - Document the maintenance trap on `DocumentCreate/Replace`'s manual `BASE_FIELD_NAMES` lists. Tests: 3716 passing, 8 ignored (unchanged). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
15 wasm wrappers that wrap a rs-dpp domain type with JsonConvertible / ValueConvertible (now uniformly available in rs-dpp via PR #3573) now delegate to the inner traits via impl_wasm_conversions_inner! instead of the generic-serde fallback impl_wasm_conversions_serde!. Same JSON / Value wire output; cleaner dependency on rs-dpp's canonical conversion path. Migrated: - Shielded transitions (5): ShieldTransition, UnshieldTransition, ShieldedTransferTransition, ShieldedWithdrawalTransition, ShieldFromAssetLockTransition. - shielded::orchard_action: SerializedOrchardActionWasm wrapping rs-dpp's SerializedAction (note the name divergence — wasm wrapper is SerializedOrchardAction in JS but the rs-dpp type is SerializedAction). - asset_lock_proof::proof::AssetLockProofWasm. - tokens::contract_info::TokenContractInfoWasm. - state_transitions::batch::token_payment_info::TokenPaymentInfoWasm. - 6 platform_address transitions (IdentityCreateFromAddresses, IdentityCreditTransferToAddresses, IdentityTopUpFromAddresses, AddressFundingFromAssetLock, AddressCreditWithdrawal, AddressFundsTransfer). 20 _serde! call sites remain (the proof_result wasm-only DTOs in state_transitions/proof_result/{voting,shielded,identity,token}.rs). These wrap variants of rs-dpp's StateTransitionProofResult enum but hold extracted fields directly — they're wasm-DTOs, not rs-dpp wrappers. Generic serde is the right path for those; documented in the plan. Tangential: drop unused `self` import surfaced by the rs-dpp lib check in tokens/token_event.rs. Plan: docs/wasm-dpp2-cleanup-plan.md (added in this commit) — covers migration phases, JS-test wire-shape audit (deferred per direction "didn't update tests"), and improvements to port back to rs-dpp. 3716 -> 3716 dpp lib tests passing; wasm-dpp2 cargo check clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dary Regression introduced by ec43a2a (fix(platform-value): typed map keys — drop string-only MapKeySerializer): rs-dpp's platform_value now preserves typed keys for `BTreeMap<u32, _>` / `BTreeMap<Identifier, _>` etc., emitting `Value::U32` / `Value::Identifier` keys. Great for round-trips with binary formats; bad for the wasm boundary where `serde_wasm_bindgen` requires JS plain-object keys to be strings. Surfaced by `PartialIdentity` tests in wasm-dpp2: `platform_value_to_object: Map key is not a string and cannot be an object key` on `loaded_public_keys: BTreeMap<KeyID, IdentityPublicKey>`. Fix on the wasm-dpp2 side (rs-dpp wire shape unchanged): walk the platform_value tree before serializing and stringify any non-Text map keys. Numeric variants use Display, Identifier becomes base58, bytes become base64. Same normalization applied to `convert_value_for_json` so the JSON path doesn't have the same problem. This is wasm-dpp2-only and doesn't affect rs-dpp / consensus / persistence. Tests: 1075 → 1077 passing, 45 → 43 failing in wasm-dpp2 unit suite. The 43 remaining failures are all test fixtures hardcoding pre-PR wire shapes (data wrappers, plain `type` vs `$type`/`$action`/ `$transition`/`kind`, byte arrays vs base64) — to be updated in Phase 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates JS test fixtures to match the new rs-dpp wire shapes from PR #3573 (json-value-conversion unification). No production code changes. Groups updated (25 tests, all now passing): Group 1 — ContestedDocumentVotePollWinnerInfo (4 tests): WonByIdentity wire shape: `data` -> `identity` Custom Serialize emits flat `{type, identity}` instead of adjacent `{type, data: <id>}`. Identifier remains base58 in JSON, Uint8Array in toObject(). Group 2 — GroupAction (6 tests): Inherits wire-shape change from inner GroupActionEvent + TokenEvent. GroupAction itself unchanged: `tag = "$formatVersion"`, V0 -> "0", V0 fields snake_case at top level (contract_id, proposer_id, token_contract_position, event). Group 3 — GroupActionEvent (8 tests): Internally tagged `kind:` (was adjacent `type/data`). Inner TokenEvent flatten at the same level. Wire shape: {kind: "tokenEvent", type: "mint", amount, recipient, publicNote} `kind` chosen over `type` to avoid collision with inner TokenEvent's own `type` discriminator. Group 4 — TokenEvent (7 tests): Custom Serialize maps positional tuple fields to named JSON keys: Mint: {type, amount, recipient, publicNote} Burn: {type, amount, burnFromIdentifier, publicNote} Freeze: {type, frozenIdentifier, publicNote} Internally tagged `type:`, no `data` wrapper. Was adjacent with positional tuple in `data: [...]`. Test results: 1077 -> 1102 passing (+25), 43 -> 18 failing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final batch of JS test fixture updates for the rs-dpp PR #3573 json-value unification wire-shape changes. No production code changes. Group 5 — StateTransitionProofResult (VerifiedMasternodeVote / VerifiedNextDistribution, 2 tests): Inner Vote shape updated. The `vote` field now carries a flat Vote with `$type`/`$formatVersion` at the top level (was adjacent `type`/`data` with nested `data: {...}`). Group 6 — ResourceVote (2 tests): VotePoll inside is internally tagged — fields flat at votePoll level, no `data` wrapper. Tests now read `json.votePoll.contractId` instead of `json.votePoll.data.contractId`. Group 7 — ResourceVoteChoice (4 tests): Custom serde renames `data` -> `identity` for TowardsIdentity. Flat `{type, identity}` shape. `toObject()` puts a Uint8Array at `identity`. Group 8 — TokenContractInfo (5 tests): Versioned enum gained `tag = "$formatVersion"`. Fixtures now include `$formatVersion: "0"` at the top level alongside `contractId` / `tokenContractPosition`. Group 9 — Vote (2 tests): Internal tag `$type` ($-prefix because the level also carries the inner ResourceVote's `$formatVersion`). The single ResourceVote variant flattens its V0 body — no `data` wrapper. Group 10 — VotePoll (3 tests): Internal tag `type` (plain — no $-prefixed neighbors). Inner ContestedDocumentResourceVotePoll fields flatten at the same level — no `data` wrapper. Test results: 1102 -> 1120 passing (+18), 18 -> 0 failing. PR #3573 wire-shape migration is now complete on the JS test side. The remaining work is the wasm-dpp2 TypeScript type definitions (e.g. VoteJSON in src/voting/vote.rs) which still document the old shape; those are docstring-only and will be addressed in a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates the typescript_custom_section blocks that document VoteObject / VoteJSON / VotePollObject / VotePollJSON / ResourceVoteChoiceObject / ResourceVoteChoiceJSON / ContestedDocumentVotePollWinnerInfoObject / ContestedDocumentVotePollWinnerInfoJSON / GroupActionEventObject / GroupActionEventJSON / TokenEventObject / TokenEventJSON / TokenContractInfoObject / TokenContractInfoJSON / GroupActionObject / GroupActionJSON. These types are surfaced through the generated dist/dpp.d.ts and were still describing the pre-PR-#3573 wire shapes (`type/data` adjacent tagging with `data: {...}` wrappers, plus a few drift items like the GroupAction type using camelCase fields when the Rust struct emits snake_case). Updates per type: - Vote: internal tag `$type` + flat ResourceVote body - VotePoll: internal tag `type` + flat poll body, no `data` - ResourceVoteChoice: flat `{type, identity?}` (was `{type, data}`) - WinnerInfo: flat `{type, identity?}` (was `{type, data}`) - GroupActionEvent: internal tag `kind` intersected with TokenEvent - TokenEvent: flexible interface — variant-specific fields documented in the JSDoc comment (Mint, Burn, Freeze, Transfer, Claim, …) - TokenContractInfo: added `$formatVersion: "0"` discriminator - GroupAction: snake_case fields (matches Rust no-rename), `$formatVersion: "0"` discriminator typed Build verified, 1120 JS unit tests still passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit conclusion: the "delete \`impl_wasm_conversions_serde!\` macro
entirely" goal is infeasible without inventing 17 new rs-dpp types
that exist solely to mirror StateTransitionProofResult tuple variants.
The 17 remaining \`_serde!\` callers in
\`packages/wasm-dpp2/src/state_transitions/proof_result/\` are all
wasm-only DTOs decomposing tuple variants (e.g., VerifiedBalanceTransfer
(PartialIdentity, PartialIdentity)) into named-field JS classes
({ sender, recipient }). They have no rs-dpp counterpart with
JsonConvertible/ValueConvertible impls, and the named-field form is
JS-ergonomics, not a domain concept worth promoting to rs-dpp.
Updates:
- Macro doc: reframe \`_serde!\` from "fallback awaiting migration" to
"canonical path for wasm-only DTOs". \`_inner!\` remains the preferred
path when an rs-dpp domain type with the canonical traits exists.
- Plan doc: mark Phase E with corrected scope, document the audit of
manual Serialize/Deserialize impls (IdentifierWasm, PlatformAddressWasm
— JS interop adapters, not backport candidates) and manual to_*/from_*
methods (carry context the trait signatures don't accept).
- Note one small follow-up backport candidate: rs-dpp's serde_bytes
module currently handles only [u8; N], but wasm-dpp2's bytes_b64 is a
Vec<u8> variant with a single user — could be unified or deleted in a
separate PR.
No production code changes. cargo check passes; 1120 wasm-dpp2 unit
tests still passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The wasm-dpp2 \`serialization::bytes_b64\` module duplicated functionality
already provided in dpp:
- \`Vec<u8>\` codec — already in \`dpp::serialization::serde_bytes_var\` with
the more robust dual-shape visitor that handles serde's
ContentDeserializer for internally-tagged enums
- \`[u8; N]\` codec — already in \`dpp::serialization::serde_bytes\`
The only thing missing in dpp was the \`Option<[u8; N]>\` flavor, used by
wasm-dpp2 for the optional \`\$entropy\` field on a Document.
Changes:
- Add \`dpp::serialization::serde_bytes::option\` submodule for
\`Option<[u8; N]>\`. Uses the parent module's codec for the inner bytes
via a Serialize-wrapper struct so the outer serializer writes the
Option variant tag (avoids bincode "UnexpectedVariant" mismatches when
the inner shape is bytes vs. Option<bytes>). Tests cover JSON null /
Some round-trips and bincode binary round-trip.
- Switch the single \`Option<[u8; 32]>\` user (wasm-dpp2 DocumentWasm
\$entropy field) from \`serialization::bytes_b64::option\` to
\`dpp::serialization::serde_bytes::option\`.
- Switch the 5 \`Vec<u8>\` users in wasm-sdk (queries/mod.rs:
ResponseMetadata.chain_id, ProofInfo.{grovedb_proof, quorum_hash,
signature, block_id_hash}) from \`wasm_dpp2::serialization::bytes_b64\`
to \`dash_sdk::dpp::serialization::serde_bytes_var\` (aliased as
\`bytes_b64\` to avoid touching every \`#[serde(with = ...)]\` attr).
- Delete \`packages/wasm-dpp2/src/serialization/bytes_b64.rs\` and remove
its module declaration.
Wire shape unchanged across all callers — both helpers emit the same
base64-string-in-HR / raw-bytes-in-binary shape.
Test results:
- rs-dpp \`serialization::serde_bytes\` tests: 11 passing (was 8 — added
3 for the option submodule).
- wasm-dpp2 unit tests: 1120 passing, 0 failing (unchanged).
- cargo check -p wasm-dpp2 -p wasm-sdk: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Found four manual implementations in wasm-dpp2 that wrap rs-dpp domain
types and duplicate logic that lives in dpp:
1. BatchTransitionWasm: manual to_object/to_json/from_object/from_json
using generic serde via wasm-dpp2::serialization::{to,from}_{object,
json}. dpp::BatchTransition has JsonConvertible/ValueConvertible
from Phase C — switch to impl_wasm_conversions_inner! to delegate.
2. GroupWasm: same pattern. Bonus cleanup: the manual to_object had a
workaround comment noting it intentionally called toJSON to handle
BTreeMap<Identifier, u32>. That workaround is now obsolete after
commit 4e9d1ee added stringify_map_keys_for_object at the
wasm-dpp2 boundary, so toObject can route through the canonical
ValueConvertible path correctly.
3. TokenConfigurationLocalizationWasm: same pattern.
4. PoolingWasm Deserialize: 38-line custom impl that accepts both
string variants ("never"/"ifAvailable"/"standard") and numeric
discriminants (0/1/2). dpp::withdrawal::pooling_serde::deserialize
already does exactly this with a richer visitor (also accepts
"Never"/"IfAvailable"/"Standard" capitalized variants). Delegate to
it and convert dpp::Pooling -> PoolingWasm via the existing From
impl. Net: -28 lines, fewer divergent paths to keep in sync.
For the remaining manual to_*/from_* methods in wasm-dpp2 (Identity,
PartialIdentity, IdentityPublicKey, Document, DataContract): these
take context arguments (platform_version, data_contract) that the
canonical trait signatures don't accept, OR rely on version-aware
methods like to_cleaned_object / from_json_object. Cannot migrate to
_inner! — they're legitimate wasm-side extensions.
Test results: 1120 passing, 0 failing (unchanged).
cargo check -p wasm-dpp2: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…entity
Identity and PartialIdentity wasm wrappers had to_json / from_json /
to_object methods that went through generic serde via the wasm-side
helper (\`serialization::to_json\` etc.). These calls produce the same
wire shape as the canonical \`JsonConvertible\` / \`ValueConvertible\`
trait methods on dpp::Identity / dpp::PartialIdentity, but bypass the
trait — so any future custom impl on dpp wouldn't propagate to the
wasm boundary.
Switch to canonical traits where possible:
IdentityWasm:
- to_object: already used canonical \`self.0.to_object()\` (no change)
- to_json: \`serialization::to_json\` -> \`self.0.to_json()\` + json_to_js
- from_json: \`serialization::from_json\` -> \`Identity::from_json(json)\`
- from_object: KEPT — uses \`Identity::try_from_platform_versioned\`
because the wasm API dispatches on the \`platform_version\` arg, not
on the value's \`\$formatVersion\` tag (intentional SDK convention).
PartialIdentityWasm:
- to_object: \`platform_value::to_value\` -> \`self.0.to_object()\` (canonical)
- to_json: \`platform_value::to_value\` -> \`self.0.to_object()\` then
\`platform_value_to_json\`
- from_object / from_json: KEPT — manual field-by-field with
\`platform_version\` for inner IdentityPublicKey deserialization.
For IdentityPublicKey: the wasm \`to_object\` calls \`to_cleaned_object\`
(which strips disabledAt: None) — different semantic from canonical
to_object, so kept as-is to avoid changing the JS wire shape.
Disambiguation: PartialIdentity now imports \`ValueConvertible\`, which
collides with \`IdentityPublicKeyPlatformValueConversionMethodsV0::from_object\`
on IdentityPublicKey (both have a \`from_object\` method). Used the
fully-qualified \`<IdentityPublicKey as IdentityPublicKeyPlatformValueConversionMethodsV0>::from_object\`
form at the one collision site.
Test results: 1120 passing, 0 failing (unchanged).
cargo check -p wasm-dpp2: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dpp traits
Two more callers of the wasm-side generic-serde helper that wrap a
versioned dpp type with the canonical traits available — switched to
delegate via JsonConvertible / ValueConvertible:
- \`DataContractWasm::config()\` getter (data_contract/model.rs:397):
was \`serialization::to_object(self.0.config())\` (generic serde).
Now \`config().to_object()\` -> platform_value -> JS. DataContractConfig
is a versioned enum with \`#[derive(JsonConvertible, ValueConvertible)]\`.
- \`TokenConfigurationLocalizationWasm::TryFrom<&JsValue>\` fallback path
(tokens/configuration/localization.rs:121): was
\`serialization::from_object(...)\`. Now goes through
\`platform_value_from_object\` then
\`TokenConfigurationLocalization::from_object\` (the canonical
ValueConvertible trait method).
After these and the prior commits in this branch, the only remaining
callers of wasm-dpp2's generic-serde helpers (\`serialization::to_object\`
/ \`from_object\`) are over leaf types that aren't versioned dpp
structures — \`BTreeMap<String, JsonSchema>\` (document_schemas) and
\`BTreeMap<String, Value>\` (document data). Those are fine to keep on
generic serde.
Manual audit summary across all wasm-dpp2 wrappers of dpp domain types:
✅ Already routed through dpp methods (no change):
IdentityPublicKeyWasm - to_cleaned_object / to_json_object /
from_object / from_json_object
DocumentWasm - to_map_value / from_platform_value /
Document::to_json / from_json_value
DataContractWasm - to_value / from_value / from_json /
from_bytes / to_bytes (all dpp methods)
✅ Migrated in this branch:
IdentityWasm - to_object / to_json / from_json now via
JsonConvertible / ValueConvertible (was
generic serde via wasm helper)
PartialIdentityWasm - to_object / to_json now via
ValueConvertible (was direct
platform_value::to_value)
BatchTransitionWasm - now via _inner! macro (delegates to
JsonConvertible / ValueConvertible)
GroupWasm - same
TokenConfigurationLocalizationWasm - same
PoolingWasm Deserialize - delegates to dpp::pooling_serde
❌ Cannot migrate (legitimate context-aware extensions):
IdentityWasm.from_object - wasm SDK convention: dispatch on
platform_version arg, not value's
\$formatVersion tag
PartialIdentityWasm.from_* - manual field-by-field deserialization
with platform_version for inner keys
IdentityPublicKeyWasm.{to,from}_*- already context-aware via dpp methods
DocumentWasm.from_* - composite wrapper (Document + metadata)
DataContractWasm.{to,from}_* - take \`platform_version\` + \`full_validation\`
❌ Cannot migrate (semantic divergence):
IdentityPublicKeyWasm.to_object - to_cleaned_object strips disabledAt:None
❌ Cannot migrate (JS interop adapters):
IdentifierWasm / PlatformAddressWasm Serialize/Deserialize -
visit_seq for Uint8Array, visit_map for {type,data} JS quirks
Test results: 1120 passing, 0 failing (unchanged).
cargo check -p wasm-dpp2: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Unification work for the canonical
JsonConvertible/ValueConvertibletraits acrosspackages/rs-dpp. Two passes:Pass 1 — added trait impls to ~80 domain types (commit
9f23d675af). All types now expose the canonical conversion surface.Pass 2 — added 197 round-trip tests using non-default fixtures + per-property assertions per the new test convention. Surfaced 3 platform_value bugs that were previously silent.
Test results
json_convertible_testsacross ~95 types.#[ignore]d — each documents either a real bug found or a structural issue (BLS keys, untagged-enum ambiguity, known-broken serde).Bugs surfaced (logged in
docs/json-value-unification-plan.md§10b)OutPointround-trip viaplatform_value::Value::Mapfails — affectsChainAssetLockProof,AddressFundingFromAssetLockTransition,ShieldFromAssetLockTransition. JSON works.[u8; N]fixed-array fields with custom serializers fail platform_value round-trip ("Invalid symbol 17, offset 0") — affectsExtendedBlockInfosignature and all 5 shielded transitions' Orchard fields. JSON works.DataContractdocument_schemaslose sized integer types via JSON round-trip (U32(63)→U64(63),I32(0)→U64(0)) — Critical-1 manifestation; affects every state transition embedding a DataContract. Value round-trip works.Convention added
Every J/V test follows
docs/json-value-unification-plan.md§8:to_json→from_json(and same for value).$formatVersionpreservation test where applicable.Fixtures source priority: hand-built struct literals >
random_*constructors >from_*factories >crate::tests::fixtures::*helpers >Default::default()only for unit-only enums.Code improvements
PartialEqonStateTransitionProofResult+StoredAssetLockInfo(was missing, blocked round-tripassert_eq).Out of scope (genuinely residual — needs upstream work)
ValidatorSet— needs real BLS public key construction (crypto setup).ExtendedDocument— Critical-3 known-broken serde (writesversion, reads$version).StateTransitionumbrella —serde(untagged), deserialize ambiguity (logged#[ignore]).Docs
docs/json-value-conversion-inventory.md— pre-pass-1 structural inventory.docs/json-value-unification-plan.md— full plan with phases, bug log, lessons learned, fixture conventions.Test plan
cargo test -p dpp --features=json-conversion,value-conversion,serde-conversionand sees 3619 pass / 0 fail.docs/json-value-unification-plan.md§10b for the 3 surfaced bugs and decides priority for follow-up fix.🤖 Generated with Claude Code