diff --git a/.serena/project.yml b/.serena/project.yml index 48df7c44058..0f21782f2c1 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -3,21 +3,27 @@ project_name: "platform" # list of languages for which language servers are started; choose from: -# al bash clojure cpp csharp -# csharp_omnisharp dart elixir elm erlang -# fortran fsharp go groovy haskell -# java julia kotlin lua markdown -# matlab nix pascal perl php -# php_phpactor powershell python python_jedi r -# rego ruby ruby_solargraph rust scala -# swift terraform toml typescript typescript_vts -# vue yaml zig +# al angular ansible bash clojure +# cpp cpp_ccls crystal csharp csharp_omnisharp +# dart elixir elm erlang fortran +# fsharp go groovy haskell haxe +# hlsl html java json julia +# kotlin lean4 lua luau markdown +# matlab msl nix ocaml pascal +# perl php php_phpactor powershell python +# python_jedi python_ty r rego ruby +# ruby_solargraph rust scala scss solidity +# svelte swift systemverilog terraform toml +# typescript typescript_vts vue yaml zig # (This list may be outdated. For the current list, see values of Language enum here: # https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py # For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) # Note: # - For C, use cpp # - For JavaScript, use typescript +# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root) +# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm) +# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three) # - For Free Pascal/Lazarus, use pascal # Special requirements: # Some languages require additional setup/installations. @@ -59,53 +65,17 @@ read_only: false # list of tool names to exclude. # This extends the existing exclusions (e.g. from the global configuration) -# -# Below is the complete list of tools for convenience. -# To make sure you have the latest list of tools, and to view their descriptions, -# execute `uv run scripts/print_tool_overview.py`. -# -# * `activate_project`: Activates a project by name. -# * `check_onboarding_performed`: Checks whether project onboarding was already performed. -# * `create_text_file`: Creates/overwrites a file in the project directory. -# * `delete_lines`: Deletes a range of lines within a file. -# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. -# * `execute_shell_command`: Executes a shell command. -# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. -# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). -# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). -# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. -# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. -# * `initial_instructions`: Gets the initial instructions for the current project. -# Should only be used in settings where the system prompt cannot be set, -# e.g. in clients you have no control over, like Claude Desktop. -# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. -# * `insert_at_line`: Inserts content at a given line in a file. -# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. -# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). -# * `list_memories`: Lists memories in Serena's project-specific memory store. -# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). -# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). -# * `read_file`: Reads a file within the project directory. -# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. -# * `remove_project`: Removes a project from the Serena configuration. -# * `replace_lines`: Replaces a range of lines within a file with new content. -# * `replace_symbol_body`: Replaces the full definition of a symbol. -# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. -# * `search_for_pattern`: Performs a search for a pattern in the project. -# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. -# * `switch_modes`: Activates modes by providing a list of their names -# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. -# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. -# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. -# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html excluded_tools: [] # list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default). # This extends the existing inclusions (e.g. from the global configuration). +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html included_optional_tools: [] # fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. # This cannot be combined with non-empty excluded_tools or included_optional_tools. +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html fixed_tools: [] # list of mode names to that are always to be included in the set of active modes @@ -116,11 +86,14 @@ fixed_tools: [] # Set this to a list of mode names to always include the respective modes for this project. base_modes: -# list of mode names that are to be activated by default. -# The full set of modes to be activated is base_modes + default_modes. -# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. +# list of mode names that are to be activated by default, overriding the setting in the global configuration. +# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes. +# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply. # Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply +# for this project. # This setting can, in turn, be overridden by CLI parameters (--mode). +# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes default_modes: # initial prompt for the project. It will always be given to the LLM upon activating the project @@ -150,3 +123,19 @@ ignored_memory_patterns: [] # Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. # No documentation on options means no options are available. ls_specific_settings: {} + +# list of mode names to be activated additionally for this project, e.g. ["query-projects"] +# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes. +# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes +added_modes: + +# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos). +# Paths can be absolute or relative to the project root. +# Each folder is registered as an LSP workspace folder, enabling language servers to discover +# symbols and references across package boundaries. +# Currently supported for: TypeScript. +# Example: +# additional_workspace_folders: +# - ../sibling-package +# - ../shared-lib +additional_workspace_folders: [] diff --git a/docs/json-value-conversion-canonical-pattern.md b/docs/json-value-conversion-canonical-pattern.md new file mode 100644 index 00000000000..24eddf604f0 --- /dev/null +++ b/docs/json-value-conversion-canonical-pattern.md @@ -0,0 +1,268 @@ +# JSON / Value conversion: canonical pattern + +Authoritative reference for adding a new domain type to `rs-dpp` and exposing +it through `wasm-dpp2`. If you're tempted to write a custom `to_json` / +`to_object` inherent method, read this first. + +This doc covers **how to do it correctly today**. + +## TL;DR + +For a new struct or enum: + +1. Derive `Serialize`, `Deserialize`, `JsonConvertible`, `ValueConvertible`. +2. If versioned, use `#[serde(tag = "$formatVersion")]` (see [tag conventions](#tag-key-conventions)). +3. Write a round-trip test using the [test template](#test-template) below. +4. In `wasm-dpp2`, expose via `impl_wasm_conversions_inner!` — one line. + +If you find yourself writing `pub fn to_json(&self) -> JsonValue` outside a +trait impl, stop and re-read this doc. + +## The canonical traits + +```rust +// packages/rs-dpp/src/serialization/serialization_traits.rs +pub trait JsonConvertible: Serialize + DeserializeOwned { + fn to_json(&self) -> Result; + fn from_json(json: JsonValue) -> Result; +} + +pub trait ValueConvertible: Serialize + DeserializeOwned { + fn to_object(&self) -> Result; + fn into_object(self) -> Result; + fn from_object(value: Value) -> Result; +} +``` + +Default implementations route through `serde_json::to_value` / +`platform_value::to_value`. For 95% of types, the derive is sufficient and +no custom impl is needed. + +The wasm-dpp2 `impl_wasm_conversions_inner!` macro delegates to these traits, +then handles JS-boundary concerns (`platform_value` ↔ `JsValue`, large-number +stringification, Map-key normalization). + +## When to derive vs when to hand-roll + +| Type shape | Action | +|---|---| +| Plain struct or enum, all fields serde-derive cleanly | `#[derive(JsonConvertible, ValueConvertible)]` — done. | +| Versioned enum (V0, V1, …) | Derive both, add `#[serde(tag = "$formatVersion")]`. See [tag conventions](#tag-key-conventions). | +| Tagged enum with tuple variants whose inners aren't named structs | Custom `Serialize`/`Deserialize` flattening tuple fields to named JSON keys. See [escape hatches](#escape-hatches). | +| Type whose canonical wire shape needs extra context (`platform_version`, `data_contract`) | Define an inherent context-aware method on a versioned-conversion trait (e.g., `try_from_platform_versioned`). Don't try to fit it into `JsonConvertible`/`ValueConvertible`. | + +## Tag-key conventions + +When you have `#[serde(tag = "...")]`, pick the discriminator key by this +rule: **`$`-prefix iff the same JSON level carries other `$`-prefixed +fields**. Plain key otherwise. + +| Tag key | When | Example types | +|---|---|---| +| `$formatVersion` | Versioned enums (V0/V1/…) — versioned structs always sit next to `$`-fields. | `Identity`, `IdentityPublicKey`, `DataContractConfig`, `Group`, `Validator`, `ValidatorSet`, `AssetLockValue`, `TokenContractInfo`, all 17 leaf transition wrappers, ~40 others. | +| `$baseFormatVersion` | Inner-versioned envelope when the outer also carries `$formatVersion`. | inner-base of certain transition wrappers. | +| `$extendedFormatVersion` | Outer envelope when the inner is already `$formatVersion`-tagged via `serde(flatten)`. | `ExtendedDocument`. | +| `$type` | Discriminator at a level that already has `$`-fields and isn't a version dimension. | `StateTransition`, `Vote`. | +| `$transition` | Inner umbrella inside `BatchedTransition` (where `$type` would collide with `document_type_name`). | `BatchedTransition`. | +| `$action` | Inner action discriminator inside `DocumentTransition` / `TokenTransition` umbrellas (same collision). | `DocumentTransition`, `TokenTransition`. | +| `kind` | Plain key chosen instead of `$type` because the inner content has its own `type` discriminator that would collide. | `GroupActionEvent` (carries inner `TokenEvent` whose internal tag is `type`). | +| `type` | Plain `type` key when the level has no `$`-prefixed neighbors and no inner-tag collision. | `VotePoll`, `ResourceVoteChoice`, `ContestedDocumentVotePollWinnerInfo`. | + +If you're unsure, follow the rule, run round-trip tests, and document any +collision-avoidance reasoning inline. + +## Test template + +Every J or V impl gets a unit test using a **non-default fixture** with a +**round-trip + per-property** assertion. Pure round-trip can pass tautologically +when fields silently drop both ways; per-property catches that. + +```rust +#[cfg(all(test, feature = "json-conversion", feature = "value-conversion", feature = "serde-conversion"))] +mod json_convertible_tests { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + fn fixture() -> MyType { + // Non-default values for every field; identifiers non-zero. + MyType { /* ... */ } + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Lock the wire shape — catches silent renames / type narrowing / + // tag-key drift on top of the round-trip check below. + assert_eq!(json, json!({ + "$formatVersion": "0", + // ... fields with explicit expected values + })); + let recovered = MyType::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // platform_value preserves typed variants (Value::U16, Value::Identifier); + // assert against the typed shape, not the JSON-erased form. + assert_eq!(value, platform_value!({ + "$formatVersion": "0", + // ... + })); + let recovered = MyType::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} +``` + +Tagged enums get an additional **tag-preservation** test asserting that +`V0` doesn't silently round-trip back as `V1`. + +## Escape hatches + +Use **only** when the derived path can't express the shape. Document why +inline. + +### Tuple-variant enums needing internal tagging + +Serde's auto-derive can't internal-tag a tuple variant — internal tagging +requires struct variants or newtype-of-named-struct. If the variants are +shape-stable tuples that map cleanly to named JSON keys, write a custom +`Serialize` / `Deserialize` that emits the flat shape. + +Reference impls: +- `packages/rs-dpp/src/tokens/token_event.rs` — 11 variants, mapped + positional fields to named keys (`amount`, `recipient`, `publicNote`, …). +- `packages/rs-dpp/src/voting/vote_choices/resource_vote_choice/mod.rs` — + `TowardsIdentity(Identifier)` flattened to `{type, identity}`. +- `packages/rs-dpp/src/voting/vote_info_storage/contested_document_vote_poll_winner_info/mod.rs` — + same pattern. + +Bincode `Encode` / `Decode` derives are independent of serde and **stay +untouched** — reshaping serde wire is safe for the consensus binary path. + +### Field-level shaping + +For specific field shapes, prefer existing serde helpers over custom +visitors: + +- `#[serde(with = "crate::serialization::serde_bytes")]` — `[u8; N]`, + base64 in HR / bytes in binary. +- `#[serde(with = "crate::serialization::serde_bytes::option")]` — + `Option<[u8; N]>`. +- `#[serde(with = "crate::serialization::serde_bytes_var")]` — `Vec`, + base64 in HR / bytes in binary. +- `#[serde(with = "crate::serialization::json::safe_integer::json_safe_u64")]` — + large `u64` stringified above `2^53` for JS safety. +- `#[serde(with = "crate::withdrawal::pooling_serde")]` — `Pooling` enum + with lenient string + numeric variant acceptance. +- `#[json_safe_fields]` proc macro — auto-injects the bytes / json_safe_u64 + helpers across a whole struct based on field types. + +If you find yourself writing `with = "my_module"` for a primitive serde +shape that smells generic (base64 bytes, lenient enum, large u64), check +this list first or extend an existing module. + +### Critical findings to be aware of + +The unification plan documents 5 critical findings — all are addressed +in current code, but they shape the testing approach: + +- **Critical-1**: `is_human_readable` divergence. Serde's + `ContentDeserializer` (used by internally-tagged enums) reports HR=true + even when wrapping a non-HR Content. Bytes deserializers must accept + **both** string and bytes paths. See `dpp::serialization::serde_bytes`'s + `AnyShapeVisitor` for the canonical pattern. +- **Critical-2**: `From for Value` array→bytes silent + coercion. A JSON array of u8 (length ≥ 10, all ≤ 255) is silently + treated as `Value::Bytes`. Round-trip tests must include arrays of + small numbers explicitly. +- **Critical-3**: `ExtendedDocument` had a non-round-trippable manual + serde (resolved). Outer enum uses + `#[serde(tag = "$extendedFormatVersion")]` so it can coexist with the + inner Document's `$formatVersion`. +- **Critical-4**: `DataContract::Serialize` is impure — depends on + `PlatformVersion::get_current()`. Treat DataContract conversion as + context-aware, route through `from_value(value, full_validation, + &platform_version)`. +- **Critical-5**: `to_canonical_object` sorts keys — signature-load + bearing. Don't change ordering in canonical paths. + +## wasm-dpp2 wrapper patterns + +When exposing an rs-dpp domain type to JavaScript: + +```rust +// Preferred — delegates to canonical traits, handles JS-boundary +// concerns (BigInt, Uint8Array, Map-key stringification, base58/base64). +impl_wasm_conversions_inner!( + MyTypeWasm, // the wasm wrapper struct + MyType, // the inner rs-dpp domain type with the canonical traits + MyType, // the JS class name + MyTypeObjectJs, // typed return for toObject() / fromObject() (optional) + MyTypeJSONJs, // typed return for toJSON() / fromJSON() (optional) +); +``` + +For wasm-only DTOs (e.g., decompositions of `StateTransitionProofResult` +tuple variants into named-field JS classes — `VerifiedBalanceTransfer`, +`VerifiedMasternodeVote`, etc.) where there is no rs-dpp domain type to +delegate to: + +```rust +// Wasm-only DTO — uses serde derives directly, not a JsonConvertible +// inner type. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MyDtoWasm { /* ... */ } + +impl_wasm_conversions_serde!(MyDtoWasm, MyDto); +``` + +`_serde!` is **not** a fallback awaiting migration — it's the canonical +path for wasm-only structs. Don't try to invent a sibling rs-dpp type just +to convert a `_serde!` site to `_inner!`. + +For context-aware wrappers (`Identity` accepting `platform_version`, +`Document` accepting `data_contract`, etc.), write the methods manually +on the wasm struct and call the rs-dpp conversion methods directly. See +`packages/wasm-dpp2/src/identity/model.rs` for a representative example — +`to_object` / `to_json` / `from_json` use the canonical traits, only +`from_object` is manual because of the `platform_version` arg. + +## Things to avoid + +- **`pub fn to_json(&self) -> JsonValue` inherent methods on rs-dpp + types.** If `JsonConvertible::to_json` works, use it. If it doesn't, + the fix is to make the type derive the trait or hand-roll the trait + impl, not to add a parallel inherent method that diverges. +- **Re-implementing canonical wire shapes in wasm wrappers.** If the + rs-dpp type has `JsonConvertible` / `ValueConvertible`, the wasm + wrapper must delegate through them (via `impl_wasm_conversions_inner!` + or by calling the trait methods directly). Don't go through + `serialization::to_object` / `to_json` (the generic-serde wasm helper) + when the canonical trait is available. +- **Custom Serialize impls on top of structs that already have a derived + `Serialize`.** Two competing impls confuses readers and breaks if + someone later changes the field set. +- **Mocking the bytes encoding twice.** Use `dpp::serialization::serde_bytes` + / `serde_bytes_var` / `serde_bytes::option`. Don't fork another + `bytes_b64` helper. + +## Quick references + +- Trait definitions: `packages/rs-dpp/src/serialization/serialization_traits.rs` +- Bytes serde helpers: `packages/rs-dpp/src/serialization/serde_bytes.rs`, + `serde_bytes_var.rs` +- Macros: `packages/wasm-dpp2/src/serialization/conversions.rs` + (`impl_wasm_conversions_inner!` and `impl_wasm_conversions_serde!`) +- Tag-convention precedent: `packages/rs-dpp/src/state_transition/mod.rs` + (StateTransition `$type`), + `packages/rs-dpp/src/voting/votes/mod.rs` (Vote `$type`), + `packages/rs-dpp/src/group/action_event.rs` (GroupActionEvent `kind`). diff --git a/packages/js-dash-sdk/src/SDK/Client/Client.spec.ts b/packages/js-dash-sdk/src/SDK/Client/Client.spec.ts index ce4d8578ba2..95021cdbd5c 100644 --- a/packages/js-dash-sdk/src/SDK/Client/Client.spec.ts +++ b/packages/js-dash-sdk/src/SDK/Client/Client.spec.ts @@ -1,6 +1,8 @@ import { expect } from 'chai'; import { Transaction, BlockHeader, PrivateKey } from '@dashevo/dashcore-lib'; -import { IdentityPublicKey, IdentityPublicKeyWithWitness, StateTransitionTypes } from '@dashevo/wasm-dpp'; +import loadWasmDpp, { + IdentityPublicKey, IdentityPublicKeyWithWitness, StateTransitionTypes, getLatestProtocolVersion, +} from '@dashevo/wasm-dpp'; import { Client } from './index'; import 'mocha'; @@ -36,8 +38,18 @@ describe('Dash - Client', function suite() { testMnemonic = 'agree country attract master mimic ball load beauty join gentle turtle hover'; testHDKey = 'tprv8ZgxMBicQKsPeGi4CikhacVPz6UmErenu1PoD3S4XcEDSPP8auRaS8hG3DQtsQ2i9HACgohHwF5sgMVJNksoKqYoZbis8o75Pp1koCme2Yo'; + // wasm-dpp must be loaded before `getLatestProtocolVersion()` can be called. + await loadWasmDpp(); + client = new Client({ network: 'testnet', + // The `dash` SDK pins `testnet` to protocol version 1 (a stale mapping), + // while the wasm-dpp fixtures are built at the latest protocol version. + // Publishing at v1 downgrades the V1 contract config (sized integer types) + // to V0, so the published-then-read-back contract no longer matches the + // latest-built fixture. Pin the client to the latest protocol version so + // the round-trip stays version-consistent with the fixtures. + driveProtocolVersion: getLatestProtocolVersion(), wallet: { HDPrivateKey: testHDKey, }, diff --git a/packages/platform-test-suite/test/functional/platform/DataContract.spec.js b/packages/platform-test-suite/test/functional/platform/DataContract.spec.js index 18cf12f1132..c1a7ee99d80 100644 --- a/packages/platform-test-suite/test/functional/platform/DataContract.spec.js +++ b/packages/platform-test-suite/test/functional/platform/DataContract.spec.js @@ -16,6 +16,24 @@ const { }, } = Dash; +// A published data contract carries platform-stamped metadata (createdAt, +// updatedAt and their block-height / epoch counterparts) that a locally +// created, never-published fixture does not. `toObject()` emits these fields +// from the contract's native serialization format, so strip them before +// comparing structural equality between a fixture and a fetched contract. +const getContractObject = (dataContract) => { + const contractObject = dataContract.toObject(); + + delete contractObject.createdAt; + delete contractObject.updatedAt; + delete contractObject.createdAtBlockHeight; + delete contractObject.updatedAtBlockHeight; + delete contractObject.createdAtEpoch; + delete contractObject.updatedAtEpoch; + + return contractObject; +}; + describe('Platform', () => { describe('Data Contract', function main() { this.timeout(700000); @@ -104,7 +122,7 @@ describe('Platform', () => { ); expect(fetchedDataContract).to.be.not.null(); - expect(dataContractFixture.toObject()).to.deep.equal(fetchedDataContract.toObject()); + expect(getContractObject(dataContractFixture)).to.deep.equal(getContractObject(fetchedDataContract)); }); it('should not be able to update an existing data contract if version is incorrect', async () => { @@ -259,13 +277,13 @@ describe('Platform', () => { expect(historyPairs).to.have.lengthOf(2); const [originalContractDate, originalContract] = Object.entries(contractHistory)[0]; - expect(originalContract.toObject()).to.be.deep.equal(dataContractFixture.toObject()); + expect(getContractObject(originalContract)).to.be.deep.equal(getContractObject(dataContractFixture)); const [updatedContractDate, updatedContract] = Object.entries(contractHistory)[1]; // Version is updated separately inside SDK on a cloned contract, so we need to update it // here manually to compare fetchedDataContract.incrementVersion(); - expect(updatedContract.toObject()).to.be.deep.equal(fetchedDataContract.toObject()); + expect(getContractObject(updatedContract)).to.be.deep.equal(getContractObject(fetchedDataContract)); expect(Number(updatedContractDate)).to.be.greaterThan(Number(originalContractDate)); }); diff --git a/packages/rs-dpp-json-convertible-derive/src/lib.rs b/packages/rs-dpp-json-convertible-derive/src/lib.rs index 83d81044ac4..b1afcb4f6a9 100644 --- a/packages/rs-dpp-json-convertible-derive/src/lib.rs +++ b/packages/rs-dpp-json-convertible-derive/src/lib.rs @@ -321,6 +321,9 @@ fn annotate_fields(fields: &mut syn::FieldsNamed, base_path: &str) -> Vec /// - `[u8; N]` for any `N` → `serde_bytes` (const-generic; raw bytes in binary, /// base64 string in JSON) /// - `Vec` → `serde_bytes_var` (variable-length variant of the above) +/// - `Option` / `Option` (both +/// `(u32, u32, Vec)` aliases) → `json::safe_integer::json_safe_option_encrypted_note` +/// (3-tuple where the inner `Vec` is base64 in JSON HR) /// /// When adding a new `type X = u64` alias in rs-dpp, add it to the appropriate list below. fn serde_with_suffix_for_type(ty: &Type) -> Option<&'static str> { @@ -363,6 +366,11 @@ fn serde_with_suffix_for_type(ty: &Type) -> Option<&'static str> { if is_i64_type(&inner_last.ident) { return Some("json_safe_option_i64"); } + if is_encrypted_note_alias(&inner_last.ident) { + return Some( + "json::safe_integer::json_safe_option_encrypted_note", + ); + } } } } @@ -396,6 +404,16 @@ const U64_ALIASES: &[&str] = &[ /// Known type aliases that resolve to i64. const I64_ALIASES: &[&str] = &["SignedCredits", "SignedTokenAmount"]; +/// Known type aliases that resolve to `(u32, u32, Vec)` (encrypted-note +/// shape on token transitions). Routed to +/// `json_safe_option_encrypted_note` so the inner `Vec` is base64 in +/// JSON HR. Add new aliases of the same shape here. +const ENCRYPTED_NOTE_ALIASES: &[&str] = &["SharedEncryptedNote", "PrivateEncryptedNote"]; + +fn is_encrypted_note_alias(ident: &Ident) -> bool { + ENCRYPTED_NOTE_ALIASES.iter().any(|alias| ident == alias) +} + /// Check if the struct has serde derives (Serialize or Deserialize) in its attributes. /// /// NOTE: `cfg_attr` is evaluated by the compiler BEFORE attribute macros run. diff --git a/packages/rs-dpp/src/address_funds/fee_strategy/mod.rs b/packages/rs-dpp/src/address_funds/fee_strategy/mod.rs index f34728902d2..61db79a241c 100644 --- a/packages/rs-dpp/src/address_funds/fee_strategy/mod.rs +++ b/packages/rs-dpp/src/address_funds/fee_strategy/mod.rs @@ -25,7 +25,7 @@ impl Default for AddressFundsFeeStrategyStep { pub type AddressFundsFeeStrategy = Vec; // Custom serde impls so JSON / wasm Object output uses the standard -// `{ "type": "...", "index": N }` discriminator shape used elsewhere in +// `{ "$type": "...", "index": N }` discriminator shape used elsewhere in // the DPP wasm bindings. The bincode `Encode` / `Decode` derives above are // the consensus-critical binary format and are intentionally untouched. #[cfg(feature = "serde-conversion")] @@ -39,11 +39,11 @@ impl Serialize for AddressFundsFeeStrategyStep { let mut state = serializer.serialize_struct("AddressFundsFeeStrategyStep", 2)?; match self { AddressFundsFeeStrategyStep::DeductFromInput(index) => { - state.serialize_field("type", "deductFromInput")?; + state.serialize_field("$type", "deductFromInput")?; state.serialize_field("index", index)?; } AddressFundsFeeStrategyStep::ReduceOutput(index) => { - state.serialize_field("type", "reduceOutput")?; + state.serialize_field("$type", "reduceOutput")?; state.serialize_field("index", index)?; } } @@ -78,9 +78,9 @@ impl<'de> Deserialize<'de> for AddressFundsFeeStrategyStep { while let Some(key) = map.next_key::()? { match key.as_str() { - "type" => { + "$type" => { if step_type.is_some() { - return Err(de::Error::duplicate_field("type")); + return Err(de::Error::duplicate_field("$type")); } step_type = Some(map.next_value()?); } @@ -96,7 +96,7 @@ impl<'de> Deserialize<'de> for AddressFundsFeeStrategyStep { } } - let step_type = step_type.ok_or_else(|| de::Error::missing_field("type"))?; + let step_type = step_type.ok_or_else(|| de::Error::missing_field("$type"))?; let index = index.ok_or_else(|| de::Error::missing_field("index"))?; match step_type.as_str() { @@ -112,7 +112,7 @@ impl<'de> Deserialize<'de> for AddressFundsFeeStrategyStep { deserializer.deserialize_struct( "AddressFundsFeeStrategyStep", - &["type", "index"], + &["$type", "index"], StepVisitor, ) } @@ -128,7 +128,7 @@ mod tests { let json = serde_json::to_value(&step).unwrap(); assert_eq!( json, - serde_json::json!({ "type": "deductFromInput", "index": 7 }) + serde_json::json!({ "$type": "deductFromInput", "index": 7 }) ); } @@ -138,19 +138,19 @@ mod tests { let json = serde_json::to_value(&step).unwrap(); assert_eq!( json, - serde_json::json!({ "type": "reduceOutput", "index": 3 }) + serde_json::json!({ "$type": "reduceOutput", "index": 3 }) ); } #[test] fn deserializes_from_type_and_index() { let step: AddressFundsFeeStrategyStep = - serde_json::from_value(serde_json::json!({ "type": "deductFromInput", "index": 9 })) + serde_json::from_value(serde_json::json!({ "$type": "deductFromInput", "index": 9 })) .unwrap(); assert_eq!(step, AddressFundsFeeStrategyStep::DeductFromInput(9)); let step: AddressFundsFeeStrategyStep = - serde_json::from_value(serde_json::json!({ "type": "reduceOutput", "index": 2 })) + serde_json::from_value(serde_json::json!({ "$type": "reduceOutput", "index": 2 })) .unwrap(); assert_eq!(step, AddressFundsFeeStrategyStep::ReduceOutput(2)); } @@ -158,7 +158,7 @@ mod tests { #[test] fn rejects_unknown_variant() { let result: Result = - serde_json::from_value(serde_json::json!({ "type": "burn", "index": 0 })); + serde_json::from_value(serde_json::json!({ "$type": "burn", "index": 0 })); assert!(result.is_err()); } @@ -176,3 +176,83 @@ mod tests { } } } + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for AddressFundsFeeStrategyStep {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for AddressFundsFeeStrategyStep {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_address_funds_fee_strategy_step { + use super::*; + + use platform_value::platform_value; + use serde_json::json; + + #[test] + fn json_round_trip_deduct_from_input() { + use crate::serialization::JsonConvertible; + let original = AddressFundsFeeStrategyStep::DeductFromInput(7); + let json = original.to_json().expect("to_json"); + // `index` is a `u16` in the source type. JSON has only one number + // type, so the wire shape erases the U16 distinction (the value-path + // assertion below uses `7u16` explicitly to lock in the typed variant). + assert_eq!(json, json!({"$type": "deductFromInput", "index": 7})); + let recovered = AddressFundsFeeStrategyStep::from_json(json).expect("from_json"); + assert_eq!(recovered, AddressFundsFeeStrategyStep::DeductFromInput(7)); + } + + #[test] + fn json_round_trip_reduce_output() { + use crate::serialization::JsonConvertible; + let original = AddressFundsFeeStrategyStep::ReduceOutput(u16::MAX); + let json = original.to_json().expect("to_json"); + // `index` is a `u16`; JSON erases the size — see the deduct test above. + assert_eq!(json, json!({"$type": "reduceOutput", "index": u16::MAX})); + let recovered = AddressFundsFeeStrategyStep::from_json(json).expect("from_json"); + assert_eq!( + recovered, + AddressFundsFeeStrategyStep::ReduceOutput(u16::MAX) + ); + } + + #[test] + fn value_round_trip_deduct_from_input() { + use crate::serialization::ValueConvertible; + let original = AddressFundsFeeStrategyStep::DeductFromInput(7); + let value = original.to_object().expect("to_object"); + // `7u16`: explicit suffix forces `Value::U16` in the expected, matching + // the field's actual u16 type. A bare `7` would expand via + // `to_value(&7i32)` and produce `Value::I32`, which would fail — that + // distinction is exactly what JSON can't preserve but `platform_value` + // does, and what we want this test to lock in. + assert_eq!( + value, + platform_value!({"$type": "deductFromInput", "index": 7u16}) + ); + let recovered = AddressFundsFeeStrategyStep::from_object(value).expect("from_object"); + assert_eq!(recovered, AddressFundsFeeStrategyStep::DeductFromInput(7)); + } + + #[test] + fn value_round_trip_reduce_output() { + use crate::serialization::ValueConvertible; + let original = AddressFundsFeeStrategyStep::ReduceOutput(u16::MAX); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({"$type": "reduceOutput", "index": u16::MAX}) + ); + let recovered = AddressFundsFeeStrategyStep::from_object(value).expect("from_object"); + assert_eq!( + recovered, + AddressFundsFeeStrategyStep::ReduceOutput(u16::MAX) + ); + } +} diff --git a/packages/rs-dpp/src/address_funds/platform_address.rs b/packages/rs-dpp/src/address_funds/platform_address.rs index 959e1c5550a..4d7641e71f5 100644 --- a/packages/rs-dpp/src/address_funds/platform_address.rs +++ b/packages/rs-dpp/src/address_funds/platform_address.rs @@ -47,6 +47,77 @@ pub enum PlatformAddress { P2sh([u8; 20]), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for PlatformAddress {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for PlatformAddress {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::Value; + use serde_json::json; + + // `PlatformAddress` has a manual `Serialize`/`Deserialize`: it serializes + // as a 21-byte payload (1 type byte + 20 hash bytes), shown as a hex + // string in HR formats and raw bytes in non-HR. Both variants share the + // same wire shape — only the leading type byte differs. + + #[test] + fn json_round_trip_p2pkh() { + use crate::serialization::JsonConvertible; + let original = PlatformAddress::P2pkh([0xab; 20]); + let json = original.to_json().expect("to_json"); + // Type byte 0x00 (storage variant index for P2pkh) || 20 × 0xab + assert_eq!(json, json!("00abababababababababababababababababababab")); + let recovered = PlatformAddress::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_p2sh() { + use crate::serialization::JsonConvertible; + let original = PlatformAddress::P2sh([0xcd; 20]); + let json = original.to_json().expect("to_json"); + // Type byte 0x01 (storage variant index for P2sh) || 20 × 0xcd + assert_eq!(json, json!("01cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd")); + let recovered = PlatformAddress::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_p2pkh() { + use crate::serialization::ValueConvertible; + let original = PlatformAddress::P2pkh([0xab; 20]); + let value = original.to_object().expect("to_object"); + // `platform_value` is treated as non-HR by `is_human_readable()`, so + // the address serializes as raw bytes here. + let mut expected = vec![0x00]; + expected.extend_from_slice(&[0xab; 20]); + assert_eq!(value, Value::Bytes(expected)); + let recovered = PlatformAddress::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_p2sh() { + use crate::serialization::ValueConvertible; + let original = PlatformAddress::P2sh([0xcd; 20]); + let value = original.to_object().expect("to_object"); + let mut expected = vec![0x01]; + expected.extend_from_slice(&[0xcd; 20]); + assert_eq!(value, Value::Bytes(expected)); + let recovered = PlatformAddress::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} + // Custom serde impls so JSON / `platform_value` output is the canonical 21-byte // address representation (hex string in human-readable formats, raw bytes in // binary formats) — matching the wasm wrapper's serde and what consumers expect. diff --git a/packages/rs-dpp/src/address_funds/witness.rs b/packages/rs-dpp/src/address_funds/witness.rs index 65012f7838b..16efe7de0fd 100644 --- a/packages/rs-dpp/src/address_funds/witness.rs +++ b/packages/rs-dpp/src/address_funds/witness.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use bincode::de::{BorrowDecoder, Decoder}; use bincode::enc::Encoder; use bincode::error::{DecodeError, EncodeError}; @@ -13,13 +15,29 @@ pub const MAX_P2SH_SIGNATURES: usize = 17; /// The input witness data required to spend from a PlatformAddress. /// /// This enum captures the different spending patterns for P2PKH and P2SH addresses. +/// +/// Wire shape (internally tagged on `type`, camelCase variants/fields): +/// `{ "$type": "p2pkh", "signature": }` +/// `{ "$type": "p2sh", "signatures": [, ...], "redeemScript": }` +/// +/// Note: `MAX_P2SH_SIGNATURES` is enforced by the bincode `Decode` path (the +/// load-bearing wire format). The serde JSON/Value deserialize path does not +/// enforce it; downstream consumers must validate signature counts before +/// re-serializing for storage. +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, PartialEq, Ord, PartialOrd, Eq)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$type") +)] pub enum AddressWitness { /// P2PKH witness: recoverable signature only /// /// Used for spending from a Pay-to-Public-Key-Hash address. /// The public key is recovered from the signature during verification, /// saving 33 bytes per witness compared to including the public key. + #[cfg_attr(feature = "serde-conversion", serde(rename = "p2pkh"))] P2pkh { /// The recoverable ECDSA signature (65 bytes with recovery byte prefix) signature: BinaryData, //todo change to [u8;65] @@ -29,10 +47,12 @@ pub enum AddressWitness { /// Used for spending from a Pay-to-Script-Hash address (e.g., multisig). /// For a 2-of-3 multisig, signatures would be `[OP_0, sig1, sig2]` and /// redeem_script would be `OP_2 OP_3 OP_CHECKMULTISIG`. + #[cfg_attr(feature = "serde-conversion", serde(rename = "p2sh"))] P2sh { /// The signatures (may include placeholder bytes like OP_0 for CHECKMULTISIG bug) signatures: Vec, /// The redeem script that hashes to the address + #[cfg_attr(feature = "serde-conversion", serde(rename = "redeemScript"))] redeem_script: BinaryData, }, } @@ -121,123 +141,6 @@ impl<'de, C> bincode::BorrowDecode<'de, C> for AddressWitness { } } -#[cfg(feature = "serde-conversion")] -impl Serialize for AddressWitness { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - - match self { - AddressWitness::P2pkh { signature } => { - let mut state = serializer.serialize_struct("AddressWitness", 2)?; - state.serialize_field("type", "p2pkh")?; - state.serialize_field("signature", signature)?; - state.end() - } - AddressWitness::P2sh { - signatures, - redeem_script, - } => { - let mut state = serializer.serialize_struct("AddressWitness", 3)?; - state.serialize_field("type", "p2sh")?; - state.serialize_field("signatures", signatures)?; - state.serialize_field("redeemScript", redeem_script)?; - state.end() - } - } - } -} - -#[cfg(feature = "serde-conversion")] -impl<'de> Deserialize<'de> for AddressWitness { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - use serde::de::{self, MapAccess, Visitor}; - use std::fmt; - - struct AddressWitnessVisitor; - - impl<'de> Visitor<'de> for AddressWitnessVisitor { - type Value = AddressWitness; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("an AddressWitness struct") - } - - fn visit_map(self, mut map: V) -> Result - where - V: MapAccess<'de>, - { - let mut witness_type: Option = None; - let mut signature: Option = None; - let mut signatures: Option> = None; - let mut redeem_script: Option = None; - - while let Some(key) = map.next_key::()? { - match key.as_str() { - "type" => { - witness_type = Some(map.next_value()?); - } - "signature" => { - signature = Some(map.next_value()?); - } - "signatures" => { - signatures = Some(map.next_value()?); - } - "redeemScript" => { - redeem_script = Some(map.next_value()?); - } - _ => { - let _: serde::de::IgnoredAny = map.next_value()?; - } - } - } - - let witness_type = witness_type.ok_or_else(|| de::Error::missing_field("type"))?; - - match witness_type.as_str() { - "p2pkh" => { - let signature = - signature.ok_or_else(|| de::Error::missing_field("signature"))?; - Ok(AddressWitness::P2pkh { signature }) - } - "p2sh" => { - let signatures = - signatures.ok_or_else(|| de::Error::missing_field("signatures"))?; - if signatures.len() > MAX_P2SH_SIGNATURES { - return Err(de::Error::custom(format!( - "P2SH signatures count {} exceeds maximum {}", - signatures.len(), - MAX_P2SH_SIGNATURES, - ))); - } - let redeem_script = redeem_script - .ok_or_else(|| de::Error::missing_field("redeemScript"))?; - Ok(AddressWitness::P2sh { - signatures, - redeem_script, - }) - } - _ => Err(de::Error::unknown_variant( - &witness_type, - &["p2pkh", "p2sh"], - )), - } - } - } - - deserializer.deserialize_struct( - "AddressWitness", - &["type", "signature", "signatures", "redeemScript"], - AddressWitnessVisitor, - ) - } -} - impl AddressWitness { /// Generates a unique identifier for this witness based on its contents. /// @@ -731,3 +634,106 @@ mod tests { assert!(result.is_err()); } } + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for AddressWitness {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for AddressWitness {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::{platform_value, BinaryData}; + use serde_json::json; + + // `AddressWitness` has a manual Serialize/Deserialize that emits a + // `{ "$type": "p2pkh"|"p2sh", ... }` discriminator shape. `BinaryData` is + // base64-encoded in JSON (HR), and stored as `Value::Bytes` in non-HR. + + #[test] + fn json_round_trip_p2pkh_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = AddressWitness::P2pkh { + signature: BinaryData::new(vec![0xa1; 65]), + }; + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({ + "$type": "p2pkh", + "signature": "oaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaE=", + }) + ); + let recovered = AddressWitness::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_p2sh_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = AddressWitness::P2sh { + redeem_script: BinaryData::new(vec![0xb2; 30]), + signatures: vec![BinaryData::new(vec![0xc3; 65])], + }; + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({ + "$type": "p2sh", + "signatures": [ + "w8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8M=", + ], + "redeemScript": "srKysrKysrKysrKysrKysrKysrKysrKysrKysrKy", + }) + ); + let recovered = AddressWitness::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_p2pkh_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + use platform_value::Value; + let original = AddressWitness::P2pkh { + signature: BinaryData::new(vec![0xa1; 65]), + }; + let value = original.to_object().expect("to_object"); + // `BinaryData` serializes as `Value::Bytes(Vec)` in non-HR mode. + assert_eq!( + value, + platform_value!({ + "$type": "p2pkh", + "signature": Value::Bytes(vec![0xa1; 65]), + }) + ); + let recovered = AddressWitness::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_p2sh_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + use platform_value::Value; + let original = AddressWitness::P2sh { + redeem_script: BinaryData::new(vec![0xb2; 30]), + signatures: vec![BinaryData::new(vec![0xc3; 65])], + }; + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({ + "$type": "p2sh", + "signatures": [Value::Bytes(vec![0xc3; 65])], + "redeemScript": Value::Bytes(vec![0xb2; 30]), + }) + ); + let recovered = AddressWitness::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/asset_lock/mod.rs b/packages/rs-dpp/src/asset_lock/mod.rs index 3fa57ebb2c3..c27ede918a8 100644 --- a/packages/rs-dpp/src/asset_lock/mod.rs +++ b/packages/rs-dpp/src/asset_lock/mod.rs @@ -6,7 +6,8 @@ pub type PastAssetLockStateTransitionHashes = Vec>; /// An enumeration of the possible states when querying platform to get the stored state of an outpoint /// representing if the asset lock was already used or not. -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "$type")] pub enum StoredAssetLockInfo { /// The asset lock was fully consumed in the past FullyConsumed, @@ -15,3 +16,129 @@ pub enum StoredAssetLockInfo { /// The asset lock is not yet known to Platform NotPresent, } + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for StoredAssetLockInfo {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for StoredAssetLockInfo {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::{platform_value, Bytes32, Value}; + use platform_version::version::PlatformVersion; + use serde_json::json; + + fn partially_consumed_fixture() -> StoredAssetLockInfo { + let asset_lock_value = AssetLockValue::new( + 1_000_000, + vec![0xaa, 0xbb, 0xcc, 0xdd], + 500_000, + vec![Bytes32::new([0x42; 32])], + PlatformVersion::latest(), + ) + .expect("fixture"); + StoredAssetLockInfo::PartiallyConsumed(asset_lock_value) + } + + // `StoredAssetLockInfo` is internally tagged (`#[serde(tag = "$type")]`): + // unit variants serialize as `{"$type": "FullyConsumed"}`, and the + // `PartiallyConsumed` newtype variant flattens its inner `AssetLockValue` + // (itself `tag = "$formatVersion"`) up to the same wire level, giving + // `{"$type": "PartiallyConsumed", "$formatVersion": "0", ...}`. The `$` + // prefix follows the codebase rule: the discriminator is `$`-prefixed when + // other `$`-fields (here `$formatVersion`) share the same wire level. + + #[test] + fn json_round_trip_fully_consumed() { + use crate::serialization::JsonConvertible; + let original = StoredAssetLockInfo::FullyConsumed; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!({ "$type": "FullyConsumed" })); + let recovered = StoredAssetLockInfo::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_not_present() { + use crate::serialization::JsonConvertible; + let original = StoredAssetLockInfo::NotPresent; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!({ "$type": "NotPresent" })); + let recovered = StoredAssetLockInfo::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_partially_consumed() { + use crate::serialization::JsonConvertible; + let original = partially_consumed_fixture(); + let json = original.to_json().expect("to_json"); + // Internal `$type` tag sits flat alongside the inner `AssetLockValue`'s + // own `$formatVersion` tag (`tag = "$formatVersion"`). `Bytes32` is + // base64 in JSON HR, and `tx_out_script` (`Vec`) is base64 too via + // `#[json_safe_fields]`'s `serde_bytes_var` annotation. + assert_eq!( + json, + json!({ + "$type": "PartiallyConsumed", + "$formatVersion": "0", + "initial_credit_value": 1_000_000, + "tx_out_script": "qrvM3Q==", + "remaining_credit_value": 500_000, + "used_tags": ["QkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkI="], + }) + ); + let recovered = StoredAssetLockInfo::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_fully_consumed() { + use crate::serialization::ValueConvertible; + let original = StoredAssetLockInfo::FullyConsumed; + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!({ "$type": "FullyConsumed" })); + let recovered = StoredAssetLockInfo::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_not_present() { + use crate::serialization::ValueConvertible; + let original = StoredAssetLockInfo::NotPresent; + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!({ "$type": "NotPresent" })); + let recovered = StoredAssetLockInfo::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_partially_consumed() { + use crate::serialization::ValueConvertible; + let original = partially_consumed_fixture(); + let value = original.to_object().expect("to_object"); + // `Credits` (u64) → `Value::U64`. `tx_out_script` (`Vec`) → + // `Value::Bytes` via `#[json_safe_fields]`. `used_tags` → + // `Array(Vec)`. + assert_eq!( + value, + platform_value!({ + "$type": "PartiallyConsumed", + "$formatVersion": "0", + "initial_credit_value": 1_000_000u64, + "tx_out_script": Value::Bytes(vec![0xaa, 0xbb, 0xcc, 0xdd]), + "remaining_credit_value": 500_000u64, + "used_tags": [Value::Bytes32([0x42; 32])], + }) + ); + let recovered = StoredAssetLockInfo::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/asset_lock/reduced_asset_lock_value/mod.rs b/packages/rs-dpp/src/asset_lock/reduced_asset_lock_value/mod.rs index 7ccabcd9c96..ac4c17608f1 100644 --- a/packages/rs-dpp/src/asset_lock/reduced_asset_lock_value/mod.rs +++ b/packages/rs-dpp/src/asset_lock/reduced_asset_lock_value/mod.rs @@ -24,10 +24,18 @@ pub use v0::{AssetLockValueGettersV0, AssetLockValueSettersV0}; serde::Deserialize, )] #[platform_serialize(unversioned)] +#[serde(tag = "$formatVersion")] pub enum AssetLockValue { + #[serde(rename = "0")] V0(AssetLockValueV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for AssetLockValue {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for AssetLockValue {} + impl AssetLockValue { pub fn new( initial_credit_value: Credits, @@ -120,3 +128,100 @@ impl AssetLockValueSettersV0 for AssetLockValue { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::platform_value; + use platform_version::version::PlatformVersion; + use serde_json::json; + + fn fixture() -> AssetLockValue { + AssetLockValue::new( + 1_000_000, + vec![0xaa, 0xbb, 0xcc, 0xdd], + 500_000, + vec![Bytes32::new([0x42; 32])], + PlatformVersion::latest(), + ) + .expect("fixture") + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `AssetLockValue` uses the standard `tag = "$formatVersion"` + // convention. `Bytes32` is base64 in JSON HR, and `tx_out_script` + // (`Vec`) is base64 too: `#[json_safe_fields]` annotates it with + // `serde_bytes_var` (raw bytes in binary, base64 string in JSON). + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "initial_credit_value": 1_000_000, + "tx_out_script": "qrvM3Q==", + "remaining_credit_value": 500_000, + "used_tags": ["QkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkI="], + }) + ); + let recovered = AssetLockValue::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + use platform_value::Value; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `#[json_safe_fields]` annotates `tx_out_script` (`Vec`) with + // `serde_bytes_var`, so it encodes as `Value::Bytes` (raw bytes, not an + // array of `U8`). `used_tags` is `Array(Vec)`. + // `initial_credit_value` / `remaining_credit_value` are `Credits` (u64); + // in non-human-readable `Value` they stay `Value::U64`. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "initial_credit_value": 1_000_000u64, + "tx_out_script": Value::Bytes(vec![0xaa, 0xbb, 0xcc, 0xdd]), + "remaining_credit_value": 500_000u64, + "used_tags": [Value::Bytes32([0x42; 32])], + }) + ); + let recovered = AssetLockValue::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn json_large_credits_serialize_as_strings_for_js_safety() { + use crate::serialization::JsonConvertible; + // `initial_credit_value` exceeds JS `Number.MAX_SAFE_INTEGER` (2^53 - 1), + // so `#[json_safe_fields]` must emit it as a string in human-readable JSON + // to avoid silent precision loss when the value crosses into JavaScript. + // Without the attribute this serializes as a bare number and the + // assertion below fails. + let original = AssetLockValue::new( + 9_007_199_254_740_993, // 2^53 + 1, above MAX_SAFE_INTEGER + vec![0xaa, 0xbb, 0xcc, 0xdd], + 500_000, + vec![Bytes32::new([0x42; 32])], + PlatformVersion::latest(), + ) + .expect("fixture"); + let json = original.to_json().expect("to_json"); + assert_eq!(json["initial_credit_value"], json!("9007199254740993")); + // Values within the safe range stay numbers. + assert_eq!(json["remaining_credit_value"], json!(500_000)); + // And the string form round-trips back to the exact u64. + let recovered = AssetLockValue::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/asset_lock/reduced_asset_lock_value/v0/mod.rs b/packages/rs-dpp/src/asset_lock/reduced_asset_lock_value/v0/mod.rs index 6ad46de91e8..a855bafb542 100644 --- a/packages/rs-dpp/src/asset_lock/reduced_asset_lock_value/v0/mod.rs +++ b/packages/rs-dpp/src/asset_lock/reduced_asset_lock_value/v0/mod.rs @@ -1,7 +1,13 @@ use crate::fee::Credits; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use bincode::{Decode, Encode}; use platform_value::Bytes32; +// `initial_credit_value` / `remaining_credit_value` are `Credits` (u64) and can +// exceed JS's `MAX_SAFE_INTEGER`; `#[json_safe_fields]` serializes them as +// strings in human-readable JSON to avoid precision loss when crossing to JS. +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, Encode, Decode, PartialEq, serde::Serialize, serde::Deserialize)] pub struct AssetLockValueV0 { pub(super) initial_credit_value: Credits, diff --git a/packages/rs-dpp/src/block/block_info/mod.rs b/packages/rs-dpp/src/block/block_info/mod.rs index 32eccd52878..e9685f5d8e3 100644 --- a/packages/rs-dpp/src/block/block_info/mod.rs +++ b/packages/rs-dpp/src/block/block_info/mod.rs @@ -179,3 +179,68 @@ mod tests { assert_eq!(block_info, restored); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_blockinfo { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + // Distinct per-field values (not `default()`'s all-zeros) so the fixture + // catches field-name renames, field swaps, and integer-size changes. + fn fixture() -> BlockInfo { + BlockInfo { + time_ms: 1_700_000_000_000, + height: 123, + core_height: 456, + epoch: Epoch::new(7).expect("epoch"), + } + } + + #[test] + fn json_round_trip_blockinfo_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `#[serde(rename_all = "camelCase")]` → `timeMs` / `coreHeight`. + // `#[json_safe_fields]` keeps `timeMs` / `height` (u64) as numbers + // below 2^53. Nested `Epoch` serializes as `{"index": }` (`key` + // is `#[serde(skip)]`, reconstructed from `index` on deserialize). + assert_eq!( + json, + json!({ + "timeMs": 1_700_000_000_000u64, + "height": 123, + "coreHeight": 456, + "epoch": {"index": 7}, + }) + ); + let recovered = BlockInfo::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_blockinfo_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Non-HR `Value` keeps integers typed: `timeMs` / `height` are u64, + // `coreHeight` is u32, and `Epoch::index` is u16. + assert_eq!( + value, + platform_value!({ + "timeMs": 1_700_000_000_000u64, + "height": 123u64, + "coreHeight": 456u32, + "epoch": {"index": 7u16}, + }) + ); + let recovered = BlockInfo::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/block/epoch/mod.rs b/packages/rs-dpp/src/block/epoch/mod.rs index a08cda6a0e2..17852a9f1a6 100644 --- a/packages/rs-dpp/src/block/epoch/mod.rs +++ b/packages/rs-dpp/src/block/epoch/mod.rs @@ -81,6 +81,10 @@ impl Encode for Epoch { } } +// Manual Deserialize (Serialize stays derived with `serde(skip)` on `key`): +// the `key` field is derived from `index`, so deserialization must recompute +// it via `Epoch::new` to preserve the invariant rather than trusting wire +// input. Not a wire-shape customization — the shape matches the derive. impl<'de> Deserialize<'de> for Epoch { fn deserialize(deserializer: D) -> Result where @@ -113,3 +117,50 @@ impl<'de, C> BorrowDecode<'de, C> for Epoch { Epoch::new(index).map_err(|e| bincode::error::DecodeError::OtherString(e.to_string())) } } + +// --- canonical conversion trait impls (unification pass 1) --- +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for Epoch {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for Epoch {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_epoch { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + fn fixture() -> Epoch { + Epoch::new(7).expect("epoch") + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `key` is `#[serde(skip)]` and reconstructed from `index` on deserialize. + // Only `index` appears on the wire. JSON erases the u16 distinction — + // the value-path assertion below uses `7u16` to lock in the typed variant. + assert_eq!(json, json!({"index": 7})); + let recovered = Epoch::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `index` is `EpochIndex` (u16) → `Value::U16`. + assert_eq!(value, platform_value!({"index": 7u16})); + let recovered = Epoch::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/block/extended_block_info/mod.rs b/packages/rs-dpp/src/block/extended_block_info/mod.rs index dee56ab6bf2..99cb4bb1ea7 100644 --- a/packages/rs-dpp/src/block/extended_block_info/mod.rs +++ b/packages/rs-dpp/src/block/extended_block_info/mod.rs @@ -176,3 +176,94 @@ mod tests { assert_eq!(block_info, decoded); } } + +// (TODO replaced) extendedblockinfo — needs explicit fixture (no Default). + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_extendedblockinfo { + use super::*; + use crate::block::block_info::BlockInfo; + use crate::block::extended_block_info::v0::ExtendedBlockInfoV0; + use platform_value::platform_value; + use serde_json::json; + + fn fixture() -> ExtendedBlockInfo { + ExtendedBlockInfo::V0(ExtendedBlockInfoV0 { + basic_info: BlockInfo::default(), + app_hash: [0x11; 32], + quorum_hash: [0x22; 32], + block_id_hash: [0x33; 32], + proposer_pro_tx_hash: [0x44; 32], + signature: [0x55; 96], + round: 3, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `json_safe_fields` proc-macro converts u64 -> string only when above + // JS_MAX_SAFE_INTEGER. Default `BlockInfo` (zeros) stays numeric. + // 32-byte arrays are emitted as base64 strings (`appHash`, etc.); the + // 96-byte signature is also base64 (no Bytes32 path). JSON erases + // size for `round` (u32) — value-path locks `3u32` below. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "basicInfo": { + "timeMs": 0, + "height": 0, + "coreHeight": 0, + "epoch": {"index": 0}, + }, + "appHash": "ERERERERERERERERERERERERERERERERERERERERERE=", + "quorumHash": "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=", + "blockIdHash": "MzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM=", + "proposerProTxHash": "REREREREREREREREREREREREREREREREREREREREREQ=", + "signature": "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV", + "round": 3, + }) + ); + let recovered = ExtendedBlockInfo::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + use platform_value::Value; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `[u8; 32]` -> `Value::Bytes32`, `[u8; 96]` -> `Value::Bytes`, + // `round` is `u32` -> `Value::U32`. `BlockInfo` fields use their + // native typed variants (U64 / U32 / U16). + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "basicInfo": { + "timeMs": 0u64, + "height": 0u64, + "coreHeight": 0u32, + "epoch": {"index": 0u16}, + }, + "appHash": Value::Bytes32([0x11; 32]), + "quorumHash": Value::Bytes32([0x22; 32]), + "blockIdHash": Value::Bytes32([0x33; 32]), + "proposerProTxHash": Value::Bytes32([0x44; 32]), + "signature": Value::Bytes(vec![0x55; 96]), + "round": 3u32, + }) + ); + let recovered = ExtendedBlockInfo::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/block/extended_block_info/v0/mod.rs b/packages/rs-dpp/src/block/extended_block_info/v0/mod.rs index 3856cc04576..6c9cd89b658 100644 --- a/packages/rs-dpp/src/block/extended_block_info/v0/mod.rs +++ b/packages/rs-dpp/src/block/extended_block_info/v0/mod.rs @@ -21,7 +21,12 @@ pub struct ExtendedBlockInfoV0 { /// The proposer pro_tx_hash pub proposer_pro_tx_hash: [u8; 32], /// Signature - #[serde(with = "signature_serializer")] + // serde has no built-in `Serialize`/`Deserialize` for `[u8; N]` when N > 32, + // so this 96-byte field needs an explicit byte serializer. `json_safe_fields` + // injects one for byte fields, but only under `json-conversion`; this struct + // derives serde unconditionally (no feature gate), so the annotation is written + // out here to keep it compiling whenever `json-conversion` is off. + #[serde(with = "crate::serialization::serde_bytes")] pub signature: [u8; 96], /// Round pub round: u32, @@ -132,29 +137,3 @@ impl ExtendedBlockInfoV0Setters for ExtendedBlockInfoV0 { self.round = round; } } - -mod signature_serializer { - use super::*; - use serde::de::Error; - use serde::{Deserializer, Serializer}; - - pub fn serialize(signature: &[u8; 96], serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_bytes(signature) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 96], D::Error> - where - D: Deserializer<'de>, - { - let buf: Vec = Deserialize::deserialize(deserializer)?; - if buf.len() != 96 { - return Err(Error::invalid_length(buf.len(), &"array of length 96")); - } - let mut arr = [0u8; 96]; - arr.copy_from_slice(&buf); - Ok(arr) - } -} diff --git a/packages/rs-dpp/src/block/extended_epoch_info/mod.rs b/packages/rs-dpp/src/block/extended_epoch_info/mod.rs index 31efb290b9e..70b95c88ad7 100644 --- a/packages/rs-dpp/src/block/extended_epoch_info/mod.rs +++ b/packages/rs-dpp/src/block/extended_epoch_info/mod.rs @@ -121,3 +121,77 @@ mod tests { assert_eq!(info, restored); } } + +// (TODO replaced) extendedepochinfo — needs explicit fixture (no Default). + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_extendedepochinfo { + use super::*; + use crate::block::extended_epoch_info::v0::ExtendedEpochInfoV0; + use platform_value::platform_value; + use serde_json::json; + + fn fixture() -> ExtendedEpochInfo { + ExtendedEpochInfo::V0(ExtendedEpochInfoV0 { + index: 7, + first_block_time: 1_700_000_000_000, + first_block_height: 100, + first_core_block_height: 50, + fee_multiplier_permille: 1500, + protocol_version: 9, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `json_safe_fields` wraps u64 values above JS_MAX_SAFE_INTEGER as + // strings. 1_700_000_000_000 is below the threshold (~9.0e15), so it + // stays numeric. JSON erases u16/u32/u64 size — the value-path + // assertion below uses explicit suffixes. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "index": 7, + "firstBlockTime": 1_700_000_000_000_u64, + "firstBlockHeight": 100, + "firstCoreBlockHeight": 50, + "feeMultiplierPermille": 1500, + "protocolVersion": 9, + }) + ); + let recovered = ExtendedEpochInfo::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Source field types: index u16, first_block_time u64, first_block_height u64, + // first_core_block_height u32, fee_multiplier_permille u64, protocol_version u32. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "index": 7u16, + "firstBlockTime": 1_700_000_000_000_u64, + "firstBlockHeight": 100u64, + "firstCoreBlockHeight": 50u32, + "feeMultiplierPermille": 1500u64, + "protocolVersion": 9u32, + }) + ); + let recovered = ExtendedEpochInfo::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/block/finalized_epoch_info/mod.rs b/packages/rs-dpp/src/block/finalized_epoch_info/mod.rs index ba9d3f9ed83..b759e374a72 100644 --- a/packages/rs-dpp/src/block/finalized_epoch_info/mod.rs +++ b/packages/rs-dpp/src/block/finalized_epoch_info/mod.rs @@ -115,3 +115,104 @@ mod tests { assert_eq!(info, restored); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_finalizedepochinfo { + use super::*; + use crate::block::finalized_epoch_info::v0::FinalizedEpochInfoV0; + use platform_value::Identifier; + use std::collections::BTreeMap; + + fn fixture() -> FinalizedEpochInfo { + let mut block_proposers = BTreeMap::new(); + block_proposers.insert(Identifier::new([0xab; 32]), 5u64); + FinalizedEpochInfo::V0(FinalizedEpochInfoV0 { + first_block_time: 1_700_000_000_000, + first_block_height: 100, + total_blocks_in_epoch: 250, + first_core_block_height: 50, + next_epoch_start_core_block_height: 75, + total_processing_fees: 1_000_000, + total_distributed_storage_fees: 200_000, + total_created_storage_fees: 250_000, + core_block_rewards: 500_000, + block_proposers, + fee_multiplier_permille: 1500, + protocol_version: 9, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + use serde_json::json; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `block_proposers` is `BTreeMap` with the + // `json_safe_identifier_u64_map` serde adapter: in JSON HR mode, keys + // become base58-encoded `Identifier` strings and values stay numeric + // (or string for u64 above JS_MAX_SAFE_INTEGER; 5 is well below). + // All Credits / Heights are u64/u32 — JSON erases size; the value-path + // assertion uses explicit suffixes to lock the typed variants. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "firstBlockTime": 1_700_000_000_000_u64, + "firstBlockHeight": 100, + "totalBlocksInEpoch": 250, + "firstCoreBlockHeight": 50, + "nextEpochStartCoreBlockHeight": 75, + "totalProcessingFees": 1_000_000, + "totalDistributedStorageFees": 200_000, + "totalCreatedStorageFees": 250_000, + "coreBlockRewards": 500_000, + "blockProposers": { + "CZ8YUVdk7znjrUmnb5n7kgySk9yRAsQDYmyCxzfSky9t": 5, + }, + "feeMultiplierPermille": 1500, + "protocolVersion": 9, + }) + ); + let recovered = FinalizedEpochInfo::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + use platform_value::platform_value; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // In non-HR mode, `block_proposers` keeps `Value::Identifier` keys (no + // base58 stringification). Heights/Credits are u64; core heights u32; + // protocol_version u32. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "firstBlockTime": 1_700_000_000_000_u64, + "firstBlockHeight": 100u64, + "totalBlocksInEpoch": 250u64, + "firstCoreBlockHeight": 50u32, + "nextEpochStartCoreBlockHeight": 75u32, + "totalProcessingFees": 1_000_000u64, + "totalDistributedStorageFees": 200_000u64, + "totalCreatedStorageFees": 250_000u64, + "coreBlockRewards": 500_000u64, + "blockProposers": { + Identifier::new([0xab; 32]): 5u64, + }, + "feeMultiplierPermille": 1500u64, + "protocolVersion": 9u32, + }) + ); + let recovered = FinalizedEpochInfo::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/core_types/validator/mod.rs b/packages/rs-dpp/src/core_types/validator/mod.rs index f926ab7953d..b0fef4654a3 100644 --- a/packages/rs-dpp/src/core_types/validator/mod.rs +++ b/packages/rs-dpp/src/core_types/validator/mod.rs @@ -9,12 +9,23 @@ pub mod v0; /// A validator in the context of a quorum #[derive(Clone, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum Validator { /// Version 0 + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(ValidatorV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for Validator {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for Validator {} + impl ValidatorV0Getters for Validator { fn pro_tx_hash(&self) -> &ProTxHash { match self { @@ -114,3 +125,93 @@ impl ValidatorV0Setters for Validator { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::core_types::validator::v0::ValidatorV0; + use dashcore::hashes::Hash; + use dashcore::{ProTxHash, PubkeyHash}; + use platform_value::platform_value; + use serde_json::json; + + fn fixture() -> Validator { + Validator::V0(ValidatorV0 { + pro_tx_hash: ProTxHash::from_byte_array([0x11; 32]), + // Tier 4 caveat: BlsPublicKey serializes as hex in HR / bytes in non-HR, + // and a default fixture value (e.g. generator) would be deterministic + // but the Bls12381G2 (96-byte) literal is huge; we keep `None` here so + // the wire-shape stays compact while still locking down the option/Null + // representation. The dedicated BLS unit tests cover the public key + // round-trip on its own. + public_key: None, + node_ip: "127.0.0.1".to_string(), + node_id: PubkeyHash::from_byte_array([0x22; 20]), + core_port: 9999, + platform_http_port: 443, + platform_p2p_port: 26656, + is_banned: false, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `Validator` uses the standard `tag = "$formatVersion"` convention. + // Inner fields are serialized snake_case (no rename_all directive on V0). + // Sized-int fields whose JSON wire encoding loses size info: + // `core_port`/`platform_http_port`/`platform_p2p_port` (u16). The + // value-path assertion uses explicit `u16` suffixes. Hash fields + // (`pro_tx_hash` ProTxHash, `node_id` PubkeyHash) serialize as + // lowercase hex strings in HR; in non-HR they become typed + // `Value::Bytes32` / `Value::Bytes20`. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "pro_tx_hash": "1111111111111111111111111111111111111111111111111111111111111111", + "public_key": serde_json::Value::Null, + "node_ip": "127.0.0.1", + "node_id": "2222222222222222222222222222222222222222", + "core_port": 9999, + "platform_http_port": 443, + "platform_p2p_port": 26656, + "is_banned": false, + }) + ); + let recovered = Validator::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Explicit `u16` suffixes lock the port variants. Hash byte arrays + // become `Value::Bytes32` (32) and `Value::Bytes20` (20) on non-HR. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "pro_tx_hash": platform_value::Value::Bytes32([0x11; 32]), + "public_key": platform_value::Value::Null, + "node_ip": "127.0.0.1", + "node_id": platform_value::Value::Bytes20([0x22; 20]), + "core_port": 9999u16, + "platform_http_port": 443u16, + "platform_p2p_port": 26656u16, + "is_banned": false, + }) + ); + let recovered = Validator::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/core_types/validator/v0/mod.rs b/packages/rs-dpp/src/core_types/validator/v0/mod.rs index 905a28c6ed7..e8388544fee 100644 --- a/packages/rs-dpp/src/core_types/validator/v0/mod.rs +++ b/packages/rs-dpp/src/core_types/validator/v0/mod.rs @@ -23,6 +23,13 @@ pub struct ValidatorV0 { /// The proTxHash pub pro_tx_hash: ProTxHash, /// The public key share of this validator for this quorum + // `BlsPublicKey` is a dashcore type, so its serde wrapper lives in + // `serialization::dashcore::bls_pubkey` (now self-sufficient — no upstream + // dependency; accepts hex string or byte sequence through any deserializer). + #[cfg_attr( + feature = "serde-conversion", + serde(with = "crate::serialization::dashcore::bls_pubkey::option") + )] pub public_key: Option>, /// The node address pub node_ip: String, diff --git a/packages/rs-dpp/src/core_types/validator_set/mod.rs b/packages/rs-dpp/src/core_types/validator_set/mod.rs index 02754453339..e032b4bc88d 100644 --- a/packages/rs-dpp/src/core_types/validator_set/mod.rs +++ b/packages/rs-dpp/src/core_types/validator_set/mod.rs @@ -21,7 +21,11 @@ pub mod v0; /// The validator set is only slightly different from a quorum as it does not contain non valid /// members #[derive(Clone, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] #[cfg_attr( feature = "core-types-serialization", derive(Encode, Decode, PlatformDeserialize, PlatformSerialize), @@ -29,9 +33,16 @@ pub mod v0; )] pub enum ValidatorSet { /// Version 0 + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(ValidatorSetV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for ValidatorSet {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for ValidatorSet {} + impl Display for ValidatorSet { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -115,3 +126,168 @@ impl ValidatorSetV0Setters for ValidatorSet { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::core_types::validator::v0::ValidatorV0; + use crate::core_types::validator_set::v0::ValidatorSetV0; + use dashcore::blsful::{Bls12381G2Impl, SecretKey}; + use dashcore::hashes::Hash; + use dashcore::{ProTxHash, PubkeyHash, QuorumHash}; + use platform_value::{platform_value, Value}; + use rand::rngs::StdRng; + use rand::SeedableRng; + use serde_json::json; + use std::collections::BTreeMap; + + /// Build the fixture with deterministic BLS keys (seeded RNG) plus the + /// derived public-key wire forms — both as `serde_json::Value` (HR: 96-char + /// hex) and `platform_value::Value` (non-HR: typed-bytes variant). The BLS + /// keys ARE deterministic, but the 96-char hex / 48-byte literal is too + /// unwieldy to inline as a string constant, so we interpolate the actual + /// `to_value`/`to_json` of the same pubkey objects we put in the fixture. + /// (The dedicated `serialization::dashcore::bls_pubkey` unit tests + /// independently cover the pubkey round-trip.) + fn build_fixture() -> ( + ValidatorSet, + BlsPublicKey, + BlsPublicKey, + ) { + let mut rng = StdRng::seed_from_u64(42); + let pro_tx_hash = ProTxHash::from_byte_array([0x11; 32]); + let validator_pubkey = SecretKey::::random(&mut rng).public_key(); + let validator_v0 = ValidatorV0 { + pro_tx_hash, + public_key: Some(validator_pubkey), + node_ip: "127.0.0.1".to_string(), + node_id: PubkeyHash::from_byte_array([0x22; 20]), + core_port: 9999, + platform_http_port: 443, + platform_p2p_port: 26656, + is_banned: false, + }; + let mut members = BTreeMap::new(); + members.insert(pro_tx_hash, validator_v0); + + let threshold_pubkey = SecretKey::::random(&mut rng).public_key(); + let set = ValidatorSet::V0(ValidatorSetV0 { + quorum_hash: QuorumHash::from_byte_array([0x33; 32]), + quorum_index: Some(7), + core_height: 1234, + members, + threshold_public_key: threshold_pubkey, + }); + (set, validator_pubkey, threshold_pubkey) + } + + // Deterministic compressed-G1 BLS pubkeys for the seeded `StdRng(42)` + // fixture, spelled out as hex (96 chars = 48 bytes) so the wire format is + // visible and verifiable in-place — neither path interpolates `to_json` / + // `to_value` of the object under test. JSON (HR) uses the hex string + // directly; the non-HR `Value` form is the *same* 48 bytes decoded into an + // `Array` of `U8` (blstrs serializes the pubkey through a `u8` tuple, which + // platform_value collects as `Array[U8]` — NOT a typed `Bytes` variant). + const VALIDATOR_PK_HEX: &str = + "85d81dd12c73cca83f7d1bf8b78fadb695e3a2bc21d53b35ff2f74eaa28c6e163c98d3d5f9bb7252b4d836e484c7cc60"; + const THRESHOLD_PK_HEX: &str = + "969c5d5873f49aa994c5f6a850924ca1840c4ad1791aaaecd90093d4a5c0c3799f2d98540f5366cfa0a33f143fd69263"; + + fn bls_pubkey_value(hex: &str) -> Value { + Value::Array( + (0..hex.len() / 2) + .map(|i| Value::U8(u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).expect("hex"))) + .collect(), + ) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let (original, ..) = build_fixture(); + let json = original.to_json().expect("to_json"); + + // BLS public keys serialize as 96-char compressed-G1 hex on the HR + // path. The keys are deterministic (seeded `StdRng(42)`), so the exact + // hex is baked in below — the on-wire format is directly visible and + // verifiable in-place. The rest of the wire structure is fully + // asserted too: `tag = "$formatVersion"` convention, snake_case inner + // fields (no `rename_all`), `BTreeMap` members emitted as a struct + // keyed by ProTxHash hex, hash fields as lowercase hex strings, + // sized-int fields preserved. The inner Validator's own + // `$formatVersion` tag (now applied) appears alongside its other + // snake_case fields. (`serialization::dashcore::bls_pubkey` additionally + // has its own dedicated BLS round-trip tests.) + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "quorum_hash": "3333333333333333333333333333333333333333333333333333333333333333", + "quorum_index": 7, + "core_height": 1234, + "members": { + "1111111111111111111111111111111111111111111111111111111111111111": { + // Note: members are typed `BTreeMap` (not + // `BTreeMap`), so the inner is the bare V0 + // struct without its enum's `$formatVersion` tag. + "pro_tx_hash": "1111111111111111111111111111111111111111111111111111111111111111", + "public_key": VALIDATOR_PK_HEX, + "node_ip": "127.0.0.1", + "node_id": "2222222222222222222222222222222222222222", + "core_port": 9999, + "platform_http_port": 443, + "platform_p2p_port": 26656, + "is_banned": false, + } + }, + "threshold_public_key": THRESHOLD_PK_HEX, + }) + ); + let recovered = ValidatorSet::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let (original, ..) = build_fixture(); + let value = original.to_object().expect("to_object"); + + // BLS pubkeys on the non-HR path = the 48 compressed-G1 bytes as an + // `Array` of `U8`, decoded from the *same* hex consts the JSON test + // uses (one source of truth, no interpolation of the object under + // test). Hash fields (`Bytes32`/`Bytes20`) are explicit. The + // `platform_value!` macro doesn't accept non-string keys, so the + // members map (keyed by a typed-bytes `ProTxHash`) is built by hand. + // Note: members are typed `BTreeMap` (not + // `BTreeMap`), so the inner is the bare V0 + // struct without its enum's `$formatVersion` tag. + let inner_validator = platform_value!({ + "pro_tx_hash": Value::Bytes32([0x11; 32]), + "public_key": bls_pubkey_value(VALIDATOR_PK_HEX), + "node_ip": "127.0.0.1", + "node_id": Value::Bytes20([0x22; 20]), + "core_port": 9999u16, + "platform_http_port": 443u16, + "platform_p2p_port": 26656u16, + "is_banned": false, + }); + let members_value = Value::Map(vec![(Value::Bytes32([0x11; 32]), inner_validator)]); + let expected = platform_value!({ + "$formatVersion": "0", + "quorum_hash": Value::Bytes32([0x33; 32]), + "quorum_index": 7u32, + "core_height": 1234u32, + "members": members_value, + "threshold_public_key": bls_pubkey_value(THRESHOLD_PK_HEX), + }); + assert_eq!(value, expected); + let recovered = ValidatorSet::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/core_types/validator_set/v0/mod.rs b/packages/rs-dpp/src/core_types/validator_set/v0/mod.rs index 916050ddab6..f00993628f6 100644 --- a/packages/rs-dpp/src/core_types/validator_set/v0/mod.rs +++ b/packages/rs-dpp/src/core_types/validator_set/v0/mod.rs @@ -33,6 +33,13 @@ pub struct ValidatorSetV0 { /// The list of masternodes pub members: BTreeMap, /// The threshold quorum public key + // `BlsPublicKey` is a dashcore type, so its serde wrapper lives in + // `serialization::dashcore::bls_pubkey` (now self-sufficient — no upstream + // dependency; accepts hex string or byte sequence through any deserializer). + #[cfg_attr( + feature = "serde-conversion", + serde(with = "crate::serialization::dashcore::bls_pubkey") + )] pub threshold_public_key: BlsPublicKey, } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs index 9b37f451a1f..3e96fee2b43 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs @@ -62,3 +62,112 @@ mod tests { assert_eq!(config, restored); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; + + /// `default_most_restrictive` already populates ~25 inner fields with + /// non-default values (decimals=8, base_supply=100_000, etc.) — exactly + /// what we want for the round-trip structural check below. + fn fixture() -> TokenConfiguration { + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()) + } + + /// Tier 3: TokenConfiguration embeds ~25 fields, several of which are + /// themselves versioned enums (TokenConfigurationConvention, + /// ChangeControlRules x7, TokenKeepsHistoryRules, TokenDistributionRules, + /// TokenMarketplaceRules). An inline wire-shape literal would be 200+ + /// lines and would re-test the nested types' own assertions. Instead we + /// assert only the envelope (top-level keys + `$formatVersion`) and trust + /// the nested types' tests for inner shape correctness. + #[test] + fn json_round_trip_with_envelope_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Envelope check: format version + top-level keys present. + assert_eq!( + json.get("$formatVersion").and_then(|v| v.as_str()), + Some("0") + ); + for key in [ + "conventions", + "conventionsChangeRules", + "baseSupply", + "maxSupply", + "keepsHistory", + "startAsPaused", + "allowTransferToFrozenBalance", + "maxSupplyChangeRules", + "distributionRules", + "marketplaceRules", + "manualMintingRules", + "manualBurningRules", + "freezeRules", + "unfreezeRules", + "destroyFrozenFundsRules", + "emergencyActionRules", + "mainControlGroup", + "mainControlGroupCanBeModified", + "description", + ] { + assert!( + json.get(key).is_some(), + "expected top-level key {:?} in JSON envelope", + key + ); + } + let recovered = TokenConfiguration::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_envelope_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Same envelope-only check on the platform_value side. + let map = value.as_map().expect("value is a Map"); + let has_key = |k: &str| { + map.iter() + .any(|(key, _)| matches!(key, platform_value::Value::Text(t) if t == k)) + }; + assert!(has_key("$formatVersion")); + for key in [ + "conventions", + "conventionsChangeRules", + "baseSupply", + "maxSupply", + "keepsHistory", + "startAsPaused", + "allowTransferToFrozenBalance", + "maxSupplyChangeRules", + "distributionRules", + "marketplaceRules", + "manualMintingRules", + "manualBurningRules", + "freezeRules", + "unfreezeRules", + "destroyFrozenFundsRules", + "emergencyActionRules", + "mainControlGroup", + "mainControlGroupCanBeModified", + "description", + ] { + assert!( + has_key(key), + "expected top-level key {:?} in Value envelope", + key + ); + } + let recovered = TokenConfiguration::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs index 2d759f669aa..8aa6b50f751 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs @@ -293,6 +293,7 @@ pub enum TokenConfigurationPresetFeatures { /// This abstraction allows users to choose between common control configurations /// ranging from immutable tokens to fully administrator-controlled assets. #[derive(Serialize, Deserialize, Decode, Encode, Debug, Clone, PartialEq, Eq, PartialOrd)] +#[serde(rename_all = "camelCase")] pub struct TokenConfigurationPreset { /// Defines the set of capabilities enabled in this preset (e.g., whether minting, /// burning, freezing, or emergency actions are permitted). @@ -307,6 +308,113 @@ pub struct TokenConfigurationPreset { /// operations, or performing emergency control (depending on the selected feature set). pub action_taker: AuthorizedActionTakers, } + +// Manual impls because the preset types are flat (not versioned V0/V1). +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenConfigurationPresetFeatures {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenConfigurationPresetFeatures {} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenConfigurationPreset {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenConfigurationPreset {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_preset { + use super::*; + use crate::serialization::{JsonConvertible, ValueConvertible}; + use platform_value::platform_value; + use serde_json::json; + + fn fixture() -> TokenConfigurationPreset { + TokenConfigurationPreset { + features: TokenConfigurationPresetFeatures::WithAllAdvancedActions, + action_taker: AuthorizedActionTakers::Group(7), + } + } + + #[test] + fn preset_json_round_trip_with_full_wire_shape() { + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `features` is a unit-only enum (bare PascalCase string); + // `actionTaker` uses AuthorizedActionTakers' internally-tagged shape. + // `position` is u16 — JSON erases the size; the value path locks it. + assert_eq!( + json, + json!({ + "features": "WithAllAdvancedActions", + "actionTaker": {"$type": "group", "position": 7}, + }) + ); + let recovered = TokenConfigurationPreset::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn preset_value_round_trip_with_full_wire_shape() { + let original = fixture(); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({ + "features": "WithAllAdvancedActions", + "actionTaker": {"$type": "group", "position": 7u16}, + }) + ); + let recovered = TokenConfigurationPreset::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn preset_features_round_trips_all_variants() { + let cases = [ + ( + TokenConfigurationPresetFeatures::MostRestrictive, + "MostRestrictive", + ), + ( + TokenConfigurationPresetFeatures::WithOnlyEmergencyAction, + "WithOnlyEmergencyAction", + ), + ( + TokenConfigurationPresetFeatures::WithMintingAndBurningActions, + "WithMintingAndBurningActions", + ), + ( + TokenConfigurationPresetFeatures::WithAllAdvancedActions, + "WithAllAdvancedActions", + ), + ( + TokenConfigurationPresetFeatures::WithExtremeActions, + "WithExtremeActions", + ), + ]; + for (original, expected) in cases { + let json_v = original.to_json().expect("to_json"); + assert_eq!(json_v, json!(expected)); + assert_eq!( + TokenConfigurationPresetFeatures::from_json(json_v).expect("from_json"), + original + ); + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!(expected)); + assert_eq!( + TokenConfigurationPresetFeatures::from_object(value).expect("from_object"), + original + ); + } + } +} + impl TokenConfigurationPreset { pub fn default_main_control_group_can_be_modified(&self) -> AuthorizedActionTakers { match self.features { diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_convention/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_convention/mod.rs index cb08ee480ab..6c247bd5721 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_convention/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_convention/mod.rs @@ -42,3 +42,85 @@ impl fmt::Display for TokenConfigurationConvention { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0; + use crate::data_contract::associated_token::token_configuration_localization::v0::TokenConfigurationLocalizationV0; + use crate::data_contract::associated_token::token_configuration_localization::TokenConfigurationLocalization; + use std::collections::BTreeMap; + + fn fixture() -> TokenConfigurationConvention { + let mut localizations = BTreeMap::new(); + localizations.insert( + "en".to_string(), + TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "Token".to_string(), + plural_form: "Tokens".to_string(), + }), + ); + TokenConfigurationConvention::V0(TokenConfigurationConventionV0 { + localizations, + decimals: 8, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + use serde_json::json; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `decimals` is `u8`; JSON erases the size — value-path locks `8u8` below. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "localizations": { + "en": { + "$formatVersion": "0", + "shouldCapitalize": true, + "singularForm": "Token", + "pluralForm": "Tokens", + } + }, + "decimals": 8, + }) + ); + let recovered = TokenConfigurationConvention::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + use platform_value::platform_value; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `decimals` is u8 → `Value::U8`. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "localizations": { + "en": { + "$formatVersion": "0", + "shouldCapitalize": true, + "singularForm": "Token", + "pluralForm": "Tokens", + } + }, + "decimals": 8u8, + }) + ); + let recovered = TokenConfigurationConvention::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs index 7f315f44a0f..8729a4567e8 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs @@ -28,7 +28,10 @@ use std::fmt; #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), - serde(rename_all = "camelCase") + serde( + into = "TokenConfigurationChangeItemRepr", + from = "TokenConfigurationChangeItemRepr" + ) )] pub enum TokenConfigurationChangeItem { #[default] @@ -65,6 +68,87 @@ pub enum TokenConfigurationChangeItem { MarketplaceTradeModeAdminGroup(AuthorizedActionTakers), MainControlGroup(Option), } + +// Internal-`$type` serde shape via a struct-variant Repr (the outer enum mixes +// AuthorizedActionTakers/struct variants with primitive/Option/unit variants +// that serde cannot auto-internal-tag). Unit -> `{"$type":"..."}`; data -> +// `{"$type":"...","value":...}`. `MaxSupply` gains the json_safe protection it +// previously lacked (Option above MAX_SAFE_INTEGER -> string in HR JSON; +// Content-safe — never emits u128). `MainControlGroup` is u16 (always JS-safe). +// The macro keeps the Repr and both `From` impls in lockstep with one variant +// list — avoiding the "update Serialize AND Deserialize" maintenance trap. +#[cfg(feature = "serde-conversion")] +macro_rules! token_configuration_change_item_repr { + ( + unit: $unit:ident, + $( $variant:ident : $ty:ty $(, with = $with:literal)? );* $(;)? + ) => { + #[derive(Serialize, Deserialize)] + #[serde(tag = "$type", rename_all = "camelCase")] + enum TokenConfigurationChangeItemRepr { + $unit, + $( $variant { + $(#[serde(with = $with)])? + value: $ty, + } ),* + } + + impl From for TokenConfigurationChangeItemRepr { + fn from(item: TokenConfigurationChangeItem) -> Self { + match item { + TokenConfigurationChangeItem::$unit => Self::$unit, + $( TokenConfigurationChangeItem::$variant(value) => Self::$variant { value }, )* + } + } + } + + impl From for TokenConfigurationChangeItem { + fn from(repr: TokenConfigurationChangeItemRepr) -> Self { + match repr { + TokenConfigurationChangeItemRepr::$unit => Self::$unit, + $( TokenConfigurationChangeItemRepr::$variant { value } => Self::$variant(value), )* + } + } + } + }; +} + +#[cfg(feature = "serde-conversion")] +token_configuration_change_item_repr! { + unit: TokenConfigurationNoChange, + Conventions: TokenConfigurationConvention; + ConventionsControlGroup: AuthorizedActionTakers; + ConventionsAdminGroup: AuthorizedActionTakers; + MaxSupply: Option, with = "crate::serialization::json_safe_option_u64"; + MaxSupplyControlGroup: AuthorizedActionTakers; + MaxSupplyAdminGroup: AuthorizedActionTakers; + PerpetualDistribution: Option; + PerpetualDistributionControlGroup: AuthorizedActionTakers; + PerpetualDistributionAdminGroup: AuthorizedActionTakers; + NewTokensDestinationIdentity: Option; + NewTokensDestinationIdentityControlGroup: AuthorizedActionTakers; + NewTokensDestinationIdentityAdminGroup: AuthorizedActionTakers; + MintingAllowChoosingDestination: bool; + MintingAllowChoosingDestinationControlGroup: AuthorizedActionTakers; + MintingAllowChoosingDestinationAdminGroup: AuthorizedActionTakers; + ManualMinting: AuthorizedActionTakers; + ManualMintingAdminGroup: AuthorizedActionTakers; + ManualBurning: AuthorizedActionTakers; + ManualBurningAdminGroup: AuthorizedActionTakers; + Freeze: AuthorizedActionTakers; + FreezeAdminGroup: AuthorizedActionTakers; + Unfreeze: AuthorizedActionTakers; + UnfreezeAdminGroup: AuthorizedActionTakers; + DestroyFrozenFunds: AuthorizedActionTakers; + DestroyFrozenFundsAdminGroup: AuthorizedActionTakers; + EmergencyAction: AuthorizedActionTakers; + EmergencyActionAdminGroup: AuthorizedActionTakers; + MarketplaceTradeMode: TokenTradeMode; + MarketplaceTradeModeControlGroup: AuthorizedActionTakers; + MarketplaceTradeModeAdminGroup: AuthorizedActionTakers; + MainControlGroup: Option; +} + impl TokenConfigurationChangeItem { pub fn payload_serialization(&self) -> Result>, ProtocolError> { Ok(match self { @@ -750,3 +834,76 @@ mod tests { assert!(dbg.contains("ManualMinting")); } } + +// --- canonical conversion trait impls (unification pass 1) --- +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenConfigurationChangeItem {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenConfigurationChangeItem {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + /// Non-default variant (`MaxSupply(Some(...))`) with a non-zero inner amount + /// so the wire-shape assertion catches a silent variant flip or inner-zero + /// on round-trip. + fn fixture() -> TokenConfigurationChangeItem { + TokenConfigurationChangeItem::MaxSupply(Some(123_456_789u64)) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `TokenConfigurationChangeItem` is internally tagged via its Repr: + // `{ "$type":"maxSupply", "value": }` where `Some(x)` -> `x`. + // `MaxSupply` now carries json_safe (Option -> string above + // MAX_SAFE_INTEGER in HR JSON); here 123_456_789 stays numeric. + // The value-path assertion uses `123_456_789u64` to lock in `Value::U64`. + assert_eq!(json, json!({"$type": "maxSupply", "value": 123_456_789u64})); + let recovered = TokenConfigurationChangeItem::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `123_456_789u64`: explicit suffix forces `Value::U64`, matching + // `TokenAmount`'s u64 type. Bare integer would expand to `Value::I32`. + assert_eq!( + value, + platform_value!({"$type": "maxSupply", "value": 123_456_789u64}) + ); + let recovered = TokenConfigurationChangeItem::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn json_large_max_supply_serializes_as_string_for_js_safety() { + use crate::serialization::JsonConvertible; + // `MaxSupply` previously had NO json_safe annotation — a latent JS + // precision bug. Above `Number.MAX_SAFE_INTEGER` it must now serialize + // as a string in human-readable JSON. This pins that the Repr's + // json_safe_option_u64 survives the internal-tag Content buffer. + let original = TokenConfigurationChangeItem::MaxSupply(Some(9_007_199_254_740_993)); // 2^53 + 1 + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({"$type": "maxSupply", "value": "9007199254740993"}) + ); + let recovered = TokenConfigurationChangeItem::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_localization/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_localization/mod.rs index 2ac12e320de..0ec8f738abf 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_localization/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_localization/mod.rs @@ -42,3 +42,60 @@ impl fmt::Display for TokenConfigurationLocalization { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::data_contract::associated_token::token_configuration_localization::v0::TokenConfigurationLocalizationV0; + + fn fixture() -> TokenConfigurationLocalization { + TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "Token".to_string(), + plural_form: "Tokens".to_string(), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + use serde_json::json; + let original = fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "shouldCapitalize": true, + "singularForm": "Token", + "pluralForm": "Tokens", + }) + ); + let recovered = TokenConfigurationLocalization::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + use platform_value::platform_value; + let original = fixture(); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "shouldCapitalize": true, + "singularForm": "Token", + "pluralForm": "Tokens", + }) + ); + let recovered = TokenConfigurationLocalization::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs b/packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs index d355b062634..9261cc24dac 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs @@ -29,6 +29,10 @@ pub enum TokenDistributionType { /// - `PreProgrammed(Identifier)`: A predefined recipient for a scheduled distribution. /// - `Perpetual(TokenDistributionResolvedRecipient)`: A resolved recipient for an ongoing distribution. #[derive(Serialize, Deserialize, Decode, Encode, Debug, Clone, PartialEq, Eq, PartialOrd)] +#[serde( + into = "TokenDistributionTypeWithResolvedRecipientRepr", + from = "TokenDistributionTypeWithResolvedRecipientRepr" +)] pub enum TokenDistributionTypeWithResolvedRecipient { /// A scheduled distribution with a known recipient. PreProgrammed(Identifier), @@ -37,12 +41,56 @@ pub enum TokenDistributionTypeWithResolvedRecipient { Perpetual(TokenDistributionResolvedRecipient), } +// Internal-`$type` serde shape with a uniform `value` payload (single-payload +// variants). Bincode `Encode`/`Decode` on the outer enum are untouched. +#[derive(Serialize, Deserialize)] +#[serde(tag = "$type", rename_all = "camelCase")] +enum TokenDistributionTypeWithResolvedRecipientRepr { + PreProgrammed { + value: Identifier, + }, + Perpetual { + value: TokenDistributionResolvedRecipient, + }, +} + +impl From + for TokenDistributionTypeWithResolvedRecipientRepr +{ + fn from(m: TokenDistributionTypeWithResolvedRecipient) -> Self { + match m { + TokenDistributionTypeWithResolvedRecipient::PreProgrammed(value) => { + Self::PreProgrammed { value } + } + TokenDistributionTypeWithResolvedRecipient::Perpetual(value) => { + Self::Perpetual { value } + } + } + } +} + +impl From + for TokenDistributionTypeWithResolvedRecipient +{ + fn from(r: TokenDistributionTypeWithResolvedRecipientRepr) -> Self { + match r { + TokenDistributionTypeWithResolvedRecipientRepr::PreProgrammed { value } => { + Self::PreProgrammed(value) + } + TokenDistributionTypeWithResolvedRecipientRepr::Perpetual { value } => { + Self::Perpetual(value) + } + } + } +} + /// Contains information about a specific token distribution instance. /// /// - `PreProgrammed(TimestampMillis, Identifier)`: A scheduled distribution with a timestamp and recipient. /// - `Perpetual(RewardDistributionMoment, RewardDistributionMoment, TokenDistributionResolvedRecipient)`: /// A perpetual distribution with previous and next distribution moments, along with the resolved recipient. #[derive(Serialize, Deserialize, Decode, Encode, Debug, Clone, PartialEq, Eq, PartialOrd)] +#[serde(into = "TokenDistributionInfoRepr", from = "TokenDistributionInfoRepr")] pub enum TokenDistributionInfo { /// A pre-programmed token distribution set for a specific time. /// Contains the scheduled timestamp and the recipient’s identifier. @@ -54,6 +102,55 @@ pub enum TokenDistributionInfo { Perpetual(RewardDistributionMoment, TokenDistributionResolvedRecipient), } +// Internal-`$type` serde shape with named fields (multi-field variants). +// `TimestampMillis` (u64) carries `json_safe_u64` on the Repr field — JS-safe +// (string above MAX_SAFE_INTEGER in HR JSON), Content-safe (never u128). +// `RewardDistributionMoment` is itself internally tagged; bincode untouched. +#[derive(Serialize, Deserialize)] +#[serde(tag = "$type", rename_all = "camelCase")] +enum TokenDistributionInfoRepr { + PreProgrammed { + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::serialization::json_safe_u64") + )] + timestamp: TimestampMillis, + identity: Identifier, + }, + Perpetual { + moment: RewardDistributionMoment, + recipient: TokenDistributionResolvedRecipient, + }, +} + +impl From for TokenDistributionInfoRepr { + fn from(m: TokenDistributionInfo) -> Self { + match m { + TokenDistributionInfo::PreProgrammed(timestamp, identity) => Self::PreProgrammed { + timestamp, + identity, + }, + TokenDistributionInfo::Perpetual(moment, recipient) => { + Self::Perpetual { moment, recipient } + } + } + } +} + +impl From for TokenDistributionInfo { + fn from(r: TokenDistributionInfoRepr) -> Self { + match r { + TokenDistributionInfoRepr::PreProgrammed { + timestamp, + identity, + } => Self::PreProgrammed(timestamp, identity), + TokenDistributionInfoRepr::Perpetual { moment, recipient } => { + Self::Perpetual(moment, recipient) + } + } + } +} + impl From for TokenDistributionTypeWithResolvedRecipient { fn from(info: TokenDistributionInfo) -> Self { match info { @@ -107,3 +204,205 @@ pub struct TokenDistributionKey { pub recipient: TokenDistributionRecipient, pub distribution_type: TokenDistributionType, } + +// --- canonical conversion trait impls (unification pass 1) --- +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenDistributionTypeWithResolvedRecipient {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenDistributionTypeWithResolvedRecipient {} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenDistributionInfo {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenDistributionInfo {} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenDistributionType {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenDistributionType {} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenDistributionKey {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenDistributionKey {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_token_distribution_type_and_key { + use super::*; + use crate::serialization::{JsonConvertible, ValueConvertible}; + use platform_value::{platform_value, Value}; + use serde_json::json; + + #[test] + fn token_distribution_type_round_trips_all_variants() { + // Unit-only enum: serde default emits bare PascalCase strings on both + // wire formats. + let cases = [ + (TokenDistributionType::PreProgrammed, "PreProgrammed"), + (TokenDistributionType::Perpetual, "Perpetual"), + ]; + for (original, expected) in cases { + let json_v = original.to_json().expect("to_json"); + assert_eq!(json_v, json!(expected)); + assert_eq!( + TokenDistributionType::from_json(json_v).expect("from_json"), + original + ); + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!(expected)); + assert_eq!( + TokenDistributionType::from_object(value).expect("from_object"), + original + ); + } + } + + fn key_fixture() -> TokenDistributionKey { + TokenDistributionKey { + token_id: Identifier::new([0x42; 32]), + recipient: TokenDistributionRecipient::EvonodesByParticipation, + distribution_type: TokenDistributionType::Perpetual, + } + } + + #[test] + fn token_distribution_key_json_round_trip_with_full_wire_shape() { + let original = key_fixture(); + let json = original.to_json().expect("to_json"); + // `recipient` uses TokenDistributionRecipient's custom internally-tagged + // shape; `token_id` renders as base58. Field names are snake_case (no + // rename_all on this struct — internal key type, not user-authored JSON). + assert_eq!( + json, + json!({ + "token_id": "5TeWSsjg2gbxCyWVniXeCmwM7UtHTCK7svzJr5xYJzHf", + "recipient": {"$type": "evonodesByParticipation"}, + "distribution_type": "Perpetual", + }) + ); + let recovered = TokenDistributionKey::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn token_distribution_key_value_round_trip_with_full_wire_shape() { + let original = key_fixture(); + let value = original.to_object().expect("to_object"); + let expected = Value::Map(vec![ + ( + Value::Text("token_id".to_string()), + Value::Identifier([0x42; 32]), + ), + ( + Value::Text("recipient".to_string()), + Value::Map(vec![( + Value::Text("$type".to_string()), + Value::Text("evonodesByParticipation".to_string()), + )]), + ), + ( + Value::Text("distribution_type".to_string()), + Value::Text("Perpetual".to_string()), + ), + ]); + assert_eq!(value, expected); + let recovered = TokenDistributionKey::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_token_distribution_info { + use super::*; + use platform_value::{Identifier, Value}; + use serde_json::json; + + /// Non-default `PreProgrammed` variant with distinct timestamp + identifier + /// so the wire-shape assertion catches a silent variant flip or inner-zero + /// on round-trip. + fn fixture() -> TokenDistributionInfo { + TokenDistributionInfo::PreProgrammed(1_700_000_000_000, Identifier::new([0x42; 32])) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Internally tagged with named fields: + // `{ "$type":"preProgrammed", "timestamp":, "identity": }`. + // `TimestampMillis` is `u64`; JSON erases the size — see the value- + // path assertion which uses `Value::U64` to lock it in. + // `Identifier` is rendered as the base58-encoded string in JSON. + assert_eq!( + json, + json!({ + "$type": "preProgrammed", + "timestamp": 1_700_000_000_000u64, + "identity": "5TeWSsjg2gbxCyWVniXeCmwM7UtHTCK7svzJr5xYJzHf", + }) + ); + let recovered = TokenDistributionInfo::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Internally tagged with named fields. `Identifier`'s Serialize emits + // the typed `Value::Identifier` variant (NOT `Value::Bytes32`), which + // survives serde's internal-tag Content buffer. Built by hand so the + // typed-bytes variant is preserved exactly. + let expected = Value::Map(vec![ + ( + Value::Text("$type".to_string()), + Value::Text("preProgrammed".to_string()), + ), + ( + Value::Text("timestamp".to_string()), + Value::U64(1_700_000_000_000), + ), + ( + Value::Text("identity".to_string()), + Value::Identifier([0x42; 32]), + ), + ]); + assert_eq!(value, expected); + let recovered = TokenDistributionInfo::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_perpetual_variant() { + use crate::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionResolvedRecipient; + use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; + use crate::serialization::JsonConvertible; + // The Perpetual variant (moment + resolved recipient) complements the + // PreProgrammed wire-shape test above; pin its `$type` discriminator and + // full round-trip so a silent variant flip is caught. + let original = TokenDistributionInfo::Perpetual( + RewardDistributionMoment::BlockBasedMoment(500), + TokenDistributionResolvedRecipient::Identity(Identifier::new([0x77; 32])), + ); + let json = original.to_json().expect("to_json"); + assert_eq!(json["$type"], json!("perpetual")); + let recovered = TokenDistributionInfo::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_distribution_rules/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_distribution_rules/mod.rs index 77357413d0f..5390a106116 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_distribution_rules/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_distribution_rules/mod.rs @@ -1,5 +1,7 @@ #[cfg(feature = "json-conversion")] use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; use bincode::{Decode, Encode}; use derive_more::From; use serde::{Deserialize, Serialize}; @@ -8,6 +10,7 @@ pub mod accessors; pub mod v0; #[cfg_attr(feature = "json-conversion", derive(JsonConvertible))] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] #[derive(Serialize, Deserialize, Encode, Decode, Debug, Clone, PartialEq, Eq, From)] #[serde(tag = "$formatVersion")] pub enum TokenDistributionRules { @@ -27,3 +30,110 @@ impl fmt::Display for TokenDistributionRules { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::data_contract::associated_token::token_distribution_rules::v0::TokenDistributionRulesV0; + use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0; + use crate::data_contract::change_control_rules::ChangeControlRules; + use platform_value::{platform_value, Identifier, Value}; + use serde_json::json; + + /// Non-default values per inner field (set destination_identity to a + /// specific identifier and `minting_allow_choosing_destination` to true) + /// so the wire-shape assertion catches silent zero-out / flip on round-trip. + fn fixture() -> TokenDistributionRules { + let ccr = || ChangeControlRules::V0(ChangeControlRulesV0::default()); + TokenDistributionRules::V0(TokenDistributionRulesV0 { + perpetual_distribution: None, + perpetual_distribution_rules: ccr(), + pre_programmed_distribution: None, + new_tokens_destination_identity: Some(Identifier::new([0x42; 32])), + new_tokens_destination_identity_rules: ccr(), + minting_allow_choosing_destination: true, + minting_allow_choosing_destination_rules: ccr(), + change_direct_purchase_pricing_rules: ccr(), + }) + } + + fn default_ccr_json() -> serde_json::Value { + json!({ + "$formatVersion": "0", + "authorizedToMakeChange": {"$type": "noOne"}, + "adminActionTakers": {"$type": "noOne"}, + "changingAuthorizedActionTakersToNoOneAllowed": false, + "changingAdminActionTakersToNoOneAllowed": false, + "selfChangingAdminActionTakersAllowed": false, + }) + } + + fn default_ccr_value() -> Value { + platform_value!({ + "$formatVersion": "0", + "authorizedToMakeChange": {"$type": "noOne"}, + "adminActionTakers": {"$type": "noOne"}, + "changingAuthorizedActionTakersToNoOneAllowed": false, + "changingAdminActionTakersToNoOneAllowed": false, + "selfChangingAdminActionTakersAllowed": false, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `Identifier` renders as base58 string in JSON. None Options become + // `null`. Inner `ChangeControlRules` round-trips its own envelope. + // No sized integers in this fixture. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "perpetualDistribution": null, + "perpetualDistributionRules": default_ccr_json(), + "preProgrammedDistribution": null, + "newTokensDestinationIdentity": "5TeWSsjg2gbxCyWVniXeCmwM7UtHTCK7svzJr5xYJzHf", + "newTokensDestinationIdentityRules": default_ccr_json(), + "mintingAllowChoosingDestination": true, + "mintingAllowChoosingDestinationRules": default_ccr_json(), + "changeDirectPurchasePricingRules": default_ccr_json(), + }) + ); + let recovered = TokenDistributionRules::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `Identifier`'s Serialize emits `Value::Identifier`; interpolating the + // Identifier through `platform_value!{...}` runs Serialize and produces + // the typed variant. None becomes `Value::Null`. + let id = Identifier::new([0x42; 32]); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "perpetualDistribution": Value::Null, + "perpetualDistributionRules": default_ccr_value(), + "preProgrammedDistribution": Value::Null, + "newTokensDestinationIdentity": id, + "newTokensDestinationIdentityRules": default_ccr_value(), + "mintingAllowChoosingDestination": true, + "mintingAllowChoosingDestinationRules": default_ccr_value(), + "changeDirectPurchasePricingRules": default_ccr_value(), + }) + ); + let recovered = TokenDistributionRules::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_keeps_history_rules/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_keeps_history_rules/mod.rs index d90a6a5d504..8fd9bb0e2c2 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_keeps_history_rules/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_keeps_history_rules/mod.rs @@ -1,5 +1,7 @@ #[cfg(feature = "json-conversion")] use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; use bincode::{Decode, Encode}; use derive_more::From; use serde::{Deserialize, Serialize}; @@ -8,6 +10,7 @@ pub mod accessors; pub mod v0; #[cfg_attr(feature = "json-conversion", derive(JsonConvertible))] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] #[derive(Serialize, Deserialize, Encode, Decode, Debug, Clone, Copy, PartialEq, Eq, From)] #[serde(tag = "$formatVersion")] pub enum TokenKeepsHistoryRules { @@ -18,6 +21,71 @@ pub enum TokenKeepsHistoryRules { use crate::data_contract::associated_token::token_keeps_history_rules::v0::TokenKeepsHistoryRulesV0; use std::fmt; +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + + fn fixture() -> TokenKeepsHistoryRules { + TokenKeepsHistoryRules::V0(TokenKeepsHistoryRulesV0 { + keeps_transfer_history: true, + keeps_freezing_history: false, + keeps_minting_history: true, + keeps_burning_history: false, + keeps_direct_pricing_history: true, + keeps_direct_purchase_history: false, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + use serde_json::json; + let original = fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "keepsTransferHistory": true, + "keepsFreezingHistory": false, + "keepsMintingHistory": true, + "keepsBurningHistory": false, + "keepsDirectPricingHistory": true, + "keepsDirectPurchaseHistory": false, + }) + ); + let recovered = TokenKeepsHistoryRules::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + use platform_value::platform_value; + let original = fixture(); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "keepsTransferHistory": true, + "keepsFreezingHistory": false, + "keepsMintingHistory": true, + "keepsBurningHistory": false, + "keepsDirectPricingHistory": true, + "keepsDirectPurchaseHistory": false, + }) + ); + let recovered = TokenKeepsHistoryRules::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} + impl fmt::Display for TokenKeepsHistoryRules { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_marketplace_rules/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_marketplace_rules/mod.rs index 506a36b7e40..f9f99d8b8eb 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_marketplace_rules/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_marketplace_rules/mod.rs @@ -1,5 +1,7 @@ #[cfg(feature = "json-conversion")] use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; use bincode::{Decode, Encode}; use derive_more::From; use serde::{Deserialize, Serialize}; @@ -8,6 +10,7 @@ pub mod accessors; pub mod v0; #[cfg_attr(feature = "json-conversion", derive(JsonConvertible))] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] #[derive(Serialize, Deserialize, Encode, Decode, Debug, Clone, PartialEq, Eq, From)] #[serde(tag = "$formatVersion")] pub enum TokenMarketplaceRules { @@ -27,3 +30,90 @@ impl fmt::Display for TokenMarketplaceRules { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::data_contract::associated_token::token_marketplace_rules::v0::{ + TokenMarketplaceRulesV0, TokenTradeMode, + }; + use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; + use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0; + use crate::data_contract::change_control_rules::ChangeControlRules; + use platform_value::platform_value; + use serde_json::json; + + /// Non-default values per inner field (non-NoOne action takers + flipped + /// bool flags) so the wire-shape assertion catches silent zero-out / flip. + fn fixture() -> TokenMarketplaceRules { + TokenMarketplaceRules::V0(TokenMarketplaceRulesV0 { + trade_mode: TokenTradeMode::NotTradeable, + trade_mode_change_rules: ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::MainGroup, + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: true, + }), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `TokenTradeMode` is an externally-tagged enum with a single unit + // variant (`NotTradeable`) that serializes as a bare string. + // `ChangeControlRules` is a versioned enum with `tag = "$formatVersion"`, + // and `AuthorizedActionTakers` unit variants serialize as bare strings. + // No sized integers in this fixture — only Text + Bool. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "tradeMode": "NotTradeable", + "tradeModeChangeRules": { + "$formatVersion": "0", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "mainGroup"}, + "changingAuthorizedActionTakersToNoOneAllowed": true, + "changingAdminActionTakersToNoOneAllowed": false, + "selfChangingAdminActionTakersAllowed": true, + }, + }) + ); + let recovered = TokenMarketplaceRules::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // No sized integers here — Text + Bool only. Both wire formats agree. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "tradeMode": "NotTradeable", + "tradeModeChangeRules": { + "$formatVersion": "0", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "mainGroup"}, + "changingAuthorizedActionTakersToNoOneAllowed": true, + "changingAdminActionTakersToNoOneAllowed": false, + "selfChangingAdminActionTakersAllowed": true, + }, + }) + ); + let recovered = TokenMarketplaceRules::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_marketplace_rules/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_marketplace_rules/v0/mod.rs index ad1f8d76e76..a1716ad19bd 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_marketplace_rules/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_marketplace_rules/v0/mod.rs @@ -18,6 +18,15 @@ pub enum TokenTradeMode { NotTradeable, } +// Manual impls because TokenTradeMode is a flat enum (not versioned V0/V1). +// Unit-only enum — serde default emits the bare "NotTradeable" string on both +// wire formats (pinned by TokenMarketplaceRules' wire-shape tests). +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenTradeMode {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenTradeMode {} + #[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Serialize, Deserialize, Decode, Encode, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/mod.rs index a2125199604..fcdd0a83852 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/mod.rs @@ -45,6 +45,7 @@ pub const MAX_POL_A_PARAM: i64 = 256; #[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd)] +#[serde(tag = "$type", rename_all = "camelCase")] pub enum DistributionFunction { /// Emits a constant (fixed) number of tokens for every period. /// @@ -1295,3 +1296,107 @@ mod tests { } } } + +// --- canonical conversion trait impls (unification pass 1) --- +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for DistributionFunction {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for DistributionFunction {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + // `DistributionFunction` is internally tagged (`#[serde(tag = "$type", + // rename_all = "camelCase")]`). Round-trip tests cover one variant per + // shape: + // - struct variant with named fields (`FixedAmount` → `fixedAmount`) + // - struct variant with multiple named fields (`Random` → `random`) + // - newtype-of-map variant (`Stepwise` → `stepwise`, flattened) + // The other struct variants share the `FixedAmount`/`Random` shape. + + #[test] + fn json_round_trip_fixed_amount() { + use crate::serialization::JsonConvertible; + let original = DistributionFunction::FixedAmount { amount: 1_000 }; + let json = original.to_json().expect("to_json"); + // Internally-tagged struct variant → `{"$type":"fixedAmount", }`. + // `TokenAmount` is `u64`; JSON erases the size. + assert_eq!(json, json!({ "$type": "fixedAmount", "amount": 1_000 })); + let recovered = DistributionFunction::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_random() { + use crate::serialization::JsonConvertible; + let original = DistributionFunction::Random { min: 10, max: 100 }; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!({ "$type": "random", "min": 10, "max": 100 })); + let recovered = DistributionFunction::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_stepwise() { + use crate::serialization::JsonConvertible; + // `Stepwise(BTreeMap)` is a newtype-of-map variant. + // Internal tagging flattens it: the `"type"` discriminator sits + // alongside the map's numeric-string keys (u64 keys can never collide + // with `"type"`). Matches the convention's "no data wrapper" rule. + let original = + DistributionFunction::Stepwise(std::collections::BTreeMap::from([(0, 100), (100, 50)])); + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!({ "$type": "stepwise", "0": 100, "100": 50 })); + let recovered = DistributionFunction::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_stepwise() { + use crate::serialization::ValueConvertible; + let original = + DistributionFunction::Stepwise(std::collections::BTreeMap::from([(0, 100), (100, 50)])); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({ "$type": "stepwise", "0": 100u64, "100": 50u64 }) + ); + let recovered = DistributionFunction::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_fixed_amount() { + use crate::serialization::ValueConvertible; + let original = DistributionFunction::FixedAmount { amount: 1_000 }; + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({ "$type": "fixedAmount", "amount": 1_000u64 }) + ); + let recovered = DistributionFunction::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_random() { + use crate::serialization::ValueConvertible; + let original = DistributionFunction::Random { min: 10, max: 100 }; + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({ "$type": "random", "min": 10u64, "max": 100u64 }) + ); + let recovered = DistributionFunction::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_recipient.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_recipient.rs index f4479e3e5c7..9cb14dd2c3a 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_recipient.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_recipient.rs @@ -9,20 +9,17 @@ use serde::{Deserialize, Serialize}; use std::fmt; #[derive( - Serialize, - Deserialize, - Decode, - Encode, - PlatformSerialize, - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Default, + Decode, Encode, PlatformSerialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Default, )] #[platform_serialize(unversioned)] +// Custom `Serialize` / `Deserialize` below — `derive(Serialize, Deserialize)` +// can't produce the desired flat wire shape because the `Identity` variant +// wraps `Identifier` (serializes as a base58 string, not a map), so internal +// tagging doesn't apply. The custom impl emits a flat +// `{"$type": ..., "identity": ...}` shape with a synthesized field name (same +// pattern as `ResourceVoteChoice` / `AuthorizedActionTakers`). Bincode +// `Encode` / `Decode` derives are untouched (consensus binary format is +// unaffected). pub enum TokenDistributionRecipient { /// Distribute to the contract Owner #[default] @@ -34,6 +31,103 @@ pub enum TokenDistributionRecipient { EvonodesByParticipation, } +impl Serialize for TokenDistributionRecipient { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeMap; + match self { + TokenDistributionRecipient::ContractOwner => { + let mut m = serializer.serialize_map(Some(1))?; + m.serialize_entry("$type", "contractOwner")?; + m.end() + } + TokenDistributionRecipient::Identity(id) => { + let mut m = serializer.serialize_map(Some(2))?; + m.serialize_entry("$type", "identity")?; + m.serialize_entry("identity", id)?; + m.end() + } + TokenDistributionRecipient::EvonodesByParticipation => { + let mut m = serializer.serialize_map(Some(1))?; + m.serialize_entry("$type", "evonodesByParticipation")?; + m.end() + } + } + } +} + +impl<'de> Deserialize<'de> for TokenDistributionRecipient { + fn deserialize>(deserializer: D) -> Result { + use serde::de::{self, MapAccess, Visitor}; + + struct V; + + impl<'de> Visitor<'de> for V { + type Value = TokenDistributionRecipient; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + // Mention the old shape: contract JSON authored before + // 4.0.0-beta.4 used bare strings / externally-tagged maps, and + // this message is the only hint users get on ingest failure. + f.write_str( + "TokenDistributionRecipient as a map with a `type` discriminator, \ + e.g. {\"type\": \"contractOwner\"} or {\"type\": \"identity\", \"identity\": \"\"} \ + (the pre-4.0.0-beta.4 shapes \"ContractOwner\" / {\"Identity\": \"\"} are no longer accepted)", + ) + } + + fn visit_map>(self, mut map: A) -> Result { + let mut variant: Option = None; + let mut identity: Option = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "$type" => { + if variant.is_some() { + return Err(de::Error::duplicate_field("$type")); + } + variant = Some(map.next_value()?); + } + "identity" => { + if identity.is_some() { + return Err(de::Error::duplicate_field("identity")); + } + identity = Some(map.next_value()?); + } + _ => { + let _: serde::de::IgnoredAny = map.next_value()?; + } + } + } + + let variant = variant.ok_or_else(|| de::Error::missing_field("$type"))?; + match variant.as_str() { + "contractOwner" => Ok(TokenDistributionRecipient::ContractOwner), + "identity" => { + let id = identity.ok_or_else(|| de::Error::missing_field("identity"))?; + Ok(TokenDistributionRecipient::Identity(id)) + } + "evonodesByParticipation" => { + Ok(TokenDistributionRecipient::EvonodesByParticipation) + } + other => Err(de::Error::unknown_variant( + other, + &["contractOwner", "identity", "evonodesByParticipation"], + )), + } + } + } + + deserializer.deserialize_map(V) + } +} + +// Manual impls because TokenDistributionRecipient is a flat enum (not versioned V0/V1). +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenDistributionRecipient {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenDistributionRecipient {} + impl TokenDistributionRecipient { /// Simple resolve matches the contract owner but does not try to resolve the evonodes pub fn simple_resolve_with_distribution_type( @@ -95,19 +189,13 @@ pub type TokenDistributionWeight = u64; // pub total_blocks: u64, // } -#[derive( - Serialize, - Deserialize, - Decode, - Encode, - PlatformSerialize, - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, -)] +#[derive(Decode, Encode, PlatformSerialize, Debug, Clone, PartialEq, Eq, PartialOrd)] #[platform_serialize(unversioned)] +// Custom `Serialize` / `Deserialize` below — every variant wraps `Identifier` +// (a base58 string in JSON, not a map), so serde's internal tagging can't +// auto-derive. The custom impl emits a flat `{"$type": ..., "identity": ...}` +// shape (same pattern as `TokenDistributionRecipient` above). Bincode +// `Encode` / `Decode` derives are untouched. pub enum TokenDistributionResolvedRecipient { /// Distribute to a single identity ContractOwnerIdentity(Identifier), @@ -117,6 +205,91 @@ pub enum TokenDistributionResolvedRecipient { Evonode(Identifier), } +impl Serialize for TokenDistributionResolvedRecipient { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeMap; + let (variant, id) = match self { + TokenDistributionResolvedRecipient::ContractOwnerIdentity(id) => { + ("contractOwnerIdentity", id) + } + TokenDistributionResolvedRecipient::Identity(id) => ("identity", id), + TokenDistributionResolvedRecipient::Evonode(id) => ("evonode", id), + }; + let mut m = serializer.serialize_map(Some(2))?; + m.serialize_entry("$type", variant)?; + m.serialize_entry("identity", id)?; + m.end() + } +} + +impl<'de> Deserialize<'de> for TokenDistributionResolvedRecipient { + fn deserialize>(deserializer: D) -> Result { + use serde::de::{self, MapAccess, Visitor}; + + struct V; + + impl<'de> Visitor<'de> for V { + type Value = TokenDistributionResolvedRecipient; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str( + "TokenDistributionResolvedRecipient as a map with a `type` discriminator, \ + e.g. {\"type\": \"identity\", \"identity\": \"\"} \ + (the pre-4.0.0-beta.4 externally-tagged {\"Identity\": \"\"} shape is no longer accepted)", + ) + } + + fn visit_map>(self, mut map: A) -> Result { + let mut variant: Option = None; + let mut identity: Option = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "$type" => { + if variant.is_some() { + return Err(de::Error::duplicate_field("$type")); + } + variant = Some(map.next_value()?); + } + "identity" => { + if identity.is_some() { + return Err(de::Error::duplicate_field("identity")); + } + identity = Some(map.next_value()?); + } + _ => { + let _: serde::de::IgnoredAny = map.next_value()?; + } + } + } + + let variant = variant.ok_or_else(|| de::Error::missing_field("$type"))?; + let id = identity.ok_or_else(|| de::Error::missing_field("identity"))?; + match variant.as_str() { + "contractOwnerIdentity" => Ok( + TokenDistributionResolvedRecipient::ContractOwnerIdentity(id), + ), + "identity" => Ok(TokenDistributionResolvedRecipient::Identity(id)), + "evonode" => Ok(TokenDistributionResolvedRecipient::Evonode(id)), + other => Err(de::Error::unknown_variant( + other, + &["contractOwnerIdentity", "identity", "evonode"], + )), + } + } + } + + deserializer.deserialize_map(V) + } +} + +// Manual impls because TokenDistributionResolvedRecipient is a flat enum (not versioned V0/V1). +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenDistributionResolvedRecipient {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenDistributionResolvedRecipient {} + impl From for TokenDistributionRecipient { fn from(value: TokenDistributionResolvedRecipient) -> Self { match value { @@ -489,3 +662,139 @@ mod tests { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::serialization::{JsonConvertible, ValueConvertible}; + use platform_value::{platform_value, Value}; + use serde_json::json; + + fn id() -> Identifier { + Identifier::from([0x42u8; 32]) + } + + const ID_B58: &str = "5TeWSsjg2gbxCyWVniXeCmwM7UtHTCK7svzJr5xYJzHf"; + + /// Per-variant wire-shape coverage — the custom Serialize/Deserialize pair + /// must stay in sync when variants are added. + #[test] + fn recipient_json_round_trip_with_full_wire_shape_all_variants() { + let cases = vec![ + ( + TokenDistributionRecipient::ContractOwner, + json!({"$type": "contractOwner"}), + ), + ( + TokenDistributionRecipient::Identity(id()), + json!({"$type": "identity", "identity": ID_B58}), + ), + ( + TokenDistributionRecipient::EvonodesByParticipation, + json!({"$type": "evonodesByParticipation"}), + ), + ]; + for (original, expected) in cases { + let json_v = original.to_json().expect("to_json"); + assert_eq!(json_v, expected, "json wire shape for {original}"); + let recovered = TokenDistributionRecipient::from_json(json_v).expect("from_json"); + assert_eq!(original, recovered); + } + } + + #[test] + fn recipient_value_round_trip_with_full_wire_shape_all_variants() { + // `identity` round-trips as the typed `Value::Identifier` variant. + let cases = vec![ + ( + TokenDistributionRecipient::ContractOwner, + platform_value!({"$type": "contractOwner"}), + ), + ( + TokenDistributionRecipient::Identity(id()), + Value::Map(vec![ + ( + Value::Text("$type".to_string()), + Value::Text("identity".to_string()), + ), + ( + Value::Text("identity".to_string()), + Value::Identifier([0x42; 32]), + ), + ]), + ), + ( + TokenDistributionRecipient::EvonodesByParticipation, + platform_value!({"$type": "evonodesByParticipation"}), + ), + ]; + for (original, expected) in cases { + let value = original.to_object().expect("to_object"); + assert_eq!(value, expected, "value wire shape for {original}"); + let recovered = TokenDistributionRecipient::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + } + + #[test] + fn resolved_recipient_json_round_trip_with_full_wire_shape_all_variants() { + let cases = vec![ + ( + TokenDistributionResolvedRecipient::ContractOwnerIdentity(id()), + json!({"$type": "contractOwnerIdentity", "identity": ID_B58}), + ), + ( + TokenDistributionResolvedRecipient::Identity(id()), + json!({"$type": "identity", "identity": ID_B58}), + ), + ( + TokenDistributionResolvedRecipient::Evonode(id()), + json!({"$type": "evonode", "identity": ID_B58}), + ), + ]; + for (original, expected) in cases { + let json_v = original.to_json().expect("to_json"); + assert_eq!(json_v, expected, "json wire shape for {original}"); + let recovered = + TokenDistributionResolvedRecipient::from_json(json_v).expect("from_json"); + assert_eq!(original, recovered); + } + } + + #[test] + fn resolved_recipient_value_round_trip_all_variants() { + let cases = vec![ + ( + TokenDistributionResolvedRecipient::ContractOwnerIdentity(id()), + "contractOwnerIdentity", + ), + ( + TokenDistributionResolvedRecipient::Identity(id()), + "identity", + ), + (TokenDistributionResolvedRecipient::Evonode(id()), "evonode"), + ]; + for (original, expected_tag) in cases { + let value = original.to_object().expect("to_object"); + let expected = Value::Map(vec![ + ( + Value::Text("$type".to_string()), + Value::Text(expected_tag.to_string()), + ), + ( + Value::Text("identity".to_string()), + Value::Identifier([0x42; 32]), + ), + ]); + assert_eq!(value, expected, "value wire shape for {original}"); + let recovered = + TokenDistributionResolvedRecipient::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/mod.rs index 7bfd175e50f..8ed10179900 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/mod.rs @@ -2,6 +2,8 @@ use crate::data_contract::associated_token::token_perpetual_distribution::v0::To use crate::errors::ProtocolError; #[cfg(feature = "json-conversion")] use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; use bincode::{Decode, Encode}; use derive_more::From; use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize}; @@ -16,6 +18,7 @@ pub mod reward_distribution_type; pub mod v0; #[cfg_attr(feature = "json-conversion", derive(JsonConvertible))] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] #[derive( Serialize, Deserialize, @@ -46,3 +49,81 @@ impl fmt::Display for TokenPerpetualDistribution { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + use crate::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient; + use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; + use crate::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; + use platform_value::platform_value; + use serde_json::json; + + /// Non-default values (interval=1000, amount=100, ContractOwner) so the + /// wire-shape assertion catches any silent zero-out / variant flip. + fn fixture() -> TokenPerpetualDistribution { + TokenPerpetualDistribution::V0(TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 1000, + function: DistributionFunction::FixedAmount { amount: 100 }, + }, + distribution_recipient: TokenDistributionRecipient::ContractOwner, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `RewardDistributionType` and `DistributionFunction` are externally + // tagged enums (no `#[serde(tag = "...")]`), so struct variants + // serialize as `{ "VariantName": { ...fields... } }`. `interval` is + // `u64` (BlockHeightInterval); `amount` is `u64` (TokenAmount); JSON + // erases the size — the value-path assertion below uses `1000u64` / + // `100u64` to lock in `Value::U64`. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "distributionType": { + "$type": "blockBasedDistribution", + "interval": 1000, + "function": { "$type": "fixedAmount", "amount": 100 }, + }, + "distributionRecipient": {"$type": "contractOwner"}, + }) + ); + let recovered = TokenPerpetualDistribution::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `1000u64` / `100u64`: explicit suffix forces `Value::U64`, matching + // the `BlockHeightInterval` / `TokenAmount` aliases (both u64). + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "distributionType": { + "$type": "blockBasedDistribution", + "interval": 1000u64, + "function": { "$type": "fixedAmount", "amount": 100u64 }, + }, + "distributionRecipient": {"$type": "contractOwner"}, + }) + ); + let recovered = TokenPerpetualDistribution::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs index 1ecb92904d5..bbdc9bf342a 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_moment/mod.rs @@ -24,6 +24,14 @@ use crate::ProtocolError; PartialOrd, )] #[platform_serialize(unversioned)] +// serde routes through `RewardDistributionMomentRepr` to get internal `type` +// tagging (`{"$type":"blockBasedMoment","value":N}`). The outer enum keeps its +// tuple variants for the Rust/bincode API; `Encode`/`Decode` are independent of +// serde, so the consensus binary path is unchanged by this reshape. +#[serde( + into = "RewardDistributionMomentRepr", + from = "RewardDistributionMomentRepr" +)] pub enum RewardDistributionMoment { /// The reward was distributed at a block height BlockBasedMoment(BlockHeight), @@ -33,6 +41,146 @@ pub enum RewardDistributionMoment { EpochBasedMoment(EpochIndex), } +// Internal-`$type` serde shape with a uniform `value` payload. This struct-variant +// helper auto-derives the internal tagging the tuple-variant outer enum cannot. +// `json_safe_u64` keeps block/time values JS-safe (string above +// `Number.MAX_SAFE_INTEGER`) in human-readable JSON; no effect on `Value` or +// bincode. `EpochIndex` is `u16` — always JS-safe, no annotation needed. +#[derive(Serialize, Deserialize)] +#[serde(tag = "$type", rename_all = "camelCase")] +// Variant names mirror the outer enum (they drive the `$type` discriminator); +// the shared `BasedMoment` postfix is intentional. +#[allow(clippy::enum_variant_names)] +enum RewardDistributionMomentRepr { + BlockBasedMoment { + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::serialization::json_safe_u64") + )] + value: BlockHeight, + }, + TimeBasedMoment { + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::serialization::json_safe_u64") + )] + value: TimestampMillis, + }, + EpochBasedMoment { + value: EpochIndex, + }, +} + +impl From for RewardDistributionMomentRepr { + fn from(m: RewardDistributionMoment) -> Self { + match m { + RewardDistributionMoment::BlockBasedMoment(value) => Self::BlockBasedMoment { value }, + RewardDistributionMoment::TimeBasedMoment(value) => Self::TimeBasedMoment { value }, + RewardDistributionMoment::EpochBasedMoment(value) => Self::EpochBasedMoment { value }, + } + } +} + +impl From for RewardDistributionMoment { + fn from(r: RewardDistributionMomentRepr) -> Self { + match r { + RewardDistributionMomentRepr::BlockBasedMoment { value } => { + Self::BlockBasedMoment(value) + } + RewardDistributionMomentRepr::TimeBasedMoment { value } => Self::TimeBasedMoment(value), + RewardDistributionMomentRepr::EpochBasedMoment { value } => { + Self::EpochBasedMoment(value) + } + } + } +} +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for RewardDistributionMoment {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for RewardDistributionMoment {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::serialization::{JsonConvertible, ValueConvertible}; + use platform_value::platform_value; + use serde_json::json; + + /// Per-variant wire-shape coverage. `BlockHeight`/`TimestampMillis` are + /// u64 behind `json_safe_u64` (numbers below 2^53, strings above); JSON + /// erases the size — the value-path assertions lock the typed variants + /// (`u64` for block/time, `u16` for `EpochIndex`). + #[test] + fn json_round_trip_with_full_wire_shape_all_variants() { + let cases = vec![ + ( + RewardDistributionMoment::BlockBasedMoment(123_456), + json!({"$type": "blockBasedMoment", "value": 123_456}), + ), + ( + RewardDistributionMoment::TimeBasedMoment(1_700_000_000_000u64), + json!({"$type": "timeBasedMoment", "value": 1_700_000_000_000u64}), + ), + ( + RewardDistributionMoment::EpochBasedMoment(42), + json!({"$type": "epochBasedMoment", "value": 42}), + ), + ]; + for (original, expected) in cases { + let json_v = original.to_json().expect("to_json"); + assert_eq!(json_v, expected, "json wire shape for {original:?}"); + let recovered = RewardDistributionMoment::from_json(json_v).expect("from_json"); + assert_eq!(original, recovered); + } + } + + #[test] + fn value_round_trip_with_full_wire_shape_all_variants() { + let cases = vec![ + ( + RewardDistributionMoment::BlockBasedMoment(123_456), + platform_value!({"$type": "blockBasedMoment", "value": 123_456u64}), + ), + ( + RewardDistributionMoment::TimeBasedMoment(1_700_000_000_000u64), + platform_value!({"$type": "timeBasedMoment", "value": 1_700_000_000_000u64}), + ), + ( + RewardDistributionMoment::EpochBasedMoment(42), + platform_value!({"$type": "epochBasedMoment", "value": 42u16}), + ), + ]; + for (original, expected) in cases { + let value = original.to_object().expect("to_object"); + assert_eq!(value, expected, "value wire shape for {original:?}"); + let recovered = RewardDistributionMoment::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + } + + #[test] + fn json_large_block_height_serializes_as_string_for_js_safety() { + // Above `Number.MAX_SAFE_INTEGER` (2^53 - 1) → must serialize as a + // string in human-readable JSON so JS doesn't silently lose precision. + // This pins that `json_safe_u64` survives the `into/from = Repr` path + // (the Repr's `value` field carries the `serde(with = json_safe_u64)`). + let original = RewardDistributionMoment::BlockBasedMoment(9_007_199_254_740_993); // 2^53 + 1 + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({"$type": "blockBasedMoment", "value": "9007199254740993"}) + ); + let recovered = RewardDistributionMoment::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } +} + impl RewardDistributionMoment { /// Checks if two `RewardDistributionMoment`s are of the same type. pub fn same_type(&self, other: &Self) -> bool { diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs index ecc652a783d..0949f96d16a 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/mod.rs @@ -15,6 +15,7 @@ use crate::ProtocolError; #[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Serialize, Deserialize, Decode, Encode, Debug, Clone, PartialEq, Eq, PartialOrd)] +#[serde(tag = "$type", rename_all = "camelCase")] pub enum RewardDistributionType { /// An amount of tokens is emitted every n blocks. /// The start and end are included if set. @@ -567,3 +568,110 @@ impl fmt::Display for RewardDistributionType { } } } + +// --- canonical conversion trait impls (unification pass 1) --- +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for RewardDistributionType {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for RewardDistributionType {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + // Externally tagged enum: each variant becomes `{: {}}`. + // Inner `function: DistributionFunction` is itself externally tagged. + // Round-trip covers one variant per interval-type to lock in the typed + // sizes (`u64` for block/timestamp, `u16` for epoch). + + #[test] + fn json_round_trip_block_based() { + use crate::serialization::JsonConvertible; + let original = RewardDistributionType::BlockBasedDistribution { + interval: 100, + function: DistributionFunction::FixedAmount { amount: 50 }, + }; + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({ + "$type": "blockBasedDistribution", + "interval": 100, + "function": { "$type": "fixedAmount", "amount": 50 } + }) + ); + let recovered = RewardDistributionType::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_epoch_based() { + use crate::serialization::JsonConvertible; + let original = RewardDistributionType::EpochBasedDistribution { + interval: 7, + function: DistributionFunction::FixedAmount { amount: 1_000 }, + }; + let json = original.to_json().expect("to_json"); + // `EpochInterval` is `u16` but JSON erases the size. + assert_eq!( + json, + json!({ + "$type": "epochBasedDistribution", + "interval": 7, + "function": { "$type": "fixedAmount", "amount": 1_000 } + }) + ); + let recovered = RewardDistributionType::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_block_based() { + use crate::serialization::ValueConvertible; + let original = RewardDistributionType::BlockBasedDistribution { + interval: 100, + function: DistributionFunction::FixedAmount { amount: 50 }, + }; + let value = original.to_object().expect("to_object"); + // `BlockHeightInterval` is `u64`. `TokenAmount` is `u64`. + assert_eq!( + value, + platform_value!({ + "$type": "blockBasedDistribution", + "interval": 100u64, + "function": { "$type": "fixedAmount", "amount": 50u64 } + }) + ); + let recovered = RewardDistributionType::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_epoch_based() { + use crate::serialization::ValueConvertible; + let original = RewardDistributionType::EpochBasedDistribution { + interval: 7, + function: DistributionFunction::FixedAmount { amount: 1_000 }, + }; + let value = original.to_object().expect("to_object"); + // `EpochInterval` is `u16` → `Value::U16`. + assert_eq!( + value, + platform_value!({ + "$type": "epochBasedDistribution", + "interval": 7u16, + "function": { "$type": "fixedAmount", "amount": 1_000u64 } + }) + ); + let recovered = RewardDistributionType::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_pre_programmed_distribution/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_pre_programmed_distribution/mod.rs index 4e1141141c0..508c3fb3ad7 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_pre_programmed_distribution/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_pre_programmed_distribution/mod.rs @@ -1,6 +1,8 @@ use crate::data_contract::associated_token::token_pre_programmed_distribution::v0::TokenPreProgrammedDistributionV0; #[cfg(feature = "json-conversion")] use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; use bincode::{Decode, Encode}; use derive_more::From; use serde::{Deserialize, Serialize}; @@ -11,6 +13,7 @@ pub mod accessors; pub mod v0; #[cfg_attr(feature = "json-conversion", derive(JsonConvertible))] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] #[derive(Serialize, Deserialize, Encode, Decode, Debug, Clone, PartialEq, Eq, From)] #[serde(tag = "$formatVersion")] pub enum TokenPreProgrammedDistribution { @@ -27,3 +30,101 @@ impl fmt::Display for TokenPreProgrammedDistribution { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::data_contract::associated_token::token_pre_programmed_distribution::v0::TokenPreProgrammedDistributionV0; + use platform_value::{Identifier, Value}; + use serde_json::json; + use std::collections::BTreeMap; + + /// Non-default fixture with two distinct timestamps and two recipients per + /// timestamp so the wire-shape assertion catches silent map-flatten / + /// key-swap on round-trip. + fn fixture() -> TokenPreProgrammedDistribution { + let mut early = BTreeMap::new(); + early.insert(Identifier::new([0xab; 32]), 1000u64); + early.insert(Identifier::new([0xcd; 32]), 2000u64); + + let mut late = BTreeMap::new(); + late.insert(Identifier::new([0xef; 32]), 3000u64); + + let mut distributions = BTreeMap::new(); + distributions.insert(1_700_000_000_000u64, early); + distributions.insert(1_800_000_000_000u64, late); + TokenPreProgrammedDistribution::V0(TokenPreProgrammedDistributionV0 { distributions }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `distributions` is `BTreeMap>`. + // JSON requires string keys so the u64 timestamps render as quoted strings + // ("1700000000000") and the Identifier keys render as base58 strings. + // Inner amounts are `u64`; JSON erases the size — the value-path assertion + // below uses `1000u64` etc. to lock in `Value::U64`. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "distributions": { + "1700000000000": { + "CZ8YUVdk7znjrUmnb5n7kgySk9yRAsQDYmyCxzfSky9t": 1000, + "ErNbLjU6E8tSbZH3REsMeTDP3Z8G52k6YedWwvBpAJ7v": 2000, + }, + "1800000000000": { + "H9ceCyJSLGz9LdnJFPxbYDTKLxH6yC5yYXHpvqiBZd5x": 3000, + }, + }, + }) + ); + let recovered = TokenPreProgrammedDistribution::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // platform_value preserves typed keys: the outer `BTreeMap` + // emits `Value::U64` keys (NOT stringified); the inner + // `BTreeMap` emits `Value::Identifier` keys. Inner + // values are `Value::U64` because `TokenAmount` is u64. We construct + // the expected map directly because `platform_value!{...}` only emits + // string-keyed maps. + let expected = Value::Map(vec![ + ( + Value::Text("$formatVersion".to_string()), + Value::Text("0".to_string()), + ), + ( + Value::Text("distributions".to_string()), + Value::Map(vec![ + ( + Value::U64(1_700_000_000_000), + Value::Map(vec![ + (Value::Identifier([0xab; 32]), Value::U64(1000)), + (Value::Identifier([0xcd; 32]), Value::U64(2000)), + ]), + ), + ( + Value::U64(1_800_000_000_000), + Value::Map(vec![(Value::Identifier([0xef; 32]), Value::U64(3000))]), + ), + ]), + ), + ]); + assert_eq!(value, expected); + let recovered = TokenPreProgrammedDistribution::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs b/packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs index cf0a4cd0c93..068f0180ff7 100644 --- a/packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs +++ b/packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs @@ -2,6 +2,10 @@ use crate::data_contract::group::accessors::v0::GroupV0Getters; use crate::data_contract::group::{Group, GroupMemberPower}; use crate::data_contract::GroupContractPosition; use crate::group::action_taker::{ActionGoal, ActionTaker}; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; use crate::ProtocolError; use bincode::{Decode, Encode}; use platform_value::Identifier; @@ -9,9 +13,16 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::fmt; -#[derive( - Serialize, Deserialize, Decode, Encode, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Default, -)] +#[derive(Decode, Encode, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Default)] +// Custom `Serialize` / `Deserialize` below — `derive(Serialize, Deserialize)` +// can't produce the desired flat wire shape because the `Identity` variant +// wraps `Identifier` (serializes as a base58 string, not a map) and `Group` +// wraps a bare `u16`, so internal tagging doesn't apply. The custom impl +// emits a flat `{"$type": ..., "identity"/"position": ...}` shape with +// synthesized field names (same pattern as `ResourceVoteChoice`). Bincode +// `Encode` / `Decode` derives are untouched (consensus binary format is +// unaffected). +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] pub enum AuthorizedActionTakers { #[default] NoOne, @@ -21,6 +32,122 @@ pub enum AuthorizedActionTakers { Group(GroupContractPosition), } +impl Serialize for AuthorizedActionTakers { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeMap; + match self { + AuthorizedActionTakers::NoOne => { + let mut m = serializer.serialize_map(Some(1))?; + m.serialize_entry("$type", "noOne")?; + m.end() + } + AuthorizedActionTakers::ContractOwner => { + let mut m = serializer.serialize_map(Some(1))?; + m.serialize_entry("$type", "contractOwner")?; + m.end() + } + AuthorizedActionTakers::Identity(id) => { + let mut m = serializer.serialize_map(Some(2))?; + m.serialize_entry("$type", "identity")?; + m.serialize_entry("identity", id)?; + m.end() + } + AuthorizedActionTakers::MainGroup => { + let mut m = serializer.serialize_map(Some(1))?; + m.serialize_entry("$type", "mainGroup")?; + m.end() + } + AuthorizedActionTakers::Group(position) => { + let mut m = serializer.serialize_map(Some(2))?; + m.serialize_entry("$type", "group")?; + m.serialize_entry("position", position)?; + m.end() + } + } + } +} + +impl<'de> Deserialize<'de> for AuthorizedActionTakers { + fn deserialize>(deserializer: D) -> Result { + use serde::de::{self, MapAccess, Visitor}; + + struct V; + + impl<'de> Visitor<'de> for V { + type Value = AuthorizedActionTakers; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + // Mention the old shape: contract JSON authored before + // 4.0.0-beta.4 used bare strings / externally-tagged maps, and + // this message is the only hint users get on ingest failure. + f.write_str( + "AuthorizedActionTakers as a map with a `type` discriminator, \ + e.g. {\"type\": \"contractOwner\"} or {\"type\": \"identity\", \"identity\": \"\"} \ + (the pre-4.0.0-beta.4 shapes \"ContractOwner\" / {\"Identity\": \"\"} are no longer accepted)", + ) + } + + fn visit_map>(self, mut map: A) -> Result { + let mut variant: Option = None; + let mut identity: Option = None; + let mut position: Option = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "$type" => { + if variant.is_some() { + return Err(de::Error::duplicate_field("$type")); + } + variant = Some(map.next_value()?); + } + "identity" => { + if identity.is_some() { + return Err(de::Error::duplicate_field("identity")); + } + identity = Some(map.next_value()?); + } + "position" => { + if position.is_some() { + return Err(de::Error::duplicate_field("position")); + } + position = Some(map.next_value()?); + } + _ => { + let _: serde::de::IgnoredAny = map.next_value()?; + } + } + } + + let variant = variant.ok_or_else(|| de::Error::missing_field("$type"))?; + match variant.as_str() { + "noOne" => Ok(AuthorizedActionTakers::NoOne), + "contractOwner" => Ok(AuthorizedActionTakers::ContractOwner), + "identity" => { + let id = identity.ok_or_else(|| de::Error::missing_field("identity"))?; + Ok(AuthorizedActionTakers::Identity(id)) + } + "mainGroup" => Ok(AuthorizedActionTakers::MainGroup), + "group" => { + let position = + position.ok_or_else(|| de::Error::missing_field("position"))?; + Ok(AuthorizedActionTakers::Group(position)) + } + other => Err(de::Error::unknown_variant( + other, + &["noOne", "contractOwner", "identity", "mainGroup", "group"], + )), + } + } + } + + deserializer.deserialize_map(V) + } +} + +// Manual impl because AuthorizedActionTakers is a flat enum (not versioned V0/V1). +#[cfg(feature = "json-conversion")] +impl JsonConvertible for AuthorizedActionTakers {} + impl fmt::Display for AuthorizedActionTakers { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -686,3 +813,87 @@ mod tests { )); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + fn id() -> Identifier { + Identifier::from([0x42u8; 32]) + } + + /// Per-variant wire-shape coverage — the custom Serialize/Deserialize pair + /// must stay in sync when variants are added (the maintenance trap of + /// custom-impl enums). + #[test] + fn json_round_trip_with_full_wire_shape_all_variants() { + use crate::serialization::JsonConvertible; + let cases = vec![ + (AuthorizedActionTakers::NoOne, json!({"$type": "noOne"})), + ( + AuthorizedActionTakers::ContractOwner, + json!({"$type": "contractOwner"}), + ), + ( + AuthorizedActionTakers::Identity(id()), + json!({"$type": "identity", "identity": "5TeWSsjg2gbxCyWVniXeCmwM7UtHTCK7svzJr5xYJzHf"}), + ), + ( + AuthorizedActionTakers::MainGroup, + json!({"$type": "mainGroup"}), + ), + // `position` is u16 (GroupContractPosition); JSON erases the size — + // the value-path test locks the typed variant. + ( + AuthorizedActionTakers::Group(42), + json!({"$type": "group", "position": 42}), + ), + ]; + for (original, expected) in cases { + let json_v = original.to_json().expect("to_json"); + assert_eq!(json_v, expected, "json wire shape for {original}"); + let recovered = AuthorizedActionTakers::from_json(json_v).expect("from_json"); + assert_eq!(original, recovered); + } + } + + #[test] + fn value_round_trip_with_full_wire_shape_all_variants() { + use crate::serialization::ValueConvertible; + let cases = vec![ + ( + AuthorizedActionTakers::NoOne, + platform_value!({"$type": "noOne"}), + ), + ( + AuthorizedActionTakers::ContractOwner, + platform_value!({"$type": "contractOwner"}), + ), + ( + AuthorizedActionTakers::Identity(id()), + platform_value!({"$type": "identity", "identity": id()}), + ), + ( + AuthorizedActionTakers::MainGroup, + platform_value!({"$type": "mainGroup"}), + ), + ( + AuthorizedActionTakers::Group(42), + platform_value!({"$type": "group", "position": 42u16}), + ), + ]; + for (original, expected) in cases { + let value = original.to_object().expect("to_object"); + assert_eq!(value, expected, "value wire shape for {original}"); + let recovered = AuthorizedActionTakers::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + } +} diff --git a/packages/rs-dpp/src/data_contract/change_control_rules/mod.rs b/packages/rs-dpp/src/data_contract/change_control_rules/mod.rs index 486683c94e9..aed6523009c 100644 --- a/packages/rs-dpp/src/data_contract/change_control_rules/mod.rs +++ b/packages/rs-dpp/src/data_contract/change_control_rules/mod.rs @@ -193,3 +193,75 @@ mod tests { assert_eq!(rules, restored); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; + use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0; + use platform_value::platform_value; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + fn fixture() -> ChangeControlRules { + ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::MainGroup, + changing_authorized_action_takers_to_no_one_allowed: true, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: true, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `AuthorizedActionTakers` uses a custom internally-tagged serde impl + // (`{"$type": ...}` maps — see authorized_action_takers.rs); unit and + // payload variants share the same flat map shape. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "mainGroup"}, + "changingAuthorizedActionTakersToNoOneAllowed": true, + "changingAdminActionTakersToNoOneAllowed": false, + "selfChangingAdminActionTakersAllowed": true, + }) + ); + let recovered = ChangeControlRules::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // No sized integers in this fixture — only Text + Bool. The custom + // `AuthorizedActionTakers` impl emits `{"$type": ...}` maps on both + // wire formats. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "mainGroup"}, + "changingAuthorizedActionTakersToNoOneAllowed": true, + "changingAdminActionTakersToNoOneAllowed": false, + "selfChangingAdminActionTakersAllowed": true, + }) + ); + let recovered = ChangeControlRules::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/config/mod.rs b/packages/rs-dpp/src/data_contract/config/mod.rs index 8f30512f985..194446c4023 100644 --- a/packages/rs-dpp/src/data_contract/config/mod.rs +++ b/packages/rs-dpp/src/data_contract/config/mod.rs @@ -9,6 +9,8 @@ use crate::data_contract::config::v1::{ use crate::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements; #[cfg(feature = "json-conversion")] use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; use crate::version::PlatformVersion; use crate::ProtocolError; use bincode::{Decode, Encode}; @@ -20,6 +22,7 @@ use std::collections::BTreeMap; use v0::{DataContractConfigGettersV0, DataContractConfigSettersV0, DataContractConfigV0}; #[cfg_attr(feature = "json-conversion", derive(JsonConvertible))] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] #[derive(Serialize, Deserialize, Encode, Decode, Debug, Clone, Copy, PartialEq, Eq, From)] #[serde(tag = "$formatVersion")] pub enum DataContractConfig { @@ -76,6 +79,11 @@ impl DataContractConfig { } } + /// **KEEP-AS-EXCEPTION** in the JSON/Value canonical-trait migration — + /// this is a context-aware constructor, not a parallel conversion path: + /// it dispatches the config variant on `platform_version` (the input map + /// carries no `$formatVersion` tag in the contract-creation flow), so + /// canonical `ValueConvertible::from_object` cannot replace it. pub fn from_value( value: Value, platform_version: &PlatformVersion, @@ -807,3 +815,85 @@ mod tests { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::data_contract::config::v0::DataContractConfigV0; + use crate::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements; + use platform_value::platform_value; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + fn fixture() -> DataContractConfig { + DataContractConfig::V0(DataContractConfigV0 { + can_be_deleted: true, + readonly: true, + keeps_history: true, + documents_keep_history_contract_default: true, + documents_mutable_contract_default: false, + documents_can_be_deleted_contract_default: false, + requires_identity_encryption_bounded_key: Some(StorageKeyRequirements::Unique), + requires_identity_decryption_bounded_key: Some(StorageKeyRequirements::Multiple), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `requiresIdentity{En,De}cryptionBoundedKey` are `Option` + // where `StorageKeyRequirements` is `#[repr(u8)]` with `Serialize_repr` + // (Unique = 0, Multiple = 1). JSON has only one number type, so the + // u8-ness of these fields is erased on the wire — the Value-path + // assertion below uses `0u8` / `1u8` to lock in the sized variant. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "canBeDeleted": true, + "readonly": true, + "keepsHistory": true, + "documentsKeepHistoryContractDefault": true, + "documentsMutableContractDefault": false, + "documentsCanBeDeletedContractDefault": false, + "requiresIdentityEncryptionBoundedKey": 0, + "requiresIdentityDecryptionBoundedKey": 1, + }) + ); + let recovered = DataContractConfig::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `0u8` / `1u8`: `StorageKeyRequirements` is `#[repr(u8)]`, and + // platform_value preserves sized variants (`Value::U8`, not `Value::U64`). + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "canBeDeleted": true, + "readonly": true, + "keepsHistory": true, + "documentsKeepHistoryContractDefault": true, + "documentsMutableContractDefault": false, + "documentsCanBeDeletedContractDefault": false, + "requiresIdentityEncryptionBoundedKey": 0u8, + "requiresIdentityDecryptionBoundedKey": 1u8, + }) + ); + let recovered = DataContractConfig::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/config/v0/mod.rs b/packages/rs-dpp/src/data_contract/config/v0/mod.rs index c8e83e2854e..f40f16912ac 100644 --- a/packages/rs-dpp/src/data_contract/config/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/config/v0/mod.rs @@ -119,10 +119,6 @@ impl Default for DataContractConfigV0 { } impl DataContractConfigV0 { - pub fn from_value(value: Value) -> Result { - platform_value::from_value(value).map_err(ProtocolError::ValueError) - } - pub fn default_with_version() -> DataContractConfig { Self::default().into() } diff --git a/packages/rs-dpp/src/data_contract/config/v1/mod.rs b/packages/rs-dpp/src/data_contract/config/v1/mod.rs index fec1a5102f1..f69435e6b37 100644 --- a/packages/rs-dpp/src/data_contract/config/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/config/v1/mod.rs @@ -76,10 +76,6 @@ impl Default for DataContractConfigV1 { } impl DataContractConfigV1 { - pub fn from_value(value: Value) -> Result { - platform_value::from_value(value).map_err(ProtocolError::ValueError) - } - pub fn default_with_version() -> DataContractConfig { Self::default().into() } diff --git a/packages/rs-dpp/src/data_contract/conversion/json/mod.rs b/packages/rs-dpp/src/data_contract/conversion/json/mod.rs index 269d628a2f5..68cad843aa8 100644 --- a/packages/rs-dpp/src/data_contract/conversion/json/mod.rs +++ b/packages/rs-dpp/src/data_contract/conversion/json/mod.rs @@ -34,23 +34,6 @@ impl DataContractJsonConversionMethodsV0 for DataContract { }), } } - - fn to_json(&self, platform_version: &PlatformVersion) -> Result { - match self { - DataContract::V0(v0) => v0.to_json(platform_version), - DataContract::V1(v1) => v1.to_json(platform_version), - } - } - - fn to_validating_json( - &self, - platform_version: &PlatformVersion, - ) -> Result { - match self { - DataContract::V0(v0) => v0.to_validating_json(platform_version), - DataContract::V1(v1) => v1.to_validating_json(platform_version), - } - } } #[cfg(test)] @@ -93,19 +76,16 @@ mod tests { "perpetualDistribution": { "$formatVersion": "0", "distributionType": { - "BlockBasedDistribution": { - "interval": 10, - "function": { - "Stepwise": { "0": 100, "10": 50 } - } - } + "$type": "blockBasedDistribution", + "interval": 10, + "function": { "$type": "stepwise", "0": 100, "10": 50 } }, - "distributionRecipient": "ContractOwner" + "distributionRecipient": {"$type": "contractOwner"} }, "perpetualDistributionRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false @@ -113,8 +93,8 @@ mod tests { "preProgrammedDistribution": null, "preProgrammedDistributionRules": { "$formatVersion": "0", - "authorizedToMakeChange": "NoOne", - "adminActionTakers": "NoOne", + "authorizedToMakeChange": {"$type": "noOne"}, + "adminActionTakers": {"$type": "noOne"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false @@ -122,8 +102,8 @@ mod tests { "newTokensDestinationIdentity": null, "newTokensDestinationIdentityRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false @@ -131,16 +111,16 @@ mod tests { "mintingAllowChoosingDestination": false, "mintingAllowChoosingDestinationRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "changeDirectPurchasePricingRules": { "$formatVersion": "0", - "authorizedToMakeChange": "NoOne", - "adminActionTakers": "NoOne", + "authorizedToMakeChange": {"$type": "noOne"}, + "adminActionTakers": {"$type": "noOne"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false @@ -149,62 +129,62 @@ mod tests { "marketplaceRules": {"$formatVersion": "0", "tradeMode": "NotTradeable"}, "manualMintingRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "manualBurningRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "freezeRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "unfreezeRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "destroyFrozenFundsRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "emergencyActionRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "directPurchaseRules": { "$formatVersion": "0", - "authorizedToMakeChange": "NoOne", - "adminActionTakers": "NoOne", + "authorizedToMakeChange": {"$type": "noOne"}, + "adminActionTakers": {"$type": "noOne"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "mainControlGroup": null, - "mainControlGroupCanBeModified": "NoOne" + "mainControlGroupCanBeModified": {"$type": "noOne"} } } }); @@ -249,8 +229,8 @@ mod tests { "perpetualDistribution": null, "perpetualDistributionRules": { "$formatVersion": "0", - "authorizedToMakeChange": "NoOne", - "adminActionTakers": "NoOne", + "authorizedToMakeChange": {"$type": "noOne"}, + "adminActionTakers": {"$type": "noOne"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false @@ -265,8 +245,8 @@ mod tests { }, "preProgrammedDistributionRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false @@ -274,8 +254,8 @@ mod tests { "newTokensDestinationIdentity": null, "newTokensDestinationIdentityRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false @@ -283,16 +263,16 @@ mod tests { "mintingAllowChoosingDestination": false, "mintingAllowChoosingDestinationRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "changeDirectPurchasePricingRules": { "$formatVersion": "0", - "authorizedToMakeChange": "NoOne", - "adminActionTakers": "NoOne", + "authorizedToMakeChange": {"$type": "noOne"}, + "adminActionTakers": {"$type": "noOne"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false @@ -301,62 +281,62 @@ mod tests { "marketplaceRules": {"$formatVersion": "0", "tradeMode": "NotTradeable"}, "manualMintingRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "manualBurningRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "freezeRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "unfreezeRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "destroyFrozenFundsRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "emergencyActionRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "directPurchaseRules": { "$formatVersion": "0", - "authorizedToMakeChange": "NoOne", - "adminActionTakers": "NoOne", + "authorizedToMakeChange": {"$type": "noOne"}, + "adminActionTakers": {"$type": "noOne"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "mainControlGroup": null, - "mainControlGroupCanBeModified": "NoOne" + "mainControlGroupCanBeModified": {"$type": "noOne"} } } }); diff --git a/packages/rs-dpp/src/data_contract/conversion/json/v0/mod.rs b/packages/rs-dpp/src/data_contract/conversion/json/v0/mod.rs index 459ed3b4598..cbb7b091081 100644 --- a/packages/rs-dpp/src/data_contract/conversion/json/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/conversion/json/v0/mod.rs @@ -2,7 +2,19 @@ use crate::version::PlatformVersion; use crate::ProtocolError; use serde_json::Value as JsonValue; +/// JSON deserialization for `DataContract` with an explicit validation flag. +/// +/// `from_json(value, full_validation, pv)` is the single entry point: pass +/// `true` on trust boundaries (SDK ingest, gRPC handlers, fixture loaders) to +/// run full schema validation, and `false` to reconstruct already-trusted data +/// (e.g. storage reads) without re-validating it. +/// +/// For *serialization*, use canonical `JsonConvertible::to_json` / +/// `serde_json::to_value(&data_contract)` directly — there is no validation +/// dimension to writing. pub trait DataContractJsonConversionMethodsV0 { + /// Deserialize from JSON, running full schema validation when + /// `full_validation` is `true`. fn from_json( json_value: JsonValue, full_validation: bool, @@ -10,12 +22,4 @@ pub trait DataContractJsonConversionMethodsV0 { ) -> Result where Self: Sized; - - /// Returns Data Contract as a JSON Value - fn to_json(&self, platform_version: &PlatformVersion) -> Result; - /// Returns Data Contract as a JSON Value - fn to_validating_json( - &self, - platform_version: &PlatformVersion, - ) -> Result; } diff --git a/packages/rs-dpp/src/data_contract/conversion/serde/mod.rs b/packages/rs-dpp/src/data_contract/conversion/serde/mod.rs index a2106f394c1..283e2dbc367 100644 --- a/packages/rs-dpp/src/data_contract/conversion/serde/mod.rs +++ b/packages/rs-dpp/src/data_contract/conversion/serde/mod.rs @@ -1,3 +1,41 @@ +//! Manual `Serialize` / `Deserialize` for the outer `DataContract` enum. +//! +//! # Critical-4: platform-version coupling (pinned by tests below) +//! +//! Both impls call `PlatformVersion::get_version_or_current_or_latest(None)`, +//! making serialization output *depend on a process-global thread-local-ish* +//! state — same DataContract value, different bytes if the active platform +//! version changes. This is by design: `DataContract` is a versioned enum +//! routed through `DataContractInSerializationFormat`, and the format depends +//! on the current platform. +//! +//! # Validation policy: validate by default +//! +//! The `Deserialize` impl runs **full schema validation** — it decodes the +//! wire form into `DataContractInSerializationFormat`, then converts it to a +//! `DataContract` with `full_validation = true`. So +//! `serde_json::from_value::(...)` / `from_str` reject a +//! structurally-decodable but schema-invalid contract: a `DataContract` you +//! can hold is a valid one. +//! +//! To reconstruct already-trusted data *without* re-validating it (e.g. +//! storage reads, round-trips of a known-good value), use the explicit +//! no-validation path +//! `DataContractJsonConversionMethodsV0::from_json(value, false, pv)` / +//! `DataContractValueConversionMethodsV0::from_value(value, false, pv)` — those +//! call `try_from_platform_versioned` directly and skip schema validation. +//! +//! **Why this is KEEP-AS-EXCEPTION**: the manual impls also inject the active +//! `PlatformVersion` (via `get_version_or_current_or_latest`), coupling the +//! output to process-global state. This is by design — `DataContract` is a +//! versioned enum routed through `DataContractInSerializationFormat`, and the +//! format depends on the current platform. The stateless alternative is the +//! bincode storage path (`serialize_to_bytes_with_platform_version`). +//! +//! The `data_contract_serde_pins_critical_4` test module below pins this +//! behavior (validate-by-default + explicit `false` opt-out) so future +//! refactors can't silently change it. + use crate::data_contract::serialized_version::DataContractInSerializationFormat; use crate::prelude::DataContract; use crate::version::PlatformVersionCurrentVersion; @@ -28,7 +66,10 @@ impl<'de> Deserialize<'de> for DataContract { let serialization_format = DataContractInSerializationFormat::deserialize(deserializer)?; let current_version = PlatformVersion::get_version_or_current_or_latest(None) .map_err(|e| serde::de::Error::custom(e.to_string()))?; - // when deserializing from json/platform_value/cbor we always want to validate (as this is not coming from the state) + // Full schema validation: a deserialized `DataContract` is a valid one. + // To reconstruct already-trusted data without re-validating, use the + // explicit `from_json`/`from_value(_, false, _)` path instead of serde. + // See the module-level doc comment for the rationale. DataContract::try_from_platform_versioned( serialization_format, true, @@ -38,3 +79,141 @@ impl<'de> Deserialize<'de> for DataContract { .map_err(serde::de::Error::custom) } } + +#[cfg(test)] +mod data_contract_serde_pins_critical_4 { + //! Behavior pins for Critical-4 (DataContract serde impurity). + //! + //! These tests don't fix anything — they snapshot the current behavior + //! so a future refactor that quietly changes either the + //! `PlatformVersion::get_current()` coupling or the hardcoded + //! `full_validation = true` will fail loudly. + //! + //! See module-level doc above and the unification plan §3.0 Critical-4. + use super::*; + use crate::data_contract::accessors::v0::DataContractV0Getters; + use crate::data_contract::serialized_version::DataContractInSerializationFormat; + use crate::tests::fixtures::get_data_contract_fixture; + use platform_version::version::LATEST_PLATFORM_VERSION; + + /// PIN: `DataContract` round-trips through `serde_json` at the active + /// platform version. Documents that `Serialize` / `Deserialize` are + /// load-bearing for JSON-shape interchange (not just bincode). + #[test] + fn data_contract_round_trips_through_serde_json() { + let created = get_data_contract_fixture(None, 0, 1); + let original = created.data_contract().clone(); + + let json = serde_json::to_value(&original).expect("serialize to json"); + let recovered: DataContract = serde_json::from_value(json).expect("deserialize from json"); + + assert_eq!(original.id(), recovered.id()); + assert_eq!(original.owner_id(), recovered.owner_id()); + assert_eq!(original.version(), recovered.version()); + } + + /// PIN: `DataContract::serialize` produces the same wire shape as + /// `DataContractInSerializationFormat::serialize`. This documents that + /// the manual impl is a thin wrapper that injects + /// `PlatformVersion::get_current()` and forwards to the format type — + /// not a custom shape. + #[test] + fn data_contract_serialize_matches_serialization_format_at_current_version() { + let created = get_data_contract_fixture(None, 0, 1); + let original = created.data_contract().clone(); + + let direct_json = serde_json::to_value(&original).expect("DataContract -> json"); + + let format: DataContractInSerializationFormat = original + .try_into_platform_versioned(LATEST_PLATFORM_VERSION) + .expect("DataContract -> SerializationFormat at latest"); + let format_json = serde_json::to_value(&format).expect("SerializationFormat -> json"); + + assert_eq!( + direct_json, format_json, + "DataContract::serialize should be byte-equivalent to \ + DataContractInSerializationFormat::serialize at the current \ + platform version. If this fails, the manual serde impl has \ + diverged from the format-routing pattern documented in the \ + module-level comment." + ); + } + + /// PIN: `DataContract::deserialize` runs **full schema validation** — a + /// deserialized `DataContract` is a valid one. The explicit + /// `from_json(_, false, _)` path is the opt-out for reconstructing + /// already-trusted data without re-validation. + /// + /// We exercise this with a structurally well-formed payload whose document + /// schema is semantically invalid (an `indices` entry referencing a + /// nonexistent property): + /// + /// - canonical `DataContract::deserialize` REJECTS it (validation runs). + /// - explicit `from_json(_, false, _)` ACCEPTS it (validation skipped). + /// - explicit `from_json(_, true, _)` REJECTS it (validation runs). + /// + /// If a future refactor flips canonical Deserialize back to not validating, + /// this test fails loudly. See module-level doc above for the rationale. + #[test] + fn data_contract_deserialize_validates_by_default() { + use crate::data_contract::conversion::json::DataContractJsonConversionMethodsV0; + + // Build a valid contract, then mutate its JSON to make the schema + // semantically invalid: declare an index over a property not in + // the schema's `properties` map. Structurally well-formed JSON; + // only schema validation catches the issue. + let created = get_data_contract_fixture(None, 0, 1); + let original = created.data_contract().clone(); + + let mut json = serde_json::to_value(&original).expect("to_json"); + + let document_schemas = json + .get_mut("documentSchemas") + .and_then(|v| v.as_object_mut()) + .expect("documentSchemas object"); + let (_, first_schema) = document_schemas + .iter_mut() + .next() + .expect("at least one document schema"); + let schema_obj = first_schema.as_object_mut().expect("schema is object"); + schema_obj.insert( + "indices".to_string(), + serde_json::json!([ + { + "name": "invalid_idx", + "properties": [{"definitelyDoesNotExist": "asc"}], + "unique": false, + } + ]), + ); + + // Format-level deserialize succeeds (never validated). + let _: DataContractInSerializationFormat = serde_json::from_value(json.clone()) + .expect("format-level deserialize should accept structurally-valid input"); + + // PIN: canonical Deserialize REJECTS the invalid schema (validates). + let canonical_result: Result = serde_json::from_value(json.clone()); + assert!( + canonical_result.is_err(), + "DataContract::deserialize should run schema validation and reject \ + an invalid index. If this passes, validate-by-default has been \ + silently reverted." + ); + + // PIN: explicit opt-out accepts the same payload without validating. + let unvalidated = DataContract::from_json(json.clone(), false, LATEST_PLATFORM_VERSION); + assert!( + unvalidated.is_ok(), + "DataContract::from_json(_, false, _) should skip schema validation \ + and accept the structurally-well-formed payload." + ); + + // PIN: explicit validated path also rejects. + let validated_result = DataContract::from_json(json, true, LATEST_PLATFORM_VERSION); + assert!( + validated_result.is_err(), + "DataContract::from_json(_, true, _) should reject contracts with \ + invalid indices." + ); + } +} diff --git a/packages/rs-dpp/src/data_contract/conversion/value/mod.rs b/packages/rs-dpp/src/data_contract/conversion/value/mod.rs index 59d07245ca2..d8f940cffa4 100644 --- a/packages/rs-dpp/src/data_contract/conversion/value/mod.rs +++ b/packages/rs-dpp/src/data_contract/conversion/value/mod.rs @@ -26,24 +26,10 @@ impl DataContractValueConversionMethodsV0 for DataContract { DataContractV1::from_value(raw_object, full_validation, platform_version)?.into(), ), version => Err(ProtocolError::UnknownVersionMismatch { - method: "DataContract::from_object".to_string(), + method: "DataContract::from_value".to_string(), known_versions: vec![0, 1], received: version, }), } } - - fn to_value(&self, platform_version: &PlatformVersion) -> Result { - match self { - DataContract::V0(v0) => v0.to_value(platform_version), - DataContract::V1(v1) => v1.to_value(platform_version), - } - } - - fn into_value(self, platform_version: &PlatformVersion) -> Result { - match self { - DataContract::V0(v0) => v0.into_value(platform_version), - DataContract::V1(v1) => v1.into_value(platform_version), - } - } } diff --git a/packages/rs-dpp/src/data_contract/conversion/value/v0/mod.rs b/packages/rs-dpp/src/data_contract/conversion/value/v0/mod.rs index 614330cdb99..21df9ce37ca 100644 --- a/packages/rs-dpp/src/data_contract/conversion/value/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/conversion/value/v0/mod.rs @@ -2,7 +2,20 @@ use crate::version::PlatformVersion; use crate::ProtocolError; use platform_value::Value; +/// `platform_value` deserialization for `DataContract` with an explicit +/// validation flag. +/// +/// `from_value(value, full_validation, pv)` is the single entry point: pass +/// `true` on trust boundaries (SDK ingest, fixture loaders, contract +/// registration) to run full schema validation, and `false` to reconstruct +/// already-trusted data (e.g. storage reads) without re-validating it. +/// +/// For *serialization*, use canonical `ValueConvertible::to_object` / +/// `platform_value::to_value(&data_contract)` directly — there is no +/// validation dimension to writing. pub trait DataContractValueConversionMethodsV0 { + /// Deserialize from a `platform_value::Value`, running full schema + /// validation when `full_validation` is `true`. fn from_value( raw_object: Value, full_validation: bool, @@ -10,6 +23,4 @@ pub trait DataContractValueConversionMethodsV0 { ) -> Result where Self: Sized; - fn to_value(&self, platform_version: &PlatformVersion) -> Result; - fn into_value(self, platform_version: &PlatformVersion) -> Result; } diff --git a/packages/rs-dpp/src/data_contract/created_data_contract/mod.rs b/packages/rs-dpp/src/data_contract/created_data_contract/mod.rs index c2ecc99126b..2059eb0fcae 100644 --- a/packages/rs-dpp/src/data_contract/created_data_contract/mod.rs +++ b/packages/rs-dpp/src/data_contract/created_data_contract/mod.rs @@ -16,8 +16,6 @@ use crate::serialization::{ PlatformSerializableWithPlatformVersion, }; use crate::ProtocolError::{PlatformDeserializationError, PlatformSerializationError}; -#[cfg(feature = "value-conversion")] -use platform_value::Value; use platform_version::TryIntoPlatformVersioned; /// The created data contract is a intermediate structure that can be consumed by a @@ -194,31 +192,6 @@ impl CreatedDataContract { }), } } - - #[cfg(feature = "value-conversion")] - pub fn from_object( - raw_object: Value, - full_validation: bool, - platform_version: &PlatformVersion, - ) -> Result { - match platform_version - .dpp - .contract_versions - .created_data_contract_structure - { - 0 => Ok(CreatedDataContractV0::from_object( - raw_object, - full_validation, - platform_version, - )? - .into()), - version => Err(ProtocolError::UnknownVersionMismatch { - method: "CreatedDataContract::from_object".to_string(), - known_versions: vec![0], - received: version, - }), - } - } } impl CreatedDataContractInSerializationFormat { diff --git a/packages/rs-dpp/src/data_contract/created_data_contract/v0/mod.rs b/packages/rs-dpp/src/data_contract/created_data_contract/v0/mod.rs index c7ab1d409c0..37495e20841 100644 --- a/packages/rs-dpp/src/data_contract/created_data_contract/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/created_data_contract/v0/mod.rs @@ -3,17 +3,6 @@ use bincode::{Decode, Encode}; use crate::data_contract::serialized_version::DataContractInSerializationFormat; use crate::prelude::IdentityNonce; -#[cfg(feature = "value-conversion")] -use crate::{ - data_contract::{ - conversion::value::v0::DataContractValueConversionMethodsV0, - created_data_contract::fields::property_names::{DATA_CONTRACT, IDENTITY_NONCE}, - }, - version::PlatformVersion, - ProtocolError, -}; -#[cfg(feature = "value-conversion")] -use platform_value::{btreemap_extensions::BTreeValueRemoveFromMapHelper, Error, Value}; // TODO: Decide on what we need ExtendedDataContract with metadata or CreatedDataContract or both. #[derive(Clone, Debug, PartialEq)] @@ -27,32 +16,3 @@ pub struct CreatedDataContractInSerializationFormatV0 { pub data_contract: DataContractInSerializationFormat, pub identity_nonce: IdentityNonce, } - -impl CreatedDataContractV0 { - #[cfg(feature = "value-conversion")] - pub fn from_object( - raw_object: Value, - full_validation: bool, - platform_version: &PlatformVersion, - ) -> Result { - let mut raw_map = raw_object - .into_btree_string_map() - .map_err(ProtocolError::ValueError)?; - - let raw_data_contract = raw_map.remove(DATA_CONTRACT).ok_or_else(|| { - Error::StructureError("unable to remove property dataContract".to_string()) - })?; - - let identity_nonce = raw_map - .remove_integer(IDENTITY_NONCE) - .map_err(ProtocolError::ValueError)?; - - let data_contract = - DataContract::from_value(raw_data_contract, full_validation, platform_version)?; - - Ok(Self { - data_contract, - identity_nonce, - }) - } -} diff --git a/packages/rs-dpp/src/data_contract/document_type/index/mod.rs b/packages/rs-dpp/src/data_contract/document_type/index/mod.rs index f1312345661..2c5931bd36c 100644 --- a/packages/rs-dpp/src/data_contract/document_type/index/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/index/mod.rs @@ -1,5 +1,5 @@ #[cfg(feature = "serde-conversion")] -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, PartialOrd, Clone, Eq)] #[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] @@ -21,11 +21,7 @@ use crate::data_contract::errors::DataContractError::RegexError; use platform_value::{Value, ValueMap}; use rand::distributions::{Alphanumeric, DistString}; use regex::Regex; -#[cfg(feature = "serde-conversion")] -use serde::de::{VariantAccess, Visitor}; use std::cmp::Ordering; -#[cfg(feature = "serde-conversion")] -use std::fmt; use std::sync::OnceLock; use std::{collections::BTreeMap, convert::TryFrom}; @@ -54,17 +50,86 @@ impl TryFrom for ContestedIndexResolution { #[repr(u8)] #[derive(Debug)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde( + into = "ContestedIndexFieldMatchRepr", + from = "ContestedIndexFieldMatchRepr" + ) +)] pub enum ContestedIndexFieldMatch { Regex(LazyRegex), PositiveIntegerMatch(u128), } +// Internal-`$type` serde shape with a uniform `value` payload, via a +// struct-variant Repr (tuple variants can't auto-internal-tag). `LazyRegex` +// round-trips as a bare string; the `u128` uses `json_safe_u128_content` rather +// than the plain `json_safe_u128` because internal tagging buffers the map +// through serde's `Content`, which can't hold a `u128` — see that helper's docs. +#[cfg(feature = "serde-conversion")] +#[derive(Serialize, Deserialize)] +#[serde(tag = "$type", rename_all = "camelCase")] +enum ContestedIndexFieldMatchRepr { + Regex { + value: LazyRegex, + }, + PositiveIntegerMatch { + #[serde(with = "crate::serialization::json_safe_u128_content")] + value: u128, + }, +} + +#[cfg(feature = "serde-conversion")] +impl From for ContestedIndexFieldMatchRepr { + fn from(m: ContestedIndexFieldMatch) -> Self { + match m { + ContestedIndexFieldMatch::Regex(value) => Self::Regex { value }, + ContestedIndexFieldMatch::PositiveIntegerMatch(value) => { + Self::PositiveIntegerMatch { value } + } + } + } +} + +#[cfg(feature = "serde-conversion")] +impl From for ContestedIndexFieldMatch { + fn from(r: ContestedIndexFieldMatchRepr) -> Self { + match r { + ContestedIndexFieldMatchRepr::Regex { value } => Self::Regex(value), + ContestedIndexFieldMatchRepr::PositiveIntegerMatch { value } => { + Self::PositiveIntegerMatch(value) + } + } + } +} + #[derive(Debug, Clone)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(from = "String", into = "String") +)] pub struct LazyRegex { regex: OnceLock, regex_str: String, } +#[cfg(feature = "serde-conversion")] +impl From for LazyRegex { + fn from(regex_str: String) -> Self { + LazyRegex::new(regex_str) + } +} + +#[cfg(feature = "serde-conversion")] +impl From for String { + fn from(value: LazyRegex) -> Self { + value.regex_str + } +} + impl LazyRegex { pub fn new(regex_str: String) -> Self { LazyRegex { @@ -86,101 +151,13 @@ impl LazyRegex { } } -#[cfg(feature = "serde-conversion")] -impl Serialize for ContestedIndexFieldMatch { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match *self { - ContestedIndexFieldMatch::Regex(ref regex) => serializer.serialize_newtype_variant( - "ContestedIndexFieldMatch", - 0, - "Regex", - regex.as_str(), - ), - ContestedIndexFieldMatch::PositiveIntegerMatch(ref num) => serializer - .serialize_newtype_variant( - "ContestedIndexFieldMatch", - 1, - "PositiveIntegerMatch", - num, - ), - } - } -} - -#[cfg(feature = "serde-conversion")] -impl<'de> Deserialize<'de> for ContestedIndexFieldMatch { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(field_identifier, rename_all = "snake_case")] - enum Field { - Regex, - PositiveIntegerMatch, - } - - struct FieldVisitor; - - impl Visitor<'_> for FieldVisitor { - type Value = Field; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("`regex` or `positive_integer_match`") - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - match value { - "regex" => Ok(Field::Regex), - "positive_integer_match" => Ok(Field::PositiveIntegerMatch), - _ => Err(de::Error::unknown_variant( - value, - &["regex", "positive_integer_match"], - )), - } - } - } - - struct ContestedIndexFieldMatchVisitor; - - impl<'de> Visitor<'de> for ContestedIndexFieldMatchVisitor { - type Value = ContestedIndexFieldMatch; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("enum ContestedIndexFieldMatch") - } - - fn visit_enum(self, visitor: V) -> Result - where - V: de::EnumAccess<'de>, - { - match visitor.variant()? { - (Field::Regex, v) => { - let regex_str: String = v.newtype_variant()?; - - Ok(ContestedIndexFieldMatch::Regex(LazyRegex::new(regex_str))) - } - (Field::PositiveIntegerMatch, v) => { - let num: u128 = v.newtype_variant()?; - Ok(ContestedIndexFieldMatch::PositiveIntegerMatch(num)) - } - } - } - } - - deserializer.deserialize_enum( - "ContestedIndexFieldMatch", - &["regex", "positive_integer_match"], - ContestedIndexFieldMatchVisitor, - ) - } -} +// Manual Serialize/Deserialize impls deleted in Phase D step 11. +// The previous custom Serialize emitted PascalCase variant tags +// (`{"Regex": ...}`) while the custom Deserialize expected snake_case +// (`{"regex": ...}`) — non-round-trippable. The replacement uses serde +// `rename_all = "camelCase"` matching the rest of the codebase's +// JSON wire-shape convention. `LazyRegex` round-trips as a plain +// string via `serde(from = "String", into = "String")` above. #[allow(clippy::non_canonical_partial_ord_impl)] impl PartialOrd for ContestedIndexFieldMatch { @@ -355,6 +332,12 @@ pub struct Index { pub contested_index: Option, /// Whether and how the index supports count fast paths. See /// [`IndexCountability`]. + // + // `serde(default)` on this and the three fields below: they were added + // after the struct's serde shape was already in the wild (#3623 count + // fields, #3661 sum fields), so JSON serialized before then must still + // deserialize. + #[cfg_attr(feature = "serde-conversion", serde(default))] pub countable: IndexCountability, /// Whether the index supports O(log n) count queries over a *range* of /// values for the index's last property (the terminator). The flag @@ -372,6 +355,7 @@ pub struct Index { /// /// `range_countable: true` requires `countable` to be `Countable` or /// `CountableAllowingOffset` (it's additive, not a replacement). + #[cfg_attr(feature = "serde-conversion", serde(default))] pub range_countable: bool, /// When set to `Some(property_name)`, this index's value-tree is laid out /// as a `SumTree` (or `CountSumTree` if [`Index::countable`] is also set @@ -394,6 +378,7 @@ pub struct Index { /// non-null (the terminal is a bare reference at key `[0]`), and it /// does meaningful sum-aggregation work only for null-bearing entries /// (which take the same sum-tree branch a non-unique index uses). + #[cfg_attr(feature = "serde-conversion", serde(default))] pub summable: Option, /// When `true`, this index supports O(log n) range-sum queries on its /// last property. The storage-layout effect mirrors @@ -415,6 +400,7 @@ pub struct Index { /// single tree carries both metrics. The dispatcher in /// `packages/rs-drive/src/drive/document/primary_key_tree_type.rs` /// picks the appropriate variant. + #[cfg_attr(feature = "serde-conversion", serde(default))] pub range_summable: bool, } @@ -2160,3 +2146,317 @@ mod tests { assert!(err_msg.contains("more than one")); } } + +// --- canonical conversion trait impls (unification pass 1) --- +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for OrderBy {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for OrderBy {} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for ContestedIndexResolution {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for ContestedIndexResolution {} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for ContestedIndexFieldMatch {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for ContestedIndexFieldMatch {} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for ContestedIndexInformation {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for ContestedIndexInformation {} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for Index {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for Index {} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for IndexProperty {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for IndexProperty {} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for IndexCountability {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for IndexCountability {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + + fn ix_info_fixture() -> ContestedIndexInformation { + ContestedIndexInformation::default() + } + + #[test] + fn json_round_trip_contested_index_information() { + use crate::serialization::JsonConvertible; + let original = ix_info_fixture(); + let json = original.to_json().expect("to_json"); + let recovered = ContestedIndexInformation::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_contested_index_information() { + use crate::serialization::ValueConvertible; + let original = ix_info_fixture(); + let value = original.to_object().expect("to_object"); + let recovered = ContestedIndexInformation::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_order_by() { + use crate::serialization::JsonConvertible; + for original in [OrderBy::Asc, OrderBy::Desc] { + let json = original.to_json().expect("to_json"); + let recovered = OrderBy::from_json(json).expect("from_json"); + assert_eq!(original, recovered, "variant: {:?}", original); + } + } + + #[test] + fn json_round_trip_contested_index_resolution() { + use crate::serialization::JsonConvertible; + let original = ContestedIndexResolution::MasternodeVote; + let json = original.to_json().expect("to_json"); + let recovered = ContestedIndexResolution::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + // --- ContestedIndexFieldMatch (internal `$type` tag) --- + // Wire shape: internally tagged with a uniform `value` payload. + // `{"$type":"regex","value":""}` -> Regex(LazyRegex) + // `{"$type":"positiveIntegerMatch","value":}` -> PositiveIntegerMatch + // LazyRegex serializes as the bare regex string via + // `serde(from = "String", into = "String")`, carried in `value`. + + #[test] + fn json_round_trip_contested_index_field_match_regex() { + use crate::serialization::JsonConvertible; + let original = ContestedIndexFieldMatch::Regex(LazyRegex::new("^dash$".to_string())); + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + serde_json::json!({ "$type": "regex", "value": "^dash$" }) + ); + let recovered = ContestedIndexFieldMatch::from_json(json).expect("from_json"); + match recovered { + ContestedIndexFieldMatch::Regex(r) => assert_eq!(r.as_str(), "^dash$"), + other => panic!("expected Regex, got {:?}", other), + } + } + + #[test] + fn json_round_trip_contested_index_field_match_positive_integer() { + use crate::serialization::JsonConvertible; + let original = ContestedIndexFieldMatch::PositiveIntegerMatch(42); + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + serde_json::json!({ "$type": "positiveIntegerMatch", "value": 42 }) + ); + let recovered = ContestedIndexFieldMatch::from_json(json).expect("from_json"); + match recovered { + ContestedIndexFieldMatch::PositiveIntegerMatch(n) => assert_eq!(n, 42), + other => panic!("expected PositiveIntegerMatch, got {:?}", other), + } + } + + #[test] + fn value_round_trip_contested_index_field_match_regex() { + use crate::serialization::ValueConvertible; + let original = ContestedIndexFieldMatch::Regex(LazyRegex::new("[a-z]+".to_string())); + let value = original.to_object().expect("to_object"); + // LazyRegex serializes as a bare string in non-HR Value too. + assert_eq!( + value, + platform_value::platform_value!({ "$type": "regex", "value": "[a-z]+" }) + ); + let recovered = ContestedIndexFieldMatch::from_object(value).expect("from_object"); + match recovered { + ContestedIndexFieldMatch::Regex(r) => assert_eq!(r.as_str(), "[a-z]+"), + other => panic!("expected Regex, got {:?}", other), + } + } + + #[test] + fn value_round_trip_contested_index_field_match_positive_integer() { + use crate::serialization::ValueConvertible; + let original = ContestedIndexFieldMatch::PositiveIntegerMatch(u128::MAX); + let value = original.to_object().expect("to_object"); + // u128::MAX exceeds u64::MAX, so it's encoded as a string (Content-safe; + // serde's internal-tag buffer can't hold a 128-bit int). Values that fit + // in u64 stay numeric. + assert_eq!( + value, + platform_value::platform_value!({ "$type": "positiveIntegerMatch", "value": "340282366920938463463374607431768211455" }) + ); + let recovered = ContestedIndexFieldMatch::from_object(value).expect("from_object"); + match recovered { + ContestedIndexFieldMatch::PositiveIntegerMatch(n) => assert_eq!(n, u128::MAX), + other => panic!("expected PositiveIntegerMatch, got {:?}", other), + } + } + + // --- Index / IndexProperty / IndexCountability (count + sum fields from + // base PRs #3623 / #3661) --- + + fn index_fixture() -> Index { + Index { + name: "byOwnerAndPrice".to_string(), + properties: vec![ + IndexProperty { + name: "ownerId".to_string(), + ascending: true, + }, + IndexProperty { + name: "price".to_string(), + ascending: false, + }, + ], + unique: false, + null_searchable: true, + contested_index: None, + countable: IndexCountability::CountableAllowingOffset, + range_countable: true, + summable: Some("price".to_string()), + range_summable: true, + } + } + + #[test] + fn index_json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = index_fixture(); + let json = original.to_json().expect("to_json"); + // Internal (non-user-authored) shape: snake_case field names, no + // rename_all on the struct. `countable` is the camelCase-renamed + // IndexCountability unit enum. + assert_eq!( + json, + serde_json::json!({ + "name": "byOwnerAndPrice", + "properties": [ + {"name": "ownerId", "ascending": true}, + {"name": "price", "ascending": false}, + ], + "unique": false, + "null_searchable": true, + "contested_index": serde_json::Value::Null, + "countable": "countableAllowingOffset", + "range_countable": true, + "summable": "price", + "range_summable": true, + }) + ); + let recovered = Index::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn index_value_round_trip() { + use crate::serialization::ValueConvertible; + let original = index_fixture(); + let value = original.to_object().expect("to_object"); + let recovered = Index::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + /// JSON serialized before the count (#3623) and sum (#3661) fields existed + /// must still deserialize — the four new fields default (NotCountable / + /// false / None / false). Without `serde(default)` this fails with + /// "missing field `countable`". + #[test] + fn index_deserializes_pre_count_sum_json() { + use crate::serialization::JsonConvertible; + let old_json = serde_json::json!({ + "name": "byOwner", + "properties": [{"name": "ownerId", "ascending": true}], + "unique": true, + "null_searchable": false, + "contested_index": serde_json::Value::Null, + }); + let recovered = Index::from_json(old_json).expect("pre-#3623 JSON must deserialize"); + assert_eq!(recovered.countable, IndexCountability::NotCountable); + assert!(!recovered.range_countable); + assert_eq!(recovered.summable, None); + assert!(!recovered.range_summable); + } + + #[test] + fn index_property_json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = IndexProperty { + name: "ownerId".to_string(), + ascending: false, + }; + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + serde_json::json!({"name": "ownerId", "ascending": false}) + ); + let recovered = IndexProperty::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn index_property_value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + use platform_value::platform_value; + let original = IndexProperty { + name: "ownerId".to_string(), + ascending: false, + }; + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({"name": "ownerId", "ascending": false}) + ); + let recovered = IndexProperty::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn index_countability_round_trips_all_variants() { + use crate::serialization::{JsonConvertible, ValueConvertible}; + let cases = [ + (IndexCountability::NotCountable, "notCountable"), + (IndexCountability::Countable, "countable"), + ( + IndexCountability::CountableAllowingOffset, + "countableAllowingOffset", + ), + ]; + for (original, expected) in cases { + let json_v = original.to_json().expect("to_json"); + assert_eq!(json_v, serde_json::json!(expected)); + assert_eq!( + IndexCountability::from_json(json_v).expect("from_json"), + original + ); + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value::Value::Text(expected.to_string())); + assert_eq!( + IndexCountability::from_object(value).expect("from_object"), + original + ); + } + } +} diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs index f771b3d916a..88ce7c0f39d 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs @@ -560,6 +560,46 @@ mod tests { } } + #[test] + fn sanitize_document_properties_converts_integer_array_bytearray_to_bytes() { + // A binary property re-hydrated through a schemaless JSON layer (an edited + // and replaced cached document) arrives as a plain array of numbers that + // decode to wider Value integer variants (U64), not Value::U8. Sanitize must + // normalize it to Value::Bytes so the strict binary serializer accepts it; + // otherwise the replace fails with "not an array of bytes". + let schema = platform_value!({ + "type": "object", + "properties": { + "payload": { + "type": "array", + "byteArray": true, + "minItems": 1_u32, + "maxItems": 64_u32, + "position": 0 + } + }, + "additionalProperties": false, + }); + let dt = build_doc_type("blob_doc", schema); + let mut props: BTreeMap = BTreeMap::new(); + props.insert( + "payload".to_string(), + Value::Array(vec![ + Value::U64(0xde), + Value::U64(0xad), + Value::U64(0xbe), + Value::U64(0xef), + ]), + ); + + dt.as_ref().sanitize_document_properties_ref(&mut props); + + match props.get("payload").unwrap() { + Value::Bytes(bytes) => assert_eq!(bytes.as_slice(), &[0xde, 0xad, 0xbe, 0xef]), + other => panic!("expected sanitized Bytes, got {:?}", other), + } + } + #[test] fn sanitize_document_properties_leaves_unknown_fields_untouched() { let schema = platform_value!({ diff --git a/packages/rs-dpp/src/data_contract/document_type/property/array.rs b/packages/rs-dpp/src/data_contract/document_type/property/array.rs index b14c0188d08..ee26594c276 100644 --- a/packages/rs-dpp/src/data_contract/document_type/property/array.rs +++ b/packages/rs-dpp/src/data_contract/document_type/property/array.rs @@ -5,6 +5,7 @@ use platform_value::Value; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[serde(into = "ArrayItemTypeRepr", from = "ArrayItemTypeRepr")] pub enum ArrayItemType { Integer, Number, @@ -15,6 +16,70 @@ pub enum ArrayItemType { Date, } +// Internal-`$type` serde shape. Mixed unit + 2-tuple variants, so a +// struct-variant Repr (serde can't auto-internal-tag tuple variants). Unit +// variants -> `{"$type":"integer"}`; the tuple variants get named size bounds +// (`#[serde(default)]` so an omitted bound deserializes as `None`). Serde-only +// type (no bincode); its on-wire form is exercised solely by these tests — +// document-schema parsing goes through `TryFrom<&Value>`, not serde. +#[derive(Serialize, Deserialize)] +#[serde(tag = "$type", rename_all = "camelCase")] +enum ArrayItemTypeRepr { + Integer, + Number, + String { + #[serde(default, rename = "minLength")] + min_length: Option, + #[serde(default, rename = "maxLength")] + max_length: Option, + }, + ByteArray { + #[serde(default, rename = "minSize")] + min_size: Option, + #[serde(default, rename = "maxSize")] + max_size: Option, + }, + Identifier, + Boolean, + Date, +} + +impl From for ArrayItemTypeRepr { + fn from(t: ArrayItemType) -> Self { + match t { + ArrayItemType::Integer => Self::Integer, + ArrayItemType::Number => Self::Number, + ArrayItemType::String(min_length, max_length) => Self::String { + min_length, + max_length, + }, + ArrayItemType::ByteArray(min_size, max_size) => Self::ByteArray { min_size, max_size }, + ArrayItemType::Identifier => Self::Identifier, + ArrayItemType::Boolean => Self::Boolean, + ArrayItemType::Date => Self::Date, + } + } +} + +impl From for ArrayItemType { + fn from(r: ArrayItemTypeRepr) -> Self { + match r { + ArrayItemTypeRepr::Integer => Self::Integer, + ArrayItemTypeRepr::Number => Self::Number, + ArrayItemTypeRepr::String { + min_length, + max_length, + } => Self::String(min_length, max_length), + ArrayItemTypeRepr::ByteArray { min_size, max_size } => { + Self::ByteArray(min_size, max_size) + } + ArrayItemTypeRepr::Identifier => Self::Identifier, + ArrayItemTypeRepr::Boolean => Self::Boolean, + ArrayItemTypeRepr::Date => Self::Date, + } + } +} + impl ArrayItemType { /// Sanitize a value to match the expected array item type pub fn sanitize_value_mut(&self, value: &mut Value) { @@ -630,3 +695,172 @@ mod tests { assert_eq!(val, Value::Text("not a number".to_string())); } } + +// --- canonical conversion trait impls (unification pass 1) --- +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for ArrayItemType {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for ArrayItemType {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + + #[test] + fn json_round_trip_integer_variant() { + use crate::serialization::JsonConvertible; + use serde_json::json; + // `Integer` is a unit variant — internally tagged it serializes as + // `{"$type":"integer"}` (camelCase variant name). + let original = ArrayItemType::Integer; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!({"$type": "integer"})); + let recovered = ArrayItemType::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_number_variant() { + use crate::serialization::JsonConvertible; + use serde_json::json; + let original = ArrayItemType::Number; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!({"$type": "number"})); + let recovered = ArrayItemType::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_string_variant() { + use crate::serialization::JsonConvertible; + use serde_json::json; + // `String(Option, Option)` — tuple variant, internally + // tagged with named size bounds: `{"$type":"string","minLength":min, + // "maxLength":max}`. JSON erases the `usize` size. + let original = ArrayItemType::String(Some(3), Some(50)); + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({"$type": "string", "minLength": 3, "maxLength": 50}) + ); + let recovered = ArrayItemType::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_byte_array_variant() { + use crate::serialization::JsonConvertible; + use serde_json::json; + let original = ArrayItemType::ByteArray(Some(0), Some(64)); + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({"$type": "byteArray", "minSize": 0, "maxSize": 64}) + ); + let recovered = ArrayItemType::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_identifier_variant() { + use crate::serialization::JsonConvertible; + use serde_json::json; + let original = ArrayItemType::Identifier; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!({"$type": "identifier"})); + let recovered = ArrayItemType::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_boolean_variant() { + use crate::serialization::JsonConvertible; + use serde_json::json; + // `Boolean` is a unit variant → `{"$type":"boolean"}` (camelCase name). + let original = ArrayItemType::Boolean; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!({"$type": "boolean"})); + let recovered = ArrayItemType::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_date_variant() { + use crate::serialization::JsonConvertible; + use serde_json::json; + // `Date` is a unit variant → `{"$type":"date"}` (camelCase name). + let original = ArrayItemType::Date; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!({"$type": "date"})); + let recovered = ArrayItemType::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_integer_variant() { + use crate::serialization::ValueConvertible; + use platform_value::platform_value; + let original = ArrayItemType::Integer; + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!({"$type": "integer"})); + let recovered = ArrayItemType::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_number_variant() { + use crate::serialization::ValueConvertible; + use platform_value::platform_value; + let original = ArrayItemType::Number; + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!({"$type": "number"})); + let recovered = ArrayItemType::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_string_variant() { + use crate::serialization::ValueConvertible; + use platform_value::platform_value; + // `usize` serializes through serde as `u64`-like → `Value::U64` in non-HR. + let original = ArrayItemType::String(Some(3), Some(50)); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({"$type": "string", "minLength": 3u64, "maxLength": 50u64}) + ); + let recovered = ArrayItemType::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_byte_array_variant() { + use crate::serialization::ValueConvertible; + use platform_value::platform_value; + let original = ArrayItemType::ByteArray(Some(0), Some(64)); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({"$type": "byteArray", "minSize": 0u64, "maxSize": 64u64}) + ); + let recovered = ArrayItemType::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_identifier_variant() { + use crate::serialization::ValueConvertible; + use platform_value::platform_value; + let original = ArrayItemType::Identifier; + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!({"$type": "identifier"})); + let recovered = ArrayItemType::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/document_type/property/mod.rs b/packages/rs-dpp/src/data_contract/document_type/property/mod.rs index cf8401be797..1b37b3ca8e3 100644 --- a/packages/rs-dpp/src/data_contract/document_type/property/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/property/mod.rs @@ -2088,6 +2088,57 @@ impl DocumentPropertyType { // If decoding fails, leave the value as is (validation will catch it later) } + // Normalize an array of integers to bytes for ByteArray fields. A + // binary property re-hydrated through a schemaless JSON layer (e.g. an + // edited-and-replaced cached document) arrives as a plain array of + // numbers rather than Value::Bytes; convert it here, on the client + // build path, so the strict binary serializer receives Value::Bytes. + // (The block-processing serialize path never sanitizes, so this does + // not change which state transitions are accepted.) + (DocumentPropertyType::ByteArray(property_sizes), Value::Array(array)) => { + let decoded: Result, _> = + array.iter().map(|byte| byte.to_integer::()).collect(); + + if let Ok(bytes) = decoded { + let byte_len = bytes.len(); + + let size_ok = match (property_sizes.min_size, property_sizes.max_size) { + (Some(min), Some(max)) => { + byte_len >= min as usize && byte_len <= max as usize + } + (Some(min), None) => byte_len >= min as usize, + (None, Some(max)) => byte_len <= max as usize, + (None, None) => true, + }; + + if size_ok { + match bytes.len() { + 20 => { + if let Ok(arr) = bytes.try_into() { + *value = Value::Bytes20(arr); + } + } + 32 => { + if let Ok(arr) = bytes.try_into() { + *value = Value::Bytes32(arr); + } + } + 36 => { + if let Ok(arr) = bytes.try_into() { + *value = Value::Bytes36(arr); + } + } + _ => { + *value = Value::Bytes(bytes); + } + } + } + // If size constraints are not met, leave the value as is. + } + // If any element is not a 0..=255 integer, leave the value as is + // (validation will reject it later). + } + // Convert hex or base58 strings to identifiers for Identifier fields (DocumentPropertyType::Identifier, Value::Text(str_value)) => { // First try base58 decoding (most common for identifiers) diff --git a/packages/rs-dpp/src/data_contract/document_type/v0/random_document_type.rs b/packages/rs-dpp/src/data_contract/document_type/v0/random_document_type.rs index 34a844509d1..a247d23c1bd 100644 --- a/packages/rs-dpp/src/data_contract/document_type/v0/random_document_type.rs +++ b/packages/rs-dpp/src/data_contract/document_type/v0/random_document_type.rs @@ -354,11 +354,11 @@ impl DocumentTypeV0 { ArrayItemType::Integer => json!({"type": "integer"}), ArrayItemType::Number => json!({"type": "number"}), ArrayItemType::ByteArray(min, max) => { - json!({"type": "array", "items": {"type": "byte"}, "minItems": min, "maxItems": max}) + json!({"type": "array", "items": {"$type": "byte"}, "minItems": min, "maxItems": max}) }, ArrayItemType::Identifier => json!({"type": "array"}), - ArrayItemType::Boolean => json!({"type": "bool"}), - ArrayItemType::Date => json!({"type": "date"}), + ArrayItemType::Boolean => json!({"$type": "bool"}), + ArrayItemType::Date => json!({"$type": "date"}), }; json!({ diff --git a/packages/rs-dpp/src/data_contract/factory/v0/mod.rs b/packages/rs-dpp/src/data_contract/factory/v0/mod.rs index 1f4a08b992e..6931d83badd 100644 --- a/packages/rs-dpp/src/data_contract/factory/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/factory/v0/mod.rs @@ -113,18 +113,24 @@ impl DataContractFactoryV0 { .contract_versions .contract_structure_version { - 0 => Ok(DataContractV0::from_value( - data_contract_object, - full_validation, - platform_version, - )? - .into()), - 1 => Ok(DataContractV1::from_value( - data_contract_object, - full_validation, - platform_version, - )? - .into()), + 0 => { + let v0 = if full_validation { + DataContractV0::from_value(data_contract_object, true, platform_version)? + } else { + platform_value::from_value::(data_contract_object) + .map_err(ProtocolError::ValueError)? + }; + Ok(v0.into()) + } + 1 => { + let v1 = if full_validation { + DataContractV1::from_value(data_contract_object, true, platform_version)? + } else { + platform_value::from_value::(data_contract_object) + .map_err(ProtocolError::ValueError)? + }; + Ok(v1.into()) + } version => Err(ProtocolError::UnknownVersionMismatch { method: "DataContractFactoryV0::create_from_object".to_string(), known_versions: vec![0, 1], @@ -229,10 +235,8 @@ mod tests { let created_data_contract = get_data_contract_fixture(None, 0, platform_version.protocol_version); - let raw_data_contract = created_data_contract - .data_contract() - .to_value(platform_version) - .unwrap(); + let raw_data_contract = + platform_value::to_value(created_data_contract.data_contract()).unwrap(); let factory = DataContractFactoryV0::new(platform_version.protocol_version); TestData { @@ -349,14 +353,15 @@ mod tests { result.identity_nonce() ); - let contract_value = DataContract::try_from_platform_versioned( - result.data_contract().to_owned(), - false, - &mut vec![], - platform_version, + let contract_value = platform_value::to_value( + &DataContract::try_from_platform_versioned( + result.data_contract().to_owned(), + false, + &mut vec![], + platform_version, + ) + .unwrap(), ) - .unwrap() - .to_value(platform_version) .unwrap(); assert_eq!(raw_data_contract, contract_value); diff --git a/packages/rs-dpp/src/data_contract/group/mod.rs b/packages/rs-dpp/src/data_contract/group/mod.rs index f90dd77e7ed..e46c58815dd 100644 --- a/packages/rs-dpp/src/data_contract/group/mod.rs +++ b/packages/rs-dpp/src/data_contract/group/mod.rs @@ -107,3 +107,85 @@ impl GroupMethodsV0 for Group { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::data_contract::group::v0::GroupV0; + use platform_value::Identifier; + use serde_json::json; + use std::collections::BTreeMap; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + fn fixture() -> Group { + let mut members = BTreeMap::new(); + members.insert(Identifier::new([0xa0; 32]), 1u32); + members.insert(Identifier::new([0xb1; 32]), 2u32); + Group::V0(GroupV0 { + members, + required_power: 2, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `members` keys are `Identifier` — JSON renders them as the + // base58-encoded string (e.g. "Bp2HuBWdciXFKV2CnoC1Z4V44QfCmArCQHdzKpArYJc7" + // for `[0xa0; 32]`). Member-power values are `u32`; JSON has only one + // number type, so the U32 distinction is erased on the wire — the + // value-path assertion below uses `1u32` / `2u32` to lock it in. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "members": { + "Bp2HuBWdciXFKV2CnoC1Z4V44QfCmArCQHdzKpArYJc7": 1, + "CxeKLJRofna6h2GqCsjdVwc2D7EdDFX8uDy9KGw3Ey68": 2, + }, + "requiredPower": 2, + }) + ); + let recovered = Group::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `members` is `BTreeMap`. platform_value renders + // the BTreeMap as a `Value::Map([(Identifier, U32), ...])` (NOT the + // textual-keyed `{ ... }` form, because the keys are Identifiers, not + // strings) and preserves the U32 variant on the values. We construct + // the expected map directly because `platform_value!{...}` only emits + // string-keyed Maps. + use platform_value::Value; + let expected = Value::Map(vec![ + ( + Value::Text("$formatVersion".to_string()), + Value::Text("0".to_string()), + ), + ( + Value::Text("members".to_string()), + Value::Map(vec![ + (Value::Identifier([0xa0; 32]), Value::U32(1)), + (Value::Identifier([0xb1; 32]), Value::U32(2)), + ]), + ), + (Value::Text("requiredPower".to_string()), Value::U32(2)), + ]); + assert_eq!(value, expected); + let recovered = Group::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/mod.rs b/packages/rs-dpp/src/data_contract/mod.rs index bb91c2ac147..c0b96345f1f 100644 --- a/packages/rs-dpp/src/data_contract/mod.rs +++ b/packages/rs-dpp/src/data_contract/mod.rs @@ -109,6 +109,26 @@ pub enum DataContract { V1(DataContractV1), } +// Note: DataContract intentionally does NOT implement JsonConvertible / ValueConvertible. +// Round-tripping goes through the manual `Serialize` / `Deserialize` impls in +// `data_contract/conversion/serde/mod.rs`, which thread `DataContractInSerializationFormat` +// at the *currently active* `PlatformVersion` (see Critical-4 doc there). +// +// The two version-aware conversion traits are: +// * `DataContractJsonConversionMethodsV0::from_json(value, full_validation, pv)` — +// deserialize from JSON, running full schema validation when `full_validation` is +// `true` (use on trust boundaries). Pass `false` to reconstruct already-trusted data +// (e.g. storage reads) without re-validating. The canonical +// `serde_json::from_value::` path also validates (it routes through +// this with `full_validation = true`) — see the Critical-4 doc. +// * `DataContractValueConversionMethodsV0::from_value(value, full_validation, pv)` — +// same shape for `platform_value::Value`. +// +// For non-validating *serialization*, just use `serde_json::to_value(&dc)?` / +// `platform_value::to_value(&dc)?` — the manual Serialize impl handles versioning. +// `DataContractInSerializationFormat` (the underlying serialization shape) DOES implement the +// canonical traits — see `data_contract/serialized_version/mod.rs`. + impl PlatformSerializableWithPlatformVersion for DataContract { type Error = ProtocolError; diff --git a/packages/rs-dpp/src/data_contract/serialized_version/mod.rs b/packages/rs-dpp/src/data_contract/serialized_version/mod.rs index 188c8a7a841..a71dd8cb0b7 100644 --- a/packages/rs-dpp/src/data_contract/serialized_version/mod.rs +++ b/packages/rs-dpp/src/data_contract/serialized_version/mod.rs @@ -12,6 +12,8 @@ use crate::data_contract::{ }; #[cfg(feature = "json-conversion")] use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; use crate::validation::operations::ProtocolValidationOperation; use crate::version::PlatformVersion; use crate::ProtocolError; @@ -97,6 +99,10 @@ impl fmt::Display for DataContractMismatch { all(feature = "json-conversion", feature = "serde-conversion"), derive(JsonConvertible) )] +#[cfg_attr( + all(feature = "value-conversion", feature = "serde-conversion"), + derive(ValueConvertible) +)] #[derive(Debug, Clone, Encode, Decode, PartialEq, PlatformVersioned, From)] #[cfg_attr( feature = "serde-conversion", @@ -1166,3 +1172,97 @@ mod tests { assert_eq!(pv.dpp.contract_versions.contract_structure_version, 1); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::data_contract::config::v0::DataContractConfigV0; + use crate::data_contract::config::DataContractConfig; + use crate::data_contract::serialized_version::v0::DataContractInSerializationFormatV0; + use platform_value::Identifier; + use std::collections::BTreeMap; + + fn fixture() -> DataContractInSerializationFormat { + DataContractInSerializationFormat::V0(DataContractInSerializationFormatV0 { + id: Identifier::new([0xa1; 32]), + config: DataContractConfig::V0(DataContractConfigV0::default()), + version: 1, + owner_id: Identifier::new([0xb2; 32]), + schema_defs: None, + document_schemas: BTreeMap::new(), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + use serde_json::json; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Tier 3 envelope-only: `DataContractInSerializationFormat` embeds a + // versioned `DataContractConfig` and arbitrary `document_schemas` / + // `schema_defs` Values. The full inline expansion is verified for the + // `DataContractConfig` in its own module. We still pin the top-level + // envelope keys + their types here so that any silent drop / rename / + // re-keying at this layer would fail the test. + assert_eq!(json["$formatVersion"], "0"); + assert_eq!( + json["id"], + json!("Bswb3UyeD1pUTaGiE6WvqwFpJZsQSEY1xhJePCDTHdvp") + ); + assert_eq!( + json["ownerId"], + json!("D2ZcUbtpG5sKq7XLeB4YnpNnTGSptKCxTddoNeydzJQq") + ); + assert_eq!(json["version"], json!(1)); + assert_eq!(json["schemaDefs"], json!(null)); + assert_eq!(json["documentSchemas"], json!({})); + assert!(json.get("config").is_some(), "config envelope present"); + assert_eq!(json["config"]["$formatVersion"], "0"); + let recovered = DataContractInSerializationFormat::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + use platform_value::Value; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Tier 3 envelope-only: see JSON test above. Keys remain `Identifier` / + // `Map` / typed integers in non-HR mode (no base58 stringification). + let map = match &value { + Value::Map(m) => m, + other => panic!("expected Value::Map, got {:?}", other), + }; + let get = |k: &str| -> &Value { + map.iter() + .find(|(key, _)| matches!(key, Value::Text(t) if t == k)) + .map(|(_, v)| v) + .unwrap_or_else(|| panic!("missing key {k}")) + }; + assert_eq!(get("$formatVersion"), &Value::Text("0".to_string())); + assert_eq!(get("id"), &Value::Identifier([0xa1; 32])); + assert_eq!(get("ownerId"), &Value::Identifier([0xb2; 32])); + assert_eq!(get("version"), &Value::U32(1)); + assert_eq!(get("schemaDefs"), &Value::Null); + // documentSchemas: empty Map + assert!(matches!(get("documentSchemas"), Value::Map(m) if m.is_empty())); + // config: nested Map with its own $formatVersion="0" + assert!(matches!(get("config"), Value::Map(_))); + let recovered = DataContractInSerializationFormat::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn json_preserves_format_version_tag() { + use crate::serialization::JsonConvertible; + let json = fixture().to_json().expect("to_json"); + assert_eq!(json["$formatVersion"], "0"); + } +} diff --git a/packages/rs-dpp/src/data_contract/storage_requirements/keys_for_document_type.rs b/packages/rs-dpp/src/data_contract/storage_requirements/keys_for_document_type.rs index b825466eba2..0b1838d4cc3 100644 --- a/packages/rs-dpp/src/data_contract/storage_requirements/keys_for_document_type.rs +++ b/packages/rs-dpp/src/data_contract/storage_requirements/keys_for_document_type.rs @@ -49,3 +49,91 @@ impl TryFrom for StorageKeyRequirements { } } } + +// --- canonical conversion trait impls (unification pass 1) --- +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for StorageKeyRequirements {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for StorageKeyRequirements {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + + // `StorageKeyRequirements` is `#[repr(u8)]` with `serde_repr` — + // it serializes as the bare numeric discriminant (not a struct/string). + // JSON erases the u8 distinction; the value-path tests use `0u8`/`1u8`/`2u8`. + + #[test] + fn json_round_trip_unique() { + use crate::serialization::JsonConvertible; + use serde_json::json; + let original = StorageKeyRequirements::Unique; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!(0)); + let recovered = StorageKeyRequirements::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_multiple() { + use crate::serialization::JsonConvertible; + use serde_json::json; + let original = StorageKeyRequirements::Multiple; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!(1)); + let recovered = StorageKeyRequirements::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_multiple_reference_to_latest() { + use crate::serialization::JsonConvertible; + use serde_json::json; + let original = StorageKeyRequirements::MultipleReferenceToLatest; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!(2)); + let recovered = StorageKeyRequirements::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_unique() { + use crate::serialization::ValueConvertible; + use platform_value::Value; + let original = StorageKeyRequirements::Unique; + let value = original.to_object().expect("to_object"); + // `0u8`: `#[repr(u8)]` with `Serialize_repr` produces `Value::U8(0)`. + assert_eq!(value, Value::U8(0)); + let recovered = StorageKeyRequirements::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_multiple() { + use crate::serialization::ValueConvertible; + use platform_value::Value; + let original = StorageKeyRequirements::Multiple; + let value = original.to_object().expect("to_object"); + assert_eq!(value, Value::U8(1)); + let recovered = StorageKeyRequirements::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_multiple_reference_to_latest() { + use crate::serialization::ValueConvertible; + use platform_value::Value; + let original = StorageKeyRequirements::MultipleReferenceToLatest; + let value = original.to_object().expect("to_object"); + assert_eq!(value, Value::U8(2)); + let recovered = StorageKeyRequirements::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/data_contract/v0/conversion/cbor.rs b/packages/rs-dpp/src/data_contract/v0/conversion/cbor.rs index c2aba05b124..29c0d14c218 100644 --- a/packages/rs-dpp/src/data_contract/v0/conversion/cbor.rs +++ b/packages/rs-dpp/src/data_contract/v0/conversion/cbor.rs @@ -1,5 +1,6 @@ use crate::data_contract::conversion::cbor::DataContractCborConversionMethodsV0; use crate::data_contract::conversion::value::v0::DataContractValueConversionMethodsV0; +use crate::data_contract::serialized_version::DataContractInSerializationFormat; use crate::data_contract::v0::DataContractV0; use crate::util::cbor_value::CborCanonicalMap; @@ -7,6 +8,7 @@ use crate::version::PlatformVersion; use crate::ProtocolError; use ciborium::Value as CborValue; use platform_value::{Identifier, Value}; +use platform_version::TryFromPlatformVersioned; use std::convert::TryFrom; impl DataContractCborConversionMethodsV0 for DataContractV0 { @@ -37,11 +39,17 @@ impl DataContractCborConversionMethodsV0 for DataContractV0 { let data_contract_value: Value = Value::try_from(data_contract_cbor_value).map_err(ProtocolError::ValueError)?; - Self::from_value(data_contract_value, full_validation, platform_version) + if full_validation { + Self::from_value(data_contract_value, true, platform_version) + } else { + platform_value::from_value(data_contract_value).map_err(ProtocolError::ValueError) + } } fn to_cbor(&self, platform_version: &PlatformVersion) -> Result, ProtocolError> { - let value = self.to_value(platform_version)?; + let format = + DataContractInSerializationFormat::try_from_platform_versioned(self, platform_version)?; + let value = platform_value::to_value(format).map_err(ProtocolError::ValueError)?; let mut buf: Vec = Vec::new(); diff --git a/packages/rs-dpp/src/data_contract/v0/conversion/json.rs b/packages/rs-dpp/src/data_contract/v0/conversion/json.rs index 90e05b5f1fd..882b4aa7093 100644 --- a/packages/rs-dpp/src/data_contract/v0/conversion/json.rs +++ b/packages/rs-dpp/src/data_contract/v0/conversion/json.rs @@ -6,7 +6,6 @@ use crate::version::PlatformVersion; use crate::ProtocolError; use serde_json::Value as JsonValue; -use std::convert::TryInto; impl DataContractJsonConversionMethodsV0 for DataContractV0 { fn from_json( @@ -16,23 +15,4 @@ impl DataContractJsonConversionMethodsV0 for DataContractV0 { ) -> Result { Self::from_value(json_value.into(), full_validation, platform_version) } - - /// Returns Data Contract as a JSON Value - fn to_json(&self, platform_version: &PlatformVersion) -> Result { - self.to_value(platform_version)? - .try_into() - .map_err(ProtocolError::ValueError) - - // TODO: I guess we should convert the binary fields back to base64/base58? - } - - /// Returns Data Contract as a JSON Value that can be used for validation - fn to_validating_json( - &self, - platform_version: &PlatformVersion, - ) -> Result { - self.to_value(platform_version)? - .try_into_validating_json() - .map_err(ProtocolError::ValueError) - } } diff --git a/packages/rs-dpp/src/data_contract/v0/conversion/value.rs b/packages/rs-dpp/src/data_contract/v0/conversion/value.rs index fd6bc358e5e..a868d18dd91 100644 --- a/packages/rs-dpp/src/data_contract/v0/conversion/value.rs +++ b/packages/rs-dpp/src/data_contract/v0/conversion/value.rs @@ -1,12 +1,10 @@ use crate::data_contract::conversion::value::v0::DataContractValueConversionMethodsV0; use crate::data_contract::serialized_version::property_names; use crate::data_contract::serialized_version::v0::DataContractInSerializationFormatV0; -use crate::data_contract::serialized_version::DataContractInSerializationFormat; use crate::data_contract::v0::DataContractV0; use crate::version::PlatformVersion; use crate::ProtocolError; use platform_value::{ReplacementType, Value}; -use platform_version::TryFromPlatformVersioned; pub const DATA_CONTRACT_IDENTIFIER_FIELDS_V0: [&str; 2] = [property_names::ID, property_names::OWNER_ID]; @@ -43,24 +41,4 @@ impl DataContractValueConversionMethodsV0 for DataContractV0 { }), } } - - fn to_value(&self, platform_version: &PlatformVersion) -> Result { - let data_contract_data = - DataContractInSerializationFormat::try_from_platform_versioned(self, platform_version)?; - - let value = - platform_value::to_value(data_contract_data).map_err(ProtocolError::ValueError)?; - - Ok(value) - } - - fn into_value(self, platform_version: &PlatformVersion) -> Result { - let data_contract_data = - DataContractInSerializationFormat::try_from_platform_versioned(self, platform_version)?; - - let value = - platform_value::to_value(data_contract_data).map_err(ProtocolError::ValueError)?; - - Ok(value) - } } diff --git a/packages/rs-dpp/src/data_contract/v1/conversion/cbor.rs b/packages/rs-dpp/src/data_contract/v1/conversion/cbor.rs index 08e5e700819..3847697b204 100644 --- a/packages/rs-dpp/src/data_contract/v1/conversion/cbor.rs +++ b/packages/rs-dpp/src/data_contract/v1/conversion/cbor.rs @@ -1,5 +1,6 @@ use crate::data_contract::conversion::cbor::DataContractCborConversionMethodsV0; use crate::data_contract::conversion::value::v0::DataContractValueConversionMethodsV0; +use crate::data_contract::serialized_version::DataContractInSerializationFormat; use crate::util::cbor_value::CborCanonicalMap; use crate::data_contract::DataContractV1; @@ -7,6 +8,7 @@ use crate::version::PlatformVersion; use crate::ProtocolError; use ciborium::Value as CborValue; use platform_value::{Identifier, Value}; +use platform_version::TryFromPlatformVersioned; use std::convert::TryFrom; impl DataContractCborConversionMethodsV0 for DataContractV1 { @@ -37,11 +39,17 @@ impl DataContractCborConversionMethodsV0 for DataContractV1 { let data_contract_value: Value = Value::try_from(data_contract_cbor_value).map_err(ProtocolError::ValueError)?; - Self::from_value(data_contract_value, full_validation, platform_version) + if full_validation { + Self::from_value(data_contract_value, true, platform_version) + } else { + platform_value::from_value(data_contract_value).map_err(ProtocolError::ValueError) + } } fn to_cbor(&self, platform_version: &PlatformVersion) -> Result, ProtocolError> { - let value = self.to_value(platform_version)?; + let format = + DataContractInSerializationFormat::try_from_platform_versioned(self, platform_version)?; + let value = platform_value::to_value(format).map_err(ProtocolError::ValueError)?; let mut buf: Vec = Vec::new(); diff --git a/packages/rs-dpp/src/data_contract/v1/conversion/json.rs b/packages/rs-dpp/src/data_contract/v1/conversion/json.rs index 56478ddb8ff..e7ba9554e0b 100644 --- a/packages/rs-dpp/src/data_contract/v1/conversion/json.rs +++ b/packages/rs-dpp/src/data_contract/v1/conversion/json.rs @@ -1,12 +1,11 @@ use crate::data_contract::conversion::json::DataContractJsonConversionMethodsV0; use crate::data_contract::conversion::value::v0::DataContractValueConversionMethodsV0; +use crate::data_contract::DataContractV1; use crate::version::PlatformVersion; use crate::ProtocolError; -use crate::data_contract::DataContractV1; use serde_json::Value as JsonValue; -use std::convert::TryInto; impl DataContractJsonConversionMethodsV0 for DataContractV1 { fn from_json( @@ -16,23 +15,4 @@ impl DataContractJsonConversionMethodsV0 for DataContractV1 { ) -> Result { Self::from_value(json_value.into(), full_validation, platform_version) } - - /// Returns Data Contract as a JSON Value - fn to_json(&self, platform_version: &PlatformVersion) -> Result { - self.to_value(platform_version)? - .try_into() - .map_err(ProtocolError::ValueError) - - // TODO: I guess we should convert the binary fields back to base64/base58? - } - - /// Returns Data Contract as a JSON Value that can be used for validation - fn to_validating_json( - &self, - platform_version: &PlatformVersion, - ) -> Result { - self.to_value(platform_version)? - .try_into_validating_json() - .map_err(ProtocolError::ValueError) - } } diff --git a/packages/rs-dpp/src/data_contract/v1/conversion/value.rs b/packages/rs-dpp/src/data_contract/v1/conversion/value.rs index 2413e38a938..8ee2f897cbc 100644 --- a/packages/rs-dpp/src/data_contract/v1/conversion/value.rs +++ b/packages/rs-dpp/src/data_contract/v1/conversion/value.rs @@ -1,13 +1,12 @@ use crate::data_contract::conversion::value::v0::DataContractValueConversionMethodsV0; +use crate::data_contract::serialized_version::property_names; use crate::data_contract::serialized_version::v0::DataContractInSerializationFormatV0; use crate::data_contract::serialized_version::v1::DataContractInSerializationFormatV1; -use crate::data_contract::serialized_version::{property_names, DataContractInSerializationFormat}; use crate::data_contract::DataContractV1; use crate::version::PlatformVersion; use crate::ProtocolError; use platform_value::{ReplacementType, Value}; -use platform_version::TryFromPlatformVersioned; pub const DATA_CONTRACT_IDENTIFIER_FIELDS_V0: [&str; 2] = [property_names::ID, property_names::OWNER_ID]; @@ -55,24 +54,4 @@ impl DataContractValueConversionMethodsV0 for DataContractV1 { }), } } - - fn to_value(&self, platform_version: &PlatformVersion) -> Result { - let data_contract_data = - DataContractInSerializationFormat::try_from_platform_versioned(self, platform_version)?; - - let value = - platform_value::to_value(data_contract_data).map_err(ProtocolError::ValueError)?; - - Ok(value) - } - - fn into_value(self, platform_version: &PlatformVersion) -> Result { - let data_contract_data = - DataContractInSerializationFormat::try_from_platform_versioned(self, platform_version)?; - - let value = - platform_value::to_value(data_contract_data).map_err(ProtocolError::ValueError)?; - - Ok(value) - } } diff --git a/packages/rs-dpp/src/document/document_patch/mod.rs b/packages/rs-dpp/src/document/document_patch/mod.rs index 5c85e5d6c73..d007b862bce 100644 --- a/packages/rs-dpp/src/document/document_patch/mod.rs +++ b/packages/rs-dpp/src/document/document_patch/mod.rs @@ -5,6 +5,10 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; /// Documents contain the data that goes into data contracts. +// Auto-injects `json_safe_option_u64` on `revision: Option` and +// `updated_at: Option` (both Option). The `properties: +// BTreeMap` flatten catchall is skipped by the macro. +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] pub struct DocumentPatch { /// The unique document ID. @@ -19,3 +23,79 @@ pub struct DocumentPatch { #[serde(rename = "$updatedAt")] pub updated_at: Option, } + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for DocumentPatch {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for DocumentPatch {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_documentpatch { + use super::*; + use platform_value::Identifier; + + fn fixture() -> DocumentPatch { + let mut properties = BTreeMap::new(); + properties.insert("name".to_string(), Value::Text("alice".to_string())); + properties.insert("count".to_string(), Value::U64(42)); + DocumentPatch { + id: Identifier::new([0x77; 32]), + properties, + revision: Some(3), + updated_at: Some(1_700_000_000_000), + } + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + use serde_json::json; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `DocumentPatch` uses `#[serde(flatten)]` for `properties`, so the + // `name` / `count` keys are inlined at the top level. `Identifier` is + // base58 in JSON. `revision` is `Option` (u64) and + // `updated_at` is `Option` (u64); JSON erases the + // u64 distinction (value-path locks `3u64` / `1_700_000_000_000_u64`). + assert_eq!( + json, + json!({ + "$id": "93MB2qRDNVLxbmmPuYpLdAqn3u2x9ZhaVZK5wELHueP8", + "count": 42, + "name": "alice", + "$revision": 3, + "$updatedAt": 1_700_000_000_000_u64, + }) + ); + let recovered = DocumentPatch::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + use platform_value::platform_value; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Non-HR: `$id` stays `Value::Identifier`; `count` was stored as + // `Value::U64(42)` directly in the fixture; revision/updated_at are u64. + assert_eq!( + value, + platform_value!({ + "$id": Identifier::new([0x77; 32]), + "count": 42u64, + "name": "alice", + "$revision": 3u64, + "$updatedAt": 1_700_000_000_000_u64, + }) + ); + let recovered = DocumentPatch::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/document/extended_document/mod.rs b/packages/rs-dpp/src/document/extended_document/mod.rs index 7d1da656f92..6b9fd509bde 100644 --- a/packages/rs-dpp/src/document/extended_document/mod.rs +++ b/packages/rs-dpp/src/document/extended_document/mod.rs @@ -1,7 +1,5 @@ mod accessors; mod fields; -#[cfg(feature = "serde-conversion")] -mod serde_serialize; mod serialize; pub(crate) mod v0; @@ -13,8 +11,6 @@ use crate::ProtocolError; use crate::document::extended_document::v0::ExtendedDocumentV0; -#[cfg(feature = "json-conversion")] -use crate::document::serialization_traits::DocumentJsonMethodsV0; #[cfg(feature = "validation")] use crate::validation::SimpleConsensusValidationResult; use derive_more::From; @@ -27,10 +23,179 @@ use serde_json::Value as JsonValue; use std::collections::BTreeMap; #[derive(Debug, Clone, PlatformVersioned, From)] +#[cfg_attr( + feature = "serde-conversion", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "$extendedFormatVersion") +)] pub enum ExtendedDocument { + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(ExtendedDocumentV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for ExtendedDocument {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for ExtendedDocument {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::data_contract::accessors::v0::DataContractV0Getters; + use crate::document::extended_document::v0::ExtendedDocumentV0; + use crate::document::v0::DocumentV0; + use crate::document::Document; + use crate::tests::fixtures::get_data_contract_fixture; + use platform_value::{Bytes32, Identifier}; + use platform_version::version::PlatformVersion; + use std::collections::BTreeMap; + + fn fixture() -> ExtendedDocument { + let pv = PlatformVersion::latest(); + let created = get_data_contract_fixture(None, 0, pv.protocol_version); + let data_contract = created.data_contract().clone(); + let data_contract_id = data_contract.id(); + + let document = Document::V0(DocumentV0 { + id: Identifier::new([0xa1; 32]), + owner_id: Identifier::new([0xb2; 32]), + properties: BTreeMap::new(), + revision: Some(1), + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + }); + + ExtendedDocument::V0(ExtendedDocumentV0 { + document_type_name: "niceDocument".to_string(), + data_contract_id, + document, + data_contract, + metadata: None, + entropy: Bytes32::new([0xcc; 32]), + token_payment_info: None, + }) + } + + // Tier 3: ExtendedDocument embeds a full `DataContract` (with all schemas + // + tokens + groups), so an inline wire-shape assertion would be enormous + // and brittle. We assert envelope only on the top-level discriminator and + // deterministic siblings; the embedded `Document` and `DataContract` have + // their own per-type round-trip tests that lock down their wire shapes. + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = JsonConvertible::to_json(&original).expect("to_json"); + // Envelope assertions: the inner Document and DataContract are flattened + // into the root, so the surface includes BOTH wrappers (`$extendedFormatVersion`, + // `$type`, `$dataContractId`, `$dataContract`, `$entropy`, `$tokenPaymentInfo`, + // `$metadata`) AND all the embedded Document `$id` / `$ownerId` / `$revision` + // / `$createdAt*` / `$updatedAt*` / `$transferredAt*` / `$creatorId` keys. + // We only lock down the wrapper-specific keys and trust that + // Document and DataContract have their own per-type round-trip tests. + let obj = json.as_object().expect("json is an object"); + assert_eq!( + obj.get("$extendedFormatVersion"), + Some(&serde_json::json!("0")) + ); + assert_eq!(obj.get("$type"), Some(&serde_json::json!("niceDocument"))); + // entropy is `Bytes32` → base64 in JSON + assert_eq!( + obj.get("$entropy"), + Some(&serde_json::json!( + "zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMw=" + )) + ); + assert_eq!(obj.get("$tokenPaymentInfo"), Some(&serde_json::Value::Null)); + assert_eq!(obj.get("$metadata"), Some(&serde_json::Value::Null)); + assert!(obj.get("$dataContractId").is_some_and(|v| v.is_string())); + assert!(obj.get("$dataContract").is_some_and(|v| v.is_object())); + // Document is flattened, so `$formatVersion` (the document's) is at the root too. + assert_eq!(obj.get("$formatVersion"), Some(&serde_json::json!("0"))); + let recovered = ::from_json(json).expect("from_json"); + // ExtendedDocument lacks PartialEq — match variant + assert key fields. + let ExtendedDocument::V0(orig_v0) = original; + let ExtendedDocument::V0(rec_v0) = recovered; + assert_eq!( + orig_v0.document_type_name, rec_v0.document_type_name, + "document_type_name" + ); + assert_eq!( + orig_v0.data_contract_id, rec_v0.data_contract_id, + "data_contract_id" + ); + assert_eq!(orig_v0.entropy, rec_v0.entropy, "entropy"); + assert_eq!( + orig_v0.token_payment_info, rec_v0.token_payment_info, + "token_payment_info" + ); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = ValueConvertible::to_object(&original).expect("to_object"); + // Envelope assertions: see json test above — Document + DataContract + // are flattened into the root; we only lock down wrapper-specific keys. + let map = value.as_map().expect("value is a map"); + let get = |key: &str| { + map.iter() + .find(|(k, _)| k.as_text() == Some(key)) + .map(|(_, v)| v) + }; + assert_eq!( + get("$extendedFormatVersion"), + Some(&platform_value::Value::Text("0".to_string())) + ); + assert_eq!( + get("$type"), + Some(&platform_value::Value::Text("niceDocument".to_string())) + ); + assert_eq!( + get("$entropy"), + Some(&platform_value::Value::Bytes32([0xcc; 32])) + ); + assert_eq!(get("$tokenPaymentInfo"), Some(&platform_value::Value::Null)); + assert_eq!(get("$metadata"), Some(&platform_value::Value::Null)); + assert!(get("$dataContractId") + .is_some_and(|v| matches!(v, platform_value::Value::Identifier(_)))); + assert!(get("$dataContract").is_some_and(|v| v.is_map())); + // Document is flattened into the root. + assert_eq!( + get("$formatVersion"), + Some(&platform_value::Value::Text("0".to_string())) + ); + let recovered = + ::from_object(value).expect("from_object"); + let ExtendedDocument::V0(orig_v0) = original; + let ExtendedDocument::V0(rec_v0) = recovered; + assert_eq!( + orig_v0.document_type_name, rec_v0.document_type_name, + "document_type_name" + ); + assert_eq!( + orig_v0.data_contract_id, rec_v0.data_contract_id, + "data_contract_id" + ); + assert_eq!(orig_v0.entropy, rec_v0.entropy, "entropy"); + } +} + impl ExtendedDocument { #[cfg(feature = "json-conversion")] /// Returns the properties of the document as a JSON value. @@ -185,29 +350,6 @@ impl ExtendedDocument { } } - /// Convert the extended document to a JSON object. - /// - /// This function is a passthrough to the `to_json` method. - #[cfg(feature = "json-conversion")] - pub fn to_json(&self, platform_version: &PlatformVersion) -> Result { - match self { - ExtendedDocument::V0(v0) => v0.to_json(platform_version), - } - } - - /// Convert the extended document to a pretty JSON object. - /// - /// This function is a passthrough to the `to_pretty_json` method. - #[cfg(feature = "json-conversion")] - pub fn to_pretty_json( - &self, - platform_version: &PlatformVersion, - ) -> Result { - match self { - ExtendedDocument::V0(v0) => v0.to_pretty_json(platform_version), - } - } - /// Convert the extended document to a BTreeMap of string keys and Value instances. /// /// This function is a passthrough to the `to_map_value` method. @@ -228,36 +370,6 @@ impl ExtendedDocument { } } - /// Convert the extended document to a Value instance consuming the instance. - /// - /// This function is a passthrough to the `into_value` method. - #[cfg(feature = "value-conversion")] - pub fn into_value(self) -> Result { - match self { - ExtendedDocument::V0(v0) => v0.into_value(), - } - } - - /// Convert the extended document to a Value instance. - /// - /// This function is a passthrough to the `to_value` method. - #[cfg(feature = "value-conversion")] - pub fn to_value(&self) -> Result { - match self { - ExtendedDocument::V0(v0) => v0.to_value(), - } - } - - /// Convert the extended document to a JSON object for validation. - /// - /// This function is a passthrough to the `to_json_object_for_validation` method. - #[cfg(feature = "json-conversion")] - pub fn to_json_object_for_validation(&self) -> Result { - match self { - ExtendedDocument::V0(v0) => v0.to_json_object_for_validation(), - } - } - /// Calculate the hash of the extended document. /// /// This function is a passthrough to the `hash` method. @@ -312,13 +424,12 @@ impl ExtendedDocument { #[cfg(test)] mod test { use anyhow::Result; - use serde_json::{json, Value as JsonValue}; + use serde_json::Value as JsonValue; use std::convert::TryInto; - use crate::document::extended_document::{ExtendedDocument, IDENTIFIER_FIELDS}; + use crate::document::extended_document::ExtendedDocument; use crate::data_contract::accessors::v0::DataContractV0Getters; - use crate::data_contract::DataContract; use crate::document::extended_document::v0::ExtendedDocumentV0; use crate::prelude::Identifier; @@ -332,85 +443,15 @@ mod test { use platform_version::version::{PlatformVersion, LATEST_PLATFORM_VERSION}; use pretty_assertions::assert_eq; - use crate::data_contract::conversion::value::v0::DataContractValueConversionMethodsV0; use crate::data_contract::document_type::random_document::CreateRandomDocument; use crate::document::serialization_traits::ExtendedDocumentPlatformConversionMethodsV0; use crate::tests::fixtures::get_dashpay_contract_fixture; - use base64::prelude::BASE64_STANDARD; - use base64::Engine; fn init() { let _ = env_logger::builder() .filter_level(log::LevelFilter::Debug) .try_init(); } - pub(crate) fn data_contract_with_dynamic_properties() -> DataContract { - let platform_version = PlatformVersion::latest(); - // The following is equivalent to the data contract - // { - // "protocolVersion" :0, - // "$id" : vec![0_u8;32], - // "$schema" : "schema", - // "version" : 0, - // "ownerId" : vec![0_u8;32], - // "documents" : { - // "test" : { - // "properties" : { - // "alphaIdentifier" : { - // "type": "array", - // "byteArray": true, - // "contentMediaType": "application/x.dash.dpp.identifier", - // }, - // "alphaBinary" : { - // "type": "array", - // "byteArray": true, - // } - // } - // } - // } - // } - let test_document_properties_alpha_identifier = Value::from([ - ("type", Value::Text("array".to_string())), - ("byteArray", Value::Bool(true)), - ("minItems", Value::U64(32)), - ("maxItems", Value::U64(32)), - ("position", Value::U64(0)), - ( - "contentMediaType", - Value::Text("application/x.dash.dpp.identifier".to_string()), - ), - ]); - let test_document_properties_alpha_binary = Value::from([ - ("type", Value::Text("array".to_string())), - ("byteArray", Value::Bool(true)), - ("position", Value::U64(1)), - ]); - let test_document_properties = Value::from([ - ("alphaIdentifier", test_document_properties_alpha_identifier), - ("alphaBinary", test_document_properties_alpha_binary), - ]); - let test_document = Value::from([ - ("type", Value::Text("object".to_string())), - ("properties", test_document_properties), - ("additionalProperties", Value::Bool(false)), - ]); - let documents = Value::from([("test", test_document)]); - - DataContract::from_value( - Value::from([ - ("protocolVersion", Value::U32(1)), - ("id", Value::Identifier([0_u8; 32])), - ("$schema", Value::Text("schema".to_string())), - ("version", Value::U32(0)), - ("ownerId", Value::Identifier([0_u8; 32])), - ("documentSchemas", documents), - ("$formatVersion", Value::Text("0".to_string())), - ]), - true, - platform_version, - ) - .unwrap() - } #[test] #[cfg(feature = "json-conversion")] @@ -508,30 +549,6 @@ mod test { assert_eq!(init_doc.owner_id(), doc.owner_id()); } - #[test] - fn test_to_object() { - init(); - let dpns_contract = - load_system_data_contract(SystemDataContract::DPNS, LATEST_PLATFORM_VERSION).unwrap(); - let document_json = get_data_from_file("src/tests/payloads/document_dpns.json").unwrap(); - let document = ExtendedDocument::from_json_string( - &document_json, - dpns_contract, - LATEST_PLATFORM_VERSION, - ) - .unwrap(); - let document_object = document.to_json_object_for_validation().unwrap(); - - for property in IDENTIFIER_FIELDS { - let id = document_object - .get(property) - .unwrap() - .as_array() - .expect("the property must be an array"); - assert_eq!(32, id.len()) - } - } - #[test] fn test_json_serialize() -> Result<()> { init(); @@ -544,12 +561,28 @@ mod test { dpns_contract, LATEST_PLATFORM_VERSION, )?; - let string = serde_json::to_string(&document)?; + let value: JsonValue = serde_json::to_value(&document)?; assert_eq!( - "{\"version\":0,\"$type\":\"domain\",\"$dataContractId\":\"566vcJkmebVCAb2Dkj2yVMSgGFcsshupnQqtsz1RFbcy\",\"document\":{\"$formatVersion\":\"0\",\"$id\":\"4veLBZPHDkaCPF9LfZ8fX3JZiS5q5iUVGhdBbaa9ga5E\",\"$ownerId\":\"HBNMY5QWuBVKNFLhgBTC1VmpEnscrmqKPMXpnYSHwhfn\",\"$dataContractId\":\"566vcJkmebVCAb2Dkj2yVMSgGFcsshupnQqtsz1RFbcy\",\"$protocolVersion\":0,\"$type\":\"domain\",\"label\":\"user-9999\",\"normalizedLabel\":\"user-9999\",\"normalizedParentDomainName\":\"dash\",\"preorderSalt\":\"BzQi567XVqc8wYiVHS887sJtL6MDbxLHNnp+UpTFSB0=\",\"records\":{\"identity\":\"HBNMY5QWuBVKNFLhgBTC1VmpEnscrmqKPMXpnYSHwhfn\"},\"subdomainRules\":{\"allowSubdomains\":false},\"$revision\":1,\"$createdAt\":null,\"$updatedAt\":null,\"$transferredAt\":null,\"$createdAtBlockHeight\":null,\"$updatedAtBlockHeight\":null,\"$transferredAtBlockHeight\":null,\"$createdAtCoreBlockHeight\":null,\"$updatedAtCoreBlockHeight\":null,\"$transferredAtCoreBlockHeight\":null,\"$creatorId\":null}}", - string + value["$extendedFormatVersion"], + JsonValue::String("0".to_string()), + "outer enum version is its own key, distinct from the inner Document's $formatVersion", + ); + assert_eq!( + value["$formatVersion"], + JsonValue::String("0".to_string()), + "inner Document's version surfaces at top level via serde(flatten)", + ); + assert_eq!(value["$type"], JsonValue::String("domain".to_string())); + assert_eq!( + value["$dataContractId"], + JsonValue::String("566vcJkmebVCAb2Dkj2yVMSgGFcsshupnQqtsz1RFbcy".to_string()) ); + assert_eq!( + value["$id"], + JsonValue::String("4veLBZPHDkaCPF9LfZ8fX3JZiS5q5iUVGhdBbaa9ga5E".to_string()) + ); + assert_eq!(value["label"], JsonValue::String("user-9999".to_string())); Ok(()) } @@ -566,57 +599,6 @@ mod test { Ok(()) } - #[test] - fn json_should_generate_human_readable_binaries() { - let data_contract = data_contract_with_dynamic_properties(); - let alpha_value = vec![10_u8; 32]; - let id = vec![11_u8; 32]; - let owner_id = vec![12_u8; 32]; - let data_contract_id = vec![13_u8; 32]; - - let raw_document = json!({ - "$protocolVersion" : 0, - "$id" : id, - "$ownerId" : owner_id, - "$type" : "test", - "$dataContractId" : data_contract_id, - "$revision" : 1, - "alphaBinary" : alpha_value, - "alphaIdentifier" : alpha_value, - }); - - let document = ExtendedDocument::from_raw_json_document( - raw_document, - data_contract, - LATEST_PLATFORM_VERSION, - ) - .unwrap(); - let json_document = document - .to_pretty_json(LATEST_PLATFORM_VERSION) - .expect("no errors"); - - assert_eq!( - json_document["$id"], - JsonValue::String(bs58::encode(&id).into_string()) - ); - assert_eq!( - json_document["$ownerId"], - JsonValue::String(bs58::encode(&owner_id).into_string()) - ); - assert_eq!( - json_document["$dataContractId"], - JsonValue::String(bs58::encode(&data_contract_id).into_string()) - ); - assert_eq!( - json_document["alphaBinary"], - JsonValue::String(BASE64_STANDARD.encode(&alpha_value)) - ); - assert_eq!( - json_document["alphaIdentifier"], - JsonValue::String(BASE64_STANDARD.encode(&alpha_value)) - ); - } - fn document_bytes() -> Vec { new_example_document() .serialize_to_bytes(LATEST_PLATFORM_VERSION) diff --git a/packages/rs-dpp/src/document/extended_document/serde_serialize.rs b/packages/rs-dpp/src/document/extended_document/serde_serialize.rs deleted file mode 100644 index 00c6741ebe6..00000000000 --- a/packages/rs-dpp/src/document/extended_document/serde_serialize.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::data_contract::DataContract; -use crate::document::extended_document::v0::ExtendedDocumentV0; -use crate::document::{Document, ExtendedDocument}; -use platform_value::{Bytes32, Identifier}; -use serde::de::{MapAccess, Visitor}; -use serde::ser::SerializeMap; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::fmt; - -impl Serialize for ExtendedDocument { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut state = serializer.serialize_map(None)?; - - match *self { - ExtendedDocument::V0(ref v0) => { - state.serialize_entry("version", &0u16)?; - state.serialize_entry("$type", &v0.document_type_name)?; - state.serialize_entry("$dataContractId", &v0.data_contract_id)?; - state.serialize_entry("document", &v0.document)?; - } - } - - state.end() - } -} - -struct ExtendedDocumentVisitor; - -impl<'de> Visitor<'de> for ExtendedDocumentVisitor { - type Value = ExtendedDocument; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a map representing an ExtendedDocument") - } - - fn visit_map(self, mut map: A) -> Result - where - A: MapAccess<'de>, - { - let mut version: Option = None; - let mut document_type_name: Option = None; - let mut data_contract_id: Option = None; - let mut document: Option = None; - let data_contract: Option = None; - - while let Some(key) = map.next_key()? { - match key { - "$version" => { - version = Some(map.next_value()?); - } - "$type" => { - document_type_name = Some(map.next_value()?); - } - "$dataContractId" => { - data_contract_id = Some(map.next_value()?); - } - "document" => { - document = Some(map.next_value()?); - } - _ => {} - } - } - - let version = version.ok_or_else(|| serde::de::Error::missing_field("$version"))?; - let document_type_name = - document_type_name.ok_or_else(|| serde::de::Error::missing_field("$type"))?; - let data_contract_id = - data_contract_id.ok_or_else(|| serde::de::Error::missing_field("$dataContractId"))?; - let data_contract = - data_contract.ok_or_else(|| serde::de::Error::missing_field("$dataContract"))?; - let document = document.ok_or_else(|| serde::de::Error::missing_field("document"))?; - - match version { - 0 => Ok(ExtendedDocument::V0(ExtendedDocumentV0 { - document_type_name, - data_contract_id, - document, - data_contract, - metadata: None, - entropy: Bytes32::default(), - token_payment_info: None, - })), - _ => Err(serde::de::Error::unknown_variant( - &format!("{}", version), - &[], - )), - } - } -} - -impl<'de> Deserialize<'de> for ExtendedDocument { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_map(ExtendedDocumentVisitor) - } -} diff --git a/packages/rs-dpp/src/document/extended_document/v0/json_conversion.rs b/packages/rs-dpp/src/document/extended_document/v0/json_conversion.rs deleted file mode 100644 index 1db82cf214b..00000000000 --- a/packages/rs-dpp/src/document/extended_document/v0/json_conversion.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::data_contract::conversion::json::DataContractJsonConversionMethodsV0; -use crate::document::extended_document::fields::property_names; -use crate::document::extended_document::v0::ExtendedDocumentV0; -use crate::document::serialization_traits::{ - DocumentJsonMethodsV0, DocumentPlatformValueMethodsV0, -}; - -use crate::ProtocolError; -use platform_value::Identifier; -use platform_version::version::PlatformVersion; -use serde::Deserialize; -use serde_json::Value as JsonValue; -use std::convert::TryInto; - -impl DocumentJsonMethodsV0<'_> for ExtendedDocumentV0 { - fn to_json_with_identifiers_using_bytes( - &self, - platform_version: &PlatformVersion, - ) -> Result { - let mut json = self - .document - .to_json_with_identifiers_using_bytes(platform_version)?; - let value_mut = json.as_object_mut().unwrap(); - let contract = self.data_contract.to_validating_json(platform_version)?; - value_mut.insert(property_names::DATA_CONTRACT.to_owned(), contract); - value_mut.insert( - property_names::DOCUMENT_TYPE_NAME.to_owned(), - self.document_type_name.clone().into(), - ); - Ok(json) - } - - fn to_json(&self, platform_version: &PlatformVersion) -> Result { - let mut json = self.document.to_json(platform_version)?; - let value_mut = json.as_object_mut().unwrap(); - let contract = self.data_contract.to_json(platform_version)?; - value_mut.insert(property_names::DATA_CONTRACT.to_owned(), contract); - value_mut.insert( - property_names::DOCUMENT_TYPE_NAME.to_owned(), - self.document_type_name.clone().into(), - ); - Ok(json) - } - - fn from_json_value( - document_value: JsonValue, - platform_version: &PlatformVersion, - ) -> Result - where - for<'de> S: Deserialize<'de> + TryInto, - E: Into, - { - Self::from_platform_value(document_value.into(), platform_version) - } -} diff --git a/packages/rs-dpp/src/document/extended_document/v0/mod.rs b/packages/rs-dpp/src/document/extended_document/v0/mod.rs index f77111667cd..ba2fd2002b0 100644 --- a/packages/rs-dpp/src/document/extended_document/v0/mod.rs +++ b/packages/rs-dpp/src/document/extended_document/v0/mod.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "json-conversion")] -mod json_conversion; #[cfg(feature = "value-conversion")] mod platform_value_conversion; mod serialize; @@ -33,8 +31,6 @@ use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; use crate::data_contract::document_type::methods::DocumentTypeBasicMethods; #[cfg(feature = "validation")] use crate::data_contract::validate_document::DataContractDocumentValidationMethodsV0; -#[cfg(feature = "json-conversion")] -use crate::document::serialization_traits::DocumentJsonMethodsV0; #[cfg(feature = "value-conversion")] use crate::document::serialization_traits::DocumentPlatformValueMethodsV0; use crate::document::serialization_traits::ExtendedDocumentPlatformConversionMethodsV0; @@ -49,62 +45,63 @@ use serde_json::Value as JsonValue; /// The `ExtendedDocumentV0` struct represents the data provided by the platform in response to a query. #[derive(Debug, Clone)] -#[cfg_attr( - all(feature = "serde-conversion", feature = "serde-conversion"), - derive(Serialize, Deserialize) -)] +#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] pub struct ExtendedDocumentV0 { /// The document type name, stored as a string. - #[cfg_attr( - all(feature = "serde-conversion", feature = "serde-conversion"), - serde(rename = "$type") - )] + #[cfg_attr(feature = "serde-conversion", serde(rename = "$type"))] pub document_type_name: String, /// The identifier of the associated data contract. - #[cfg_attr( - all(feature = "serde-conversion", feature = "serde-conversion"), - serde(rename = "$dataContractId") - )] + #[cfg_attr(feature = "serde-conversion", serde(rename = "$dataContractId"))] pub data_contract_id: Identifier, /// The actual document object containing the data. - #[cfg_attr( - all(feature = "serde-conversion", feature = "serde-conversion"), - serde(flatten) - )] + #[cfg_attr(feature = "serde-conversion", serde(flatten))] pub document: Document, // TODO: We should remove it from here, or at least keep a ref // also there is no point to keep both contract and its ID /// The data contract associated with the document. - #[cfg_attr( - all(feature = "serde-conversion", feature = "serde-conversion"), - serde(rename = "$dataContract") - )] + #[cfg_attr(feature = "serde-conversion", serde(rename = "$dataContract"))] pub data_contract: DataContract, /// An optional field for metadata associated with the document. - #[cfg_attr( - all(feature = "serde-conversion", feature = "serde-conversion"), - serde(rename = "$metadata", default) - )] + #[cfg_attr(feature = "serde-conversion", serde(rename = "$metadata", default))] pub metadata: Option, /// A field representing the entropy, stored as `Bytes32`. - #[cfg_attr( - all(feature = "serde-conversion", feature = "serde-conversion"), - serde(rename = "$entropy") - )] + #[cfg_attr(feature = "serde-conversion", serde(rename = "$entropy"))] pub entropy: Bytes32, /// A field representing the token payment info. - #[cfg_attr( - all(feature = "serde-conversion", feature = "serde-conversion"), - serde(rename = "$tokenPaymentInfo") - )] + #[cfg_attr(feature = "serde-conversion", serde(rename = "$tokenPaymentInfo"))] pub token_payment_info: Option, } +/// Ensures a Document `Value::Map` carries the canonical +/// `$formatVersion: "0"` tag before it's passed to canonical +/// `Document::from_object` (which is `tag = "$formatVersion"`). +/// +/// Used by ExtendedDocument's `from_trusted_platform_value` / +/// `from_untrusted_platform_value` to accept legacy un-tagged shapes +/// (DPNS / DashPay JSON test fixtures, untrusted JS user input via +/// wasm-dpp legacy) and route them through the canonical deserializer. +/// V0 is the only Document structure version, so unconditionally +/// inserting `"0"` is correct today; if a future structure version is +/// introduced, this helper needs to grow a version-selection step. +#[cfg(feature = "value-conversion")] +fn ensure_document_format_version(document_value: &mut Value) -> Result<(), ProtocolError> { + use platform_value::ValueMapHelper; + if let Value::Map(map) = document_value { + let has_tag = map + .iter() + .any(|(k, _)| k.as_text() == Some("$formatVersion")); + if !has_tag { + map.insert_string_key_value("$formatVersion".to_string(), Value::Text("0".to_string())); + } + } + Ok(()) +} + impl ExtendedDocumentV0 { #[cfg(feature = "json-conversion")] pub(super) fn properties_as_json_data(&self) -> Result { @@ -269,9 +266,9 @@ impl ExtendedDocumentV0 { /// /// Returns a `ProtocolError` if there is an error processing the trusted platform value. pub fn from_trusted_platform_value( - document_value: Value, + mut document_value: Value, data_contract: DataContract, - platform_version: &PlatformVersion, + _platform_version: &PlatformVersion, ) -> Result { let mut properties = document_value .clone() @@ -291,7 +288,14 @@ impl ExtendedDocumentV0 { .remove_string(property_names::DOCUMENT_TYPE_NAME) .map_err(ProtocolError::ValueError)?; - let document = Document::from_platform_value(document_value, platform_version)?; + // Insert the canonical `$formatVersion` tag (Phase D step 8 slice + // B): the legacy `Document::from_platform_value` accepted + // un-tagged shapes, but it has been deleted. Untrusted user + // input lacks the tag, so we add it before routing through + // canonical `Document::from_object`. + ensure_document_format_version(&mut document_value)?; + use crate::serialization::ValueConvertible; + let document = Document::from_object(document_value)?; let data_contract_id = data_contract.id(); let mut extended_document = Self { @@ -327,7 +331,7 @@ impl ExtendedDocumentV0 { pub fn from_untrusted_platform_value( mut document_value: Value, data_contract: DataContract, - platform_version: &PlatformVersion, + _platform_version: &PlatformVersion, ) -> Result { let mut properties = document_value .clone() @@ -364,7 +368,9 @@ impl ExtendedDocumentV0 { ReplacementType::BinaryBytes, )?; - let document = Document::from_platform_value(document_value, platform_version)?; + ensure_document_format_version(&mut document_value)?; + use crate::serialization::ValueConvertible; + let document = Document::from_object(document_value)?; let data_contract_id = data_contract.id(); let mut extended_document = Self { data_contract, @@ -386,36 +392,6 @@ impl ExtendedDocumentV0 { Ok(extended_document) } - #[cfg(feature = "json-conversion")] - /// Convert the extended document to a pretty JSON object. - /// - /// # Errors - /// - /// Returns a `ProtocolError` if there is an error converting the document to pretty JSON. - pub fn to_pretty_json( - &self, - platform_version: &PlatformVersion, - ) -> Result { - let mut value = self.document.to_json(platform_version)?; - let value_mut = value.as_object_mut().unwrap(); - value_mut.insert( - property_names::DOCUMENT_TYPE_NAME.to_string(), - JsonValue::String(self.document_type_name.clone()), - ); - value_mut.insert( - property_names::DATA_CONTRACT_ID.to_string(), - JsonValue::String(bs58::encode(self.data_contract_id.to_buffer()).into_string()), - ); - if let Some(token_payment_info) = self.token_payment_info { - let value: Value = token_payment_info.try_into()?; - value_mut.insert( - property_names::TOKEN_PAYMENT_INFO.to_string(), - value.try_into_validating_json()?, - ); - } - Ok(value) - } - #[cfg(feature = "value-conversion")] pub fn to_map_value(&self) -> Result, ProtocolError> { let mut object = self.document.to_map_value()?; @@ -465,23 +441,6 @@ impl ExtendedDocumentV0 { Ok(object) } - #[cfg(feature = "value-conversion")] - pub fn into_value(self) -> Result { - Ok(self.into_map_value()?.into()) - } - - #[cfg(feature = "value-conversion")] - pub fn to_value(&self) -> Result { - Ok(self.to_map_value()?.into()) - } - - #[cfg(feature = "json-conversion")] - pub fn to_json_object_for_validation(&self) -> Result { - self.to_value()? - .try_into_validating_json() - .map_err(ProtocolError::ValueError) - } - pub fn hash(&self, platform_version: &PlatformVersion) -> Result, ProtocolError> { Ok(hash_double_to_vec( ExtendedDocumentPlatformConversionMethodsV0::serialize_to_bytes( @@ -822,28 +781,6 @@ mod tests { ); } - // ================================================================ - // to_value and into_value - // ================================================================ - - #[test] - fn to_value_produces_a_map_value() { - let platform_version = PlatformVersion::latest(); - let (ext_doc, _) = make_extended_document(platform_version); - - let val = ext_doc.to_value().expect("to_value should succeed"); - assert!(val.is_map(), "to_value should produce a map Value"); - } - - #[test] - fn into_value_produces_a_map_value() { - let platform_version = PlatformVersion::latest(); - let (ext_doc, _) = make_extended_document(platform_version); - - let val = ext_doc.into_value().expect("into_value should succeed"); - assert!(val.is_map(), "into_value should produce a map Value"); - } - // ================================================================ // properties_as_json_data // ================================================================ @@ -862,53 +799,6 @@ mod tests { ); } - // ================================================================ - // to_json_object_for_validation - // ================================================================ - - #[test] - fn to_json_object_for_validation_returns_json_object() { - let platform_version = PlatformVersion::latest(); - let (ext_doc, _) = make_extended_document(platform_version); - - let json_obj = ext_doc - .to_json_object_for_validation() - .expect("to_json_object_for_validation should succeed"); - assert!( - json_obj.is_object(), - "should return a JSON object for validation" - ); - } - - // ================================================================ - // to_pretty_json - // ================================================================ - - #[test] - fn to_pretty_json_includes_type_and_contract_id() { - let platform_version = PlatformVersion::latest(); - let (ext_doc, contract) = make_extended_document(platform_version); - - let pretty = ext_doc - .to_pretty_json(platform_version) - .expect("to_pretty_json should succeed"); - let obj = pretty.as_object().expect("should be a JSON object"); - assert!( - obj.contains_key(property_names::DOCUMENT_TYPE_NAME), - "pretty JSON should contain $type" - ); - assert!( - obj.contains_key(property_names::DATA_CONTRACT_ID), - "pretty JSON should contain $dataContractId" - ); - // Verify the contract id is base58-encoded - let contract_id_str = obj[property_names::DATA_CONTRACT_ID] - .as_str() - .expect("$dataContractId should be a string"); - let expected_b58 = bs58::encode(contract.id().to_buffer()).into_string(); - assert_eq!(contract_id_str, expected_b58); - } - // ================================================================ // hash // ================================================================ @@ -1266,42 +1156,6 @@ mod tests { ); } - // ================================================================ - // to_json / to_json_with_identifiers_using_bytes - // ================================================================ - - #[test] - fn to_json_includes_type_and_data_contract() { - let platform_version = PlatformVersion::latest(); - let (ext_doc, _) = make_extended_document(platform_version); - - let json = ext_doc - .to_json(platform_version) - .expect("to_json should succeed"); - let obj = json.as_object().expect("json object"); - assert!( - obj.contains_key(property_names::DOCUMENT_TYPE_NAME), - "must contain $type" - ); - assert!( - obj.contains_key(property_names::DATA_CONTRACT), - "must contain $dataContract" - ); - } - - #[test] - fn to_json_with_identifiers_using_bytes_includes_type_and_contract() { - let platform_version = PlatformVersion::latest(); - let (ext_doc, _) = make_extended_document(platform_version); - - let json = ext_doc - .to_json_with_identifiers_using_bytes(platform_version) - .expect("to_json_with_identifiers_using_bytes should succeed"); - let obj = json.as_object().expect("json object"); - assert!(obj.contains_key(property_names::DOCUMENT_TYPE_NAME)); - assert!(obj.contains_key(property_names::DATA_CONTRACT)); - } - // ================================================================ // to_map_value: token_payment_info is serialized when Some // ================================================================ @@ -1319,8 +1173,7 @@ mod tests { } // ================================================================ - // token_payment_info: Some branches for to_map_value / into_map_value / - // to_pretty_json. + // token_payment_info: Some branches for to_map_value / into_map_value. // // These exercise the `if let Some(token_payment_info) = ...` arms // that prior tests never hit (since they all used token_payment_info = @@ -1373,20 +1226,6 @@ mod tests { ); } - #[test] - fn to_pretty_json_includes_token_payment_info_when_some() { - let platform_version = PlatformVersion::latest(); - let (ext_doc, _) = make_extended_document_with_token_payment_info(platform_version); - let json = ext_doc - .to_pretty_json(platform_version) - .expect("to_pretty_json ok"); - let obj = json.as_object().expect("json object"); - assert!( - obj.contains_key(property_names::TOKEN_PAYMENT_INFO), - "pretty JSON should include $tokenPaymentInfo when Some" - ); - } - // ================================================================ // set_untrusted: binary-path arm (exercises ReplacementType::BinaryBytes). // @@ -1580,36 +1419,6 @@ mod tests { ); } - // ================================================================ - // to_json_object_for_validation fails gracefully when the document - // type name is not in the contract. - // ================================================================ - - #[test] - fn to_pretty_json_fails_for_unknown_document_type_name() { - // to_pretty_json calls document.to_json() which succeeds, but later - // steps in from_trusted_platform_value / value_conversion would fail. - // For pretty-json itself the only invariant that matters is that - // document_type_name is a string the impl preserves. Verify that at - // least the call succeeds when document_type_name does NOT exist on - // the contract — since to_pretty_json doesn't look up the document - // type. - let platform_version = PlatformVersion::latest(); - let (mut ext_doc, _) = make_extended_document(platform_version); - ext_doc.document_type_name = "doesNotExist".to_string(); - // to_pretty_json does NOT look up the document type on the contract, - // so it should succeed and include the bogus name. - let pretty = ext_doc - .to_pretty_json(platform_version) - .expect("to_pretty_json should succeed for any string name"); - let obj = pretty.as_object().expect("json object"); - assert_eq!( - obj.get(property_names::DOCUMENT_TYPE_NAME) - .and_then(|v| v.as_str()), - Some("doesNotExist") - ); - } - // ================================================================ // document_type() fails when document_type_name points to an unknown // type (exercises the error branch in document_type()). diff --git a/packages/rs-dpp/src/document/extended_document/v0/platform_value_conversion.rs b/packages/rs-dpp/src/document/extended_document/v0/platform_value_conversion.rs index a27f8f7ea45..9beb13c8bd8 100644 --- a/packages/rs-dpp/src/document/extended_document/v0/platform_value_conversion.rs +++ b/packages/rs-dpp/src/document/extended_document/v0/platform_value_conversion.rs @@ -1,7 +1,6 @@ use crate::document::extended_document::v0::ExtendedDocumentV0; use crate::document::property_names; use crate::document::serialization_traits::DocumentPlatformValueMethodsV0; -use crate::version::PlatformVersion; use crate::ProtocolError; use platform_value::Value; @@ -59,19 +58,4 @@ impl DocumentPlatformValueMethodsV0<'_> for ExtendedDocumentV0 { Ok(map) } - - fn into_value(self) -> Result { - Ok(self.into_map_value()?.into()) - } - - fn to_object(&self) -> Result { - Ok(self.to_map_value()?.into()) - } - - fn from_platform_value( - document_value: Value, - _platform_version: &PlatformVersion, - ) -> Result { - Ok(platform_value::from_value(document_value)?) - } } diff --git a/packages/rs-dpp/src/document/mod.rs b/packages/rs-dpp/src/document/mod.rs index 9076383b6b3..9a68e87425f 100644 --- a/packages/rs-dpp/src/document/mod.rs +++ b/packages/rs-dpp/src/document/mod.rs @@ -47,18 +47,21 @@ use std::fmt::Formatter; #[derive(Clone, Debug, PartialEq, From)] #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), + feature = "serde-conversion", derive(serde::Serialize, serde::Deserialize), serde(tag = "$formatVersion") )] pub enum Document { - #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), - serde(rename = "0") - )] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(DocumentV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for Document {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for Document {} + impl fmt::Display for Document { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { @@ -722,3 +725,100 @@ mod tests { assert_eq!(raw, Some(document.id().to_vec())); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + + use platform_value::{platform_value, Identifier}; + use serde_json::json; + use std::collections::BTreeMap; + + fn fixture() -> Document { + Document::V0(DocumentV0 { + id: Identifier::new([0xa1; 32]), + owner_id: Identifier::new([0xb2; 32]), + properties: BTreeMap::new(), + revision: Some(2), + created_at: Some(1_700_000_000_000), + updated_at: Some(1_700_000_001_000), + transferred_at: None, + created_at_block_height: Some(100), + updated_at_block_height: Some(101), + transferred_at_block_height: None, + created_at_core_block_height: Some(50), + updated_at_core_block_height: Some(51), + transferred_at_core_block_height: None, + creator_id: Some(Identifier::new([0xc3; 32])), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int fields whose JSON wire encoding loses size info: + // `$revision`/`$createdAt`/`$updatedAt`/`$createdAtBlockHeight`/ + // `$updatedAtBlockHeight` (u64), `$createdAtCoreBlockHeight`/ + // `$updatedAtCoreBlockHeight` (u32). The value-path locks variants + // via explicit suffixes. `properties` is flattened into the document + // root; for an empty `BTreeMap`, no extra keys appear. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$id": Identifier::new([0xa1; 32]), + "$ownerId": Identifier::new([0xb2; 32]), + "$revision": 2, + "$createdAt": 1_700_000_000_000u64, + "$updatedAt": 1_700_000_001_000u64, + "$transferredAt": serde_json::Value::Null, + "$createdAtBlockHeight": 100, + "$updatedAtBlockHeight": 101, + "$transferredAtBlockHeight": serde_json::Value::Null, + "$createdAtCoreBlockHeight": 50, + "$updatedAtCoreBlockHeight": 51, + "$transferredAtCoreBlockHeight": serde_json::Value::Null, + "$creatorId": Identifier::new([0xc3; 32]), + }) + ); + let recovered = Document::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Explicit suffixes lock in sized variants: revision / *At / + // *AtBlockHeight are u64; *AtCoreBlockHeight are u32. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$id": Identifier::new([0xa1; 32]), + "$ownerId": Identifier::new([0xb2; 32]), + "$revision": 2u64, + "$createdAt": 1_700_000_000_000u64, + "$updatedAt": 1_700_000_001_000u64, + "$transferredAt": platform_value::Value::Null, + "$createdAtBlockHeight": 100u64, + "$updatedAtBlockHeight": 101u64, + "$transferredAtBlockHeight": platform_value::Value::Null, + "$createdAtCoreBlockHeight": 50u32, + "$updatedAtCoreBlockHeight": 51u32, + "$transferredAtCoreBlockHeight": platform_value::Value::Null, + "$creatorId": Identifier::new([0xc3; 32]), + }) + ); + let recovered = Document::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/document/serialization_traits/json_conversion/mod.rs b/packages/rs-dpp/src/document/serialization_traits/json_conversion/mod.rs deleted file mode 100644 index 6d440f55c56..00000000000 --- a/packages/rs-dpp/src/document/serialization_traits/json_conversion/mod.rs +++ /dev/null @@ -1,56 +0,0 @@ -mod v0; - -pub use v0::*; - -use crate::document::{Document, DocumentV0}; -use crate::ProtocolError; -use platform_value::Identifier; -use platform_version::version::PlatformVersion; -use serde::Deserialize; -use serde_json::Value as JsonValue; -use std::convert::TryInto; - -impl DocumentJsonMethodsV0<'_> for Document { - /// Convert the document to JSON with identifiers using bytes. - fn to_json_with_identifiers_using_bytes( - &self, - platform_version: &PlatformVersion, - ) -> Result { - match self { - Document::V0(v0) => v0.to_json_with_identifiers_using_bytes(platform_version), - } - } - - /// Convert the document to a JSON value. - fn to_json(&self, platform_version: &PlatformVersion) -> Result { - match self { - Document::V0(v0) => v0.to_json(platform_version), - } - } - - /// Create a document from a JSON value. - fn from_json_value( - document_value: JsonValue, - platform_version: &PlatformVersion, - ) -> Result - where - for<'de> S: Deserialize<'de> + TryInto, - E: Into, - { - match platform_version - .dpp - .document_versions - .document_structure_version - { - 0 => Ok(Document::V0(DocumentV0::from_json_value::( - document_value, - platform_version, - )?)), - version => Err(ProtocolError::UnknownVersionMismatch { - method: "Document::from_json_value".to_string(), - known_versions: vec![0], - received: version, - }), - } - } -} diff --git a/packages/rs-dpp/src/document/serialization_traits/json_conversion/v0/mod.rs b/packages/rs-dpp/src/document/serialization_traits/json_conversion/v0/mod.rs deleted file mode 100644 index ed7254d48d2..00000000000 --- a/packages/rs-dpp/src/document/serialization_traits/json_conversion/v0/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::document::serialization_traits::DocumentPlatformValueMethodsV0; -use crate::ProtocolError; -use platform_value::Identifier; -use platform_version::version::PlatformVersion; -use serde::Deserialize; -use serde_json::Value as JsonValue; -use std::convert::TryInto; - -pub trait DocumentJsonMethodsV0<'a>: DocumentPlatformValueMethodsV0<'a> { - fn to_json_with_identifiers_using_bytes( - &self, - platform_version: &PlatformVersion, - ) -> Result; - fn to_json(&self, platform_version: &PlatformVersion) -> Result; - fn from_json_value( - document_value: JsonValue, - platform_version: &PlatformVersion, - ) -> Result - where - for<'de> S: Deserialize<'de> + TryInto, - E: Into, - Self: Sized; -} diff --git a/packages/rs-dpp/src/document/serialization_traits/mod.rs b/packages/rs-dpp/src/document/serialization_traits/mod.rs index 12887346592..414c43ee6ee 100644 --- a/packages/rs-dpp/src/document/serialization_traits/mod.rs +++ b/packages/rs-dpp/src/document/serialization_traits/mod.rs @@ -1,15 +1,11 @@ #[cfg(feature = "document-cbor-conversion")] mod cbor_conversion; -#[cfg(feature = "json-conversion")] -mod json_conversion; mod platform_serialization_conversion; #[cfg(feature = "value-conversion")] mod platform_value_conversion; #[cfg(feature = "document-cbor-conversion")] pub use cbor_conversion::*; -#[cfg(feature = "json-conversion")] -pub use json_conversion::*; pub use platform_serialization_conversion::*; #[cfg(feature = "value-conversion")] pub use platform_value_conversion::*; diff --git a/packages/rs-dpp/src/document/serialization_traits/platform_value_conversion/mod.rs b/packages/rs-dpp/src/document/serialization_traits/platform_value_conversion/mod.rs index 80cdd018b63..5d451bce535 100644 --- a/packages/rs-dpp/src/document/serialization_traits/platform_value_conversion/mod.rs +++ b/packages/rs-dpp/src/document/serialization_traits/platform_value_conversion/mod.rs @@ -2,8 +2,7 @@ mod v0; pub use v0::*; -use crate::document::{Document, DocumentV0}; -use crate::version::PlatformVersion; +use crate::document::Document; use crate::ProtocolError; use platform_value::Value; use std::collections::BTreeMap; @@ -22,40 +21,6 @@ impl DocumentPlatformValueMethodsV0<'_> for Document { Document::V0(v0) => v0.into_map_value(), } } - - /// Convert the document to a value consuming the document. - fn into_value(self) -> Result { - match self { - Document::V0(v0) => v0.into_value(), - } - } - - /// Convert the document to an object. - fn to_object(&self) -> Result { - match self { - Document::V0(v0) => v0.to_object(), - } - } - - /// Create a document from a platform value. - fn from_platform_value( - document_value: Value, - platform_version: &PlatformVersion, - ) -> Result { - match platform_version - .dpp - .document_versions - .document_structure_version - { - 0 => Ok(Document::V0(DocumentV0::from_platform_value( - document_value, - platform_version, - )?)), - version => Err(ProtocolError::UnknownVersionError(format!( - "version {version} not known for document for call from_platform_value" - ))), - } - } } #[cfg(test)] @@ -63,13 +28,19 @@ mod tests { use super::*; use crate::data_contract::accessors::v0::DataContractV0Getters; use crate::data_contract::document_type::random_document::CreateRandomDocument; - use crate::document::DocumentV0Getters; + use crate::document::{DocumentV0, DocumentV0Getters}; + use crate::serialization::ValueConvertible; use crate::tests::json_document::json_document_to_contract; use platform_value::Identifier; use platform_version::version::PlatformVersion; + // After Phase D step 8 slice A, the Value-shape round-trip lives on + // canonical `ValueConvertible` (`to_object` / `into_object` / + // `from_object`). The `to_map_value` / `into_map_value` helpers on + // this trait are tested below — they're the only methods that stay. + // ================================================================ - // Round-trip: Document -> Value -> Document + // Round-trip: Document -> Value -> Document via canonical traits // ================================================================ #[test] @@ -91,10 +62,8 @@ mod tests { .random_document(Some(seed), platform_version) .expect("expected random document"); - let value = Document::into_value(document.clone()).expect("into_value should succeed"); - - let recovered = Document::from_platform_value(value, platform_version) - .expect("from_platform_value should succeed"); + let value = document.clone().into_object().expect("into_object"); + let recovered = Document::from_object(value).expect("from_object"); assert_eq!(document.id(), recovered.id(), "id mismatch for seed {seed}"); assert_eq!( @@ -144,32 +113,6 @@ mod tests { assert!(map.contains_key("$ownerId"), "map should contain $ownerId"); } - // ================================================================ - // to_object returns a Value - // ================================================================ - - #[test] - fn to_object_returns_map_value() { - let platform_version = PlatformVersion::latest(); - let contract = json_document_to_contract( - "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", - false, - platform_version, - ) - .expect("expected to load dashpay contract"); - - let document_type = contract - .document_type_for_name("profile") - .expect("expected profile document type"); - - let document = document_type - .random_document(Some(7), platform_version) - .expect("expected random document"); - - let obj = document.to_object().expect("to_object should succeed"); - assert!(obj.is_map(), "to_object should return a Map value"); - } - // ================================================================ // into_map_value consumes document // ================================================================ @@ -197,27 +140,21 @@ mod tests { .into_map_value() .expect("into_map_value should succeed"); - // The map should contain the id let id_val = map.get("$id").expect("should have $id"); match id_val { Value::Identifier(bytes) => { - assert_eq!( - Identifier::new(*bytes), - original_id, - "id in map should match original" - ); + assert_eq!(Identifier::new(*bytes), original_id); } _ => panic!("$id should be an Identifier value"), } } // ================================================================ - // from_platform_value with minimal document + // from_object via canonical traits with minimal document // ================================================================ #[test] - fn from_platform_value_with_minimal_data() { - let platform_version = PlatformVersion::latest(); + fn from_object_with_minimal_data() { let id = Identifier::new([1u8; 32]); let owner_id = Identifier::new([2u8; 32]); @@ -238,9 +175,9 @@ mod tests { creator_id: None, }; - let value = DocumentV0::into_value(doc_v0).expect("into_value should succeed"); - let recovered = Document::from_platform_value(value, platform_version) - .expect("from_platform_value should succeed"); + let document: Document = doc_v0.into(); + let value = document.clone().into_object().expect("into_object"); + let recovered = Document::from_object(value).expect("from_object"); assert_eq!(recovered.id(), id); assert_eq!(recovered.owner_id(), owner_id); diff --git a/packages/rs-dpp/src/document/serialization_traits/platform_value_conversion/v0/mod.rs b/packages/rs-dpp/src/document/serialization_traits/platform_value_conversion/v0/mod.rs index fb372aa0293..d07517b7585 100644 --- a/packages/rs-dpp/src/document/serialization_traits/platform_value_conversion/v0/mod.rs +++ b/packages/rs-dpp/src/document/serialization_traits/platform_value_conversion/v0/mod.rs @@ -1,18 +1,33 @@ -use crate::version::PlatformVersion; use crate::ProtocolError; use platform_value::Value; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; +/// Document-specific value-conversion helpers — after Phase D step 8 +/// slice B, holds only the **map-shape** view that has no canonical +/// equivalent. +/// +/// `to_map_value` / `into_map_value` produce `BTreeMap`. +/// Used internally by `ExtendedDocument` (which composes a Document +/// plus metadata fields like `$dataContractId`, `$type`, `$entropy`) +/// and by `wasm-dpp2`'s DocumentWasm wrapper for the same composition. +/// Canonical `ValueConvertible::to_object` returns `Value::Map(...)`, +/// not the BTreeMap directly; the conversion is one extra step but +/// callers prefer the map. +/// +/// History: +/// - Slice A deleted `to_object` / `into_value` — 1:1 of canonical +/// `ValueConvertible::to_object` / `into_object`. +/// - Slice B deleted `from_platform_value` — accepted legacy +/// un-tagged Document values, but the only production caller path +/// (wasm-dpp2 DocumentWasm.fromObject + ExtendedDocument's +/// `from_trusted_platform_value` / `from_untrusted_platform_value`) +/// was migrated to canonical `Document::from_object` after +/// `DocumentWasm.toObject` was made to emit `$formatVersion`. +/// ExtendedDocument's untrusted ingest now inserts the tag explicitly +/// via the `ensure_document_format_version` helper before calling +/// canonical. pub trait DocumentPlatformValueMethodsV0<'a>: Serialize + Deserialize<'a> { fn to_map_value(&self) -> Result, ProtocolError>; fn into_map_value(self) -> Result, ProtocolError>; - fn into_value(self) -> Result; - fn to_object(&self) -> Result; - fn from_platform_value( - document_value: Value, - platform_version: &PlatformVersion, - ) -> Result - where - Self: Sized; } diff --git a/packages/rs-dpp/src/document/v0/cbor_conversion.rs b/packages/rs-dpp/src/document/v0/cbor_conversion.rs index 626ad8d0764..190fa5ffa69 100644 --- a/packages/rs-dpp/src/document/v0/cbor_conversion.rs +++ b/packages/rs-dpp/src/document/v0/cbor_conversion.rs @@ -5,9 +5,7 @@ use crate::prelude::{BlockHeight, CoreBlockHeight, Revision}; use crate::ProtocolError; -use crate::document::serialization_traits::{ - DocumentCborMethodsV0, DocumentPlatformValueMethodsV0, -}; +use crate::document::serialization_traits::DocumentCborMethodsV0; use crate::document::v0::DocumentV0; use crate::version::PlatformVersion; use ciborium::Value as CborValue; @@ -191,8 +189,13 @@ impl DocumentCborMethodsV0 for DocumentV0 { } fn to_cbor_value(&self) -> Result { - self.to_object() - .map(|v| v.try_into().map_err(ProtocolError::ValueError))? + // After Phase D step 8 slice A, the V0 trait `to_object` was + // deleted (1:1 canonical equivalent). Inline the body — + // `IdentityPublicKeyV0` doesn't derive `ValueConvertible` directly + // (only the outer `Document` enum does), so we go through + // `platform_value::to_value` directly. + let value = platform_value::to_value(self).map_err(ProtocolError::ValueError)?; + value.try_into().map_err(ProtocolError::ValueError) } /// Serializes the Document to CBOR. diff --git a/packages/rs-dpp/src/document/v0/json_conversion.rs b/packages/rs-dpp/src/document/v0/json_conversion.rs deleted file mode 100644 index 16ae09cc6b0..00000000000 --- a/packages/rs-dpp/src/document/v0/json_conversion.rs +++ /dev/null @@ -1,643 +0,0 @@ -use crate::document::fields::property_names; -use crate::document::serialization_traits::{ - DocumentJsonMethodsV0, DocumentPlatformValueMethodsV0, -}; -use crate::document::DocumentV0; -use crate::util::json_value::JsonValueExt; -use crate::ProtocolError; -use platform_value::{Identifier, Value}; -use platform_version::version::PlatformVersion; -use serde::Deserialize; -use serde_json::{json, Value as JsonValue}; -use std::convert::TryInto; - -impl DocumentJsonMethodsV0<'_> for DocumentV0 { - fn to_json_with_identifiers_using_bytes( - &self, - _platform_version: &PlatformVersion, - ) -> Result { - let mut value = json!({ - property_names::ID: self.id, - property_names::OWNER_ID: self.owner_id, - }); - let value_mut = value.as_object_mut().unwrap(); - if let Some(created_at) = self.created_at { - value_mut.insert( - property_names::CREATED_AT.to_string(), - JsonValue::Number(created_at.into()), - ); - } - if let Some(updated_at) = self.updated_at { - value_mut.insert( - property_names::UPDATED_AT.to_string(), - JsonValue::Number(updated_at.into()), - ); - } - if let Some(created_at_block_height) = self.created_at_block_height { - value_mut.insert( - property_names::CREATED_AT_BLOCK_HEIGHT.to_string(), - JsonValue::Number(created_at_block_height.into()), - ); - } - - if let Some(updated_at_block_height) = self.updated_at_block_height { - value_mut.insert( - property_names::UPDATED_AT_BLOCK_HEIGHT.to_string(), - JsonValue::Number(updated_at_block_height.into()), - ); - } - - if let Some(created_at_core_block_height) = self.created_at_core_block_height { - value_mut.insert( - property_names::CREATED_AT_CORE_BLOCK_HEIGHT.to_string(), - JsonValue::Number(created_at_core_block_height.into()), - ); - } - - if let Some(updated_at_core_block_height) = self.updated_at_core_block_height { - value_mut.insert( - property_names::UPDATED_AT_CORE_BLOCK_HEIGHT.to_string(), - JsonValue::Number(updated_at_core_block_height.into()), - ); - } - if let Some(transferred_at) = self.transferred_at { - value_mut.insert( - property_names::TRANSFERRED_AT.to_string(), - JsonValue::Number(transferred_at.into()), - ); - } - if let Some(transferred_at_block_height) = self.transferred_at_block_height { - value_mut.insert( - property_names::TRANSFERRED_AT_BLOCK_HEIGHT.to_string(), - JsonValue::Number(transferred_at_block_height.into()), - ); - } - if let Some(transferred_at_core_block_height) = self.transferred_at_core_block_height { - value_mut.insert( - property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT.to_string(), - JsonValue::Number(transferred_at_core_block_height.into()), - ); - } - if let Some(creator_id) = self.creator_id { - value_mut.insert(property_names::CREATOR_ID.to_string(), json!(creator_id)); - } - if let Some(revision) = self.revision { - value_mut.insert( - property_names::REVISION.to_string(), - JsonValue::Number(revision.into()), - ); - } - - self.properties - .iter() - .try_for_each(|(key, property_value)| { - let serde_value: JsonValue = property_value.try_to_validating_json()?; - value_mut.insert(key.to_string(), serde_value); - Ok::<(), ProtocolError>(()) - })?; - - Ok(value) - } - - fn to_json(&self, _platform_version: &PlatformVersion) -> Result { - self.to_object() - .map(|v| v.try_into().map_err(ProtocolError::ValueError))? - } - - fn from_json_value( - mut document_value: JsonValue, - _platform_version: &PlatformVersion, - ) -> Result - where - for<'de> S: Deserialize<'de> + TryInto, - E: Into, - { - let mut document = Self { - ..Default::default() - }; - - if let Ok(value) = document_value.remove(property_names::ID) { - if !value.is_null() { - let data: S = serde_json::from_value(value)?; - document.id = data.try_into().map_err(Into::into)?; - } - } - if let Ok(value) = document_value.remove(property_names::OWNER_ID) { - if !value.is_null() { - let data: S = serde_json::from_value(value)?; - document.owner_id = data.try_into().map_err(Into::into)?; - } - } - if let Ok(value) = document_value.remove(property_names::REVISION) { - document.revision = serde_json::from_value(value)? - } - if let Ok(value) = document_value.remove(property_names::CREATED_AT) { - document.created_at = serde_json::from_value(value)? - } - if let Ok(value) = document_value.remove(property_names::UPDATED_AT) { - document.updated_at = serde_json::from_value(value)? - } - if let Ok(value) = document_value.remove(property_names::CREATED_AT_BLOCK_HEIGHT) { - document.created_at_block_height = serde_json::from_value(value)?; - } - if let Ok(value) = document_value.remove(property_names::UPDATED_AT_BLOCK_HEIGHT) { - document.updated_at_block_height = serde_json::from_value(value)?; - } - if let Ok(value) = document_value.remove(property_names::CREATED_AT_CORE_BLOCK_HEIGHT) { - document.created_at_core_block_height = serde_json::from_value(value)?; - } - if let Ok(value) = document_value.remove(property_names::UPDATED_AT_CORE_BLOCK_HEIGHT) { - document.updated_at_core_block_height = serde_json::from_value(value)?; - } - if let Ok(value) = document_value.remove(property_names::TRANSFERRED_AT) { - document.transferred_at = serde_json::from_value(value)?; - } - if let Ok(value) = document_value.remove(property_names::TRANSFERRED_AT_BLOCK_HEIGHT) { - document.transferred_at_block_height = serde_json::from_value(value)?; - } - if let Ok(value) = document_value.remove(property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT) { - document.transferred_at_core_block_height = serde_json::from_value(value)?; - } - if let Ok(value) = document_value.remove(property_names::CREATOR_ID) { - if !value.is_null() { - let data: S = serde_json::from_value(value)?; - document.creator_id = Some(data.try_into().map_err(Into::into)?); - } - } - - let platform_value: Value = document_value.into(); - - document.properties = platform_value - .into_btree_string_map() - .map_err(ProtocolError::ValueError)?; - Ok(document) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::data_contract::accessors::v0::DataContractV0Getters; - use crate::data_contract::document_type::random_document::CreateRandomDocument; - use crate::document::serialization_traits::DocumentJsonMethodsV0; - use crate::tests::json_document::json_document_to_contract; - use platform_version::version::PlatformVersion; - use std::collections::BTreeMap; - - fn make_document_v0_with_all_timestamps() -> DocumentV0 { - let mut properties = BTreeMap::new(); - properties.insert("label".to_string(), Value::Text("test-label".to_string())); - DocumentV0 { - id: Identifier::new([1u8; 32]), - owner_id: Identifier::new([2u8; 32]), - properties, - revision: Some(3), - created_at: Some(1_700_000_000_000), - updated_at: Some(1_700_000_100_000), - transferred_at: Some(1_700_000_200_000), - created_at_block_height: Some(100), - updated_at_block_height: Some(200), - transferred_at_block_height: Some(300), - created_at_core_block_height: Some(50), - updated_at_core_block_height: Some(60), - transferred_at_core_block_height: Some(70), - creator_id: Some(Identifier::new([9u8; 32])), - } - } - - fn make_minimal_document_v0() -> DocumentV0 { - DocumentV0 { - id: Identifier::new([0xAA; 32]), - owner_id: Identifier::new([0xBB; 32]), - properties: BTreeMap::new(), - revision: None, - created_at: None, - updated_at: None, - transferred_at: None, - created_at_block_height: None, - updated_at_block_height: None, - transferred_at_block_height: None, - created_at_core_block_height: None, - updated_at_core_block_height: None, - transferred_at_core_block_height: None, - creator_id: None, - } - } - - // ================================================================ - // to_json produces a JsonValue containing all set fields - // ================================================================ - - #[test] - fn to_json_includes_id_and_owner_id() { - let platform_version = PlatformVersion::latest(); - let doc = make_minimal_document_v0(); - let json = doc - .to_json(platform_version) - .expect("to_json should succeed"); - let obj = json.as_object().expect("should be an object"); - assert!( - obj.contains_key(property_names::ID), - "JSON should contain $id" - ); - assert!( - obj.contains_key(property_names::OWNER_ID), - "JSON should contain $ownerId" - ); - } - - #[test] - fn to_json_represents_none_timestamps_as_null() { - let platform_version = PlatformVersion::latest(); - let doc = make_minimal_document_v0(); - let json = doc - .to_json(platform_version) - .expect("to_json should succeed"); - let obj = json.as_object().expect("should be an object"); - - // to_json serializes via serde, so None fields appear as null - if let Some(val) = obj.get(property_names::CREATED_AT) { - assert!( - val.is_null(), - "$createdAt should be null when None, got: {:?}", - val - ); - } - if let Some(val) = obj.get(property_names::UPDATED_AT) { - assert!( - val.is_null(), - "$updatedAt should be null when None, got: {:?}", - val - ); - } - if let Some(val) = obj.get(property_names::REVISION) { - assert!( - val.is_null(), - "$revision should be null when None, got: {:?}", - val - ); - } - } - - // ================================================================ - // to_json_with_identifiers_using_bytes includes all timestamps - // ================================================================ - - #[test] - fn to_json_with_identifiers_using_bytes_includes_all_timestamp_fields() { - let platform_version = PlatformVersion::latest(); - let doc = make_document_v0_with_all_timestamps(); - let json = doc - .to_json_with_identifiers_using_bytes(platform_version) - .expect("to_json_with_identifiers_using_bytes should succeed"); - let obj = json.as_object().expect("should be an object"); - - assert!(obj.contains_key(property_names::ID)); - assert!(obj.contains_key(property_names::OWNER_ID)); - assert!(obj.contains_key(property_names::REVISION)); - assert!(obj.contains_key(property_names::CREATED_AT)); - assert!(obj.contains_key(property_names::UPDATED_AT)); - assert!(obj.contains_key(property_names::TRANSFERRED_AT)); - assert!(obj.contains_key(property_names::CREATED_AT_BLOCK_HEIGHT)); - assert!(obj.contains_key(property_names::UPDATED_AT_BLOCK_HEIGHT)); - assert!(obj.contains_key(property_names::TRANSFERRED_AT_BLOCK_HEIGHT)); - assert!(obj.contains_key(property_names::CREATED_AT_CORE_BLOCK_HEIGHT)); - assert!(obj.contains_key(property_names::UPDATED_AT_CORE_BLOCK_HEIGHT)); - assert!(obj.contains_key(property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT)); - assert!(obj.contains_key(property_names::CREATOR_ID)); - - // Verify numeric values - assert_eq!(obj[property_names::REVISION].as_u64(), Some(3)); - assert_eq!( - obj[property_names::CREATED_AT].as_u64(), - Some(1_700_000_000_000) - ); - assert_eq!( - obj[property_names::UPDATED_AT].as_u64(), - Some(1_700_000_100_000) - ); - assert_eq!( - obj[property_names::TRANSFERRED_AT].as_u64(), - Some(1_700_000_200_000) - ); - assert_eq!( - obj[property_names::CREATED_AT_BLOCK_HEIGHT].as_u64(), - Some(100) - ); - assert_eq!( - obj[property_names::UPDATED_AT_BLOCK_HEIGHT].as_u64(), - Some(200) - ); - assert_eq!( - obj[property_names::TRANSFERRED_AT_BLOCK_HEIGHT].as_u64(), - Some(300) - ); - assert_eq!( - obj[property_names::CREATED_AT_CORE_BLOCK_HEIGHT].as_u64(), - Some(50) - ); - assert_eq!( - obj[property_names::UPDATED_AT_CORE_BLOCK_HEIGHT].as_u64(), - Some(60) - ); - assert_eq!( - obj[property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT].as_u64(), - Some(70) - ); - } - - #[test] - fn to_json_with_identifiers_using_bytes_includes_custom_properties() { - let platform_version = PlatformVersion::latest(); - let doc = make_document_v0_with_all_timestamps(); - let json = doc - .to_json_with_identifiers_using_bytes(platform_version) - .expect("should succeed"); - let obj = json.as_object().expect("should be an object"); - assert_eq!( - obj.get("label").and_then(|v| v.as_str()), - Some("test-label") - ); - } - - // ================================================================ - // from_json_value round-trip: to_json -> from_json_value - // Uses String as the identifier deserialization type since - // to_json produces base58 string identifiers. - // ================================================================ - - #[test] - fn json_round_trip_with_random_dashpay_profile() { - let platform_version = PlatformVersion::latest(); - let contract = json_document_to_contract( - "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", - false, - platform_version, - ) - .expect("expected to load dashpay contract"); - - let document_type = contract - .document_type_for_name("profile") - .expect("expected profile document type"); - - for seed in 0..5u64 { - let document = document_type - .random_document(Some(seed), platform_version) - .expect("expected random document"); - - let crate::document::Document::V0(doc_v0) = &document; - - let json_val = doc_v0 - .to_json(platform_version) - .expect("to_json should succeed"); - - let recovered = DocumentV0::from_json_value::(json_val, platform_version) - .expect("from_json_value should succeed"); - - assert_eq!(doc_v0.id, recovered.id, "id mismatch for seed {seed}"); - assert_eq!( - doc_v0.owner_id, recovered.owner_id, - "owner_id mismatch for seed {seed}" - ); - assert_eq!( - doc_v0.revision, recovered.revision, - "revision mismatch for seed {seed}" - ); - } - } - - // ================================================================ - // from_json_value extracts all system fields correctly - // ================================================================ - - #[test] - fn from_json_value_extracts_timestamps_and_revision() { - let platform_version = PlatformVersion::latest(); - let id = Identifier::new([1u8; 32]); - let owner = Identifier::new([2u8; 32]); - let creator = Identifier::new([9u8; 32]); - - let json_val = json!({ - "$id": bs58::encode(id.to_buffer()).into_string(), - "$ownerId": bs58::encode(owner.to_buffer()).into_string(), - "$revision": 5, - "$createdAt": 1_000_000u64, - "$updatedAt": 2_000_000u64, - "$createdAtBlockHeight": 100u64, - "$updatedAtBlockHeight": 200u64, - "$createdAtCoreBlockHeight": 50u32, - "$updatedAtCoreBlockHeight": 60u32, - "$transferredAt": 3_000_000u64, - "$transferredAtBlockHeight": 300u64, - "$transferredAtCoreBlockHeight": 70u32, - "$creatorId": bs58::encode(creator.to_buffer()).into_string(), - "customProp": "hello" - }); - - let doc = DocumentV0::from_json_value::(json_val, platform_version) - .expect("from_json_value should succeed"); - - assert_eq!(doc.id, id); - assert_eq!(doc.owner_id, owner); - assert_eq!(doc.revision, Some(5)); - assert_eq!(doc.created_at, Some(1_000_000)); - assert_eq!(doc.updated_at, Some(2_000_000)); - assert_eq!(doc.created_at_block_height, Some(100)); - assert_eq!(doc.updated_at_block_height, Some(200)); - assert_eq!(doc.created_at_core_block_height, Some(50)); - assert_eq!(doc.updated_at_core_block_height, Some(60)); - assert_eq!(doc.transferred_at, Some(3_000_000)); - assert_eq!(doc.transferred_at_block_height, Some(300)); - assert_eq!(doc.transferred_at_core_block_height, Some(70)); - assert_eq!(doc.creator_id, Some(creator)); - // Custom property should be in properties map - assert_eq!( - doc.properties.get("customProp"), - Some(&Value::Text("hello".to_string())) - ); - } - - #[test] - fn from_json_value_handles_missing_optional_fields() { - let platform_version = PlatformVersion::latest(); - let id = Identifier::new([3u8; 32]); - let owner = Identifier::new([4u8; 32]); - let json_val = json!({ - "$id": bs58::encode(id.to_buffer()).into_string(), - "$ownerId": bs58::encode(owner.to_buffer()).into_string(), - }); - - let doc = DocumentV0::from_json_value::(json_val, platform_version) - .expect("from_json_value should succeed with minimal fields"); - - assert_eq!(doc.id, id); - assert_eq!(doc.owner_id, owner); - assert_eq!(doc.revision, None); - assert_eq!(doc.created_at, None); - assert_eq!(doc.updated_at, None); - assert_eq!(doc.transferred_at, None); - assert_eq!(doc.created_at_block_height, None); - assert_eq!(doc.updated_at_block_height, None); - assert_eq!(doc.transferred_at_block_height, None); - assert_eq!(doc.created_at_core_block_height, None); - assert_eq!(doc.updated_at_core_block_height, None); - assert_eq!(doc.transferred_at_core_block_height, None); - assert_eq!(doc.creator_id, None); - } - - // ================================================================ - // to_json_with_identifiers_using_bytes: minimal document has only - // $id and $ownerId keys (no optional fields rendered). - // ================================================================ - - #[test] - fn to_json_with_identifiers_using_bytes_minimal_document_has_only_id_and_owner() { - let platform_version = PlatformVersion::latest(); - let doc = make_minimal_document_v0(); - let json = doc - .to_json_with_identifiers_using_bytes(platform_version) - .expect("to_json_with_identifiers_using_bytes should succeed"); - let obj = json.as_object().expect("object"); - assert!(obj.contains_key(property_names::ID)); - assert!(obj.contains_key(property_names::OWNER_ID)); - // None-valued optional fields are NOT emitted by this serializer. - assert!(!obj.contains_key(property_names::CREATED_AT)); - assert!(!obj.contains_key(property_names::UPDATED_AT)); - assert!(!obj.contains_key(property_names::TRANSFERRED_AT)); - assert!(!obj.contains_key(property_names::CREATED_AT_BLOCK_HEIGHT)); - assert!(!obj.contains_key(property_names::UPDATED_AT_BLOCK_HEIGHT)); - assert!(!obj.contains_key(property_names::TRANSFERRED_AT_BLOCK_HEIGHT)); - assert!(!obj.contains_key(property_names::CREATED_AT_CORE_BLOCK_HEIGHT)); - assert!(!obj.contains_key(property_names::UPDATED_AT_CORE_BLOCK_HEIGHT)); - assert!(!obj.contains_key(property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT)); - assert!(!obj.contains_key(property_names::CREATOR_ID)); - assert!(!obj.contains_key(property_names::REVISION)); - } - - // ================================================================ - // to_json_with_identifiers_using_bytes: id/owner emitted as - // base58 strings (via serde_json derive on Identifier). - // ================================================================ - - #[test] - fn to_json_with_identifiers_using_bytes_emits_base58_identifiers() { - let platform_version = PlatformVersion::latest(); - let doc = make_minimal_document_v0(); - let json = doc - .to_json_with_identifiers_using_bytes(platform_version) - .expect("should succeed"); - let obj = json.as_object().expect("object"); - // $id and $ownerId are serialized as base58 strings by Identifier's - // Serialize impl, which is what the underlying json! macro uses. - let id_val = obj.get(property_names::ID).expect("id present"); - assert!(id_val.is_string(), "expected base58 string for $id"); - let owner_val = obj.get(property_names::OWNER_ID).expect("owner present"); - assert!(owner_val.is_string(), "expected base58 string for $ownerId"); - } - - // ================================================================ - // from_json_value handles null creator_id by leaving it None. - // ================================================================ - - #[test] - fn from_json_value_with_null_creator_id_stays_none() { - let platform_version = PlatformVersion::latest(); - let json_val = json!({ - "$id": bs58::encode([1u8; 32]).into_string(), - "$ownerId": bs58::encode([2u8; 32]).into_string(), - "$creatorId": JsonValue::Null, - }); - let doc = DocumentV0::from_json_value::(json_val, platform_version) - .expect("from_json_value should succeed with null creator_id"); - assert_eq!(doc.creator_id, None); - } - - // ================================================================ - // from_json_value handles null id/owner by leaving them defaulted. - // ================================================================ - - #[test] - fn from_json_value_with_null_id_leaves_default() { - let platform_version = PlatformVersion::latest(); - let json_val = json!({ - "$id": JsonValue::Null, - "$ownerId": bs58::encode([2u8; 32]).into_string(), - }); - let doc = DocumentV0::from_json_value::(json_val, platform_version) - .expect("from_json_value should succeed with null $id"); - // Default Identifier is all-zeros. - assert_eq!(doc.id, Identifier::new([0u8; 32])); - } - - // ================================================================ - // to_json_with_identifiers_using_bytes: multiple user-defined - // properties are all included. - // ================================================================ - - #[test] - fn to_json_with_identifiers_using_bytes_with_multiple_properties() { - let platform_version = PlatformVersion::latest(); - let mut props = BTreeMap::new(); - props.insert("a".to_string(), Value::U64(1)); - props.insert("b".to_string(), Value::Text("two".to_string())); - props.insert("c".to_string(), Value::Bool(true)); - let doc = DocumentV0 { - id: Identifier::new([1u8; 32]), - owner_id: Identifier::new([2u8; 32]), - properties: props, - revision: None, - created_at: None, - updated_at: None, - transferred_at: None, - created_at_block_height: None, - updated_at_block_height: None, - transferred_at_block_height: None, - created_at_core_block_height: None, - updated_at_core_block_height: None, - transferred_at_core_block_height: None, - creator_id: None, - }; - let json = doc - .to_json_with_identifiers_using_bytes(platform_version) - .expect("should succeed"); - let obj = json.as_object().expect("object"); - assert_eq!(obj.get("a").and_then(|v| v.as_u64()), Some(1)); - assert_eq!(obj.get("b").and_then(|v| v.as_str()), Some("two")); - assert_eq!(obj.get("c").and_then(|v| v.as_bool()), Some(true)); - } - - // ================================================================ - // from_json_value: an empty object produces a fully-defaulted doc - // ================================================================ - - #[test] - fn from_json_value_empty_object_returns_default_document() { - let platform_version = PlatformVersion::latest(); - let doc = DocumentV0::from_json_value::(json!({}), platform_version) - .expect("from_json_value should succeed with empty object"); - assert_eq!(doc.id, Identifier::new([0u8; 32])); - assert_eq!(doc.owner_id, Identifier::new([0u8; 32])); - assert_eq!(doc.revision, None); - assert!(doc.properties.is_empty()); - } - - // ================================================================ - // from_json_value with creator_id - // ================================================================ - - #[test] - fn from_json_value_parses_creator_id() { - let platform_version = PlatformVersion::latest(); - let creator = Identifier::new([0xCC; 32]); - let json_val = json!({ - "$id": bs58::encode([1u8; 32]).into_string(), - "$ownerId": bs58::encode([2u8; 32]).into_string(), - "$creatorId": bs58::encode(creator.to_buffer()).into_string(), - }); - - let doc = DocumentV0::from_json_value::(json_val, platform_version) - .expect("from_json_value with creator_id should succeed"); - - assert_eq!(doc.creator_id, Some(creator)); - } -} diff --git a/packages/rs-dpp/src/document/v0/mod.rs b/packages/rs-dpp/src/document/v0/mod.rs index 7cc93b1b362..130908e1419 100644 --- a/packages/rs-dpp/src/document/v0/mod.rs +++ b/packages/rs-dpp/src/document/v0/mod.rs @@ -6,8 +6,6 @@ mod accessors; #[cfg(feature = "document-cbor-conversion")] pub(super) mod cbor_conversion; -#[cfg(feature = "json-conversion")] -pub(super) mod json_conversion; #[cfg(feature = "value-conversion")] mod platform_value_conversion; pub mod serialize; @@ -33,93 +31,72 @@ use crate::serialization::json_safe_fields; #[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Clone, Debug, PartialEq, Default)] #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), + feature = "serde-conversion", derive(serde::Serialize, serde::Deserialize) )] pub struct DocumentV0 { /// The unique document ID. - #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), - serde(rename = "$id") - )] + #[cfg_attr(feature = "serde-conversion", serde(rename = "$id"))] pub id: Identifier, /// The ID of the document's owner. - #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), - serde(rename = "$ownerId") - )] + #[cfg_attr(feature = "serde-conversion", serde(rename = "$ownerId"))] pub owner_id: Identifier, /// The document's properties (data). - #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), - serde(flatten) - )] + #[cfg_attr(feature = "serde-conversion", serde(flatten))] pub properties: BTreeMap, /// The document revision, if the document is mutable. - #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), - serde(rename = "$revision", default) - )] + #[cfg_attr(feature = "serde-conversion", serde(rename = "$revision", default))] pub revision: Option, /// The time in milliseconds that the document was created, if it is set as required by the document type schema. - #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), - serde(rename = "$createdAt", default) - )] + #[cfg_attr(feature = "serde-conversion", serde(rename = "$createdAt", default))] pub created_at: Option, /// The time in milliseconds that the document was last updated, if it is set as required by the document type schema. - #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), - serde(rename = "$updatedAt", default) - )] + #[cfg_attr(feature = "serde-conversion", serde(rename = "$updatedAt", default))] pub updated_at: Option, /// The time in milliseconds that the document was last transferred, if it is set as required by the document type schema. #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), + feature = "serde-conversion", serde(rename = "$transferredAt", default) )] pub transferred_at: Option, /// The block that the document was created, if it is set as required by the document type schema. #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), + feature = "serde-conversion", serde(rename = "$createdAtBlockHeight", default) )] pub created_at_block_height: Option, /// The block that the document was last updated, if it is set as required by the document type schema. #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), + feature = "serde-conversion", serde(rename = "$updatedAtBlockHeight", default) )] pub updated_at_block_height: Option, /// The block that the document was last transferred to a new identity, if it is set as required by the document type schema. #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), + feature = "serde-conversion", serde(rename = "$transferredAtBlockHeight", default) )] pub transferred_at_block_height: Option, /// The core block that the document was created, if it is set as required by the document type schema. #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), + feature = "serde-conversion", serde(rename = "$createdAtCoreBlockHeight", default) )] pub created_at_core_block_height: Option, /// The core block that the document was last updated, if it is set as required by the document type schema. #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), + feature = "serde-conversion", serde(rename = "$updatedAtCoreBlockHeight", default) )] pub updated_at_core_block_height: Option, /// The core block that the document was last transferred to a new identity, if it is set as required by the document type schema. #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), + feature = "serde-conversion", serde(rename = "$transferredAtCoreBlockHeight", default) )] pub transferred_at_core_block_height: Option, /// The creator id. - #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), - serde(rename = "$creatorId", default) - )] + #[cfg_attr(feature = "serde-conversion", serde(rename = "$creatorId", default))] pub creator_id: Option, } diff --git a/packages/rs-dpp/src/document/v0/platform_value_conversion.rs b/packages/rs-dpp/src/document/v0/platform_value_conversion.rs index f30a3bd91e1..fa2902cd9eb 100644 --- a/packages/rs-dpp/src/document/v0/platform_value_conversion.rs +++ b/packages/rs-dpp/src/document/v0/platform_value_conversion.rs @@ -1,6 +1,5 @@ use crate::document::serialization_traits::DocumentPlatformValueMethodsV0; use crate::document::DocumentV0; -use crate::version::PlatformVersion; use crate::ProtocolError; use platform_value::Value; use std::collections::BTreeMap; @@ -13,21 +12,6 @@ impl DocumentPlatformValueMethodsV0<'_> for DocumentV0 { fn into_map_value(self) -> Result, ProtocolError> { Ok(platform_value::to_value(self)?.into_btree_string_map()?) } - - fn into_value(self) -> Result { - Ok(platform_value::to_value(self)?) - } - - fn to_object(&self) -> Result { - Ok(platform_value::to_value(self)?) - } - - fn from_platform_value( - document_value: Value, - _platform_version: &PlatformVersion, - ) -> Result { - Ok(platform_value::from_value(document_value)?) - } } #[cfg(test)] @@ -35,7 +19,6 @@ mod tests { use super::*; use crate::document::property_names; use platform_value::Identifier; - use platform_version::version::PlatformVersion; fn minimal_doc() -> DocumentV0 { DocumentV0 { @@ -123,60 +106,10 @@ mod tests { assert_eq!(from_ref, from_owned); } - // ================================================================ - // to_object / into_value: produce a Value::Map - // ================================================================ - - #[test] - fn to_object_returns_a_map_value() { - let doc = full_doc(); - let v = doc.to_object().expect("to_object"); - assert!(v.is_map(), "Expected a Value::Map, got {:?}", v); - } - - #[test] - fn into_value_consumes_and_returns_a_map_value() { - let doc = full_doc(); - let v = doc.into_value().expect("into_value"); - assert!(v.is_map(), "Expected a Value::Map, got {:?}", v); - } - - // ================================================================ - // from_platform_value round-trip: to_object -> from_platform_value - // ================================================================ - - #[test] - fn from_platform_value_round_trip_preserves_all_fields() { - let platform_version = PlatformVersion::latest(); - let doc = full_doc(); - let v = doc.to_object().expect("to_object"); - let recovered = DocumentV0::from_platform_value(v, platform_version) - .expect("from_platform_value should succeed"); - assert_eq!(doc, recovered); - } - - #[test] - fn from_platform_value_round_trip_with_minimal_fields() { - let platform_version = PlatformVersion::latest(); - let doc = minimal_doc(); - let v = doc.to_object().expect("to_object"); - let recovered = DocumentV0::from_platform_value(v, platform_version) - .expect("from_platform_value should succeed"); - assert_eq!(doc, recovered); - } - - // ================================================================ - // from_platform_value error path: non-map Value should fail - // ================================================================ - - #[test] - fn from_platform_value_with_non_map_value_returns_error() { - let platform_version = PlatformVersion::latest(); - let bad = Value::Text("not a document".to_string()); - let result = DocumentV0::from_platform_value(bad, platform_version); - assert!( - result.is_err(), - "from_platform_value with a non-map Value should fail" - ); - } + // After Phase D step 8 slice A, the `to_object` / `into_value` / + // `from_platform_value` tests on this V0 inner moved to the outer + // `Document` enum's canonical-trait round-trip tests in + // `serialization_traits/platform_value_conversion/mod.rs`. The + // `to_map_value` / `into_map_value` tests stay because those are + // the methods this trait still defines. } diff --git a/packages/rs-dpp/src/group/action_event.rs b/packages/rs-dpp/src/group/action_event.rs index 5c99a1e3310..24e7240142c 100644 --- a/packages/rs-dpp/src/group/action_event.rs +++ b/packages/rs-dpp/src/group/action_event.rs @@ -15,7 +15,12 @@ use serde::{Deserialize, Serialize}; #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), - serde(tag = "type", content = "data", rename_all = "camelCase") + // Internally tagged with `$kind`. The inner `TokenEvent` is itself + // internally tagged with `$type`, so its fields flatten alongside `$kind` + // into one object. `$kind` keeps the two discriminators distinct. Wire + // shape, e.g.: + // {"$kind": "tokenEvent", "$type": "mint", "amount": , "recipient": } + serde(tag = "$kind", rename_all = "camelCase") )] #[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] #[platform_serialize(unversioned)] //versioned directly, no need to use platform_version @@ -28,6 +33,201 @@ pub enum GroupActionEvent { #[cfg(feature = "json-conversion")] impl JsonConvertible for GroupActionEvent {} +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + // `GroupActionEvent` uses `tag = "$kind"` (internal). Plain `kind` + // (no `$` prefix) because the wire level has no other `$`-prefixed + // fields. Distinct from the inner `TokenEvent`'s `type` discriminator + // — both keys coexist at the flattened top level without collision. + + #[test] + fn json_round_trip_token_event_mint() { + use crate::serialization::JsonConvertible; + let original = GroupActionEvent::TokenEvent( + crate::tokens::token_event::json_convertible_tests::mint_fixture(), + ); + let json = original.to_json().expect("to_json"); + // Outer `kind: "tokenEvent"` from GroupActionEvent. Inner TokenEvent + // (custom serde) flattens its named fields at the same level. + assert_eq!( + json, + json!({ + "$kind": "tokenEvent", + "$type": "mint", + "amount": 5_000, + "recipient": "Bswb3UyeD1pUTaGiE6WvqwFpJZsQSEY1xhJePCDTHdvp", + "publicNote": "genesis mint", + }) + ); + let recovered = GroupActionEvent::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_token_event_mint() { + use crate::serialization::ValueConvertible; + let original = GroupActionEvent::TokenEvent( + crate::tokens::token_event::json_convertible_tests::mint_fixture(), + ); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({ + "$kind": "tokenEvent", + "$type": "mint", + "amount": 5_000u64, + "recipient": platform_value::Identifier::new([0xa1; 32]), + "publicNote": "genesis mint", + }) + ); + let recovered = GroupActionEvent::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + // The two tests below pin the deepest custom-serde composition in the + // crate: GroupActionEvent (`tag = "$kind"`, derive — buffers inner content + // through serde's ContentDeserializer) → TokenEvent (custom impl) → + // externally-tagged TokenConfigurationChangeItem / + // TokenDistributionTypeWithResolvedRecipient → the custom + // internally-tagged AuthorizedActionTakers / + // TokenDistributionResolvedRecipient. The Identity-carrying variants + // specifically exercise the Identifier dual-shape path (HR base58 string + // vs buffered bytes) under the ContentDeserializer HR-quirk. A one-sided + // edit to any custom Serialize/Deserialize pair in the chain fails here. + + fn change_item_fixture() -> GroupActionEvent { + use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; + use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; + GroupActionEvent::TokenEvent(TokenEvent::ConfigUpdate( + TokenConfigurationChangeItem::ConventionsControlGroup( + AuthorizedActionTakers::Identity(platform_value::Identifier::from([0x42u8; 32])), + ), + Some("rotate control".to_string()), + )) + } + + fn claim_fixture() -> GroupActionEvent { + use crate::data_contract::associated_token::token_distribution_key::TokenDistributionTypeWithResolvedRecipient; + use crate::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionResolvedRecipient; + GroupActionEvent::TokenEvent(TokenEvent::Claim( + TokenDistributionTypeWithResolvedRecipient::Perpetual( + TokenDistributionResolvedRecipient::Evonode(platform_value::Identifier::from( + [0x42u8; 32], + )), + ), + 750, + Some("payout".to_string()), + )) + } + + #[test] + fn json_round_trip_config_update_with_identity_action_taker() { + use crate::serialization::JsonConvertible; + let original = change_item_fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({ + "$kind": "tokenEvent", + "$type": "configUpdate", + "configurationChange": { + "$type": "conventionsControlGroup", + "value": { + "$type": "identity", + "identity": "5TeWSsjg2gbxCyWVniXeCmwM7UtHTCK7svzJr5xYJzHf", + }, + }, + "publicNote": "rotate control", + }) + ); + let recovered = GroupActionEvent::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_config_update_with_identity_action_taker() { + use crate::serialization::ValueConvertible; + let original = change_item_fixture(); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({ + "$kind": "tokenEvent", + "$type": "configUpdate", + "configurationChange": { + "$type": "conventionsControlGroup", + "value": { + "$type": "identity", + "identity": platform_value::Identifier::from([0x42u8; 32]), + }, + }, + "publicNote": "rotate control", + }) + ); + let recovered = GroupActionEvent::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_claim_with_resolved_recipient() { + use crate::serialization::JsonConvertible; + let original = claim_fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({ + "$kind": "tokenEvent", + "$type": "claim", + "distributionType": { + "$type": "perpetual", + "value": { + "$type": "evonode", + "identity": "5TeWSsjg2gbxCyWVniXeCmwM7UtHTCK7svzJr5xYJzHf", + }, + }, + "amount": 750, + "publicNote": "payout", + }) + ); + let recovered = GroupActionEvent::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_claim_with_resolved_recipient() { + use crate::serialization::ValueConvertible; + let original = claim_fixture(); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({ + "$kind": "tokenEvent", + "$type": "claim", + "distributionType": { + "$type": "perpetual", + "value": { + "$type": "evonode", + "identity": platform_value::Identifier::from([0x42u8; 32]), + }, + }, + "amount": 750u64, + "publicNote": "payout", + }) + ); + let recovered = GroupActionEvent::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} + use std::fmt; impl fmt::Display for GroupActionEvent { diff --git a/packages/rs-dpp/src/group/group_action/mod.rs b/packages/rs-dpp/src/group/group_action/mod.rs index 83d378b425a..2cc962059d5 100644 --- a/packages/rs-dpp/src/group/group_action/mod.rs +++ b/packages/rs-dpp/src/group/group_action/mod.rs @@ -65,3 +65,7 @@ impl GroupActionAccessors for GroupAction { } } } + +// TODO(unification pass 2): add round-trip tests for GroupAction once we have an +// explicit fixture (GroupActionV0 has no Default — its `event: GroupActionEvent` +// field is itself a versioned enum without Default). diff --git a/packages/rs-dpp/src/group/group_action_status.rs b/packages/rs-dpp/src/group/group_action_status.rs index d4b7e68d0dc..3d8fe1ca1c7 100644 --- a/packages/rs-dpp/src/group/group_action_status.rs +++ b/packages/rs-dpp/src/group/group_action_status.rs @@ -11,6 +11,67 @@ pub enum GroupActionStatus { ActionClosed, } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for GroupActionStatus {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for GroupActionStatus {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::Value; + use serde_json::json; + + // Externally tagged unit-only enum with `rename_all = "camelCase"`: + // serializes as a bare string (`"actionActive"` / `"actionClosed"`). + + #[test] + fn json_round_trip_action_active() { + use crate::serialization::JsonConvertible; + let original = GroupActionStatus::ActionActive; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!("actionActive")); + let recovered = GroupActionStatus::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_action_closed() { + use crate::serialization::JsonConvertible; + let original = GroupActionStatus::ActionClosed; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!("actionClosed")); + let recovered = GroupActionStatus::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_action_active() { + use crate::serialization::ValueConvertible; + let original = GroupActionStatus::ActionActive; + let value = original.to_object().expect("to_object"); + assert_eq!(value, Value::Text("actionActive".to_string())); + let recovered = GroupActionStatus::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_action_closed() { + use crate::serialization::ValueConvertible; + let original = GroupActionStatus::ActionClosed; + let value = original.to_object().expect("to_object"); + assert_eq!(value, Value::Text("actionClosed".to_string())); + let recovered = GroupActionStatus::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} + impl TryFrom for GroupActionStatus { type Error = anyhow::Error; fn try_from(value: u8) -> Result { diff --git a/packages/rs-dpp/src/group/mod.rs b/packages/rs-dpp/src/group/mod.rs index 70a6de4bacd..1c033923c2d 100644 --- a/packages/rs-dpp/src/group/mod.rs +++ b/packages/rs-dpp/src/group/mod.rs @@ -1,5 +1,7 @@ use crate::data_contract::group::{Group, GroupMemberPower}; use crate::data_contract::GroupContractPosition; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use bincode::{Decode, Encode}; use derive_more::Display; use platform_value::Identifier; @@ -32,6 +34,7 @@ impl From for GroupStateTransitionInfo { } } +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, Copy, Encode, Decode, Default, PartialEq, Display)] #[cfg_attr( feature = "serde-conversion", @@ -49,6 +52,12 @@ pub struct GroupStateTransitionInfo { pub action_is_proposer: bool, } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for GroupStateTransitionInfo {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for GroupStateTransitionInfo {} + #[derive(Debug, Clone, PartialEq)] pub struct GroupStateTransitionResolvedInfo { pub group_contract_position: GroupContractPosition, @@ -58,3 +67,65 @@ pub struct GroupStateTransitionResolvedInfo { pub action_is_proposer: bool, pub signer_power: GroupMemberPower, } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_groupstatetransitioninfo { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + fn fixture() -> GroupStateTransitionInfo { + GroupStateTransitionInfo { + group_contract_position: 5, + action_id: Identifier::new([0x33; 32]), + action_is_proposer: true, + } + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Each field has an explicit `serde(rename = "$..." )` so the wire keys + // are `$groupContractPosition` / `$groupActionId` / `$groupActionIsProposer`. + // `group_contract_position` is `GroupContractPosition` (= u16), so JSON + // erases the size — the value-path assertion uses `5u16`. + // `action_id` is `Identifier` and serializes as base58 in JSON. + assert_eq!( + json, + json!({ + "$groupContractPosition": 5, + "$groupActionId": "4Ss5JMkXAD9Z7cktFEdrqeMuT6jGMF1pVozTyPHZ6zT4", + "$groupActionIsProposer": true, + }) + ); + let recovered = GroupStateTransitionInfo::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `5u16` locks `Value::U16`. `Identifier` flows through as + // `Value::Identifier` when interpolated into `platform_value!`. + let action_id = Identifier::new([0x33; 32]); + assert_eq!( + value, + platform_value!({ + "$groupContractPosition": 5u16, + "$groupActionId": action_id, + "$groupActionIsProposer": true, + }) + ); + let recovered = GroupStateTransitionInfo::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/identity/conversion/json/mod.rs b/packages/rs-dpp/src/identity/conversion/json/mod.rs deleted file mode 100644 index c0000c8b0d4..00000000000 --- a/packages/rs-dpp/src/identity/conversion/json/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod v0; -pub use v0::*; diff --git a/packages/rs-dpp/src/identity/conversion/json/v0/mod.rs b/packages/rs-dpp/src/identity/conversion/json/v0/mod.rs deleted file mode 100644 index f8763d9623d..00000000000 --- a/packages/rs-dpp/src/identity/conversion/json/v0/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::ProtocolError; -use serde_json::Value as JsonValue; - -pub trait IdentityJsonConversionMethodsV0 { - fn to_json_object(&self) -> Result; - fn to_json(&self) -> Result; - fn from_json(json_object: JsonValue) -> Result - where - Self: Sized; -} diff --git a/packages/rs-dpp/src/identity/conversion/mod.rs b/packages/rs-dpp/src/identity/conversion/mod.rs index 24b3d33e6d4..f90f72d5e56 100644 --- a/packages/rs-dpp/src/identity/conversion/mod.rs +++ b/packages/rs-dpp/src/identity/conversion/mod.rs @@ -1,6 +1,4 @@ #[cfg(feature = "identity-cbor-conversion")] pub mod cbor; -#[cfg(feature = "json-conversion")] -pub mod json; #[cfg(feature = "value-conversion")] pub mod platform_value; diff --git a/packages/rs-dpp/src/identity/conversion/platform_value/mod.rs b/packages/rs-dpp/src/identity/conversion/platform_value/mod.rs index daac8dfdd92..b3006597778 100644 --- a/packages/rs-dpp/src/identity/conversion/platform_value/mod.rs +++ b/packages/rs-dpp/src/identity/conversion/platform_value/mod.rs @@ -1,13 +1,8 @@ -mod v0; - use crate::identity::{Identity, IdentityV0}; use crate::version::PlatformVersion; use crate::ProtocolError; use platform_value::Value; use platform_version::TryFromPlatformVersioned; -pub use v0::IdentityPlatformValueConversionMethodsV0; - -impl IdentityPlatformValueConversionMethodsV0 for Identity {} impl TryFromPlatformVersioned for Identity { type Error = ProtocolError; @@ -166,14 +161,13 @@ mod tests { assert!(matches!(result, Err(ProtocolError::ValueError(_)))); } - // to_cleaned_object on the Identity enum wrapper uses the default body - // (`self.to_object()`), inherited through `IdentityPlatformValueConversionMethodsV0`. - // to_object itself comes from the `ValueConvertible` derive on Identity, which - // produces the tagged `$formatVersion: "0"` form. + // After Phase D step 5, `IdentityPlatformValueConversionMethodsV0` has + // been deleted; the canonical `ValueConvertible::to_object` produces the + // tagged `$formatVersion: "0"` form for the Identity enum wrapper. #[test] - fn identity_wrapper_to_cleaned_object_includes_format_version_tag() { + fn identity_wrapper_to_object_includes_format_version_tag() { let identity: Identity = sample_identity_v0().into(); - let value = identity.to_cleaned_object().expect("to_cleaned_object"); + let value = identity.to_object().expect("to_object"); let map = value.to_map_ref().expect("map"); assert!( map.iter() diff --git a/packages/rs-dpp/src/identity/conversion/platform_value/v0/mod.rs b/packages/rs-dpp/src/identity/conversion/platform_value/v0/mod.rs deleted file mode 100644 index 92abaa26511..00000000000 --- a/packages/rs-dpp/src/identity/conversion/platform_value/v0/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[cfg(feature = "value-conversion")] -use crate::serialization::ValueConvertible; -use crate::ProtocolError; -use platform_value::Value; - -pub trait IdentityPlatformValueConversionMethodsV0: ValueConvertible { - fn to_cleaned_object(&self) -> Result - where - Self: Sized, - { - self.to_object() - } -} diff --git a/packages/rs-dpp/src/identity/identity.rs b/packages/rs-dpp/src/identity/identity.rs index 64d446e9ddf..86aee2c2d14 100644 --- a/packages/rs-dpp/src/identity/identity.rs +++ b/packages/rs-dpp/src/identity/identity.rs @@ -4,6 +4,8 @@ use crate::identity::{IdentityPublicKey, KeyID}; use crate::prelude::{AddressNonce, Revision}; #[cfg(feature = "json-conversion")] use crate::serialization::json_safe_fields; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; #[cfg(feature = "value-conversion")] use crate::serialization::ValueConvertible; @@ -29,7 +31,7 @@ use std::collections::{BTreeMap, BTreeSet}; /// untagged is needed here #[derive(Debug, Clone, PartialEq, From)] #[cfg_attr( - any( feature = "serde-conversion" ,feature = "serde-conversion",), + feature = "serde-conversion", derive(serde::Serialize, serde::Deserialize), serde(tag = "$formatVersion"), // platform_version_path("dpp.identity_versions.identity_structure_version") @@ -41,18 +43,167 @@ use std::collections::{BTreeMap, BTreeSet}; )] #[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] pub enum Identity { - #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), - serde(rename = "0") - )] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(IdentityV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl JsonConvertible for Identity {} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl JsonConvertible for PartialIdentity {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl ValueConvertible for PartialIdentity {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::identity::identity_public_key::v0::IdentityPublicKeyV0; + use crate::identity::{KeyType, Purpose, SecurityLevel}; + use platform_value::{platform_value, BinaryData, Value}; + use serde_json::json; + + fn fixture_pubkey(id: u32, byte: u8) -> IdentityPublicKey { + IdentityPublicKey::V0(IdentityPublicKeyV0 { + id, + key_type: KeyType::ECDSA_SECP256K1, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::MASTER, + contract_bounds: None, + read_only: false, + data: BinaryData::new(vec![byte; 33]), + disabled_at: None, + }) + } + + fn fixture() -> Identity { + let mut public_keys = BTreeMap::new(); + public_keys.insert(0, fixture_pubkey(0, 0xa0)); + public_keys.insert(1, fixture_pubkey(1, 0xb1)); + Identity::V0(IdentityV0 { + id: Identifier::new([0x42; 32]), + public_keys, + balance: 1_000_000, + revision: 7, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Internally-tagged enum (`tag = "$formatVersion"`); inner V0 has + // `rename_all = "camelCase"`. `public_keys` uses a custom serde wrapper + // that emits a `Vec` of `IdentityPublicKey` values (keys dropped, then + // reconstructed on deserialize from each key's `id`). Each + // `IdentityPublicKey` is itself an internally-tagged enum, so the inner + // wire shape mirrors the per-key test in + // `identity_public_key::mod::json_convertible_tests`. + // Sized-int fields with JSON loss: + // - `balance`: u64 (Credits) + // - `revision`: u64 (Revision) + // - inner `id`: u32, `purpose`/`securityLevel`/`type`: u8 reprs. + // `Identifier` serializes as base58 in JSON; `BinaryData` as base64. + // Purpose::AUTHENTICATION = 0, KeyType::ECDSA_SECP256K1 = 0, + // SecurityLevel::MASTER = 0. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "id": "5TeWSsjg2gbxCyWVniXeCmwM7UtHTCK7svzJr5xYJzHf", + // After Phase D step 4, `disabled_at` carries + // `#[serde(skip_serializing_if = "Option::is_none")]`, so + // non-disabled keys no longer emit `disabledAt: null`. + "publicKeys": [ + { + "$formatVersion": "0", + "id": 0, + "purpose": 0, + "securityLevel": 0, + "contractBounds": serde_json::Value::Null, + "type": 0, + "readOnly": false, + "data": "oKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCg", + }, + { + "$formatVersion": "0", + "id": 1, + "purpose": 0, + "securityLevel": 0, + "contractBounds": serde_json::Value::Null, + "type": 0, + "readOnly": false, + "data": "sbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGx", + }, + ], + "balance": 1_000_000u64, + "revision": 7, + }) + ); + let recovered = Identity::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Explicit `u32` / `u8` / `u64` suffixes lock typed-int variants. + // `Identifier` interpolates as `Value::Identifier`; `BinaryData` of + // length 33 lacks a fixed-sized variant, so it stays as + // `Value::Bytes(Vec)`. + let id = Identifier::new([0x42; 32]); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "id": id, + // `disabledAt: None` is now stripped per the + // `skip_serializing_if` attribute (Phase D step 4). + "publicKeys": [ + { + "$formatVersion": "0", + "id": 0u32, + "purpose": 0u8, + "securityLevel": 0u8, + "contractBounds": Value::Null, + "type": 0u8, + "readOnly": false, + "data": Value::Bytes(vec![0xa0; 33]), + }, + { + "$formatVersion": "0", + "id": 1u32, + "purpose": 0u8, + "securityLevel": 0u8, + "contractBounds": Value::Null, + "type": 0u8, + "readOnly": false, + "data": Value::Bytes(vec![0xb1; 33]), + }, + ], + "balance": 1_000_000u64, + "revision": 7u64, + }) + ); + let recovered = Identity::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} + /// An identity struct that represent partially set/loaded identity data. #[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, Eq, PartialEq)] #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion",), + feature = "serde-conversion", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] diff --git a/packages/rs-dpp/src/identity/identity_public_key/contract_bounds/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/contract_bounds/mod.rs index 4d261d1cb56..7052fea0813 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/contract_bounds/mod.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/contract_bounds/mod.rs @@ -24,7 +24,7 @@ pub type ContractBoundsType = u8; Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Encode, Decode, Ord, PartialOrd, Hash, )] #[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] -#[serde(tag = "type", rename_all = "camelCase")] +#[serde(tag = "$type", rename_all = "camelCase")] pub enum ContractBounds { /// this key can only be used within a specific contract #[serde(rename = "singleContract")] diff --git a/packages/rs-dpp/src/identity/identity_public_key/conversion/cbor/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/conversion/cbor/mod.rs deleted file mode 100644 index aa994b19272..00000000000 --- a/packages/rs-dpp/src/identity/identity_public_key/conversion/cbor/mod.rs +++ /dev/null @@ -1,44 +0,0 @@ -// mod v0; -// use crate::identity::IdentityPublicKey; -// use crate::version::PlatformVersion; -// use crate::ProtocolError; -// use ciborium::Value as CborValue; -// pub use v0::*; -// -// impl IdentityPublicKeyCborConversionMethodsV0 for IdentityPublicKey { -// fn to_cbor_buffer(&self) -> Result, ProtocolError> { -// match self { -// IdentityPublicKey::V0(v0) => v0.to_cbor_buffer(), -// } -// } -// -// fn from_cbor_value( -// cbor_value: &CborValue, -// platform_version: &PlatformVersion, -// ) -> Result { -// match platform_version -// .dpp -// .identity_versions -// .identity_key_structure_version -// { -// 0 => IdentityPublicKey::from_cbor_value(cbor_value, platform_version), -// version => Err(ProtocolError::UnknownVersionMismatch { -// method: "IdentityPublicKey::from_cbor_value".to_string(), -// known_versions: vec![0], -// received: version, -// }), -// } -// } -// -// fn to_cbor_value(&self) -> CborValue { -// match self { -// IdentityPublicKey::V0(v0) => v0.to_cbor_value(), -// } -// } -// } -// -// impl Into for &IdentityPublicKey { -// fn into(self) -> CborValue { -// self.to_cbor_value() -// } -// } diff --git a/packages/rs-dpp/src/identity/identity_public_key/conversion/cbor/v0/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/conversion/cbor/v0/mod.rs deleted file mode 100644 index 1b2fc2cfbf4..00000000000 --- a/packages/rs-dpp/src/identity/identity_public_key/conversion/cbor/v0/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -// use crate::version::PlatformVersion; -// use crate::ProtocolError; -// use ciborium::Value as CborValue; -// -// pub trait IdentityPublicKeyCborConversionMethodsV0 { -// fn to_cbor_buffer(&self) -> Result, ProtocolError>; -// fn from_cbor_value( -// cbor_value: &CborValue, -// platform_version: &PlatformVersion, -// ) -> Result -// where -// Self: Sized; -// fn to_cbor_value(&self) -> CborValue; -// } diff --git a/packages/rs-dpp/src/identity/identity_public_key/conversion/json/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/conversion/json/mod.rs deleted file mode 100644 index 8ec9105c5a8..00000000000 --- a/packages/rs-dpp/src/identity/identity_public_key/conversion/json/mod.rs +++ /dev/null @@ -1,97 +0,0 @@ -mod v0; -use crate::identity::identity_public_key::v0::IdentityPublicKeyV0; -use crate::identity::IdentityPublicKey; -use crate::version::PlatformVersion; -use crate::ProtocolError; -use serde_json::Value as JsonValue; -pub use v0::IdentityPublicKeyJsonConversionMethodsV0; - -impl IdentityPublicKeyJsonConversionMethodsV0 for IdentityPublicKey { - fn to_json(&self) -> Result { - match self { - IdentityPublicKey::V0(key) => key.to_json(), - } - } - - fn to_json_object(&self) -> Result { - match self { - IdentityPublicKey::V0(key) => key.to_json_object(), - } - } - - fn from_json_object( - raw_object: JsonValue, - platform_version: &PlatformVersion, - ) -> Result - where - Self: Sized, - { - match platform_version - .dpp - .identity_versions - .identity_key_structure_version - { - 0 => { - IdentityPublicKeyV0::from_json_object(raw_object, platform_version).map(Into::into) - } - version => Err(ProtocolError::UnknownVersionMismatch { - method: "IdentityPublicKey::from_json_object".to_string(), - known_versions: vec![0], - received: version, - }), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::identity::identity_public_key::v0::IdentityPublicKeyV0; - use crate::identity::{KeyType, Purpose, SecurityLevel}; - use platform_value::BinaryData; - use platform_version::version::LATEST_PLATFORM_VERSION; - - fn wrapper(disabled_at: Option) -> IdentityPublicKey { - IdentityPublicKey::V0(IdentityPublicKeyV0 { - id: 2, - purpose: Purpose::AUTHENTICATION, - security_level: SecurityLevel::MASTER, - contract_bounds: None, - key_type: KeyType::ECDSA_SECP256K1, - read_only: false, - data: BinaryData::new(vec![0x77; 33]), - disabled_at, - }) - } - - #[test] - fn to_json_delegates_to_v0() { - let key = wrapper(None); - let json = key.to_json().expect("to_json"); - let IdentityPublicKey::V0(inner) = &key; - assert_eq!(json, inner.to_json().unwrap()); - } - - #[test] - fn to_json_object_delegates_to_v0() { - let key = wrapper(None); - let json = key.to_json_object().expect("to_json_object"); - let IdentityPublicKey::V0(inner) = &key; - assert_eq!(json, inner.to_json_object().unwrap()); - } - - #[test] - fn from_json_object_roundtrip() { - let key = wrapper(Some(1234)); - let json = key.to_json().expect("to_json"); - let back = IdentityPublicKey::from_json_object(json, LATEST_PLATFORM_VERSION).unwrap(); - assert_eq!(back, key); - } - - #[test] - fn from_json_object_missing_fields_errors() { - let json = serde_json::json!({ "id": 0 }); - let result = IdentityPublicKey::from_json_object(json, LATEST_PLATFORM_VERSION); - assert!(result.is_err()); - } -} diff --git a/packages/rs-dpp/src/identity/identity_public_key/conversion/json/v0/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/conversion/json/v0/mod.rs deleted file mode 100644 index f27a7d7d7b8..00000000000 --- a/packages/rs-dpp/src/identity/identity_public_key/conversion/json/v0/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::version::PlatformVersion; -use crate::ProtocolError; -use serde_json::Value as JsonValue; - -pub trait IdentityPublicKeyJsonConversionMethodsV0 { - fn to_json(&self) -> Result; - fn to_json_object(&self) -> Result; - fn from_json_object( - raw_object: JsonValue, - platform_version: &PlatformVersion, - ) -> Result - where - Self: Sized; -} diff --git a/packages/rs-dpp/src/identity/identity_public_key/conversion/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/conversion/mod.rs index 24b3d33e6d4..84d96490e92 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/conversion/mod.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/conversion/mod.rs @@ -1,6 +1,2 @@ -#[cfg(feature = "identity-cbor-conversion")] -pub mod cbor; -#[cfg(feature = "json-conversion")] -pub mod json; #[cfg(feature = "value-conversion")] pub mod platform_value; diff --git a/packages/rs-dpp/src/identity/identity_public_key/conversion/platform_value/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/conversion/platform_value/mod.rs index e28e0cb7fd5..adc9c206ee9 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/conversion/platform_value/mod.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/conversion/platform_value/mod.rs @@ -1,56 +1,19 @@ -mod v0; -use crate::identity::identity_public_key::v0::IdentityPublicKeyV0; -use crate::identity::IdentityPublicKey; -use crate::version::PlatformVersion; -use crate::ProtocolError; -use platform_value::Value; -pub use v0::*; - -impl IdentityPublicKeyPlatformValueConversionMethodsV0 for IdentityPublicKey { - fn to_object(&self) -> Result { - match self { - IdentityPublicKey::V0(key) => key.to_object(), - } - } - - fn to_cleaned_object(&self) -> Result { - match self { - IdentityPublicKey::V0(key) => key.to_cleaned_object(), - } - } - - fn into_object(self) -> Result { - match self { - IdentityPublicKey::V0(key) => key.into_object(), - } - } - - fn from_object( - value: Value, - platform_version: &PlatformVersion, - ) -> Result { - match platform_version - .dpp - .identity_versions - .identity_key_structure_version - { - 0 => IdentityPublicKeyV0::from_object(value, platform_version).map(Into::into), - version => Err(ProtocolError::UnknownVersionMismatch { - method: "IdentityPublicKey::from_object".to_string(), - known_versions: vec![0], - received: version, - }), - } - } -} +// `IdentityPublicKey` value-side conversion now goes exclusively through +// the canonical `ValueConvertible` trait (derived on the outer enum). The +// legacy `IdentityPublicKeyPlatformValueConversionMethodsV0` trait has +// been deleted: it carried `to_object` / `into_object` that were +// byte-identical to canonical, plus a `from_object(value, &platform_version)` +// version-dispatch method that produced identical output to canonical for +// the only currently-defined V0 (canonical dispatches on the value's own +// `$formatVersion` tag, which all V0 values carry). #[cfg(test)] mod tests { - use super::*; use crate::identity::identity_public_key::v0::IdentityPublicKeyV0; - use crate::identity::{KeyType, Purpose, SecurityLevel}; - use platform_value::BinaryData; - use platform_version::version::LATEST_PLATFORM_VERSION; + use crate::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel}; + use crate::serialization::ValueConvertible; + use crate::ProtocolError; + use platform_value::{BinaryData, Value}; fn wrapper(disabled_at: Option) -> IdentityPublicKey { IdentityPublicKey::V0(IdentityPublicKeyV0 { @@ -66,20 +29,33 @@ mod tests { } #[test] - fn to_object_delegates_to_v0() { + fn to_object_includes_format_version_tag() { + // The outer `IdentityPublicKey` is a tagged enum + // (`#[serde(tag = "$formatVersion")]`); canonical `to_object` + // emits `$formatVersion: "0"` next to the V0 fields. let key = wrapper(Some(5)); let value = key.to_object().expect("to_object"); - // Should match what V0 produces directly. - let IdentityPublicKey::V0(inner) = &key; - assert_eq!(value, inner.to_object().unwrap()); + let map = value.to_map().expect("map"); + assert!( + map.iter().any( + |(k, v): &(Value, Value)| k.as_text() == Some("$formatVersion") + && v.as_text() == Some("0") + ), + "outer enum must surface the $formatVersion tag" + ); } #[test] - fn to_cleaned_object_removes_disabled_at_when_none() { + fn to_object_strips_disabled_at_when_none() { + // The `skip_serializing_if` attribute on + // `IdentityPublicKeyV0::disabled_at` strips the field for + // non-disabled keys directly via the canonical `to_object` path. let key = wrapper(None); - let cleaned = key.to_cleaned_object().expect("to_cleaned_object"); - let map = cleaned.to_map().expect("map"); - assert!(!map.iter().any(|(k, _)| k.as_text() == Some("disabledAt"))); + let value = key.to_object().expect("to_object"); + let map = value.to_map().expect("map"); + assert!(!map + .iter() + .any(|(k, _): &(Value, Value)| k.as_text() == Some("disabledAt"))); } #[test] @@ -94,13 +70,15 @@ mod tests { fn from_object_roundtrip_via_wrapper() { let key = wrapper(None); let value = key.to_object().unwrap(); - let back = IdentityPublicKey::from_object(value, LATEST_PLATFORM_VERSION).unwrap(); + // Canonical `ValueConvertible::from_object` dispatches on the + // value's `$formatVersion` tag. + let back = IdentityPublicKey::from_object(value).unwrap(); assert_eq!(back, key); } #[test] fn from_object_fails_on_non_map() { - let result = IdentityPublicKey::from_object(Value::Null, LATEST_PLATFORM_VERSION); + let result = IdentityPublicKey::from_object(Value::Null); assert!(matches!(result, Err(ProtocolError::ValueError(_)))); } } diff --git a/packages/rs-dpp/src/identity/identity_public_key/conversion/platform_value/v0/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/conversion/platform_value/v0/mod.rs deleted file mode 100644 index 5e98150aee2..00000000000 --- a/packages/rs-dpp/src/identity/identity_public_key/conversion/platform_value/v0/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::version::PlatformVersion; -use crate::ProtocolError; -use platform_value::Value; - -pub trait IdentityPublicKeyPlatformValueConversionMethodsV0 { - fn to_object(&self) -> Result; - fn to_cleaned_object(&self) -> Result; - fn into_object(self) -> Result; - fn from_object(value: Value, platform_version: &PlatformVersion) -> Result - where - Self: Sized; -} diff --git a/packages/rs-dpp/src/identity/identity_public_key/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/mod.rs index 541abde9ad3..c76091ccddc 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/mod.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/mod.rs @@ -2,6 +2,8 @@ use crate::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use crate::identity::identity_public_key::v0::IdentityPublicKeyV0; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; #[cfg(feature = "value-conversion")] use crate::serialization::ValueConvertible; use bincode::{Decode, Encode}; @@ -57,6 +59,99 @@ pub enum IdentityPublicKey { V0(IdentityPublicKeyV0), } +#[cfg(feature = "json-conversion")] +impl JsonConvertible for IdentityPublicKey {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::identity::identity_public_key::v0::IdentityPublicKeyV0; + use platform_value::{platform_value, BinaryData, Value}; + use serde_json::json; + + fn fixture() -> IdentityPublicKey { + IdentityPublicKey::V0(IdentityPublicKeyV0 { + id: 9, + key_type: KeyType::ECDSA_HASH160, + purpose: Purpose::TRANSFER, + security_level: SecurityLevel::CRITICAL, + contract_bounds: None, + read_only: true, + data: BinaryData::new(vec![0x55; 20]), + disabled_at: Some(1_700_000_000_000), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Internally-tagged enum (`tag = "$formatVersion"`); inner V0 has + // `rename_all = "camelCase"`, plus the explicit `serde(rename = "type")` + // on `key_type`. Sized-int fields with JSON loss: + // - `id`: KeyID = u32 (erased to JSON Number) + // - `key_type`/`purpose`/`security_level`: `#[repr(u8)]` enums with + // `Serialize_repr` -> raw u8 discriminants + // - `disabled_at`: Option + // `BinaryData` is base64-encoded in JSON (HR). + // KeyType::ECDSA_HASH160 = 2, Purpose::TRANSFER = 3, + // SecurityLevel::CRITICAL = 1. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "id": 9, + "purpose": 3, + "securityLevel": 1, + "contractBounds": serde_json::Value::Null, + "type": 2, + "readOnly": true, + "data": "VVVVVVVVVVVVVVVVVVVVVVVVVVU=", + "disabledAt": 1_700_000_000_000u64, + }) + ); + let recovered = IdentityPublicKey::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Explicit suffixes lock the typed-int variants: + // - `9u32` -> `Value::U32` for the KeyID + // - `4u8` / `1u8` -> `Value::U8` for the repr(u8) enums + // - `1_700_000_000_000u64` -> `Value::U64` for the timestamp + // `BinaryData` becomes a sized-bytes variant in non-HR — for a 20-byte + // payload it is detected as `Value::Bytes20([u8; 20])`, not the generic + // `Value::Bytes(Vec)`. (`platform_value` collapses fixed sizes + // 20/32/36 to typed variants for round-trip stability.) + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "id": 9u32, + "purpose": 3u8, + "securityLevel": 1u8, + "contractBounds": Value::Null, + "type": 2u8, + "readOnly": true, + "data": Value::Bytes20([0x55; 20]), + "disabledAt": 1_700_000_000_000u64, + }) + ); + let recovered = IdentityPublicKey::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} + impl IdentityPublicKey { /// Checks if public key security level is MASTER pub fn is_master(&self) -> bool { diff --git a/packages/rs-dpp/src/identity/identity_public_key/v0/conversion/cbor.rs b/packages/rs-dpp/src/identity/identity_public_key/v0/conversion/cbor.rs deleted file mode 100644 index 7a4811ed7dd..00000000000 --- a/packages/rs-dpp/src/identity/identity_public_key/v0/conversion/cbor.rs +++ /dev/null @@ -1,81 +0,0 @@ -// use crate::identity::identity_public_key::conversion::cbor::IdentityPublicKeyCborConversionMethodsV0; -// use crate::identity::identity_public_key::conversion::platform_value::IdentityPublicKeyPlatformValueConversionMethodsV0; -// use crate::identity::identity_public_key::v0::IdentityPublicKeyV0; -// use crate::util::cbor_serializer; -// use crate::util::cbor_value::{CborCanonicalMap, CborMapExtension, ValuesCollection}; -// use crate::version::PlatformVersion; -// use crate::ProtocolError; -// use ciborium::Value as CborValue; -// use platform_value::{BinaryData, ValueMapHelper}; -// use std::convert::TryInto; -// use crate::identity::contract_bounds::ContractBounds; -// -// impl IdentityPublicKeyCborConversionMethodsV0 for IdentityPublicKeyV0 { -// fn to_cbor_buffer(&self) -> Result, ProtocolError> { -// let mut object = self.to_cleaned_object()?; -// object -// .to_map_mut() -// .unwrap() -// .sort_by_lexicographical_byte_ordering_keys_and_inner_maps(); -// -// cbor_serializer::serializable_value_to_cbor(&object, None) -// } -// -// fn from_cbor_value( -// cbor_value: &CborValue, -// _platform_version: &PlatformVersion, -// ) -> Result { -// let key_value_map = cbor_value.as_map().ok_or_else(|| { -// ProtocolError::DecodingError(String::from( -// "Expected identity public key to be a key value map", -// )) -// })?; -// -// let id = key_value_map.as_u16("id", "A key must have an uint16 id")?; -// let key_type = key_value_map.as_u8("type", "Identity public key must have a type")?; -// let purpose = key_value_map.as_u8("purpose", "Identity public key must have a purpose")?; -// let security_level = key_value_map.as_u8( -// "securityLevel", -// "Identity public key must have a securityLevel", -// )?; -// let readonly = -// key_value_map.as_bool("readOnly", "Identity public key must have a readOnly")?; -// let public_key_bytes = -// key_value_map.as_bytes("data", "Identity public key must have a data")?; -// let disabled_at = key_value_map.as_u64("disabledAt", "").ok(); -// let contract_bounds = cbor_value.get(&CborValue::Text("contractBounds".to_string())).map(|contract_bounds_value| ContractBounds::from_cbor_value); -// -// Ok(IdentityPublicKeyV0 { -// id: id.into(), -// purpose: purpose.try_into()?, -// security_level: security_level.try_into()?, -// contract_bounds: None, -// key_type: key_type.try_into()?, -// data: BinaryData::new(public_key_bytes), -// read_only: readonly, -// disabled_at, -// }) -// } -// -// fn to_cbor_value(&self) -> CborValue { -// let mut pk_map = CborCanonicalMap::new(); -// -// pk_map.insert("id", self.id); -// pk_map.insert("data", self.data.as_slice()); -// pk_map.insert("type", self.key_type); -// pk_map.insert("purpose", self.purpose); -// pk_map.insert("readOnly", self.read_only); -// pk_map.insert("securityLevel", self.security_level); -// if let Some(ts) = self.disabled_at { -// pk_map.insert("disabledAt", ts) -// } -// -// pk_map.to_value_sorted() -// } -// } -// -// impl Into for &IdentityPublicKeyV0 { -// fn into(self) -> CborValue { -// self.to_cbor_value() -// } -// } diff --git a/packages/rs-dpp/src/identity/identity_public_key/v0/conversion/json.rs b/packages/rs-dpp/src/identity/identity_public_key/v0/conversion/json.rs deleted file mode 100644 index 2ceeca4c862..00000000000 --- a/packages/rs-dpp/src/identity/identity_public_key/v0/conversion/json.rs +++ /dev/null @@ -1,162 +0,0 @@ -use crate::identity::identity_public_key::conversion::json::IdentityPublicKeyJsonConversionMethodsV0; -use crate::identity::identity_public_key::conversion::platform_value::IdentityPublicKeyPlatformValueConversionMethodsV0; -use crate::identity::identity_public_key::fields::BINARY_DATA_FIELDS; -use crate::identity::identity_public_key::v0::IdentityPublicKeyV0; -use crate::version::PlatformVersion; -use crate::ProtocolError; -use platform_value::{ReplacementType, Value}; -use serde_json::Value as JsonValue; -use std::convert::{TryFrom, TryInto}; - -impl IdentityPublicKeyJsonConversionMethodsV0 for IdentityPublicKeyV0 { - fn to_json_object(&self) -> Result { - self.to_cleaned_object()? - .try_into_validating_json() - .map_err(ProtocolError::ValueError) - } - - fn to_json(&self) -> Result { - self.to_cleaned_object()? - .try_into() - .map_err(ProtocolError::ValueError) - } - - fn from_json_object( - raw_object: JsonValue, - platform_version: &PlatformVersion, - ) -> Result { - let mut value: Value = raw_object.into(); - value.replace_at_paths(BINARY_DATA_FIELDS, ReplacementType::BinaryBytes)?; - Self::from_object(value, platform_version) - } -} - -impl TryFrom<&str> for IdentityPublicKeyV0 { - type Error = ProtocolError; - - fn try_from(value: &str) -> Result { - let mut platform_value: Value = serde_json::from_str::(value) - .map_err(|e| ProtocolError::StringDecodeError(e.to_string()))? - .into(); - platform_value.replace_at_paths(BINARY_DATA_FIELDS, ReplacementType::BinaryBytes)?; - platform_value.try_into().map_err(ProtocolError::ValueError) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::identity::{KeyType, Purpose, SecurityLevel}; - use platform_value::BinaryData; - use platform_version::version::LATEST_PLATFORM_VERSION; - - fn sample_v0(disabled_at: Option) -> IdentityPublicKeyV0 { - IdentityPublicKeyV0 { - id: 1, - purpose: Purpose::AUTHENTICATION, - security_level: SecurityLevel::MASTER, - contract_bounds: None, - key_type: KeyType::ECDSA_SECP256K1, - read_only: false, - data: BinaryData::new(vec![0x01; 33]), - disabled_at, - } - } - - #[test] - fn to_json_returns_object() { - let key = sample_v0(None); - let json = key.to_json().expect("to_json succeeds"); - let obj = json.as_object().expect("expected json object"); - assert!(obj.contains_key("id")); - assert!(obj.contains_key("type")); - assert!(obj.contains_key("purpose")); - assert!(obj.contains_key("securityLevel")); - assert!(obj.contains_key("readOnly")); - assert!(obj.contains_key("data")); - // disabledAt is absent because it was None (to_cleaned_object removes it). - assert!(!obj.contains_key("disabledAt")); - } - - #[test] - fn to_json_includes_disabled_at_when_some() { - let key = sample_v0(Some(1_000_000)); - let json = key.to_json().expect("to_json succeeds"); - let obj = json.as_object().expect("object"); - assert!(obj.contains_key("disabledAt")); - } - - #[test] - fn to_json_object_data_is_byte_array() { - // to_json_object goes through try_into_validating_json, which renders bytes as arrays. - let key = sample_v0(None); - let json = key.to_json_object().expect("to_json_object succeeds"); - let obj = json.as_object().expect("object"); - let data = obj.get("data").expect("data field").as_array().expect( - "to_json_object should encode binary data as a JSON array of bytes (validating form)", - ); - assert_eq!(data.len(), 33); - } - - #[test] - fn from_json_object_roundtrip() { - let key = sample_v0(None); - let json = key.to_json().unwrap(); - let back = IdentityPublicKeyV0::from_json_object(json, LATEST_PLATFORM_VERSION).unwrap(); - assert_eq!(back, key); - } - - #[test] - fn try_from_str_parses_canonical_json() { - // Data is base64 of 33 0xAB bytes. - let bytes = [0xABu8; 33]; - let b64 = platform_value::string_encoding::encode( - &bytes, - platform_value::string_encoding::Encoding::Base64, - ); - let s = format!( - r#"{{ - "id": 7, - "type": 0, - "purpose": 0, - "securityLevel": 0, - "readOnly": false, - "data": "{}" - }}"#, - b64 - ); - let key: IdentityPublicKeyV0 = s.as_str().try_into().expect("parse succeeds"); - assert_eq!(key.id, 7); - assert_eq!(key.data.as_slice(), &vec![0xABu8; 33][..]); - } - - #[test] - fn try_from_str_fails_on_invalid_json() { - let result = IdentityPublicKeyV0::try_from("not valid json"); - match result { - Err(ProtocolError::StringDecodeError(_)) => {} - other => panic!("expected StringDecodeError, got {:?}", other), - } - } - - #[test] - fn try_from_str_fails_when_data_is_not_base64() { - let s = r#"{ - "id": 1, - "type": 0, - "purpose": 0, - "securityLevel": 0, - "readOnly": false, - "data": "!!!not-base64!!!" - }"#; - let result = IdentityPublicKeyV0::try_from(s); - assert!(result.is_err()); - } - - #[test] - fn from_json_object_fails_on_missing_fields() { - let json = serde_json::json!({ "id": 1 }); - let result = IdentityPublicKeyV0::from_json_object(json, LATEST_PLATFORM_VERSION); - assert!(result.is_err()); - } -} diff --git a/packages/rs-dpp/src/identity/identity_public_key/v0/conversion/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/v0/conversion/mod.rs index 469e625d4a1..8b137891791 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/v0/conversion/mod.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/v0/conversion/mod.rs @@ -1,6 +1 @@ -#[cfg(feature = "identity-cbor-conversion")] -mod cbor; -#[cfg(feature = "json-conversion")] -mod json; -#[cfg(feature = "value-conversion")] -mod platform_value; + diff --git a/packages/rs-dpp/src/identity/identity_public_key/v0/conversion/platform_value.rs b/packages/rs-dpp/src/identity/identity_public_key/v0/conversion/platform_value.rs deleted file mode 100644 index 403e1ab2792..00000000000 --- a/packages/rs-dpp/src/identity/identity_public_key/v0/conversion/platform_value.rs +++ /dev/null @@ -1,160 +0,0 @@ -use crate::identity::identity_public_key::conversion::platform_value::IdentityPublicKeyPlatformValueConversionMethodsV0; -use crate::identity::identity_public_key::v0::IdentityPublicKeyV0; -use crate::version::PlatformVersion; -use crate::ProtocolError; -use platform_value::Value; -use std::convert::{TryFrom, TryInto}; - -impl IdentityPublicKeyPlatformValueConversionMethodsV0 for IdentityPublicKeyV0 { - fn to_object(&self) -> Result { - platform_value::to_value(self).map_err(ProtocolError::ValueError) - } - - fn to_cleaned_object(&self) -> Result { - let mut value = platform_value::to_value(self).map_err(ProtocolError::ValueError)?; - if self.disabled_at.is_none() { - value - .remove("disabledAt") - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - fn into_object(self) -> Result { - platform_value::to_value(self).map_err(ProtocolError::ValueError) - } - - fn from_object( - value: Value, - _platform_version: &PlatformVersion, - ) -> Result { - value.try_into().map_err(ProtocolError::ValueError) - } -} - -impl TryFrom<&IdentityPublicKeyV0> for Value { - type Error = platform_value::Error; - - fn try_from(value: &IdentityPublicKeyV0) -> Result { - platform_value::to_value(value) - } -} - -impl TryFrom for Value { - type Error = platform_value::Error; - - fn try_from(value: IdentityPublicKeyV0) -> Result { - platform_value::to_value(value) - } -} - -impl TryFrom for IdentityPublicKeyV0 { - type Error = platform_value::Error; - - fn try_from(value: Value) -> Result { - platform_value::from_value(value) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::identity::identity_public_key::contract_bounds::ContractBounds; - use crate::identity::{KeyType, Purpose, SecurityLevel}; - use platform_value::{BinaryData, Identifier}; - use platform_version::version::LATEST_PLATFORM_VERSION; - - fn sample_v0(disabled_at: Option) -> IdentityPublicKeyV0 { - IdentityPublicKeyV0 { - id: 3, - purpose: Purpose::AUTHENTICATION, - security_level: SecurityLevel::HIGH, - contract_bounds: None, - key_type: KeyType::ECDSA_SECP256K1, - read_only: false, - data: BinaryData::new(vec![0xAB; 33]), - disabled_at, - } - } - - #[test] - fn to_object_roundtrip_to_v0() { - let key = sample_v0(Some(1_700_000_000_000)); - let value = key.to_object().expect("to_object should succeed"); - // The inner serializes its fields with camelCase (no $formatVersion tag). - assert!(value.is_map()); - let roundtripped = - IdentityPublicKeyV0::from_object(value, LATEST_PLATFORM_VERSION).expect("from_object"); - assert_eq!(roundtripped, key); - } - - #[test] - fn to_cleaned_object_removes_disabled_at_when_none() { - let key = sample_v0(None); - let cleaned = key.to_cleaned_object().expect("to_cleaned_object"); - let map = cleaned.to_map().expect("expected a map"); - assert!(!map.iter().any(|(k, _)| k.as_text() == Some("disabledAt"))); - } - - #[test] - fn to_cleaned_object_keeps_disabled_at_when_some() { - let key = sample_v0(Some(42)); - let cleaned = key.to_cleaned_object().expect("to_cleaned_object"); - let map = cleaned.to_map().expect("expected a map"); - assert!(map.iter().any(|(k, _)| k.as_text() == Some("disabledAt"))); - } - - #[test] - fn into_object_produces_same_as_to_object() { - let key = sample_v0(Some(1)); - let via_ref = key.to_object().unwrap(); - let via_owned = key.clone().into_object().unwrap(); - assert_eq!(via_ref, via_owned); - } - - #[test] - fn from_object_rejects_non_map() { - // platform_value deserialization should fail on a bare string. - let value = Value::Text("not a key".to_string()); - let result = IdentityPublicKeyV0::from_object(value, LATEST_PLATFORM_VERSION); - assert!(matches!(result, Err(ProtocolError::ValueError(_)))); - } - - #[test] - fn try_from_owned_value_succeeds() { - let key = sample_v0(None); - let value: Value = platform_value::to_value(&key).unwrap(); - let from: IdentityPublicKeyV0 = value.try_into().unwrap(); - assert_eq!(from, key); - } - - #[test] - fn try_from_ref_public_key_into_value_succeeds() { - let key = sample_v0(None); - let value: Value = (&key).try_into().expect("try_from &IdentityPublicKeyV0"); - assert!(value.is_map()); - } - - #[test] - fn try_from_owned_public_key_into_value_succeeds() { - let key = sample_v0(None); - let value: Value = key - .clone() - .try_into() - .expect("try_from IdentityPublicKeyV0"); - // Round-trip back. - let back: IdentityPublicKeyV0 = value.try_into().unwrap(); - assert_eq!(back, key); - } - - #[test] - fn from_object_with_contract_bounds_roundtrip() { - let mut key = sample_v0(None); - key.contract_bounds = Some(ContractBounds::SingleContract { - id: Identifier::from([0x12u8; 32]), - }); - let value = key.to_object().unwrap(); - let back = IdentityPublicKeyV0::from_object(value, LATEST_PLATFORM_VERSION).unwrap(); - assert_eq!(back, key); - } -} diff --git a/packages/rs-dpp/src/identity/identity_public_key/v0/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/v0/mod.rs index b07ad0a3970..d03356ab7db 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/v0/mod.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/v0/mod.rs @@ -48,7 +48,14 @@ pub struct IdentityPublicKeyV0 { pub key_type: KeyType, pub read_only: bool, pub data: BinaryData, - #[serde(default)] + // Phase D step 4: skip emitting `disabledAt: null` for non-disabled keys. + // Bincode (consensus binary path) is independent of this attribute and + // always writes the Option discriminant + payload. Identity hashing / + // Drive storage / state-transition signing all go through bincode, so + // none of those are affected. JSON / platform_value wire path becomes + // `{ ...fields }` instead of `{ ..., disabledAt: null }` for the + // common non-disabled case. + #[serde(default, skip_serializing_if = "Option::is_none")] pub disabled_at: Option, } diff --git a/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/chain/chain_asset_lock_proof.rs b/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/chain/chain_asset_lock_proof.rs index 15846404d48..247561ffec8 100644 --- a/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/chain/chain_asset_lock_proof.rs +++ b/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/chain/chain_asset_lock_proof.rs @@ -8,8 +8,8 @@ use ::serde::{Deserialize, Serialize}; use platform_value::Value; use std::convert::TryFrom; +use crate::identifier::Identifier; use crate::util::hash::hash_double; -use crate::{identifier::Identifier, ProtocolError}; use dashcore::OutPoint; /// Instant Asset Lock Proof is a part of Identity Create and Identity Topup @@ -36,13 +36,6 @@ impl TryFrom for ChainAssetLockProof { } impl ChainAssetLockProof { - pub fn to_object(&self) -> Result { - platform_value::to_value(self).map_err(ProtocolError::ValueError) - } - pub fn to_cleaned_object(&self) -> Result { - self.to_object() - } - pub fn new(core_chain_locked_height: u32, out_point: [u8; 36]) -> Self { Self { core_chain_locked_height, @@ -96,6 +89,7 @@ mod tests { #[test] fn chain_asset_lock_proof_value_round_trip() { + use crate::serialization::ValueConvertible; let txid_hex = "e8b43025641eea4fd21190f01bd870ef90f1a8b199d8fc3376c5b62c0b1a179d"; let txid = Txid::from_str(txid_hex).unwrap(); let proof = ChainAssetLockProof { @@ -108,3 +102,93 @@ mod tests { assert_eq!(proof, restored); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use dashcore::hashes::Hash; + use dashcore::{OutPoint, Txid}; + use platform_value::platform_value; + use serde_json::json; + use std::str::FromStr; + + fn fixture() -> ChainAssetLockProof { + ChainAssetLockProof { + core_chain_locked_height: 12345, + out_point: OutPoint::from_str( + "0000000000000000000000000000000000000000000000000000000000000001:0", + ) + .expect("outpoint"), + } + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `ChainAssetLockProof` is a plain struct with `rename_all = "camelCase"`. + // `out_point` uses dashcore's `OutPoint` serde directly (the local + // `outpoint_serde` wrapper was dropped once dashcore #708 landed the + // unified visitor upstream), which is HR-aware: in JSON this emits the + // `":"` string form. `core_chain_locked_height` is `u32`; + // JSON erases the size — see the value-path assertion. + // The HR string form mirrors the input we passed to `from_str`. + assert_eq!( + json, + json!({ + "coreChainLockedHeight": 12345, + "outPoint": "0000000000000000000000000000000000000000000000000000000000000001:0", + }) + ); + let recovered = ChainAssetLockProof::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `12345u32` locks `Value::U32`. The non-HR path of `OutPoint::serialize` + // emits a `{txid, vout}` STRUCT, NOT the `":"` string — + // and `Txid` itself serializes as a 32-byte array on the non-HR path + // (collapsing to `Value::Bytes32` via platform_value's sized-bytes + // detection). `vout` is `u32`. This is exactly the dual-shape behaviour + // dashcore's unified-visitor `OutPoint` serde (upstream #708; it + // replaced the local `outpoint_serde` wrapper) round-trips via + // `ContentDeserializer` (the HR=true / non-HR=false split). + // NOTE on byte order: the JSON/hex form (`00...01`, lowest nibble at + // the end) is REVERSED from the raw-bytes form. dashcore's `Txid` + // follows the Bitcoin convention — `as_byte_array()` returns the raw + // buffer where index 0 holds what shows as the LAST hex digit. So + // the displayed `00...01` corresponds to raw `[0x01, 0, 0, ..., 0]`. + // dashcore's `OutPoint` serde bridges the two shapes. + let mut raw = [0u8; 32]; + raw[0] = 0x01; + assert_eq!( + value, + platform_value!({ + "coreChainLockedHeight": 12345u32, + "outPoint": { + "txid": platform_value::Value::Bytes32(raw), + "vout": 0u32, + }, + }) + ); + let recovered = ChainAssetLockProof::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + + // Sanity check that the byte-array matches the real Txid bytes (so + // any future flip in dashcore's byte-order convention fails loud). + let txid_from_str = + Txid::from_str("0000000000000000000000000000000000000000000000000000000000000001") + .unwrap(); + assert_eq!(txid_from_str.as_byte_array(), &raw); + } +} diff --git a/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/instant/instant_asset_lock_proof.rs b/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/instant/instant_asset_lock_proof.rs index 8e978fbf383..da051ae7bd8 100644 --- a/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/instant/instant_asset_lock_proof.rs +++ b/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/instant/instant_asset_lock_proof.rs @@ -44,6 +44,10 @@ pub struct InstantAssetLockProof { pub output_index: u32, } +// Manual Serialize/Deserialize via the `RawInstantLockProof` DTO bridge: +// `InstantLock`/`Transaction` are consensus-encoded byte blobs on the wire +// (base64 in HR JSON, bytes otherwise), not serde-shaped structs, so the +// derive can't express the shape. Not a tagging customization. impl Serialize for InstantAssetLockProof { fn serialize(&self, serializer: S) -> Result where @@ -108,14 +112,6 @@ impl InstantAssetLockProof { } } - pub fn to_object(&self) -> Result { - platform_value::to_value(self).map_err(ProtocolError::ValueError) - } - - pub fn to_cleaned_object(&self) -> Result { - self.to_object() - } - pub fn instant_lock(&self) -> &InstantLock { &self.instant_lock } @@ -394,23 +390,17 @@ mod tests { } // --------------------------------------------------------------- - // to_object() + // to_object() — canonical ValueConvertible // --------------------------------------------------------------- #[test] fn test_to_object_succeeds() { + use crate::serialization::ValueConvertible; let proof = raw_instant_asset_lock_proof_fixture(None, None); let result = proof.to_object(); assert!(result.is_ok()); } - #[test] - fn test_to_cleaned_object_succeeds() { - let proof = raw_instant_asset_lock_proof_fixture(None, None); - let result = proof.to_cleaned_object(); - assert!(result.is_ok()); - } - // --------------------------------------------------------------- // RawInstantLockProof round-trip // --------------------------------------------------------------- @@ -462,6 +452,7 @@ mod tests { #[test] fn test_try_from_value_round_trip() { + use crate::serialization::ValueConvertible; let proof = raw_instant_asset_lock_proof_fixture(None, None); let value = proof.to_object().unwrap(); let recovered = InstantAssetLockProof::try_from(value).unwrap(); @@ -470,3 +461,31 @@ mod tests { assert_eq!(proof.transaction.txid(), recovered.transaction.txid()); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_instantassetlockproof { + use super::*; + + #[test] + fn json_round_trip_instantassetlockproof() { + use crate::serialization::JsonConvertible; + let original = InstantAssetLockProof::default(); + let json = original.to_json().expect("to_json"); + let recovered = InstantAssetLockProof::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_instantassetlockproof() { + use crate::serialization::ValueConvertible; + let original = InstantAssetLockProof::default(); + let value = original.to_object().expect("to_object"); + let recovered = InstantAssetLockProof::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs b/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs index 98e4fa5696b..3c6b8684490 100644 --- a/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs +++ b/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs @@ -25,17 +25,17 @@ pub mod validate_asset_lock_transaction_structure; // TODO: Serialization with bincode // TODO: Consider use Box for InstantAssetLockProof // -// Wire-shape note: this is an *internally-tagged* enum (`#[serde(tag = "type")]` +// Wire-shape note: this is an *internally-tagged* enum (`#[serde(tag = "$type")]` // with no `content`). serde's internal tagging works on newtype variants whose // inner is a struct — both `InstantAssetLockProof` and `ChainAssetLockProof` // qualify — so the inner struct's fields are flattened next to the `type` -// discriminator: `{"type": "instant", "instantLock": ..., "transaction": ..., +// discriminator: `{"$type": "instant", "instantLock": ..., "transaction": ..., // "outputIndex": ...}`. This matches the convention applied to other tagged // unions exposed to JS (see `AddressWitness`, `AddressFundsFeeStrategyStep`). // Bincode `Encode`/`Decode` derives are independent of serde, so consensus // binary format is unaffected. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Encode, Decode)] -#[serde(tag = "type", rename_all = "camelCase")] +#[serde(tag = "$type", rename_all = "camelCase")] #[allow(clippy::large_enum_variant)] pub enum AssetLockProof { Instant(#[bincode(with_serde)] InstantAssetLockProof), @@ -46,7 +46,7 @@ pub enum AssetLockProof { /// produces, but routes the instant variant through `RawInstantLockProof` so the /// dashcore `InstantLock` can be reconstructed from its raw bytes form. #[derive(Deserialize)] -#[serde(tag = "type", rename_all = "camelCase")] +#[serde(tag = "$type", rename_all = "camelCase")] enum RawAssetLockProof { Instant(RawInstantLockProof), Chain(ChainAssetLockProof), @@ -88,11 +88,98 @@ impl Default for AssetLockProof { } } +#[cfg(feature = "json-conversion")] +impl crate::serialization::JsonConvertible for AssetLockProof {} + +#[cfg(feature = "value-conversion")] +impl crate::serialization::ValueConvertible for AssetLockProof {} + impl AsRef for AssetLockProof { fn as_ref(&self) -> &AssetLockProof { self } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use dashcore::OutPoint; + use platform_value::platform_value; + use serde_json::json; + use std::str::FromStr; + + /// Non-default variant (`Chain` with non-zero core height + a real + /// outpoint) so the wire-shape assertion catches silent variant flip / + /// inner-zero on round-trip — the previous fixture used `Default::default` + /// (`Instant` zero proof). + fn fixture() -> AssetLockProof { + let out_point = OutPoint::from_str( + "0000000000000000000000000000000000000000000000000000000000000001:1", + ) + .expect("outpoint"); + AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height: 12_345, + out_point, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `AssetLockProof` is internally tagged (`#[serde(tag = "$type")]`), so + // the inner `ChainAssetLockProof`'s fields are flattened next to the + // discriminator. Surprising shape: `OutPoint` has a *string-form* + // Serialize impl (":") in dashcore which JSON consumes + // as-is — so on the JSON wire, `outPoint` is a single string. The + // platform_value layer goes through a different path (see the + // value-side test below) and produces a typed Map with `Bytes32` txid + // and `U32` vout. `coreChainLockedHeight` is `u32`; JSON erases the + // size — see the value-path assertion. + assert_eq!( + json, + json!({ + "$type": "chain", + "coreChainLockedHeight": 12_345, + "outPoint": "0000000000000000000000000000000000000000000000000000000000000001:1", + }) + ); + let recovered = AssetLockProof::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // platform_value path: `OutPoint` serializes via its derived structural + // impl producing a Map { txid: Bytes32, vout: U32 } (NOT the string form + // produced on the JSON side). `coreChainLockedHeight` is `u32` so + // `12_345u32` locks in `Value::U32`. + let mut txid_bytes = [0u8; 32]; + txid_bytes[0] = 1; + assert_eq!( + value, + platform_value!({ + "$type": "chain", + "coreChainLockedHeight": 12_345u32, + "outPoint": { + "txid": platform_value::Value::Bytes32(txid_bytes), + "vout": 1u32, + }, + }) + ); + let recovered = AssetLockProof::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} pub enum AssetLockProofType { Instant = 0, Chain = 1, @@ -164,17 +251,6 @@ impl AssetLockProof { } } - pub fn to_raw_object(&self) -> Result { - match self { - AssetLockProof::Instant(is) => { - platform_value::to_value(is).map_err(ProtocolError::ValueError) - } - AssetLockProof::Chain(cl) => { - platform_value::to_value(cl).map_err(ProtocolError::ValueError) - } - } - } - /// Validate the structure of the asset lock proof #[cfg(feature = "validation")] pub fn validate_structure( @@ -188,40 +264,21 @@ impl AssetLockProof { } } +// Canonical `TryFrom for AssetLockProof` is provided via the +// `Deserialize` impl above (which routes through `RawAssetLockProof` for +// the instant-lock raw-bytes shape) and `platform_value::from_value`. The +// previous hack here accepted legacy integer-tagged +// (`{type: 0|1, ...fields}`) and externally-tagged +// (`{Instant: {...}}`) shapes — both predated the +// `#[serde(tag = "$type")]` Critical-2 fix. Audit (Phase D step 6) +// confirmed all currently-flowing values are canonical-tagged +// (string `type`), so the hacks were dead. + impl TryFrom<&Value> for AssetLockProof { type Error = ProtocolError; fn try_from(value: &Value) -> Result { - //this is a complete hack for the moment - //todo: replace with - // from_value(value.clone()).map_err(ProtocolError::ValueError) - let proof_type_int: Option = value - .get_optional_integer("type") - .map_err(ProtocolError::ValueError)?; - if let Some(proof_type_int) = proof_type_int { - let proof_type = AssetLockProofType::try_from(proof_type_int)?; - - match proof_type { - AssetLockProofType::Instant => Ok(Self::Instant(value.clone().try_into()?)), - AssetLockProofType::Chain => Ok(Self::Chain(value.clone().try_into()?)), - } - } else { - let map = value.as_map().ok_or(ProtocolError::DecodingError( - "error decoding asset lock proof".to_string(), - ))?; - let (key, asset_lock_value) = map.first().ok_or(ProtocolError::DecodingError( - "error decoding asset lock proof as it was empty".to_string(), - ))?; - match key.as_str().ok_or(ProtocolError::DecodingError( - "error decoding asset lock proof".to_string(), - ))? { - "Instant" => Ok(Self::Instant(asset_lock_value.clone().try_into()?)), - "Chain" => Ok(Self::Chain(asset_lock_value.clone().try_into()?)), - _ => Err(ProtocolError::DecodingError( - "error decoding asset lock proof".to_string(), - )), - } - } + platform_value::from_value(value.clone()).map_err(ProtocolError::ValueError) } } @@ -229,65 +286,18 @@ impl TryFrom for AssetLockProof { type Error = ProtocolError; fn try_from(value: Value) -> Result { - let proof_type_int: Option = value - .get_optional_integer("type") - .map_err(ProtocolError::ValueError)?; - if let Some(proof_type_int) = proof_type_int { - let proof_type = AssetLockProofType::try_from(proof_type_int)?; - - match proof_type { - AssetLockProofType::Instant => Ok(Self::Instant(value.try_into()?)), - AssetLockProofType::Chain => Ok(Self::Chain(value.try_into()?)), - } - } else { - let map = value.as_map().ok_or(ProtocolError::DecodingError( - "error decoding asset lock proof".to_string(), - ))?; - let (key, asset_lock_value) = map.first().ok_or(ProtocolError::DecodingError( - "error decoding asset lock proof as it was empty".to_string(), - ))?; - match key.as_str().ok_or(ProtocolError::DecodingError( - "error decoding asset lock proof".to_string(), - ))? { - "Instant" => Ok(Self::Instant(asset_lock_value.clone().try_into()?)), - "Chain" => Ok(Self::Chain(asset_lock_value.clone().try_into()?)), - _ => Err(ProtocolError::DecodingError( - "error decoding asset lock proof".to_string(), - )), - } - } + platform_value::from_value(value).map_err(ProtocolError::ValueError) } } -impl TryInto for AssetLockProof { - type Error = ProtocolError; - - fn try_into(self) -> Result { - match self { - AssetLockProof::Instant(instant_proof) => { - platform_value::to_value(instant_proof).map_err(ProtocolError::ValueError) - } - AssetLockProof::Chain(chain_proof) => { - platform_value::to_value(chain_proof).map_err(ProtocolError::ValueError) - } - } - } -} - -impl TryInto for &AssetLockProof { - type Error = ProtocolError; - - fn try_into(self) -> Result { - match self { - AssetLockProof::Instant(instant_proof) => { - platform_value::to_value(instant_proof).map_err(ProtocolError::ValueError) - } - AssetLockProof::Chain(chain_proof) => { - platform_value::to_value(chain_proof).map_err(ProtocolError::ValueError) - } - } - } -} +// `TryInto` impls (and the inherent `to_raw_object` that mirrored +// them) used to live here, producing *untagged* `Value` (drops the variant +// tag entirely). They were structurally asymmetric with the canonical +// Deserialize, which expects the `type: "instant" | "chain"` discriminator +// to route through `RawAssetLockProof`. Confirmed zero production callers, +// so deleted in Phase D step 6. Use canonical `ValueConvertible::to_object` +// — it produces the correctly-tagged shape that `Deserialize` accepts on +// the way back. #[cfg(test)] mod tests { @@ -312,7 +322,7 @@ mod tests { let json = serde_json::to_value(&proof).expect("serialize"); - assert_eq!(json["type"], "chain"); + assert_eq!(json["$type"], "chain"); assert_eq!(json["coreChainLockedHeight"], 11); assert!( json.get("data").is_none(), @@ -464,9 +474,14 @@ mod tests { } #[test] - fn chain_proof_to_raw_object() { + fn chain_proof_to_object_canonical() { + // After Phase D step 6, `to_raw_object` (which produced an + // untagged Value) was deleted. Canonical + // `ValueConvertible::to_object` produces the correctly-tagged + // shape that round-trips through `Deserialize`. + use crate::serialization::ValueConvertible; let proof = make_chain_lock_proof(); - let result = proof.to_raw_object(); + let result = proof.to_object(); assert!(result.is_ok()); } @@ -483,22 +498,29 @@ mod tests { #[test] fn chain_proof_value_round_trip() { + // Canonical `ValueConvertible::to_object` produces a tagged + // Value (`{type: "chain", coreChainLockedHeight: ..., outPoint: ...}`) + // that round-trips through the manual `Deserialize` (which routes + // via `RawAssetLockProof`). + use crate::serialization::ValueConvertible; let chain_proof = ChainAssetLockProof::new(100, [0x42; 36]); let proof = AssetLockProof::Chain(chain_proof); - // Convert to Value - let value: Value = (&proof).try_into().expect("should convert to Value"); + let value = proof.to_object().expect("to_object"); + // The canonical `to_object` produces `type: "chain"` in the + // wire shape. `type_from_raw_value` expects an integer-typed + // tag (legacy shape), so it returns None on canonical output — + // confirm via the serde Map directly instead. + let map = value.to_map_ref().expect("map"); + assert_eq!( + map.iter() + .find_map(|(k, v)| (k.as_text() == Some("$type")).then(|| v.as_text())), + Some(Some("chain")) + ); - // Now try to read type from value - let _type_from_value = AssetLockProof::type_from_raw_value(&value); - // Chain proofs serialized via serde may or may not have "type" field depending - // on the serialization format. The untagged format may not include it. - // What matters is that the conversion itself works. - - // Convert from Value back - this tests the TryFrom path - // with the untagged serde format - let raw_value = proof.to_raw_object().expect("should convert to raw object"); - assert!(!raw_value.is_null()); + let recovered = + AssetLockProof::from_object(value).expect("from_object should round-trip"); + assert_eq!(proof, recovered); } #[test] @@ -526,25 +548,7 @@ mod tests { } } - mod try_into_value { - use super::*; - - #[test] - fn chain_proof_try_into_value() { - let chain_proof = ChainAssetLockProof::new(200, [0xDD; 36]); - let proof = AssetLockProof::Chain(chain_proof); - - let value: Result = proof.try_into(); - assert!(value.is_ok()); - } - - #[test] - fn chain_proof_ref_try_into_value() { - let chain_proof = ChainAssetLockProof::new(200, [0xDD; 36]); - let proof = AssetLockProof::Chain(chain_proof); - - let value: Result = (&proof).try_into(); - assert!(value.is_ok()); - } - } + // The `try_into_value` module previously exercised the now-deleted + // `TryInto` impls (which produced untagged `Value`). Canonical + // `ValueConvertible::to_object` is exercised in `try_from_value` above. } diff --git a/packages/rs-dpp/src/identity/v0/conversion/json.rs b/packages/rs-dpp/src/identity/v0/conversion/json.rs deleted file mode 100644 index aba1889abd8..00000000000 --- a/packages/rs-dpp/src/identity/v0/conversion/json.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::identity::conversion::json::IdentityJsonConversionMethodsV0; -use crate::identity::conversion::platform_value::IdentityPlatformValueConversionMethodsV0; -use crate::identity::{identity_public_key, IdentityV0, IDENTIFIER_FIELDS_RAW_OBJECT}; -use crate::ProtocolError; -use platform_value::{ReplacementType, Value}; -use serde_json::Value as JsonValue; -use std::convert::TryInto; - -impl IdentityJsonConversionMethodsV0 for IdentityV0 { - fn to_json_object(&self) -> Result { - self.to_cleaned_object()? - .try_into_validating_json() - .map_err(ProtocolError::ValueError) - } - - fn to_json(&self) -> Result { - self.to_cleaned_object()? - .try_into() - .map_err(ProtocolError::ValueError) - } - - /// Creates an identity from a json structure - fn from_json(json_object: JsonValue) -> Result { - let mut platform_value: Value = json_object.into(); - - platform_value - .replace_at_paths(IDENTIFIER_FIELDS_RAW_OBJECT, ReplacementType::Identifier)?; - - if let Some(public_keys_array) = platform_value.get_optional_array_mut_ref("publicKeys")? { - for public_key in public_keys_array.iter_mut() { - public_key.replace_at_paths( - identity_public_key::BINARY_DATA_FIELDS, - ReplacementType::BinaryBytes, - )?; - } - } - - let identity: Self = platform_value::from_value(platform_value)?; - - Ok(identity) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::identity::identity_public_key::v0::IdentityPublicKeyV0; - use crate::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel}; - use platform_value::{BinaryData, Identifier}; - use std::collections::BTreeMap; - - fn sample_identity_v0() -> IdentityV0 { - let mut keys: BTreeMap = BTreeMap::new(); - keys.insert( - 0, - IdentityPublicKey::V0(IdentityPublicKeyV0 { - id: 0, - purpose: Purpose::AUTHENTICATION, - security_level: SecurityLevel::MASTER, - contract_bounds: None, - key_type: KeyType::ECDSA_SECP256K1, - read_only: false, - data: BinaryData::new(vec![0x33; 33]), - disabled_at: None, - }), - ); - IdentityV0 { - id: Identifier::from([9u8; 32]), - public_keys: keys, - balance: 42, - revision: 1, - } - } - - #[test] - fn to_json_contains_expected_top_level_fields() { - let id = sample_identity_v0(); - let json = id.to_json().expect("to_json"); - let obj = json.as_object().expect("object"); - assert!(obj.contains_key("id")); - assert!(obj.contains_key("publicKeys")); - assert!(obj.contains_key("balance")); - assert!(obj.contains_key("revision")); - } - - // V0 to_json -> from_json round-trip succeeds. - // - // Previously, this combination failed because `BinaryData::Deserialize` was - // asymmetric — its human-readable visitor accepted only strings and the binary - // visitor accepted only byte sequences, while `platform_value`'s nested - // deserializers default to `is_human_readable() = true` even when the value - // carries `Value::Bytes`. The fix in PR #3235 made `BinaryData::Deserialize` - // symmetric (accepts both strings and bytes regardless of mode, mirroring the - // `Identifier` pattern). Bincode `Encode`/`Decode` derives are untouched, so - // consensus binary format is unchanged — only the serde JSON path now - // round-trips cleanly. - #[test] - fn to_json_then_from_json_round_trips_v0() { - let id = sample_identity_v0(); - let json = id.to_json().unwrap(); - let back = IdentityV0::from_json(json).expect("v0 round-trip should succeed"); - assert_eq!(id, back); - } - - #[test] - fn to_json_object_encodes_identifier_as_bytes_array() { - // to_json_object goes through try_into_validating_json, which represents - // identifiers (32 bytes) as a JSON array of numbers. - let id = sample_identity_v0(); - let json = id.to_json_object().expect("to_json_object"); - let obj = json.as_object().expect("object"); - let id_field = - obj.get("id").expect("id").as_array().expect( - "to_json_object should render the identifier as a JSON array of byte values", - ); - assert_eq!(id_field.len(), 32); - } - - #[test] - fn from_json_fails_on_garbage_input() { - let json = serde_json::json!({ "id": "not-a-valid-identifier" }); - let result = IdentityV0::from_json(json); - assert!(result.is_err()); - } - - // frozen: V0 consensus behavior - // - // The JSON fixture does not carry the inner-enum `$formatVersion` tag that - // `IdentityPublicKey` deserialization requires, so `from_json` fails on it. - // This is the canonical V0 shape of the fixture — the intent is to document - // that `from_json` cannot ingest the legacy fixture form directly. - #[test] - fn from_json_fixture_fails_missing_format_version_v0_frozen() { - use crate::tests::fixtures::identity_fixture_json; - let json = identity_fixture_json(); - let result = IdentityV0::from_json(json); - match result { - Err(e) => { - let msg = format!("{:?}", e); - assert!( - msg.contains("$formatVersion") || msg.contains("formatVersion"), - "expected missing-formatVersion error, got {msg}" - ); - } - Ok(_) => panic!("expected from_json on legacy fixture to fail"), - } - } - - #[test] - fn from_json_errors_when_public_keys_field_is_not_array() { - // publicKeys is expected to be an array; using a string should fail early. - let json = serde_json::json!({ - "id": "3bufpwQjL5qsvuP4fmCKgXJrKG852DDMYfi9J6XKqPAT", - "publicKeys": "oops", - "balance": 0, - "revision": 0, - }); - let result = IdentityV0::from_json(json); - assert!(result.is_err()); - } -} diff --git a/packages/rs-dpp/src/identity/v0/conversion/mod.rs b/packages/rs-dpp/src/identity/v0/conversion/mod.rs index 0739e30d121..8b137891791 100644 --- a/packages/rs-dpp/src/identity/v0/conversion/mod.rs +++ b/packages/rs-dpp/src/identity/v0/conversion/mod.rs @@ -1,4 +1 @@ -#[cfg(feature = "json-conversion")] -pub mod json; -#[cfg(feature = "value-conversion")] -pub mod platform_value; + diff --git a/packages/rs-dpp/src/identity/v0/conversion/platform_value.rs b/packages/rs-dpp/src/identity/v0/conversion/platform_value.rs deleted file mode 100644 index 30b25792c68..00000000000 --- a/packages/rs-dpp/src/identity/v0/conversion/platform_value.rs +++ /dev/null @@ -1,124 +0,0 @@ -use crate::identity::conversion::platform_value::IdentityPlatformValueConversionMethodsV0; -use crate::identity::{property_names, IdentityV0}; -#[cfg(feature = "value-conversion")] -use crate::serialization::ValueConvertible; -use crate::ProtocolError; -use platform_value::Value; - -impl IdentityPlatformValueConversionMethodsV0 for IdentityV0 { - fn to_cleaned_object(&self) -> Result { - //same as object for Identities - let mut value = self.to_object()?; - if let Some(keys) = value.get_optional_array_mut_ref(property_names::PUBLIC_KEYS)? { - for key in keys.iter_mut() { - key.remove_optional_value_if_null("disabledAt")?; - } - } - Ok(value) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::identity::identity_public_key::v0::IdentityPublicKeyV0; - use crate::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel}; - use platform_value::{BinaryData, Identifier}; - use std::collections::BTreeMap; - - fn sample_with_disabled(disabled_at: Option) -> IdentityV0 { - let mut keys: BTreeMap = BTreeMap::new(); - keys.insert( - 0, - IdentityPublicKey::V0(IdentityPublicKeyV0 { - id: 0, - purpose: Purpose::AUTHENTICATION, - security_level: SecurityLevel::MASTER, - contract_bounds: None, - key_type: KeyType::ECDSA_SECP256K1, - read_only: false, - data: BinaryData::new(vec![0x11; 33]), - disabled_at, - }), - ); - IdentityV0 { - id: Identifier::from([0u8; 32]), - public_keys: keys, - balance: 0, - revision: 0, - } - } - - fn key_map_at_index(value: &Value, index: usize) -> &Vec<(Value, Value)> { - let map = value.to_map_ref().expect("map"); - let pks = map - .iter() - .find(|(k, _)| k.as_text() == Some("publicKeys")) - .map(|(_, v)| v) - .expect("publicKeys"); - let arr = pks.to_array_ref().expect("array"); - arr[index].to_map_ref().expect("key map") - } - - #[test] - fn to_cleaned_object_strips_null_disabled_at_from_keys() { - let id = sample_with_disabled(None); - let cleaned = id.to_cleaned_object().expect("cleaned"); - let key_map = key_map_at_index(&cleaned, 0); - assert!( - !key_map - .iter() - .any(|(k, _)| k.as_text() == Some("disabledAt")), - "disabledAt should have been stripped" - ); - } - - #[test] - fn to_cleaned_object_preserves_present_disabled_at() { - let id = sample_with_disabled(Some(123)); - let cleaned = id.to_cleaned_object().expect("cleaned"); - let key_map = key_map_at_index(&cleaned, 0); - assert!(key_map - .iter() - .any(|(k, _)| k.as_text() == Some("disabledAt"))); - } - - #[test] - fn to_object_and_cleaned_are_same_for_empty_keys() { - let id = IdentityV0 { - id: Identifier::from([1u8; 32]), - public_keys: BTreeMap::new(), - balance: 1, - revision: 2, - }; - let object = id.to_object().expect("to_object"); - let cleaned = id.to_cleaned_object().expect("cleaned"); - assert_eq!(object, cleaned); - } - - // V0 to_object -> TryFrom round-trip succeeds. - // - // Previously this failed because of an asymmetric `BinaryData::Deserialize` - // (string-only in human-readable mode, bytes-only in binary mode), while - // `platform_value`'s nested deserializers default to - // `is_human_readable() = true` even when the value carries `Value::Bytes`. - // The fix in PR #3235 made `BinaryData::Deserialize` symmetric (accepts - // both strings and bytes regardless of mode, mirroring `Identifier`). - // Bincode `Encode`/`Decode` derives are untouched, so consensus binary - // format is unchanged — only the serde platform_value path now round-trips. - #[test] - fn to_object_then_try_from_round_trips_v0() { - let id = sample_with_disabled(Some(9)); - let value = id.to_object().unwrap(); - let back = IdentityV0::try_from(value).expect("v0 round-trip should succeed"); - assert_eq!(id, back); - } - - #[test] - fn try_from_ref_value_round_trips_v0() { - let id = sample_with_disabled(None); - let value = id.to_object().unwrap(); - let back = IdentityV0::try_from(&value).expect("v0 round-trip should succeed"); - assert_eq!(id, back); - } -} diff --git a/packages/rs-dpp/src/identity/v0/mod.rs b/packages/rs-dpp/src/identity/v0/mod.rs index 634cc842f80..a730aca3187 100644 --- a/packages/rs-dpp/src/identity/v0/mod.rs +++ b/packages/rs-dpp/src/identity/v0/mod.rs @@ -4,6 +4,8 @@ pub mod random; #[cfg(feature = "json-conversion")] use crate::serialization::json_safe_fields; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; #[cfg(feature = "value-conversion")] use crate::serialization::ValueConvertible; use std::collections::BTreeMap; @@ -28,17 +30,14 @@ use bincode::{Decode, Encode}; #[derive(Default, Debug, Clone, Eq, PartialEq)] #[cfg_attr(feature = "identity-serialization", derive(Encode, Decode))] #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), + feature = "serde-conversion", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] #[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] pub struct IdentityV0 { pub id: Identifier, - #[cfg_attr( - any(feature = "serde-conversion", feature = "serde-conversion"), - serde(with = "public_key_serialization") - )] + #[cfg_attr(feature = "serde-conversion", serde(with = "public_key_serialization"))] pub public_keys: BTreeMap, pub balance: u64, pub revision: Revision, @@ -50,6 +49,9 @@ impl Hash for IdentityV0 { } } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl JsonConvertible for IdentityV0 {} + mod public_key_serialization { use crate::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use crate::identity::{IdentityPublicKey, KeyID}; @@ -142,3 +144,31 @@ impl TryFrom<&Value> for IdentityV0 { platform_value::from_value(value.clone()).map_err(ProtocolError::ValueError) } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_identityv0 { + use super::*; + + #[test] + fn json_round_trip_identityv0() { + use crate::serialization::JsonConvertible; + let original = IdentityV0::default(); + let json = original.to_json().expect("to_json"); + let recovered = IdentityV0::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_identityv0() { + use crate::serialization::ValueConvertible; + let original = IdentityV0::default(); + let value = original.to_object().expect("to_object"); + let recovered = IdentityV0::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/metadata.rs b/packages/rs-dpp/src/metadata.rs index 16236aa0755..6dccb0de2df 100644 --- a/packages/rs-dpp/src/metadata.rs +++ b/packages/rs-dpp/src/metadata.rs @@ -2,8 +2,11 @@ use bincode::Encode; use platform_serialization::de::Decode; use serde::{Deserialize, Serialize}; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use crate::{errors::ProtocolError, prelude::TimestampMillis, util::deserializer::ProtocolVersion}; +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive( Serialize, Deserialize, Encode, Decode, Debug, Default, Clone, Copy, PartialEq, PartialOrd, Eq, )] @@ -26,3 +29,84 @@ impl std::convert::TryFrom<&str> for Metadata { serde_json::from_str(d).map_err(|e| ProtocolError::EncodingError(e.to_string())) } } + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for Metadata {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for Metadata {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::serialization::{JsonConvertible, ValueConvertible}; + use platform_value::platform_value; + use serde_json::json; + + /// Non-default values per field. `Metadata` crosses to JS as the + /// `$metadata` field of `ExtendedDocument`, so its u64 fields go through + /// `json_safe_fields` (numbers below 2^53, strings above). + fn fixture() -> Metadata { + Metadata { + block_height: 1_234_567, + core_chain_locked_height: 2_222_333, + time_ms: 1_700_000_000_000, + protocol_version: 9, + } + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int info erased in JSON; the value-path assertion locks the + // typed variants (u64 heights/time, u32 protocolVersion). + assert_eq!( + json, + json!({ + "blockHeight": 1_234_567, + "coreChainLockedHeight": 2_222_333, + "timeMs": 1_700_000_000_000u64, + "protocolVersion": 9, + }) + ); + let recovered = Metadata::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + let original = fixture(); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({ + "blockHeight": 1_234_567u64, + "coreChainLockedHeight": 2_222_333u64, + "timeMs": 1_700_000_000_000u64, + "protocolVersion": 9u32, + }) + ); + let recovered = Metadata::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + /// Values above JS `Number.MAX_SAFE_INTEGER` must stringify in JSON + /// (json_safe_fields contract) and still round-trip. + #[test] + fn json_stringifies_unsafe_u64() { + let original = Metadata { + block_height: u64::MAX, + ..fixture() + }; + let json = original.to_json().expect("to_json"); + assert_eq!(json["blockHeight"], json!(u64::MAX.to_string())); + let recovered = Metadata::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/serialization/dashcore/bls_pubkey.rs b/packages/rs-dpp/src/serialization/dashcore/bls_pubkey.rs new file mode 100644 index 00000000000..ccc6d8f852a --- /dev/null +++ b/packages/rs-dpp/src/serialization/dashcore/bls_pubkey.rs @@ -0,0 +1,273 @@ +//! Local serde wrapper for `BlsPublicKey` that tolerates +//! owned-string sources (`serde_json::Value`, `platform_value::Value`, and +//! anything routed through serde's `ContentDeserializer` for tagged-enum +//! buffering). +//! +//! ## Why this exists +//! +//! Upstream `blstrs_plus` 0.8.18 (`src/serde_impl.rs:119`) implements the +//! human-readable deserialize path as: +//! +//! ```ignore +//! if d.is_human_readable() { +//! let hex_str = <&str>::deserialize(d)?; // borrowed-only +//! ... +//! } +//! ``` +//! +//! `<&str>::deserialize` only succeeds when the deserializer's visitor +//! receives `visit_borrowed_str` — which `serde_json::from_slice` / +//! `serde_json::from_str` provide, but `serde_json::from_value`, +//! `platform_value::from_value`, and `ContentDeserializer` do **not** (they +//! produce owned `String`). Round-tripping a `BlsPublicKey` through any +//! `Value` representation therefore fails with +//! `"invalid type: string ..., expected a borrowed string"`. +//! +//! This is technically a serde compatibility quirk rather than a single +//! crate's bug — but the leaf type is the only place to patch. See plan +//! §10b "Common pattern: serde's `ContentDeserializer` HR-quirk" for +//! the broader narrative. +//! +//! ## How the workaround works +//! +//! A single `deserialize_any` visitor accepts BOTH wire forms upstream emits: +//! the 96-char hex string (`visit_str`) and the 48-byte compressed-G1 form as +//! raw bytes or a `u8` sequence (`visit_bytes` / `visit_seq`). Either way it +//! hex/byte-decodes to the compressed-G1 representation, lifts it to +//! `G1Projective` via `to_curve`, and wraps into `PublicKey` +//! directly (the inner field is `pub`). +//! +//! Driving it via `deserialize_any` (rather than branching on +//! `is_human_readable()`) is what makes it robust to serde's internal-tag +//! `Content` buffer: that buffer's `is_human_readable()` does not reliably +//! match the original deserializer, so a `Value` (non-HR) pubkey could arrive +//! as a byte *sequence* while the old HR branch expected a *string* — which is +//! exactly why the `ValidatorSet` value round-trip test used to be `#[ignore]`d. +//! Accepting both shapes in one visitor sidesteps that, and also sidesteps the +//! upstream borrowed-only `<&str>::deserialize` HR path. Both serde_json and +//! platform_value are self-describing, so `deserialize_any` is safe; bincode +//! never reaches here (it goes through the separate derived `Decode`). +//! +//! Note: `BlsPublicKey` carries a public key on the G1 curve +//! (the `Bls12381G2Impl` name refers to where signatures live, not keys). +//! Compressed G1 = 48 bytes = 96 hex chars. +//! +//! ## When to remove this +//! +//! This wrapper is now self-sufficient (no behavioral dependency on an upstream +//! fix). Once upstream `blstrs_plus` accepts owned strings AND a byte-sequence +//! HR form on its own `Deserialize`, this wrapper and the `serde(with = ...)` +//! annotations on `Validator::public_key` / `ValidatorSetV0::threshold_public_key` +//! can simply be dropped. + +use crate::bls_signatures::inner_types::{G1Affine, GroupEncoding, PrimeCurveAffine}; +use crate::bls_signatures::{Bls12381G2Impl, PublicKey as BlsPublicKey}; +use serde::de::Visitor; +use serde::{Deserializer, Serialize, Serializer}; +use std::fmt; + +/// Compressed-G1 wire size for BLS12-381 (where the public key lives in +/// `Bls12381G2Impl`). +const COMPRESSED_G1_LEN: usize = 48; + +pub fn serialize( + pk: &BlsPublicKey, + serializer: S, +) -> Result { + // Upstream serialize already produces a hex string in HR and a byte tuple + // in non-HR; both are correct on the wire. Nothing to override here. + pk.serialize(serializer) +} + +pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + // One visitor that accepts BOTH wire forms upstream emits: the + // human-readable 96-char hex string, and the non-HR 48-byte compressed-G1 + // form (as `bytes` or a `u8` sequence). Driving it via `deserialize_any` + // makes it robust to serde's internal-tag `Content` buffer, whose + // `is_human_readable()` does not always match the original deserializer — + // the bug that previously forced the ValidatorSet value round-trip test to + // be `#[ignore]`d (the non-HR bytes arrived while the old code's HR branch + // expected a string → "invalid type: sequence, expected a string"). It also + // sidesteps the upstream `<&str>::deserialize` borrowed-only HR path (the + // original reason this wrapper exists). Both serde_json and platform_value + // are self-describing, so `deserialize_any` is safe; bincode never reaches + // here (it goes through the separate derived `Decode`). + deserializer.deserialize_any(BlsPublicKeyVisitor) +} + +fn from_compressed_g1_bytes( + bytes: &[u8], +) -> Result, E> { + if bytes.len() != COMPRESSED_G1_LEN { + return Err(E::custom(format!( + "expected {COMPRESSED_G1_LEN} compressed-G1 bytes for public key, got {}", + bytes.len() + ))); + } + let mut compressed = ::Repr::default(); + compressed.as_mut().copy_from_slice(bytes); + let affine = Option::::from(G1Affine::from_bytes(&compressed)) + .ok_or_else(|| E::custom("not a valid compressed G1 point"))?; + Ok(BlsPublicKey::(affine.to_curve())) +} + +struct BlsPublicKeyVisitor; + +impl<'de> Visitor<'de> for BlsPublicKeyVisitor { + type Value = BlsPublicKey; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "a {}-char hex string or {} compressed-G1 bytes", + COMPRESSED_G1_LEN * 2, + COMPRESSED_G1_LEN + ) + } + + fn visit_str(self, s: &str) -> Result { + if s.len() != COMPRESSED_G1_LEN * 2 { + return Err(E::custom(format!( + "expected {} hex chars for compressed G1 public key, got {}", + COMPRESSED_G1_LEN * 2, + s.len() + ))); + } + let mut bytes = [0u8; COMPRESSED_G1_LEN]; + for (i, slot) in bytes.iter_mut().enumerate() { + let hi = hex_nibble(s.as_bytes()[i * 2]).map_err(E::custom)?; + let lo = hex_nibble(s.as_bytes()[i * 2 + 1]).map_err(E::custom)?; + *slot = (hi << 4) | lo; + } + from_compressed_g1_bytes(&bytes) + } + + fn visit_bytes(self, v: &[u8]) -> Result { + from_compressed_g1_bytes(v) + } + + fn visit_seq>(self, mut seq: A) -> Result { + let mut bytes = Vec::with_capacity(COMPRESSED_G1_LEN); + while let Some(b) = seq.next_element::()? { + // A valid compressed-G1 public key is exactly COMPRESSED_G1_LEN + // bytes; reject as soon as a hostile payload exceeds that rather + // than allocating/parsing an arbitrarily long sequence first. + if bytes.len() == COMPRESSED_G1_LEN { + return Err(serde::de::Error::invalid_length(bytes.len() + 1, &self)); + } + bytes.push(b); + } + from_compressed_g1_bytes(&bytes) + } +} + +fn hex_nibble(c: u8) -> Result { + match c { + b'0'..=b'9' => Ok(c - b'0'), + b'a'..=b'f' => Ok(c - b'a' + 10), + b'A'..=b'F' => Ok(c - b'A' + 10), + _ => Err("invalid hex character in compressed G1 public key"), + } +} + +/// `Option>` variant for fields like +/// `Validator::public_key`. +pub mod option { + use super::*; + + pub fn serialize( + opt: &Option>, + serializer: S, + ) -> Result { + // Option's built-in Serialize delegates to T's Serialize, which + // is the upstream BlsPublicKey impl — already correct. + opt.serialize(serializer) + } + + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result>, D::Error> { + struct OptionVisitor; + + impl<'de> Visitor<'de> for OptionVisitor { + type Value = Option>; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("Option>") + } + + fn visit_none(self) -> Result { + Ok(None) + } + + fn visit_unit(self) -> Result { + Ok(None) + } + + fn visit_some>( + self, + inner: D2, + ) -> Result { + super::deserialize(inner).map(Some) + } + } + + deserializer.deserialize_option(OptionVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::{Deserialize, Serialize}; + use serde_json::json; + + // A known-valid compressed-G1 BLS public key (deterministic — the + // ValidatorSet fixture's seeded StdRng(42) threshold key). + const PK_HEX: &str = + "969c5d5873f49aa994c5f6a850924ca1840c4ad1791aaaecd90093d4a5c0c3799f2d98540f5366cfa0a33f143fd69263"; + + // Newtypes that drive the `with` module(s) through serde. + #[derive(Serialize, Deserialize)] + struct Wrap(#[serde(with = "super")] BlsPublicKey); + + #[derive(Serialize, Deserialize)] + struct OptWrap(#[serde(with = "super::option")] Option>); + + #[test] + fn json_hr_round_trip_is_the_hex_string() { + // Human-readable (serde_json::Value): hex in, identical hex out (visit_str). + let pk: Wrap = serde_json::from_value(json!(PK_HEX)).expect("from hex"); + assert_eq!(serde_json::to_value(&pk).expect("to json"), json!(PK_HEX)); + } + + #[test] + fn json_borrowed_str_path_works() { + // `serde_json::from_str` yields borrowed strings — the path upstream's + // `<&str>::deserialize` handled but `from_value`/Content did not. Our + // `visit_str` takes `&str`, so both work. + let pk: Wrap = serde_json::from_str(&format!("\"{PK_HEX}\"")).expect("from_str"); + assert_eq!(serde_json::to_value(&pk).expect("to json"), json!(PK_HEX)); + } + + #[test] + fn value_non_hr_byte_seq_round_trip() { + // Non-HR `platform_value` serializes the key as a 48-byte sequence; the + // `visit_seq` path (the bug the un-ignored ValidatorSet test exposed) + // must reconstruct it. Re-serialize to JSON to confirm the same key. + let pk: Wrap = serde_json::from_value(json!(PK_HEX)).expect("from hex"); + let value = platform_value::to_value(&pk).expect("to value"); + let pk2: Wrap = platform_value::from_value(value).expect("from value seq"); + assert_eq!(serde_json::to_value(&pk2).expect("to json"), json!(PK_HEX)); + } + + #[test] + fn option_some_and_none_round_trip() { + let some: OptWrap = serde_json::from_value(json!(PK_HEX)).expect("some"); + assert_eq!(serde_json::to_value(&some).expect("to json"), json!(PK_HEX)); + let none: OptWrap = serde_json::from_value(json!(null)).expect("none"); + assert_eq!(serde_json::to_value(&none).expect("to json"), json!(null)); + } +} diff --git a/packages/rs-dpp/src/serialization/dashcore/mod.rs b/packages/rs-dpp/src/serialization/dashcore/mod.rs new file mode 100644 index 00000000000..65fb7e5cd97 --- /dev/null +++ b/packages/rs-dpp/src/serialization/dashcore/mod.rs @@ -0,0 +1,18 @@ +//! Serde `with` wrappers for types owned by external (dashcore) crates. +//! +//! These live here — rather than next to their dpp consumers — because the +//! type being (de)serialized is **not** a dpp type; it belongs to dashcore / +//! its transitive deps (e.g. `blstrs_plus`). Grouping them under +//! `serialization::dashcore` keeps "serde for out-of-crate types" in one place, +//! distinct from dpp-type-local serde (which lives next to its type). + +/// Compressed-G1 BLS public key (`BlsPublicKey` from dashcore's +/// `blstrs_plus`). Used via `#[serde(with = "crate::serialization::dashcore::bls_pubkey")]`. +/// +/// The wrapped type comes from `dashcore::blsful`, which is only linked when the +/// `bls-signatures` feature is on (`crate::bls_signatures`). Its only consumers — +/// the `core_types` validator/validator-set structs — are themselves gated behind +/// `core-types`, which requires `bls-signatures`, so gating here keeps the module +/// from breaking builds that omit BLS (e.g. `wasm-drive-verify`). +#[cfg(feature = "bls-signatures")] +pub mod bls_pubkey; diff --git a/packages/rs-dpp/src/serialization/json/safe_fields.rs b/packages/rs-dpp/src/serialization/json/safe_fields.rs index 6468a58af2f..4b3c27a89f7 100644 --- a/packages/rs-dpp/src/serialization/json/safe_fields.rs +++ b/packages/rs-dpp/src/serialization/json/safe_fields.rs @@ -61,6 +61,9 @@ impl JsonSafeFields for std::collections::HashSet {} impl JsonSafeFields for platform_value::Identifier {} impl JsonSafeFields for platform_value::BinaryData {} +impl JsonSafeFields for platform_value::Bytes20 {} +impl JsonSafeFields for platform_value::Bytes32 {} +impl JsonSafeFields for platform_value::Bytes36 {} impl JsonSafeFields for platform_value::Value {} impl JsonSafeFields for platform_value::string_encoding::Encoding {} @@ -94,13 +97,87 @@ impl JsonSafeFields for crate::identity::identity_public_key::IdentityPublicKey impl JsonSafeFields for crate::identity::state_transition::asset_lock_proof::AssetLockProof {} impl JsonSafeFields for crate::address_funds::PlatformAddress {} impl JsonSafeFields for crate::address_funds::AddressFundsFeeStrategy {} -impl JsonSafeFields for crate::address_funds::AddressWitness {} +// `AddressWitness` is verified via `#[json_safe_fields]` on the type itself +// (named-field variants of `BinaryData`), so no manual marker is needed here. impl JsonSafeFields for crate::withdrawal::Pooling {} impl JsonSafeFields for crate::identity::core_script::CoreScript {} impl JsonSafeFields for crate::voting::votes::Vote {} +// `DocumentBaseTransition` wraps `DocumentBaseTransitionV0` / `V1`, both of +// which are `#[json_safe_fields]`-annotated, so the wrapper enum is safe by +// induction: every u64 inside is protected by `json_safe_u64`. +impl JsonSafeFields + for crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition +{ +} +// `TokenPaymentInfo` (v0 wrapper) — V0 is `#[json_safe_fields]`-annotated. +impl JsonSafeFields for crate::tokens::token_payment_info::TokenPaymentInfo {} +// `GasFeesPaidBy` is a unit-variant enum (no u64). +impl JsonSafeFields for crate::tokens::gas_fees_paid_by::GasFeesPaidBy {} +// `GroupStateTransitionInfo` is verified via `#[json_safe_fields]` on the type +// itself (named `u16` / `Identifier` / `bool` fields) — no manual marker needed. +// `TokenBaseTransition` wraps `TokenBaseTransitionV0` which is +// `#[json_safe_fields]`-annotated, so the wrapper is safe by induction. +impl JsonSafeFields + for crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition +{ +} +// BatchTransition family wrappers — each variant's outer enum is itself +// safe by induction (every V0 inner is `#[json_safe_fields]`-annotated; +// the outer-enum manual `impl JsonConvertible` doesn't auto-impl +// JsonSafeFields, so we declare it explicitly here). +impl JsonSafeFields + for crate::state_transition::batch_transition::batched_transition::DocumentTransition +{ +} +impl JsonSafeFields + for crate::state_transition::batch_transition::batched_transition::TokenTransition +{ +} +impl JsonSafeFields + for crate::state_transition::batch_transition::batched_transition::BatchedTransition +{ +} impl JsonSafeFields for crate::voting::vote_choices::resource_vote_choice::ResourceVoteChoice {} impl JsonSafeFields for crate::group::action_event::GroupActionEvent {} // TokenEvent contains u64 aliases (TokenAmount, Credits) in tuple variants that // `#[json_safe_fields]` can't auto-annotate. Developer takes responsibility for // JS-safe serialization of these fields. See token_event.rs for details. impl JsonSafeFields for crate::tokens::token_event::TokenEvent {} +// `TokenEmergencyAction` is a unit-variant enum (Pause / Resume). +impl JsonSafeFields for crate::tokens::emergency_action::TokenEmergencyAction {} +// `TokenDistributionType` is a unit-variant enum. +impl JsonSafeFields + for crate::data_contract::associated_token::token_distribution_key::TokenDistributionType +{ +} +// `TokenPricingSchedule` has tuple variants holding `Credits` (u64) and +// `BTreeMap`. `#[json_safe_fields]` can't auto-annotate +// variant-internal u64s, so it serializes through an internally-`$type`-tagged +// `Repr` that routes both through `json_safe_u64` / `json_safe_u64_u64_map` — +// this marker is therefore truthful, not a bare escape hatch. +impl JsonSafeFields for crate::tokens::token_pricing_schedule::TokenPricingSchedule {} +// `TokenConfigurationChangeItem` has tuple variants with `Option` +// and `Option` (u64-shaped). Same escape-hatch pattern. +impl JsonSafeFields + for crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem +{ +} +// `RewardDistributionMoment` carries `BlockHeight`/`TimestampMillis` (u64) in +// tuple variants. Unlike the bare escape-hatches above, its u64 fields are +// *actually* JS-safe: `#[serde(with = "json_safe_u64")]` is applied directly on +// the variant fields (see reward_distribution_moment/mod.rs). +impl JsonSafeFields + for crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment +{ +} +// `ContestedIndexFieldMatch::PositiveIntegerMatch(u128)` is made JS-safe via +// `#[serde(with = "json_safe_u128")]` on the variant field (see +// document_type/index/mod.rs); `Regex(LazyRegex)` round-trips as a string. +impl JsonSafeFields for crate::data_contract::document_type::ContestedIndexFieldMatch {} +// `TokenDistributionInfo::PreProgrammed` carries a `TimestampMillis` (u64) made +// JS-safe via `#[serde(with = "json_safe_u64")]`; `Perpetual`'s +// `RewardDistributionMoment` is JS-safe via its own annotation. +impl JsonSafeFields + for crate::data_contract::associated_token::token_distribution_key::TokenDistributionInfo +{ +} diff --git a/packages/rs-dpp/src/serialization/json/safe_integer.rs b/packages/rs-dpp/src/serialization/json/safe_integer.rs index d4c5b8efabb..780fbfa43e6 100644 --- a/packages/rs-dpp/src/serialization/json/safe_integer.rs +++ b/packages/rs-dpp/src/serialization/json/safe_integer.rs @@ -67,6 +67,115 @@ pub mod json_safe_u64 { } } +/// Serde `with` module for `u128` fields. +/// +/// `u128` is never JS-safe as a bare number once it exceeds +/// `Number.MAX_SAFE_INTEGER`, so the human-readable (JSON) path stringifies +/// large values. The binary / `Value` path keeps the native `u128`. +pub mod json_safe_u128 { + use serde::de::{self, Deserializer, Visitor}; + use serde::ser::Serializer; + + use super::JS_MAX_SAFE_INTEGER; + + pub fn serialize(value: &u128, serializer: S) -> Result { + if serializer.is_human_readable() && *value > JS_MAX_SAFE_INTEGER as u128 { + serializer.serialize_str(&value.to_string()) + } else { + serializer.serialize_u128(*value) + } + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + if deserializer.is_human_readable() { + deserializer.deserialize_any(U128OrStringVisitor) + } else { + serde::Deserialize::deserialize(deserializer) + } + } + + struct U128OrStringVisitor; + + impl<'de> Visitor<'de> for U128OrStringVisitor { + type Value = u128; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a u128 or a string containing a u128") + } + + fn visit_u64(self, v: u64) -> Result { + Ok(v as u128) + } + + fn visit_u128(self, v: u128) -> Result { + Ok(v) + } + + fn visit_i64(self, v: i64) -> Result { + u128::try_from(v) + .map_err(|_| de::Error::custom(format!("i64 value {v} out of u128 range"))) + } + + fn visit_str(self, v: &str) -> Result { + v.parse::() + .map_err(|_| de::Error::custom(format!("invalid u128 string: {v}"))) + } + } +} + +/// Serde `with` module for a `u128` field that is buffered through serde's +/// `Content` enum — i.e. a field of an internally-tagged (`#[serde(tag = "…")]`) +/// enum or struct. +/// +/// Same JS-safety as [`json_safe_u128`] (values above `Number.MAX_SAFE_INTEGER` +/// stringify in human-readable JSON), but it **never** emits `serialize_u128`. +/// serde's `Content` enum cannot hold a 128-bit integer in this serde version, so +/// `json_safe_u128`'s `serialize_u128` round-trips to an "invalid type: integer … +/// as u128" error once internal tagging buffers it. This variant instead encodes a +/// plain number while the value fits in `u64`, and a string once it doesn't (and, +/// in human-readable JSON, once it exceeds `Number.MAX_SAFE_INTEGER`). The +/// `Value` / bincode paths keep the value lossless via the same number/string split. +pub mod json_safe_u128_content { + use serde::de::{self, Visitor}; + use serde::{Deserializer, Serializer}; + + use super::JS_MAX_SAFE_INTEGER; + + pub fn serialize(value: &u128, serializer: S) -> Result { + let stringify_above = if serializer.is_human_readable() { + JS_MAX_SAFE_INTEGER as u128 + } else { + u64::MAX as u128 + }; + if *value > stringify_above { + serializer.serialize_str(&value.to_string()) + } else { + serializer.serialize_u64(*value as u64) + } + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + struct V; + impl Visitor<'_> for V { + type Value = u128; + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("a u128 as a number or string") + } + fn visit_u64(self, v: u64) -> Result { + Ok(v as u128) + } + fn visit_u128(self, v: u128) -> Result { + Ok(v) + } + fn visit_str(self, v: &str) -> Result { + v.parse() + .map_err(|_| de::Error::custom(format!("invalid u128 string: {v}"))) + } + } + deserializer.deserialize_any(V) + } +} + /// Serde `with` module for `i64` fields. pub mod json_safe_i64 { use serde::de::{self, Deserializer, Visitor}; @@ -215,6 +324,256 @@ pub mod json_safe_option_i64 { } } +/// Serde `with` module for `Option<(String, u64)>` fields. +/// +/// Used by `DocumentCreateTransitionV0::prefunded_voting_balance`. The +/// `json_safe_fields` macro can't auto-inject on tuple-inside-Option fields, +/// so this is added explicitly via `serde(with = ...)`. JS-safety semantics +/// match `json_safe_u64`: large u64 values become strings in HR; non-HR +/// keeps native u64. +pub mod json_safe_option_string_u64_tuple { + use serde::de::{self, Deserializer, SeqAccess, Visitor}; + use serde::ser::{SerializeTuple, Serializer}; + + pub fn serialize( + value: &Option<(String, u64)>, + serializer: S, + ) -> Result { + match value { + Some((s, n)) => { + let stringify = serializer.is_human_readable() && *n > super::JS_MAX_SAFE_INTEGER; + let mut tup = serializer.serialize_tuple(2)?; + tup.serialize_element(s)?; + if stringify { + tup.serialize_element(&n.to_string())?; + } else { + tup.serialize_element(n)?; + } + tup.end() + } + None => serializer.serialize_none(), + } + } + + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { + deserializer.deserialize_option(OptStringU64TupleVisitor) + } + + struct OptStringU64TupleVisitor; + + impl<'de> Visitor<'de> for OptStringU64TupleVisitor { + type Value = Option<(String, u64)>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("null or a 2-tuple [String, u64-or-string]") + } + + fn visit_none(self) -> Result { + Ok(None) + } + + fn visit_unit(self) -> Result { + Ok(None) + } + + fn visit_some>( + self, + deserializer: D, + ) -> Result { + deserializer + .deserialize_tuple(2, StringU64TupleVisitor) + .map(Some) + } + + // Some self-describing formats (serde_json with deserialize_any) call + // visit_seq directly when the wire shape is an array — accept that too. + fn visit_seq>(self, seq: A) -> Result { + StringU64TupleVisitor.visit_seq(seq).map(Some) + } + } + + /// Newtype wrapper that delegates u64 deserialization to `json_safe_u64`, + /// accepting both numbers and strings in HR. + #[derive(serde::Deserialize)] + #[serde(transparent)] + struct SafeU64(#[serde(with = "super::json_safe_u64")] u64); + + struct StringU64TupleVisitor; + + impl<'de> Visitor<'de> for StringU64TupleVisitor { + type Value = (String, u64); + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a 2-tuple [String, u64-or-string]") + } + + fn visit_seq>(self, mut seq: A) -> Result { + let s: String = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(0, &"a 2-tuple"))?; + let n: SafeU64 = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(1, &"a 2-tuple"))?; + Ok((s, n.0)) + } + } +} + +/// Serde `with` module for `Option<(u32, u32, Vec)>` fields used by the +/// `SharedEncryptedNote` / `PrivateEncryptedNote` type aliases on token +/// transitions. +/// +/// In HR (JSON) the inner `Vec` is base64-encoded so the wire shape is +/// `[u32, u32, ""]` instead of an array-of-numbers. In non-HR +/// (platform_value, bincode) the bytes stay as raw bytes (`Value::Bytes`). +/// The two `u32` indices are always JS-safe (well below `MAX_SAFE_INTEGER`) +/// so they don't need special protection. +pub mod json_safe_option_encrypted_note { + use serde::de::{self, Deserializer, SeqAccess, Visitor}; + use serde::ser::{SerializeTuple, Serializer}; + + /// Wrapper that emits its byte payload via `serialize_bytes` (raw bytes) + /// rather than the default `Vec` Serialize (sequence of u8). Used in + /// the non-HR path so platform_value receives `Value::Bytes` and bincode + /// emits a length-prefixed byte buffer. + struct BytesAsBytes<'a>(&'a [u8]); + + impl<'a> serde::Serialize for BytesAsBytes<'a> { + fn serialize(&self, s: S) -> Result { + s.serialize_bytes(self.0) + } + } + + pub fn serialize( + value: &Option<(u32, u32, Vec)>, + serializer: S, + ) -> Result { + match value { + Some((a, b, bytes)) => { + let is_hr = serializer.is_human_readable(); + let mut tup = serializer.serialize_tuple(3)?; + tup.serialize_element(a)?; + tup.serialize_element(b)?; + if is_hr { + use base64::Engine; + let s = base64::engine::general_purpose::STANDARD.encode(bytes); + tup.serialize_element(&s)?; + } else { + tup.serialize_element(&BytesAsBytes(bytes))?; + } + tup.end() + } + None => serializer.serialize_none(), + } + } + + #[allow(clippy::type_complexity)] + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result)>, D::Error> { + deserializer.deserialize_option(OptEncryptedNoteVisitor) + } + + struct OptEncryptedNoteVisitor; + + impl<'de> Visitor<'de> for OptEncryptedNoteVisitor { + type Value = Option<(u32, u32, Vec)>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("null or a 3-tuple [u32, u32, base64-string-or-bytes]") + } + + fn visit_none(self) -> Result { + Ok(None) + } + + fn visit_unit(self) -> Result { + Ok(None) + } + + fn visit_some>( + self, + deserializer: D, + ) -> Result { + deserializer + .deserialize_tuple(3, EncryptedNoteVisitor) + .map(Some) + } + + fn visit_seq>(self, seq: A) -> Result { + EncryptedNoteVisitor.visit_seq(seq).map(Some) + } + } + + /// Newtype wrapper that accepts either a base64 string (HR) or a byte + /// sequence (non-HR) and produces a `Vec`. + struct BytesField(Vec); + + impl<'de> serde::Deserialize<'de> for BytesField { + fn deserialize>(d: D) -> Result { + struct V; + impl<'de> Visitor<'de> for V { + type Value = Vec; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("base64 string or byte sequence") + } + + fn visit_str(self, s: &str) -> Result, E> { + use base64::Engine; + base64::engine::general_purpose::STANDARD + .decode(s) + .map_err(|e| E::custom(format!("invalid base64: {e}"))) + } + + fn visit_bytes(self, b: &[u8]) -> Result, E> { + Ok(b.to_vec()) + } + + fn visit_byte_buf(self, b: Vec) -> Result, E> { + Ok(b) + } + + fn visit_seq>(self, mut seq: A) -> Result, A::Error> { + let mut out = Vec::new(); + while let Some(b) = seq.next_element::()? { + out.push(b); + } + Ok(out) + } + } + // Use `deserialize_any` so we accept whichever path the deserializer + // takes (string for JSON, bytes for bincode/platform_value). + d.deserialize_any(V).map(BytesField) + } + } + + struct EncryptedNoteVisitor; + + impl<'de> Visitor<'de> for EncryptedNoteVisitor { + type Value = (u32, u32, Vec); + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a 3-tuple [u32, u32, base64-string-or-bytes]") + } + + fn visit_seq>(self, mut seq: A) -> Result { + let a: u32 = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(0, &"a 3-tuple"))?; + let b: u32 = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(1, &"a 3-tuple"))?; + let bytes: BytesField = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(2, &"a 3-tuple"))?; + Ok((a, b, bytes.0)) + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -226,6 +585,12 @@ mod tests { value: u64, } + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct TestU128 { + #[serde(with = "json_safe_u128")] + value: u128, + } + #[derive(Debug, PartialEq, Serialize, Deserialize)] struct TestI64 { #[serde(with = "json_safe_i64")] @@ -266,6 +631,42 @@ mod tests { assert_eq!(t, restored); } + #[test] + fn u128_small_value_stays_number() { + let t = TestU128 { value: 42 }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_number()); + assert_eq!(json["value"].as_u64().unwrap(), 42); + + let restored: TestU128 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn u128_large_value_becomes_string() { + // Above u64::MAX — only representable as a string in JS-safe JSON. + let t = TestU128 { + value: (u64::MAX as u128) + 1, + }; + let json = serde_json::to_value(&t).unwrap(); + assert!(json["value"].is_string()); + assert_eq!(json["value"].as_str().unwrap(), "18446744073709551616"); + + let restored: TestU128 = serde_json::from_value(json).unwrap(); + assert_eq!(t, restored); + } + + #[test] + fn u128_value_round_trips_through_non_human_readable() { + // platform_value is non-human-readable → native u128, no stringification. + let t = TestU128 { + value: (u64::MAX as u128) + 12345, + }; + let value = platform_value::to_value(&t).unwrap(); + let restored: TestU128 = platform_value::from_value(value).unwrap(); + assert_eq!(t, restored); + } + #[test] fn u64_at_max_safe_integer_stays_number() { let t = TestU64 { diff --git a/packages/rs-dpp/src/serialization/mod.rs b/packages/rs-dpp/src/serialization/mod.rs index aa97ea9fa6c..b609b2aa0a1 100644 --- a/packages/rs-dpp/src/serialization/mod.rs +++ b/packages/rs-dpp/src/serialization/mod.rs @@ -1,6 +1,12 @@ #[cfg(feature = "json-conversion")] pub mod json; +// Not gated behind `serde-conversion`: this fixed-size byte-array helper is +// needed by modules that derive serde unconditionally (e.g. the always-serde +// `block::extended_block_info::ExtendedBlockInfo`), so it must resolve in every +// feature configuration. It only depends on `serde` and `base64`, both +// non-optional dependencies. #[cfg(feature = "serde-conversion")] +pub mod dashcore; pub mod serde_bytes; #[cfg(feature = "serde-conversion")] pub mod serde_bytes_var; @@ -13,7 +19,8 @@ pub use dpp_json_convertible_derive::JsonConvertible; pub use dpp_json_convertible_derive::ValueConvertible; #[cfg(feature = "json-conversion")] pub use json::safe_integer::{ - json_safe_i64, json_safe_option_i64, json_safe_option_u64, json_safe_u64, + json_safe_i64, json_safe_option_i64, json_safe_option_u64, json_safe_u128, + json_safe_u128_content, json_safe_u64, }; #[cfg(feature = "json-conversion")] pub use json::JsonSafeFields; diff --git a/packages/rs-dpp/src/serialization/serde_bytes.rs b/packages/rs-dpp/src/serialization/serde_bytes.rs index 1030d300684..620d4a22b0d 100644 --- a/packages/rs-dpp/src/serialization/serde_bytes.rs +++ b/packages/rs-dpp/src/serialization/serde_bytes.rs @@ -19,7 +19,7 @@ use base64::prelude::BASE64_STANDARD; use base64::Engine; use serde::de::{self, SeqAccess, Visitor}; -use serde::{Deserialize, Deserializer, Serializer}; +use serde::{Deserializer, Serializer}; use std::fmt; pub fn serialize( @@ -36,51 +36,132 @@ pub fn serialize( pub fn deserialize<'de, D: Deserializer<'de>, const N: usize>( deserializer: D, ) -> Result<[u8; N], D::Error> { + // Accept all four input shapes — base64 string, byte buffer, byte slice, + // and sequence of u8 — regardless of the deserializer's `is_human_readable` + // flag. Required because serde's `ContentDeserializer` (used for internally + // tagged enums like `#[serde(tag = "$formatVersion")]`) always reports + // `is_human_readable: true`, so a value that started as bytes through a + // non-HR deserializer (platform_value, bincode) can arrive at this visitor + // through the string path and vice versa. Mirrors the pattern used by + // `platform_value::types::{bytes_32,binary_data,identifier}`. + + struct AnyShapeVisitor; + + impl<'de, const N: usize> Visitor<'de> for AnyShapeVisitor { + type Value = [u8; N]; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{} bytes (as a byte buffer, sequence of u8, or base64-encoded string)", + N + ) + } + + fn visit_bytes(self, v: &[u8]) -> Result { + v.try_into() + .map_err(|_| E::custom(format!("expected {} bytes, got {}", N, v.len()))) + } + + fn visit_byte_buf(self, v: Vec) -> Result { + let len = v.len(); + v.try_into() + .map_err(|_| E::custom(format!("expected {} bytes, got {}", N, len))) + } + + fn visit_str(self, v: &str) -> Result { + let vec = BASE64_STANDARD + .decode(v) + .map_err(|e| E::custom(format!("expected base64-encoded {} bytes: {}", N, e)))?; + self.visit_byte_buf(vec) + } + + fn visit_seq>(self, mut seq: A) -> Result { + let mut buf = Vec::with_capacity(N); + while let Some(b) = seq.next_element::()? { + buf.push(b); + } + let len = buf.len(); + buf.try_into() + .map_err(|_| de::Error::custom(format!("expected {} bytes, got {}", N, len))) + } + } + if deserializer.is_human_readable() { - let s = ::deserialize(deserializer)?; - let vec = BASE64_STANDARD - .decode(&s) - .map_err(serde::de::Error::custom)?; - vec.try_into().map_err(|v: Vec| { - serde::de::Error::custom(format!("expected {} bytes, got {}", N, v.len())) - }) + // `deserialize_any` covers both true human-readable deserializers + // (serde_json sees a string → `visit_str`) AND serde's + // `ContentDeserializer` (which falsely reports `is_human_readable=true` + // and may wrap `Content::ByteBuf` from a non-HR source like + // platform_value → dispatches to `visit_bytes`). + deserializer.deserialize_any(AnyShapeVisitor::) } else { - // Accept both byte-buffer formats (`serde_wasm_bindgen` Uint8Array, - // `platform_value::Value::Bytes` → `visit_bytes` / `visit_byte_buf`) - // and length-prefixed sequences (bincode → `visit_seq`). Going through - // `>::deserialize` would only cover the seq path. - struct BytesOrSeqVisitor; + // Non-HR (bincode, platform_value): bincode is non-self-describing and + // requires an explicit shape hint; `deserialize_byte_buf` is what works + // for both bincode (length-prefixed bytes) and platform_value (Value::Bytes). + deserializer.deserialize_byte_buf(AnyShapeVisitor::) + } +} + +/// Serde helper for `Option<[u8; N]>` — wraps the parent module's +/// const-generic `[u8; N]` codec in `Option`-aware visitors. +/// +/// Use via `#[serde(with = "crate::serialization::serde_bytes::option")]`. +/// `None` round-trips as `null` in JSON / `unit` in binary formats; `Some` +/// values use the parent module's base64-vs-bytes shape. +pub mod option { + use serde::de::{self, Visitor}; + use serde::{Deserializer, Serializer}; + use std::fmt; + + pub fn serialize( + value: &Option<[u8; N]>, + serializer: S, + ) -> Result { + // Wrap the inner `[u8; N]` so we can call `serialize_some` and let the + // outer serializer write the Option tag (None / Some). Calling + // `super::serialize` directly with the inner serializer would bypass + // the Option variant tag in non-self-describing formats like bincode. + struct Inner<'a, const N: usize>(&'a [u8; N]); + impl<'a, const N: usize> serde::Serialize for Inner<'a, N> { + fn serialize(&self, s: S) -> Result { + super::serialize(self.0, s) + } + } + match value { + Some(bytes) => serializer.serialize_some(&Inner::(bytes)), + None => serializer.serialize_none(), + } + } - impl<'de, const N: usize> Visitor<'de> for BytesOrSeqVisitor { - type Value = [u8; N]; + pub fn deserialize<'de, D: Deserializer<'de>, const N: usize>( + deserializer: D, + ) -> Result, D::Error> { + struct OptionVisitor; + + impl<'de, const N: usize> Visitor<'de> for OptionVisitor { + type Value = Option<[u8; N]>; fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} bytes (as a byte buffer or sequence of u8)", N) + write!(f, "optional {} bytes", N) } - fn visit_bytes(self, v: &[u8]) -> Result { - v.try_into() - .map_err(|_| E::custom(format!("expected {} bytes, got {}", N, v.len()))) + fn visit_none(self) -> Result { + Ok(None) } - fn visit_byte_buf(self, v: Vec) -> Result { - let len = v.len(); - v.try_into() - .map_err(|_| E::custom(format!("expected {} bytes, got {}", N, len))) + fn visit_unit(self) -> Result { + Ok(None) } - fn visit_seq>(self, mut seq: A) -> Result { - let mut buf = Vec::with_capacity(N); - while let Some(b) = seq.next_element::()? { - buf.push(b); - } - let len = buf.len(); - buf.try_into() - .map_err(|_| de::Error::custom(format!("expected {} bytes, got {}", N, len))) + fn visit_some>( + self, + deserializer: D, + ) -> Result { + super::deserialize::(deserializer).map(Some) } } - deserializer.deserialize_byte_buf(BytesOrSeqVisitor::) + deserializer.deserialize_option(OptionVisitor::) } } @@ -143,4 +224,38 @@ mod tests { .expect("bincode decode"); assert_eq!(original, restored); } + + // --- option submodule -------------------------------------------------- + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct OptWrap32(#[serde(with = "super::option")] Option<[u8; 32]>); + + #[test] + fn option_some_json_round_trip() { + let original = OptWrap32(Some([0xab; 32])); + let value = serde_json::to_value(&original).expect("serialize"); + assert_eq!(value, serde_json::json!(BASE64_STANDARD.encode([0xab; 32]))); + let restored: OptWrap32 = serde_json::from_value(value).expect("deserialize"); + assert_eq!(original, restored); + } + + #[test] + fn option_none_json_round_trip() { + let original = OptWrap32(None); + let value = serde_json::to_value(&original).expect("serialize"); + assert_eq!(value, serde_json::Value::Null); + let restored: OptWrap32 = serde_json::from_value(value).expect("deserialize"); + assert_eq!(original, restored); + } + + #[test] + fn option_some_binary_round_trip() { + let original = OptWrap32(Some([0x77; 32])); + let bytes = bincode::serde::encode_to_vec(&original, bincode::config::standard()) + .expect("bincode encode"); + let (restored, _): (OptWrap32, usize) = + bincode::serde::decode_from_slice(&bytes, bincode::config::standard()) + .expect("bincode decode"); + assert_eq!(original, restored); + } } diff --git a/packages/rs-dpp/src/serialization/serde_bytes_var.rs b/packages/rs-dpp/src/serialization/serde_bytes_var.rs index 048f61435b1..5bf590458b0 100644 --- a/packages/rs-dpp/src/serialization/serde_bytes_var.rs +++ b/packages/rs-dpp/src/serialization/serde_bytes_var.rs @@ -14,7 +14,7 @@ use base64::prelude::BASE64_STANDARD; use base64::Engine; use serde::de::{self, SeqAccess, Visitor}; -use serde::{Deserialize, Deserializer, Serializer}; +use serde::{Deserializer, Serializer}; use std::fmt; pub fn serialize(bytes: &Vec, serializer: S) -> Result { @@ -26,41 +26,53 @@ pub fn serialize(bytes: &Vec, serializer: S) -> Result>(deserializer: D) -> Result, D::Error> { - if deserializer.is_human_readable() { - let s = ::deserialize(deserializer)?; - BASE64_STANDARD.decode(&s).map_err(serde::de::Error::custom) - } else { - // Accept both byte-buffer formats (`serde_wasm_bindgen` Uint8Array → - // `visit_bytes` / `visit_byte_buf`) and length-prefixed sequences - // (bincode, `platform_value::Value::Array(u8)` → `visit_seq`). The - // default `>::deserialize` only covers the seq path. - struct BytesOrSeqVisitor; + // Accept all four input shapes — base64 string, byte buffer, byte slice, + // and sequence of u8 — regardless of the deserializer's `is_human_readable` + // flag. Required because serde's `ContentDeserializer` (used for internally + // tagged enums like `#[serde(tag = "$formatVersion")]`) always reports + // `is_human_readable: true`, so a value that started as bytes through a + // non-HR deserializer can arrive at this visitor through any path. - impl<'de> Visitor<'de> for BytesOrSeqVisitor { - type Value = Vec; + struct AnyShapeVisitor; - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("bytes or sequence of u8") - } + impl<'de> Visitor<'de> for AnyShapeVisitor { + type Value = Vec; - fn visit_bytes(self, v: &[u8]) -> Result { - Ok(v.to_vec()) - } + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("bytes, sequence of u8, or base64-encoded string") + } - fn visit_byte_buf(self, v: Vec) -> Result { - Ok(v) - } + fn visit_bytes(self, v: &[u8]) -> Result { + Ok(v.to_vec()) + } - fn visit_seq>(self, mut seq: A) -> Result { - let mut bytes = Vec::with_capacity(seq.size_hint().unwrap_or(0)); - while let Some(b) = seq.next_element::()? { - bytes.push(b); - } - Ok(bytes) + fn visit_byte_buf(self, v: Vec) -> Result { + Ok(v) + } + + fn visit_str(self, v: &str) -> Result { + BASE64_STANDARD + .decode(v) + .map_err(|e| E::custom(format!("expected base64 for bytes: {}", e))) + } + + fn visit_seq>(self, mut seq: A) -> Result { + let mut bytes = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(b) = seq.next_element::()? { + bytes.push(b); } + Ok(bytes) } + } - deserializer.deserialize_byte_buf(BytesOrSeqVisitor) + if deserializer.is_human_readable() { + // `deserialize_any` covers true HR (serde_json string) AND + // ContentDeserializer (which reports HR but may wrap bytes from a + // non-HR source like platform_value). + deserializer.deserialize_any(AnyShapeVisitor) + } else { + // Non-HR (bincode, platform_value): explicit shape hint. + deserializer.deserialize_byte_buf(AnyShapeVisitor) } } diff --git a/packages/rs-dpp/src/serialization/serialization_traits.rs b/packages/rs-dpp/src/serialization/serialization_traits.rs index 9c620a62282..760e8692455 100644 --- a/packages/rs-dpp/src/serialization/serialization_traits.rs +++ b/packages/rs-dpp/src/serialization/serialization_traits.rs @@ -138,6 +138,40 @@ pub trait PlatformLimitDeserializableFromVersionedStructure { Self: Sized; } +/// Convert to/from `platform_value::Value` using **non-human-readable** serde +/// (`Identifier` = `Value::Identifier(bytes)`, binary = `Value::Bytes(bytes)`, +/// raw byte fields preserved without stringification). +/// +/// # ⚠️ HR / non-HR divergence (Critical-1) +/// +/// `ValueConvertible` calls `platform_value::to_value`, which uses a serializer +/// that reports `is_human_readable() == false`. The mirror trait +/// [`JsonConvertible`] uses `serde_json::to_value`, which reports `true`. +/// Types whose `Serialize` impl branches on `is_human_readable()` produce +/// **structurally different output** between the two paths: +/// +/// | Type | `to_json()` (HR) | `to_object()` (non-HR) | +/// |---|---|---| +/// | [`platform_value::Identifier`] | `"5bV6jUfh..."` (bs58 string) | `Value::Identifier([u8; 32])` | +/// | [`platform_value::BinaryData`] | `"sg=="` (base64 string) | `Value::Bytes(Vec)` | +/// | `Bytes20` / `Bytes32` / `Bytes36` | base64 string | `Value::Bytes32([u8; N])` etc. | +/// | `CoreScript` | `"dqkU..."` (base64 string) | `Value::Bytes(Vec)` | +/// +/// **Do not assume** `self.to_object()?.try_into_json()` ≡ `self.to_json()`. +/// They render the same field as a string in one and a byte array in the +/// other. Round-trip tests should exercise each path independently. +/// +/// # ⚠️ `ContentDeserializer` caveat +/// +/// Manual `Deserialize` impls that branch on `deserializer.is_human_readable()` +/// must also handle `serde::__private::de::ContentDeserializer`, used +/// internally by `#[serde(tag = "...")]` enums. ContentDeserializer **always +/// reports `is_human_readable: true`** regardless of the original source, so +/// a non-HR `platform_value::Value` flowing into a tagged enum gets shape- +/// inferred as if it were HR. Recipe: write a dual-shape visitor accepting +/// both shapes in the HR branch via `deserialize_any`. See +/// [`platform_value::Bytes32::deserialize`] for the canonical example, and +/// `rs-dpp/src/serialization/serde_bytes.rs` for `[u8; N]` / `Vec`. #[cfg(feature = "value-conversion")] pub trait ValueConvertible: Serialize + DeserializeOwned { fn to_object(&self) -> Result @@ -169,10 +203,43 @@ pub trait ValueConvertible: Serialize + DeserializeOwned { } } -/// Convert to/from JSON using human-readable serde (Identifier=base58, Bytes=base64). +/// Convert to/from JSON using **human-readable** serde (`Identifier` = base58, +/// binary = base64). /// /// This trait produces clean `serde_json::Value` with native number types. -/// Any JS-boundary concerns (large number stringification) are handled by the WASM layer. +/// Any JS-boundary concerns (large number stringification) are handled by the +/// WASM layer. +/// +/// # ⚠️ HR / non-HR divergence (Critical-1) +/// +/// `JsonConvertible` calls `serde_json::to_value`, which uses a serializer +/// that reports `is_human_readable() == true`. The mirror trait +/// [`ValueConvertible`] uses `platform_value::to_value`, which reports +/// `false`. Types whose `Serialize` impl branches on `is_human_readable()` +/// produce **structurally different output** between the two paths: +/// +/// | Type | `to_json()` (HR) | `to_object()` (non-HR) | +/// |---|---|---| +/// | [`platform_value::Identifier`] | `"5bV6jUfh..."` (bs58 string) | `Value::Identifier([u8; 32])` | +/// | [`platform_value::BinaryData`] | `"sg=="` (base64 string) | `Value::Bytes(Vec)` | +/// | `Bytes20` / `Bytes32` / `Bytes36` | base64 string | `Value::Bytes32([u8; N])` etc. | +/// | `CoreScript` | `"dqkU..."` (base64 string) | `Value::Bytes(Vec)` | +/// +/// **Do not assume** `self.to_object()?.try_into_json()` ≡ `self.to_json()`. +/// They render the same field as a string in one and a byte array in the +/// other. Round-trip tests should exercise each path independently. +/// +/// # ⚠️ `ContentDeserializer` caveat +/// +/// Manual `Deserialize` impls that branch on `deserializer.is_human_readable()` +/// must also handle `serde::__private::de::ContentDeserializer`, used +/// internally by `#[serde(tag = "...")]` enums. ContentDeserializer **always +/// reports `is_human_readable: true`** regardless of the original source — so +/// a non-HR `platform_value::Value` flowing into a tagged enum gets shape- +/// inferred as if it were HR. Recipe: write a dual-shape visitor accepting +/// both shapes in the HR branch via `deserialize_any`. See +/// [`platform_value::Bytes32::deserialize`] for the canonical example, and +/// `rs-dpp/src/serialization/serde_bytes.rs` for `[u8; N]` / `Vec`. #[cfg(feature = "json-conversion")] pub trait JsonConvertible: Serialize + DeserializeOwned { fn to_json(&self) -> Result { diff --git a/packages/rs-dpp/src/shielded/mod.rs b/packages/rs-dpp/src/shielded/mod.rs index 5659371d54c..f01b04303f4 100644 --- a/packages/rs-dpp/src/shielded/mod.rs +++ b/packages/rs-dpp/src/shielded/mod.rs @@ -195,3 +195,90 @@ pub struct SerializedAction { /// signature from one transition cannot be reused in another. pub spend_auth_sig: [u8; 64], } + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for SerializedAction {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for SerializedAction {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use serde_json::json; + + fn fixture() -> SerializedAction { + SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + // Encrypted note is variable-length (216 bytes per the field doc); a + // shorter payload still exercises the `serde_bytes_var` path. + encrypted_note: vec![0x44, 0x55, 0x66, 0x77], + cv_net: [0x88; 32], + spend_auth_sig: [0x99; 64], + } + } + + // `SerializedAction` is a struct with `serde(rename_all = "camelCase")`. + // `#[json_safe_fields]` auto-injects `#[serde(with = ...)]` on the byte + // fields: `[u8; N]` → `serde_bytes` (const-generic), `Vec` → + // `serde_bytes_var`. The wire shape is base64 strings in JSON HR and + // raw bytes in non-HR. + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + use base64::{engine::general_purpose::STANDARD, Engine}; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Each byte field is base64-encoded in HR. + assert_eq!( + json, + json!({ + "nullifier": STANDARD.encode([0x11; 32]), + "rk": STANDARD.encode([0x22; 32]), + "cmx": STANDARD.encode([0x33; 32]), + "encryptedNote": STANDARD.encode([0x44, 0x55, 0x66, 0x77]), + "cvNet": STANDARD.encode([0x88; 32]), + "spendAuthSig": STANDARD.encode([0x99; 64]), + }) + ); + let recovered = SerializedAction::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + use platform_value::Value; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `[u8; 32]` → `Value::Bytes32`, `[u8; 64]` and `Vec` (via + // `serde_bytes_var`) → `Value::Bytes(Vec)`. + assert_eq!( + value, + Value::Map(vec![ + (Value::Text("nullifier".into()), Value::Bytes32([0x11; 32])), + (Value::Text("rk".into()), Value::Bytes32([0x22; 32])), + (Value::Text("cmx".into()), Value::Bytes32([0x33; 32])), + ( + Value::Text("encryptedNote".into()), + Value::Bytes(vec![0x44, 0x55, 0x66, 0x77]), + ), + (Value::Text("cvNet".into()), Value::Bytes32([0x88; 32])), + ( + Value::Text("spendAuthSig".into()), + Value::Bytes(vec![0x99; 64]), + ), + ]) + ); + let recovered = SerializedAction::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/abstract_state_transition.rs b/packages/rs-dpp/src/state_transition/abstract_state_transition.rs deleted file mode 100644 index 92771963d7e..00000000000 --- a/packages/rs-dpp/src/state_transition/abstract_state_transition.rs +++ /dev/null @@ -1,51 +0,0 @@ -use serde::Serialize; -#[cfg(feature = "json-conversion")] -use serde_json::Value as JsonValue; - -pub mod state_transition_helpers { - use super::*; - use crate::ProtocolError; - use platform_value::Value; - #[cfg(feature = "json-conversion")] - use std::convert::TryInto; - - #[cfg(feature = "json-conversion")] - pub fn to_json<'a, I: IntoIterator>( - serializable: impl Serialize, - skip_signature_paths: I, - ) -> Result { - to_object(serializable, skip_signature_paths) - .and_then(|v| v.try_into().map_err(ProtocolError::ValueError)) - } - - pub fn to_object<'a, I: IntoIterator>( - serializable: impl Serialize, - skip_signature_paths: I, - ) -> Result { - let mut value: Value = platform_value::to_value(serializable)?; - skip_signature_paths.into_iter().try_for_each(|path| { - value - .remove_values_matching_path(path) - .map_err(ProtocolError::ValueError) - .map(|_| ()) - })?; - Ok(value) - } - - pub fn to_cleaned_object<'a, I: IntoIterator>( - serializable: impl Serialize, - skip_signature_paths: I, - ) -> Result { - let mut value: Value = platform_value::to_value(serializable)?; - - value = value.clean_recursive()?; - - skip_signature_paths.into_iter().try_for_each(|path| { - value - .remove_values_matching_path(path) - .map_err(ProtocolError::ValueError) - .map(|_| ()) - })?; - Ok(value) - } -} diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index 92bf4a4e583..ce96275de3b 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -5,8 +5,6 @@ use state_transitions::document::batch_transition::batched_transition::document_ use std::collections::BTreeMap; use std::ops::RangeInclusive; -pub use abstract_state_transition::state_transition_helpers; - use platform_value::{BinaryData, Identifier}; pub use state_transition_types::*; @@ -21,7 +19,6 @@ use dashcore::signer::double_sha; use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize, PlatformSignable}; use platform_version::version::{PlatformVersion, ProtocolVersion, ALL_VERSIONS, LATEST_VERSION}; -mod abstract_state_transition; #[cfg(any( feature = "state-transition-signing", feature = "state-transition-validation" @@ -436,10 +433,29 @@ macro_rules! call_errorable_method_identity_signed { From, PartialEq, )] +// `tag = "$type"` matches the system-field convention: every serde-injected +// discriminator key in this crate carries a `$` prefix so it never collides +// with user-data field names. Discriminates between **semantically +// different variants** of the same kind (rather than **versions** of one +// logical type, which use `tag = "$formatVersion"`). +// +// `$type` here is at the OUTERMOST level — there's no flatten path that +// would put it next to a base's `document_type_name` (renamed to `$type` +// in the wire). Inner umbrellas (`DocumentTransition`, `TokenTransition`) +// use `$action` instead because they DO flatten the document base. +// +// Was previously `serde(untagged)`, which made deserialize ambiguous (each +// variant tried in order until one matched structurally). The new +// self-describing wire shape is `{"$type": "dataContractCreate", ...inner +// fields...}`. +// +// The binary wire path (`PlatformSerialize`) is unchanged — only JSON/Value +// consumers see the new shape, and there are no rs-drive / rs-drive-abci / +// rs-sdk callers that route the umbrella through to_json/to_object today. #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), - serde(untagged) + serde(tag = "$type", rename_all = "camelCase") )] #[platform_serialize(unversioned)] //versioned directly, no need to use platform_version #[platform_serialize(limit = 100000)] @@ -467,6 +483,294 @@ pub enum StateTransition { IdentityCreateFromShieldedPool(IdentityCreateFromShieldedPoolTransition), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for StateTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for StateTransition {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + + /// Round-trip a StateTransition through both JSON and Value, asserting: + /// 1. The wire emits `{"$type": "", ...}` (umbrella's + /// `tag = "$type", rename_all = "camelCase"` is correctly applied). + /// 2. Round-trip preserves the variant. + /// 3. Round-trip preserves structural equality (PartialEq on the inner). + /// + /// Inner field shapes are covered by each inner type's dedicated + /// `*_with_full_wire_shape` test — this helper only exercises the + /// umbrella's tag-dispatch boundary. The risk it catches: an inner + /// variant whose serde body conflicts with the umbrella's `"$type"` key, + /// or a serde rename that resolves to something other than the + /// expected camelCase form. + /// + /// `lossy_json_int_variants`: when true, the JSON-side equality assertion + /// runs after `normalize_integer_variants_for_json_round_trip` on both + /// sides. Required for variants that embed a `DataContract` — + /// `document_schemas` carry sized integer variants (`U32`/`I32`) that + /// JSON's single Number type cannot preserve. See commit 7397c73f31. + fn assert_umbrella_round_trip_inner( + original: StateTransition, + expected_type_tag: &str, + lossy_json_int_variants: bool, + ) { + use crate::serialization::{JsonConvertible, ValueConvertible}; + + // JSON + let json = original.to_json().expect("to_json"); + assert_eq!( + json["$type"], expected_type_tag, + "json type tag for {expected_type_tag}", + ); + let recovered = StateTransition::from_json(json).expect("from_json round-trip"); + assert_eq!( + std::mem::discriminant(&original), + std::mem::discriminant(&recovered), + "json round-trip variant for {expected_type_tag}", + ); + if lossy_json_int_variants { + use crate::tests::utils::normalize_integer_variants_for_json_round_trip; + let mut original_canon = original.to_object().expect("to_object"); + let mut recovered_canon = recovered.to_object().expect("to_object"); + normalize_integer_variants_for_json_round_trip(&mut original_canon); + normalize_integer_variants_for_json_round_trip(&mut recovered_canon); + assert_eq!( + original_canon, recovered_canon, + "json round-trip equality (modulo int-variant) for {expected_type_tag}", + ); + } else { + assert_eq!( + original, recovered, + "json round-trip equality for {expected_type_tag}" + ); + } + + // Value + let value = original.to_object().expect("to_object"); + let map = value.as_map().expect("Value::Map"); + let tag = map + .iter() + .find(|(k, _)| k.as_text() == Some("$type")) + .map(|(_, v)| v) + .unwrap_or_else(|| panic!("type tag missing for {expected_type_tag}")); + assert_eq!( + *tag, + platform_value::Value::Text(expected_type_tag.to_string()), + "value type tag for {expected_type_tag}", + ); + let recovered = StateTransition::from_object(value).expect("from_object round-trip"); + assert_eq!( + std::mem::discriminant(&original), + std::mem::discriminant(&recovered), + "value round-trip variant for {expected_type_tag}", + ); + assert_eq!( + original, recovered, + "value round-trip equality for {expected_type_tag}" + ); + } + + fn assert_umbrella_round_trip(original: StateTransition, expected_type_tag: &str) { + assert_umbrella_round_trip_inner(original, expected_type_tag, false); + } + + /// Variant of `assert_umbrella_round_trip` for transitions that embed a + /// `DataContract` (`DataContractCreate`, `DataContractUpdate`). JSON's + /// single Number type collapses sized-int variants in the embedded + /// `document_schemas` tree, so the JSON-side equality assertion is + /// run modulo integer-variant normalization. The Value path keeps its + /// strict bit-exact assertion (platform_value preserves sized ints). + fn assert_umbrella_round_trip_lossy_json_int_variants( + original: StateTransition, + expected_type_tag: &str, + ) { + assert_umbrella_round_trip_inner(original, expected_type_tag, true); + } + + // Per-variant umbrella round-trip tests. Inner fixtures are reused from + // each transition's own `json_convertible_tests::fixture()` (made + // `pub(crate)` for this purpose) — keeps the umbrella tests in sync + // with the inner-type tests automatically. + + #[test] + fn umbrella_data_contract_create() { + let inner = crate::state_transition::data_contract_create_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip_lossy_json_int_variants( + StateTransition::DataContractCreate(inner), + "dataContractCreate", + ); + } + + #[test] + fn umbrella_data_contract_update() { + let inner = crate::state_transition::data_contract_update_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip_lossy_json_int_variants( + StateTransition::DataContractUpdate(inner), + "dataContractUpdate", + ); + } + + #[test] + fn umbrella_batch() { + let inner = crate::state_transition::batch_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip(StateTransition::Batch(inner), "batch"); + } + + #[test] + fn umbrella_identity_create() { + let inner = + crate::state_transition::identity_create_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip(StateTransition::IdentityCreate(inner), "identityCreate"); + } + + #[test] + fn umbrella_identity_top_up() { + let inner = + crate::state_transition::identity_topup_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip(StateTransition::IdentityTopUp(inner), "identityTopUp"); + } + + #[test] + fn umbrella_identity_credit_withdrawal() { + let inner = crate::state_transition::identity_credit_withdrawal_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip( + StateTransition::IdentityCreditWithdrawal(inner), + "identityCreditWithdrawal", + ); + } + + #[test] + fn umbrella_identity_update() { + let inner = + crate::state_transition::identity_update_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip(StateTransition::IdentityUpdate(inner), "identityUpdate"); + } + + #[test] + fn umbrella_identity_credit_transfer() { + let inner = crate::state_transition::identity_credit_transfer_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip( + StateTransition::IdentityCreditTransfer(inner), + "identityCreditTransfer", + ); + } + + #[test] + fn umbrella_masternode_vote() { + let inner = + crate::state_transition::masternode_vote_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip(StateTransition::MasternodeVote(inner), "masternodeVote"); + } + + #[test] + fn umbrella_identity_credit_transfer_to_addresses() { + let inner = crate::state_transition::identity_credit_transfer_to_addresses_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip( + StateTransition::IdentityCreditTransferToAddresses(inner), + "identityCreditTransferToAddresses", + ); + } + + #[test] + fn umbrella_identity_create_from_addresses() { + let inner = crate::state_transition::identity_create_from_addresses_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip( + StateTransition::IdentityCreateFromAddresses(inner), + "identityCreateFromAddresses", + ); + } + + #[test] + fn umbrella_identity_top_up_from_addresses() { + let inner = crate::state_transition::identity_topup_from_addresses_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip( + StateTransition::IdentityTopUpFromAddresses(inner), + "identityTopUpFromAddresses", + ); + } + + #[test] + fn umbrella_address_funds_transfer() { + let inner = crate::state_transition::address_funds_transfer_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip( + StateTransition::AddressFundsTransfer(inner), + "addressFundsTransfer", + ); + } + + #[test] + fn umbrella_address_funding_from_asset_lock() { + let inner = crate::state_transition::address_funding_from_asset_lock_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip( + StateTransition::AddressFundingFromAssetLock(inner), + "addressFundingFromAssetLock", + ); + } + + #[test] + fn umbrella_address_credit_withdrawal() { + let inner = crate::state_transition::address_credit_withdrawal_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip( + StateTransition::AddressCreditWithdrawal(inner), + "addressCreditWithdrawal", + ); + } + + #[test] + fn umbrella_shield() { + let inner = crate::state_transition::shield_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip(StateTransition::Shield(inner), "shield"); + } + + #[test] + fn umbrella_shielded_transfer() { + let inner = + crate::state_transition::shielded_transfer_transition::json_convertible_tests::fixture( + ); + assert_umbrella_round_trip(StateTransition::ShieldedTransfer(inner), "shieldedTransfer"); + } + + #[test] + fn umbrella_unshield() { + let inner = crate::state_transition::unshield_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip(StateTransition::Unshield(inner), "unshield"); + } + + #[test] + fn umbrella_shield_from_asset_lock() { + let inner = crate::state_transition::shield_from_asset_lock_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip( + StateTransition::ShieldFromAssetLock(inner), + "shieldFromAssetLock", + ); + } + + #[test] + fn umbrella_shielded_withdrawal() { + let inner = crate::state_transition::shielded_withdrawal_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip( + StateTransition::ShieldedWithdrawal(inner), + "shieldedWithdrawal", + ); + } + + #[test] + fn umbrella_identity_create_from_shielded_pool() { + let inner = crate::state_transition::identity_create_from_shielded_pool_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip( + StateTransition::IdentityCreateFromShieldedPool(inner), + "identityCreateFromShieldedPool", + ); + } +} + impl OptionallyAssetLockProved for StateTransition { fn optional_asset_lock_proof(&self) -> Option<&AssetLockProof> { match self { diff --git a/packages/rs-dpp/src/state_transition/proof_result.rs b/packages/rs-dpp/src/state_transition/proof_result.rs index 927f7be95d6..560cef43a49 100644 --- a/packages/rs-dpp/src/state_transition/proof_result.rs +++ b/packages/rs-dpp/src/state_transition/proof_result.rs @@ -15,7 +15,7 @@ use crate::voting::votes::Vote; use platform_value::Identifier; use std::collections::BTreeMap; -#[derive(Debug, strum::Display, derive_more::TryInto)] +#[derive(Debug, PartialEq, strum::Display, derive_more::TryInto)] #[cfg_attr( feature = "serde-conversion", derive(serde::Serialize, serde::Deserialize) @@ -24,17 +24,45 @@ pub enum StateTransitionProofResult { VerifiedDataContract(DataContract), VerifiedIdentity(Identity), VerifiedTokenBalanceAbsence(Identifier), - VerifiedTokenBalance(Identifier, TokenAmount), + // `TokenAmount`/`Credits` (u64) live in tuple variants / nested containers + // that `#[json_safe_fields]` can't reach; apply the JS-safe helpers directly + // so values above `MAX_SAFE_INTEGER` serialize as strings in human-readable + // JSON. (`AddressNonce`/`GroupSumPower` are `u32` → already JS-safe; the + // `Vec` nullifiers are arrays of bytes < 256 → no precision concern.) + VerifiedTokenBalance( + Identifier, + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::serialization::json_safe_u64") + )] + TokenAmount, + ), VerifiedTokenIdentityInfo(Identifier, IdentityTokenInfo), VerifiedTokenPricingSchedule(Identifier, Option), VerifiedTokenStatus(TokenStatus), - VerifiedTokenIdentitiesBalances(BTreeMap), + VerifiedTokenIdentitiesBalances( + #[cfg_attr( + feature = "json-conversion", + serde( + with = "crate::serialization::json::safe_integer_map::json_safe_identifier_u64_map" + ) + )] + BTreeMap, + ), VerifiedPartialIdentity(PartialIdentity), VerifiedBalanceTransfer(PartialIdentity, PartialIdentity), //from/to VerifiedDocuments(BTreeMap>), VerifiedTokenActionWithDocument(Document), VerifiedTokenGroupActionWithDocument(GroupSumPower, Option), - VerifiedTokenGroupActionWithTokenBalance(GroupSumPower, GroupActionStatus, Option), + VerifiedTokenGroupActionWithTokenBalance( + GroupSumPower, + GroupActionStatus, + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::serialization::json_safe_option_u64") + )] + Option, + ), VerifiedTokenGroupActionWithTokenIdentityInfo( GroupSumPower, GroupActionStatus, @@ -47,19 +75,37 @@ pub enum StateTransitionProofResult { ), VerifiedMasternodeVote(Vote), VerifiedNextDistribution(Vote), - VerifiedAddressInfos(BTreeMap>), + VerifiedAddressInfos( + #[cfg_attr( + feature = "json-conversion", + serde(with = "json_safe_address_info_map") + )] + BTreeMap>, + ), VerifiedIdentityFullWithAddressInfos( Identity, + #[cfg_attr( + feature = "json-conversion", + serde(with = "json_safe_address_info_map") + )] BTreeMap>, ), VerifiedIdentityWithAddressInfos( PartialIdentity, + #[cfg_attr( + feature = "json-conversion", + serde(with = "json_safe_address_info_map") + )] BTreeMap>, ), VerifiedAssetLockConsumed(StoredAssetLockInfo), VerifiedShieldedNullifiers(Vec<(Vec, bool)>), VerifiedShieldedNullifiersWithAddressInfos( Vec<(Vec, bool)>, + #[cfg_attr( + feature = "json-conversion", + serde(with = "json_safe_address_info_map") + )] BTreeMap>, ), VerifiedShieldedNullifiersWithWithdrawalDocument( @@ -83,3 +129,174 @@ pub enum StateTransitionProofResult { /// that the identity was created and that the funding nullifiers were consumed. VerifiedIdentityWithShieldedNullifiers(Identity, Vec<(Vec, bool)>), } + +/// Serde `with` module for `BTreeMap>`. +/// +/// `AddressNonce` is `u32` (JS-safe); `Credits` is `u64` and must serialize as a +/// string in human-readable JSON above `MAX_SAFE_INTEGER`. A small wrapper tuple +/// carries `#[serde(with = "json_safe_u64")]` on the credits, preserving the +/// `[nonce, credits]` array wire-shape while making the value JS-safe. Binary / +/// `Value` paths stay native (the helper checks `is_human_readable`). +#[cfg(feature = "json-conversion")] +mod json_safe_address_info_map { + use super::{AddressNonce, Credits, PlatformAddress}; + use serde::de::Deserializer; + use serde::ser::{SerializeMap, Serializer}; + use serde::{Deserialize, Serialize}; + use std::collections::BTreeMap; + + /// The address-info map shape shared by several `StateTransitionProofResult` + /// variants. Aliased to keep the helper signatures below under clippy's + /// `type_complexity` threshold. + type AddressInfoMap = BTreeMap>; + + #[derive(Serialize, Deserialize)] + struct Entry( + AddressNonce, + #[serde(with = "crate::serialization::json_safe_u64")] Credits, + ); + + pub fn serialize( + map: &AddressInfoMap, + serializer: S, + ) -> Result { + let mut s = serializer.serialize_map(Some(map.len()))?; + for (k, v) in map { + let wrapped = v.map(|(nonce, credits)| Entry(nonce, credits)); + s.serialize_entry(k, &wrapped)?; + } + s.end() + } + + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result { + let raw: BTreeMap> = BTreeMap::deserialize(deserializer)?; + Ok(raw + .into_iter() + .map(|(k, v)| (k, v.map(|Entry(nonce, credits)| (nonce, credits)))) + .collect()) + } +} + +// --- canonical conversion trait impls (unification pass 1) --- +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for StateTransitionProofResult {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for StateTransitionProofResult {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::{Identifier, Value}; + use serde_json::json; + + /// Non-default variant `VerifiedTokenBalance(id, amount)` with both + /// tuple fields set so the wire-shape assertion catches silent variant + /// flip / inner-zero on round-trip. + fn fixture() -> StateTransitionProofResult { + StateTransitionProofResult::VerifiedTokenBalance( + Identifier::new([0xab; 32]), + 123_456_789u64, + ) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `StateTransitionProofResult` uses serde external tagging (default, + // no `#[serde(tag = ...)]`). Tuple variants serialize as + // `{ "VariantName": [field0, field1, ...] }`. `Identifier` -> base58 + // string in JSON; `TokenAmount` is `u64` and JSON erases the size — + // see the value-path assertion which uses `123_456_789u64`. + assert_eq!( + json, + json!({ + "VerifiedTokenBalance": [ + "CZ8YUVdk7znjrUmnb5n7kgySk9yRAsQDYmyCxzfSky9t", + 123_456_789u64, + ], + }) + ); + let recovered = StateTransitionProofResult::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // platform_value preserves typed `Identifier` and `U64` variants. We + // construct the expected `Value::Map` by hand: `platform_value!{...}` + // would convert the `Identifier` interpolation through Serialize + // (correct) but the outer shape has only one (Text-keyed) entry whose + // value is an Array of mixed-typed Values, so it's clearer to write + // the literal Map. + let expected = Value::Map(vec![( + Value::Text("VerifiedTokenBalance".to_string()), + Value::Array(vec![Value::Identifier([0xab; 32]), Value::U64(123_456_789)]), + )]); + assert_eq!(value, expected); + let recovered = StateTransitionProofResult::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn verified_token_balance_large_amount_serializes_as_string() { + use crate::serialization::JsonConvertible; + // `TokenAmount` above `Number.MAX_SAFE_INTEGER` must serialize as a JSON + // string (it sits in a tuple variant the macro can't reach). + let original = StateTransitionProofResult::VerifiedTokenBalance( + Identifier::new([0xab; 32]), + 9_007_199_254_740_993, // 2^53 + 1 + ); + let json = original.to_json().expect("to_json"); + assert_eq!(json["VerifiedTokenBalance"][1], json!("9007199254740993")); + let recovered = StateTransitionProofResult::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn verified_address_infos_large_credits_serialize_as_string() { + use crate::serialization::{JsonConvertible, ValueConvertible}; + use std::collections::BTreeMap; + // `Credits` (u64) nested in `BTreeMap>` must be JS-safe via the bespoke `json_safe_address_info_map` + // helper, preserving the `[nonce, credits]` array wire-shape. + let big_credits: Credits = 9_007_199_254_740_993; // 2^53 + 1 + let mut infos: BTreeMap> = BTreeMap::new(); + infos.insert(PlatformAddress::P2pkh([0x11; 20]), Some((7, big_credits))); + infos.insert(PlatformAddress::P2sh([0x22; 20]), None); + let original = StateTransitionProofResult::VerifiedAddressInfos(infos); + + // Human-readable JSON: credits string, nonce number, null preserved. + let json = original.to_json().expect("to_json"); + let map = json["VerifiedAddressInfos"].as_object().expect("object"); + let non_null = map + .values() + .find(|v| !v.is_null()) + .expect("one populated entry"); + assert_eq!(non_null[0], json!(7)); + assert_eq!(non_null[1], json!("9007199254740993")); + assert!( + map.values().any(|v| v.is_null()), + "the None entry must survive" + ); + let recovered = StateTransitionProofResult::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + + // Non-human-readable (platform_value): native u64, round-trips intact. + let value = original.to_object().expect("to_object"); + let recovered = StateTransitionProofResult::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/json_conversion.rs deleted file mode 100644 index e0e2c5ae282..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/json_conversion.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::state_transition::address_credit_withdrawal_transition::AddressCreditWithdrawalTransition; -use crate::state_transition::state_transitions::address_credit_withdrawal_transition::fields::*; -use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, -}; -use crate::ProtocolError; -use serde_json::Number; -use serde_json::Value as JsonValue; - -impl StateTransitionJsonConvert<'_> for AddressCreditWithdrawalTransition { - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - match self { - AddressCreditWithdrawalTransition::V0(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(0)), - ); - Ok(value) - } - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/mod.rs index ef78bbeea44..dea5bac2b39 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/mod.rs @@ -1,17 +1,17 @@ +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; use crate::state_transition::address_credit_withdrawal_transition::v0::AddressCreditWithdrawalTransitionV0; pub mod accessors; pub mod fields; -#[cfg(feature = "json-conversion")] -mod json_conversion; pub mod methods; mod state_transition_estimated_fee_validation; mod state_transition_fee_strategy; mod state_transition_like; mod state_transition_validation; pub mod v0; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; use crate::state_transition::address_credit_withdrawal_transition::v0::AddressCreditWithdrawalTransitionV0Signable; @@ -94,6 +94,12 @@ impl AddressCreditWithdrawalTransition { } } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl JsonConvertible for AddressCreditWithdrawalTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl ValueConvertible for AddressCreditWithdrawalTransition {} + impl StateTransitionFieldTypes for AddressCreditWithdrawalTransition { fn signature_property_paths() -> Vec<&'static str> { vec![] @@ -107,3 +113,137 @@ impl StateTransitionFieldTypes for AddressCreditWithdrawalTransition { vec![OUTPUT_SCRIPT] } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::address_funds::{AddressFundsFeeStrategyStep, AddressWitness, PlatformAddress}; + use crate::identity::core_script::CoreScript; + use crate::state_transition::address_credit_withdrawal_transition::v0::AddressCreditWithdrawalTransitionV0; + use crate::withdrawal::Pooling; + use platform_value::{platform_value, BinaryData, Value}; + use serde_json::json; + use std::collections::BTreeMap; + + pub(crate) fn fixture() -> AddressCreditWithdrawalTransition { + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([0x01; 20]), (5u32, 900_000u64)); + + let v0 = AddressCreditWithdrawalTransitionV0 { + inputs, + output: Some((PlatformAddress::P2sh([0x02; 20]), 100_000u64)), + fee_strategy: vec![AddressFundsFeeStrategyStep::DeductFromInput(0)], + core_fee_per_byte: 21, + pooling: Pooling::IfAvailable, + output_script: CoreScript::from_bytes(vec![0xaa, 0xbb, 0xcc]), + user_fee_increase: 19, + input_witnesses: vec![AddressWitness::P2pkh { + signature: BinaryData::new(vec![0xef; 65]), + }], + }; + AddressCreditWithdrawalTransition::V0(v0) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int fields lose their size on the JSON wire (single number type): + // - `inputs[].nonce` is u32, `inputs[].amount` / `output.amount` are u64, + // - `feeStrategy[].index` is u16, + // - `coreFeePerByte` is u32, `userFeeIncrease` is u16. + // The Value-path assertion below locks the typed variants. + // `pooling` is encoded as the camelCase string `"ifAvailable"` in JSON + // (HR path of the custom `pooling_serde`), but as `Value::U8(1)` in + // non-HR. `outputScript` (CoreScript) is base64 in JSON, raw bytes in + // Value. `BinaryData` (witness signature, address bytes) is base64 in + // JSON, `Value::Bytes` in Value. `PlatformAddress` is hex string in + // JSON, raw bytes in Value. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "inputs": [ + { + "address": "000101010101010101010101010101010101010101", + "nonce": 5, + "amount": 900_000, + }, + ], + "output": { + "address": "010202020202020202020202020202020202020202", + "amount": 100_000, + }, + "feeStrategy": [ + {"$type": "deductFromInput", "index": 0}, + ], + "coreFeePerByte": 21, + "pooling": "ifAvailable", + "outputScript": "qrvM", + "userFeeIncrease": 19, + "inputWitnesses": [ + { + "$type": "p2pkh", + "signature": "7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+8=", + }, + ], + }) + ); + let recovered = AddressCreditWithdrawalTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // PlatformAddress emits raw bytes (21-byte: 1 type byte + 20 hash) in + // non-HR. Pooling::IfAvailable is `Value::U8(1)`. CoreScript and + // BinaryData both serialize as `Value::Bytes`. Sized integers stay + // sized: nonces are U32, credit amounts U64, fee_strategy index U16, + // core_fee_per_byte U32, user_fee_increase U16, pooling U8. + let mut input_addr_bytes = vec![0x00u8]; + input_addr_bytes.extend_from_slice(&[0x01u8; 20]); + let mut output_addr_bytes = vec![0x01u8]; + output_addr_bytes.extend_from_slice(&[0x02u8; 20]); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "inputs": [ + { + "address": Value::Bytes(input_addr_bytes), + "nonce": 5u32, + "amount": 900_000u64, + }, + ], + "output": { + "address": Value::Bytes(output_addr_bytes), + "amount": 100_000u64, + }, + "feeStrategy": [ + {"$type": "deductFromInput", "index": 0u16}, + ], + "coreFeePerByte": 21u32, + "pooling": 1u8, + "outputScript": Value::Bytes(vec![0xaa, 0xbb, 0xcc]), + "userFeeIncrease": 19u16, + "inputWitnesses": [ + { + "$type": "p2pkh", + "signature": Value::Bytes(vec![0xef; 65]), + }, + ], + }) + ); + let recovered = AddressCreditWithdrawalTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/json_conversion.rs deleted file mode 100644 index bbe35325b76..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::address_credit_withdrawal_transition::v0::AddressCreditWithdrawalTransitionV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for AddressCreditWithdrawalTransitionV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/mod.rs index f5a7f789d41..f3e3be8a864 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/mod.rs @@ -1,11 +1,7 @@ -#[cfg(feature = "json-conversion")] -mod json_conversion; mod state_transition_like; mod state_transition_validation; mod types; pub(super) mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; use bincode::{Decode, Encode}; @@ -17,8 +13,11 @@ use std::collections::BTreeMap; use crate::address_funds::{AddressFundsFeeStrategy, AddressWitness, PlatformAddress}; use crate::fee::Credits; use crate::prelude::{AddressNonce, UserFeeIncrease}; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use crate::{identity::core_script::CoreScript, withdrawal::Pooling, ProtocolError}; +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, Encode, Decode, PlatformSignable, PartialEq)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/value_conversion.rs deleted file mode 100644 index cca37ea0377..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/value_conversion.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::{IntegerReplacementType, ReplacementType, Value}; - -use crate::{state_transition::StateTransitionFieldTypes, ProtocolError}; - -use crate::state_transition::address_credit_withdrawal_transition::fields::*; -use crate::state_transition::address_credit_withdrawal_transition::v0::AddressCreditWithdrawalTransitionV0; -use crate::state_transition::StateTransitionValueConvert; - -use platform_version::version::PlatformVersion; - -impl StateTransitionValueConvert<'_> for AddressCreditWithdrawalTransitionV0 { - fn from_object( - raw_object: Value, - _platform_version: &PlatformVersion, - ) -> Result { - platform_value::from_value(raw_object).map_err(ProtocolError::ValueError) - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; - value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; - value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; - Ok(()) - } - - fn from_value_map( - raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let value: Value = raw_value_map.into(); - Self::from_object(value, platform_version) - } - - fn to_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - self.to_cleaned_object(skip_signature) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/value_conversion.rs deleted file mode 100644 index 7f7117aa581..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/value_conversion.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::Value; - -use crate::ProtocolError; - -use crate::state_transition::address_credit_withdrawal_transition::v0::AddressCreditWithdrawalTransitionV0; -use crate::state_transition::address_credit_withdrawal_transition::AddressCreditWithdrawalTransition; -use crate::state_transition::state_transitions::address_credit_withdrawal_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_version::version::{FeatureVersion, PlatformVersion}; - -impl StateTransitionValueConvert<'_> for AddressCreditWithdrawalTransition { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - AddressCreditWithdrawalTransition::V0(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - AddressCreditWithdrawalTransition::V0(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - AddressCreditWithdrawalTransition::V0(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - AddressCreditWithdrawalTransition::V0(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .address_credit_withdrawal_state_transition - .default_current_version - }); - - match version { - 0 => Ok(AddressCreditWithdrawalTransitionV0::from_object( - raw_object, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown AddressCreditWithdrawalTransition version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .address_credit_withdrawal_state_transition - .default_current_version - }); - - match version { - 0 => Ok(AddressCreditWithdrawalTransitionV0::from_value_map( - raw_value_map, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown AddressCreditWithdrawalTransition version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => AddressCreditWithdrawalTransitionV0::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown AddressCreditWithdrawalTransition version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/json_conversion.rs deleted file mode 100644 index d7489bd29f3..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/json_conversion.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::state_transition::address_funding_from_asset_lock_transition::AddressFundingFromAssetLockTransition; -use crate::state_transition::state_transitions::address_funding_from_asset_lock_transition::fields::*; -use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, -}; -use crate::ProtocolError; -use serde_json::Number; -use serde_json::Value as JsonValue; - -impl StateTransitionJsonConvert<'_> for AddressFundingFromAssetLockTransition { - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - match self { - AddressFundingFromAssetLockTransition::V0(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(0)), - ); - Ok(value) - } - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/mod.rs index 6da2475c99e..14554230ae1 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/mod.rs @@ -1,7 +1,5 @@ pub mod accessors; mod fields; -#[cfg(feature = "json-conversion")] -mod json_conversion; pub mod methods; mod proved; #[cfg(all(test, feature = "state-transition-signing"))] @@ -11,10 +9,10 @@ mod state_transition_fee_strategy; mod state_transition_like; mod state_transition_validation; pub mod v0; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; #[cfg(feature = "value-conversion")] use crate::serialization::ValueConvertible; use crate::state_transition::address_funding_from_asset_lock_transition::v0::AddressFundingFromAssetLockTransitionV0; @@ -80,6 +78,9 @@ impl AddressFundingFromAssetLockTransition { } } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl JsonConvertible for AddressFundingFromAssetLockTransition {} + impl StateTransitionFieldTypes for AddressFundingFromAssetLockTransition { fn signature_property_paths() -> Vec<&'static str> { vec![SIGNATURE] @@ -93,3 +94,171 @@ impl StateTransitionFieldTypes for AddressFundingFromAssetLockTransition { vec![] } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::address_funds::{AddressFundsFeeStrategyStep, AddressWitness, PlatformAddress}; + use crate::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; + use crate::identity::state_transition::asset_lock_proof::AssetLockProof; + use crate::state_transition::address_funding_from_asset_lock_transition::v0::AddressFundingFromAssetLockTransitionV0; + use dashcore::OutPoint; + use platform_value::{platform_value, BinaryData, Value}; + use serde_json::json; + use std::collections::BTreeMap; + use std::str::FromStr; + + pub(crate) fn fixture() -> AddressFundingFromAssetLockTransition { + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([0xa1; 20]), (4u32, 600_000u64)); + + let mut outputs = BTreeMap::new(); + outputs.insert(PlatformAddress::P2pkh([0xb2; 20]), Some(400_000u64)); + outputs.insert(PlatformAddress::P2sh([0xc3; 20]), None); // remainder + + let asset_lock_proof = AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height: 12345, + out_point: OutPoint::from_str( + "0000000000000000000000000000000000000000000000000000000000000001:1", + ) + .expect("outpoint"), + }); + + let v0 = AddressFundingFromAssetLockTransitionV0 { + asset_lock_proof, + inputs, + outputs, + fee_strategy: vec![AddressFundsFeeStrategyStep::DeductFromInput(0)], + user_fee_increase: 11, + signature: BinaryData::new(vec![0xd4; 65]), + input_witnesses: vec![AddressWitness::P2pkh { + signature: BinaryData::new(vec![0xe5; 65]), + }], + }; + AddressFundingFromAssetLockTransition::V0(v0) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `assetLockProof` is internally tagged `{type: "chain", ...}` with + // `outPoint` rendered as the human-readable `txid:vout` string in JSON + // (Value-path uses the structured `{txid: Bytes32, vout: U32}` form). + // Sized-int fields (heights, nonces, indexes, fee numbers) lose their + // size on the JSON wire — Value path locks the typed variants. + // BinaryData / PlatformAddress / outputs[].amount notes follow the + // pattern in the credit-withdrawal test next door. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "assetLockProof": { + "$type": "chain", + "coreChainLockedHeight": 12345, + "outPoint": "0000000000000000000000000000000000000000000000000000000000000001:1", + }, + "inputs": [ + { + "address": "00a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "nonce": 4, + "amount": 600_000, + }, + ], + "outputs": [ + { + "address": "00b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "amount": 400_000, + }, + { + "address": "01c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "amount": null, + }, + ], + "feeStrategy": [ + {"$type": "deductFromInput", "index": 0}, + ], + "userFeeIncrease": 11, + "signature": "1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NQ=", + "inputWitnesses": [ + { + "$type": "p2pkh", + "signature": "5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eU=", + }, + ], + }) + ); + let recovered = AddressFundingFromAssetLockTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `outPoint` is the structured `{txid: Bytes32, vout: U32}` form here + // (the JSON path collapses it to `"txid:vout"`). PlatformAddress + // serializes to `Value::Bytes` (21 bytes: 1 type byte + 20 hash) in + // non-HR; BinaryData also serializes to `Value::Bytes`. Sized integers + // stay sized: `coreChainLockedHeight`, `nonce`, `coreFeePerByte` are + // U32; credit amounts U64; `userFeeIncrease`/`feeStrategy.index` U16. + let mut input_addr = vec![0x00u8]; + input_addr.extend_from_slice(&[0xa1u8; 20]); + let mut output_pkh = vec![0x00u8]; + output_pkh.extend_from_slice(&[0xb2u8; 20]); + let mut output_sh = vec![0x01u8]; + output_sh.extend_from_slice(&[0xc3u8; 20]); + let mut txid_bytes = [0u8; 32]; + txid_bytes[0] = 1; + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "assetLockProof": { + "$type": "chain", + "coreChainLockedHeight": 12345u32, + "outPoint": { + "txid": Value::Bytes32(txid_bytes), + "vout": 1u32, + }, + }, + "inputs": [ + { + "address": Value::Bytes(input_addr), + "nonce": 4u32, + "amount": 600_000u64, + }, + ], + "outputs": [ + { + "address": Value::Bytes(output_pkh), + "amount": 400_000u64, + }, + { + "address": Value::Bytes(output_sh), + "amount": Value::Null, + }, + ], + "feeStrategy": [ + {"$type": "deductFromInput", "index": 0u16}, + ], + "userFeeIncrease": 11u16, + "signature": Value::Bytes(vec![0xd4; 65]), + "inputWitnesses": [ + { + "$type": "p2pkh", + "signature": Value::Bytes(vec![0xe5; 65]), + }, + ], + }) + ); + let recovered = + AddressFundingFromAssetLockTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/json_conversion.rs deleted file mode 100644 index 74119e612ab..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::address_funding_from_asset_lock_transition::v0::AddressFundingFromAssetLockTransitionV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for AddressFundingFromAssetLockTransitionV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/mod.rs index a251db91b44..07977f737af 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/mod.rs @@ -1,12 +1,8 @@ -#[cfg(feature = "json-conversion")] -mod json_conversion; mod proved; mod state_transition_like; mod state_transition_validation; mod types; pub(super) mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; use std::collections::BTreeMap; @@ -20,6 +16,8 @@ use crate::address_funds::{AddressFundsFeeStrategy, AddressWitness, PlatformAddr use crate::fee::Credits; use crate::identity::state_transition::asset_lock_proof::AssetLockProof; use crate::prelude::{AddressNonce, UserFeeIncrease}; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use platform_value::BinaryData; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; @@ -34,6 +32,7 @@ mod property_names { pub const TRANSITION_TYPE: &str = "type"; } +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, PartialEq, Encode, Decode, PlatformSignable)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/value_conversion.rs deleted file mode 100644 index 384de927a34..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/v0/value_conversion.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::{IntegerReplacementType, ReplacementType, Value}; - -use crate::{state_transition::StateTransitionFieldTypes, ProtocolError}; - -use crate::state_transition::address_funding_from_asset_lock_transition::fields::*; -use crate::state_transition::address_funding_from_asset_lock_transition::v0::AddressFundingFromAssetLockTransitionV0; -use crate::state_transition::StateTransitionValueConvert; - -use platform_version::version::PlatformVersion; - -impl StateTransitionValueConvert<'_> for AddressFundingFromAssetLockTransitionV0 { - fn from_object( - raw_object: Value, - _platform_version: &PlatformVersion, - ) -> Result { - platform_value::from_value(raw_object).map_err(ProtocolError::ValueError) - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; - value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; - value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; - Ok(()) - } - - fn from_value_map( - raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let value: Value = raw_value_map.into(); - Self::from_object(value, platform_version) - } - - fn to_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - self.to_cleaned_object(skip_signature) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/value_conversion.rs deleted file mode 100644 index f20e68a3dfd..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/value_conversion.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::Value; - -use crate::ProtocolError; - -use crate::state_transition::address_funding_from_asset_lock_transition::v0::AddressFundingFromAssetLockTransitionV0; -use crate::state_transition::address_funding_from_asset_lock_transition::AddressFundingFromAssetLockTransition; -use crate::state_transition::state_transitions::address_funding_from_asset_lock_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_version::version::{FeatureVersion, PlatformVersion}; - -impl StateTransitionValueConvert<'_> for AddressFundingFromAssetLockTransition { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - AddressFundingFromAssetLockTransition::V0(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - AddressFundingFromAssetLockTransition::V0(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - AddressFundingFromAssetLockTransition::V0(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - AddressFundingFromAssetLockTransition::V0(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .address_funding_from_asset_lock_state_transition - .default_current_version - }); - - match version { - 0 => Ok(AddressFundingFromAssetLockTransitionV0::from_object( - raw_object, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown AddressFundingFromAssetLockTransition version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .address_funding_from_asset_lock_state_transition - .default_current_version - }); - - match version { - 0 => Ok(AddressFundingFromAssetLockTransitionV0::from_value_map( - raw_value_map, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown AddressFundingFromAssetLockTransition version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => AddressFundingFromAssetLockTransitionV0::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown AddressFundingFromAssetLockTransition version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/json_conversion.rs deleted file mode 100644 index d9ef665aac1..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/json_conversion.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::state_transition::address_funds_transfer_transition::AddressFundsTransferTransition; -use crate::state_transition::state_transitions::address_funds_transfer_transition::fields::*; -use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, -}; -use crate::ProtocolError; -use serde_json::Number; -use serde_json::Value as JsonValue; - -impl StateTransitionJsonConvert<'_> for AddressFundsTransferTransition { - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - match self { - AddressFundsTransferTransition::V0(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(0)), - ); - Ok(value) - } - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/mod.rs index a1dcacf4bf3..ff14bdc6473 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/mod.rs @@ -1,7 +1,5 @@ pub mod accessors; pub mod fields; -#[cfg(feature = "json-conversion")] -mod json_conversion; pub mod methods; #[cfg(all(test, feature = "state-transition-signing"))] mod signing_tests; @@ -10,9 +8,9 @@ mod state_transition_fee_strategy; mod state_transition_like; mod state_transition_validation; pub mod v0; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; #[cfg(feature = "value-conversion")] use crate::serialization::ValueConvertible; use crate::state_transition::address_funds_transfer_transition::v0::AddressFundsTransferTransitionV0; @@ -78,6 +76,9 @@ impl AddressFundsTransferTransition { } } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl JsonConvertible for AddressFundsTransferTransition {} + impl OptionallyAssetLockProved for AddressFundsTransferTransition {} impl StateTransitionFieldTypes for AddressFundsTransferTransition { @@ -93,3 +94,122 @@ impl StateTransitionFieldTypes for AddressFundsTransferTransition { vec![] } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::address_funds::{AddressFundsFeeStrategyStep, AddressWitness, PlatformAddress}; + use crate::state_transition::address_funds_transfer_transition::v0::AddressFundsTransferTransitionV0; + use platform_value::{platform_value, BinaryData, Value}; + use serde_json::json; + use std::collections::BTreeMap; + + pub(crate) fn fixture() -> AddressFundsTransferTransition { + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([0xf1; 20]), (10u32, 800_000u64)); + + let mut outputs = BTreeMap::new(); + outputs.insert(PlatformAddress::P2sh([0xf2; 20]), 700_000u64); + + let v0 = AddressFundsTransferTransitionV0 { + inputs, + outputs, + fee_strategy: vec![AddressFundsFeeStrategyStep::ReduceOutput(0)], + user_fee_increase: 17, + input_witnesses: vec![AddressWitness::P2pkh { + signature: BinaryData::new(vec![0xa9; 65]), + }], + }; + AddressFundsTransferTransition::V0(v0) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Inputs/outputs go through helpers that emit `[{address, nonce, amount}]` + // and `[{address, amount}]` shapes. PlatformAddress is a hex string in + // JSON HR; BinaryData (witness signature) is base64. Sized integers + // (nonce u32, amount u64, fee_strategy index u16, user_fee_increase u16) + // are erased on the JSON wire — Value path locks the variants. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "inputs": [ + { + "address": "00f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1", + "nonce": 10, + "amount": 800_000, + }, + ], + "outputs": [ + { + "address": "01f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2", + "amount": 700_000, + }, + ], + "feeStrategy": [ + {"$type": "reduceOutput", "index": 0}, + ], + "userFeeIncrease": 17, + "inputWitnesses": [ + { + "$type": "p2pkh", + "signature": "qampqampqampqampqampqampqampqampqampqampqampqampqampqampqampqampqampqampqampqampqampqak=", + }, + ], + }) + ); + let recovered = AddressFundsTransferTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + let original = fixture(); + let value = original.to_object().expect("to_object"); + // PlatformAddress / BinaryData both serialize to `Value::Bytes` in + // non-HR. Sized ints stay sized. + let mut input_addr = vec![0x00u8]; + input_addr.extend_from_slice(&[0xf1u8; 20]); + let mut output_addr = vec![0x01u8]; + output_addr.extend_from_slice(&[0xf2u8; 20]); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "inputs": [ + { + "address": Value::Bytes(input_addr), + "nonce": 10u32, + "amount": 800_000u64, + }, + ], + "outputs": [ + { + "address": Value::Bytes(output_addr), + "amount": 700_000u64, + }, + ], + "feeStrategy": [ + {"$type": "reduceOutput", "index": 0u16}, + ], + "userFeeIncrease": 17u16, + "inputWitnesses": [ + { + "$type": "p2pkh", + "signature": Value::Bytes(vec![0xa9; 65]), + }, + ], + }) + ); + let recovered = AddressFundsTransferTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/json_conversion.rs deleted file mode 100644 index 8a2767a4460..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::address_funds_transfer_transition::v0::AddressFundsTransferTransitionV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for AddressFundsTransferTransitionV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/mod.rs index bd469959203..061053823b3 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/mod.rs @@ -1,11 +1,7 @@ -#[cfg(feature = "json-conversion")] -mod json_conversion; mod state_transition_like; mod state_transition_validation; mod types; pub(super) mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; use std::collections::BTreeMap; @@ -13,12 +9,15 @@ use std::collections::BTreeMap; use crate::address_funds::{AddressFundsFeeStrategy, AddressWitness, PlatformAddress}; use crate::fee::Credits; use crate::prelude::{AddressNonce, UserFeeIncrease}; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use crate::ProtocolError; use bincode::{Decode, Encode}; use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize, PlatformSignable}; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive( Debug, Clone, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/value_conversion.rs deleted file mode 100644 index af958e0a1a9..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/v0/value_conversion.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::{IntegerReplacementType, ReplacementType, Value}; - -use crate::{state_transition::StateTransitionFieldTypes, ProtocolError}; - -use crate::state_transition::address_funds_transfer_transition::fields::*; -use crate::state_transition::address_funds_transfer_transition::v0::AddressFundsTransferTransitionV0; -use crate::state_transition::StateTransitionValueConvert; - -use platform_version::version::PlatformVersion; - -impl StateTransitionValueConvert<'_> for AddressFundsTransferTransitionV0 { - fn from_object( - raw_object: Value, - _platform_version: &PlatformVersion, - ) -> Result { - platform_value::from_value(raw_object).map_err(ProtocolError::ValueError) - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; - value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; - value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; - Ok(()) - } - - fn from_value_map( - raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let value: Value = raw_value_map.into(); - Self::from_object(value, platform_version) - } - - fn to_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - // Override to_canonical_cleaned_object to manage add_public_keys individually - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - self.to_cleaned_object(skip_signature) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/value_conversion.rs deleted file mode 100644 index e2c814336a9..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funds_transfer_transition/value_conversion.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::Value; - -use crate::ProtocolError; - -use crate::state_transition::address_funds_transfer_transition::v0::AddressFundsTransferTransitionV0; -use crate::state_transition::address_funds_transfer_transition::AddressFundsTransferTransition; -use crate::state_transition::state_transitions::address_funds_transfer_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_version::version::{FeatureVersion, PlatformVersion}; - -impl StateTransitionValueConvert<'_> for AddressFundsTransferTransition { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - AddressFundsTransferTransition::V0(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - AddressFundsTransferTransition::V0(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - AddressFundsTransferTransition::V0(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - AddressFundsTransferTransition::V0(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok( - AddressFundsTransferTransitionV0::from_object(raw_object, platform_version)?.into(), - ), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown UTXOTransferTransition version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(AddressFundsTransferTransitionV0::from_value_map( - raw_value_map, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown UTXOTransferTransition version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => AddressFundsTransferTransitionV0::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown UTXOTransferTransition version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/json_conversion.rs deleted file mode 100644 index b7b51af827d..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/json_conversion.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::state_transition::data_contract_create_transition::DataContractCreateTransition; -use crate::state_transition::state_transitions::data_contract_create_transition::fields::*; -use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, -}; -use crate::ProtocolError; -use serde_json::Number; -use serde_json::Value as JsonValue; - -impl StateTransitionJsonConvert<'_> for DataContractCreateTransition { - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - match self { - DataContractCreateTransition::V0(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(0)), - ); - Ok(value) - } - } - } -} - -#[cfg(test)] -mod test { - use crate::state_transition::state_transitions::data_contract_create_transition::fields::*; - use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, - }; - - use crate::prelude::IdentityNonce; - use dpp::util::json_value::JsonValueExt; - - #[test] - fn should_return_state_transition_in_json_format() { - let data = crate::state_transition::data_contract_create_transition::test::get_test_data(); - let mut json_object = data - .state_transition - .to_json(JsonStateTransitionSerializationOptions { - skip_signature: false, - into_validating_json: false, - }) - .expect("conversion to JSON shouldn't fail"); - - assert_eq!( - 0, - json_object - .get_u64(STATE_TRANSITION_PROTOCOL_VERSION) - .expect("the protocol version should be present") as u32 - ); - - assert_eq!( - 0, - json_object - .get_u64(SIGNATURE_PUBLIC_KEY_ID) - .expect("default public key id should be defined"), - ); - assert_eq!( - "", - json_object - .remove_into::(SIGNATURE) - .expect("default string value for signature should be present") - ); - - assert_eq!( - data.created_data_contract.identity_nonce(), - json_object - .remove_into::(IDENTITY_NONCE) - .expect("the identity_nonce should be present") - ) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/mod.rs index 544e97b4c09..e42cc3bde14 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/mod.rs @@ -1,14 +1,10 @@ pub mod accessors; mod fields; mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; pub mod methods; mod state_transition_estimated_fee_validation; mod state_transition_like; mod v0; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; #[cfg(feature = "json-conversion")] @@ -161,21 +157,16 @@ impl OptionallyAssetLockProved for DataContractCreateTransition {} #[cfg(test)] mod test { - use crate::data_contract::conversion::json::DataContractJsonConversionMethodsV0; use crate::data_contract::created_data_contract::CreatedDataContract; use super::*; use crate::data_contract::accessors::v0::DataContractV0Getters; - use crate::data_contract::conversion::value::v0::DataContractValueConversionMethodsV0; use crate::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; use crate::state_transition::traits::StateTransitionLike; - use crate::state_transition::{ - StateTransitionOwned, StateTransitionType, StateTransitionValueConvert, - }; + use crate::state_transition::{StateTransitionOwned, StateTransitionType}; use crate::tests::fixtures::get_data_contract_fixture; use crate::version::LATEST_PLATFORM_VERSION; - use platform_value::Value; pub(crate) struct TestData { pub(crate) state_transition: DataContractCreateTransition, @@ -185,25 +176,11 @@ mod test { pub(crate) fn get_test_data() -> TestData { let created_data_contract = get_data_contract_fixture(None, 0, 1); - let state_transition = - ::from_object( - Value::from([ - (STATE_TRANSITION_PROTOCOL_VERSION, Value::U16(0)), - ( - IDENTITY_NONCE, - Value::U64(created_data_contract.identity_nonce()), - ), - ( - DATA_CONTRACT, - created_data_contract - .data_contract() - .to_value(LATEST_PLATFORM_VERSION) - .unwrap(), - ), - ]), - LATEST_PLATFORM_VERSION, - ) - .expect("state transition should be created without errors"); + let state_transition = DataContractCreateTransition::try_from_platform_versioned( + created_data_contract.clone(), + LATEST_PLATFORM_VERSION, + ) + .expect("state transition should be created without errors"); TestData { created_data_contract, @@ -246,12 +223,8 @@ mod test { .expect("to get data contract"); assert_eq!( - data_contract - .to_json(LATEST_PLATFORM_VERSION) - .expect("conversion to object shouldn't fail"), - data.created_data_contract - .data_contract() - .to_json(LATEST_PLATFORM_VERSION) + serde_json::to_value(&data_contract).expect("conversion to object shouldn't fail"), + serde_json::to_value(data.created_data_contract.data_contract()) .expect("conversion to object shouldn't fail") ); } @@ -273,48 +246,11 @@ mod test { assert!(!data.state_transition.is_identity_state_transition()); } - #[test] - fn should_roundtrip_via_from_object() { - let data = get_test_data(); - - // Convert to object and back - let mut obj = StateTransitionValueConvert::to_object(&data.state_transition, false) - .expect("to_object should succeed"); - - // Add the protocol version field for from_object - obj.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0)) - .expect("insert should succeed"); - - let restored = ::from_object( - obj, - LATEST_PLATFORM_VERSION, - ) - .expect("from_object should succeed"); - - assert_eq!(data.state_transition, restored); - } - - #[test] - fn should_roundtrip_via_from_value_map() { - let data = get_test_data(); - - let obj = StateTransitionValueConvert::to_object(&data.state_transition, false) - .expect("to_object should succeed"); - - let mut map = obj - .into_btree_string_map() - .expect("should convert to btree map"); - map.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0)); - - let restored = - ::from_value_map( - map, - LATEST_PLATFORM_VERSION, - ) - .expect("from_value_map should succeed"); - - assert_eq!(data.state_transition, restored); - } + // Legacy `StateTransitionValueConvert` round-trip tests deleted in + // Phase D step 9. The canonical `JsonConvertible` / `ValueConvertible` + // round-trip is exercised on the outer enum derive — the legacy + // round-trip via `to_object(false)` + `from_object(value, pv)` was + // testing methods that no longer exist. #[test] fn should_validate_estimated_fee_with_sufficient_balance() { @@ -381,40 +317,9 @@ mod test { } } - #[test] - fn v0_should_roundtrip_via_from_object() { - let data = get_test_data(); - match &data.state_transition { - DataContractCreateTransition::V0(v0) => { - let obj = v0.to_object(false).expect("to_object should succeed"); - - let restored = - DataContractCreateTransitionV0::from_object(obj, LATEST_PLATFORM_VERSION) - .expect("from_object should succeed"); - - assert_eq!(*v0, restored); - } - } - } - - #[test] - fn v0_should_roundtrip_via_from_value_map() { - let data = get_test_data(); - match &data.state_transition { - DataContractCreateTransition::V0(v0) => { - let obj = v0.to_object(false).expect("to_object should succeed"); - let map = obj - .into_btree_string_map() - .expect("should convert to btree map"); - - let restored = - DataContractCreateTransitionV0::from_value_map(map, LATEST_PLATFORM_VERSION) - .expect("from_value_map should succeed"); - - assert_eq!(*v0, restored); - } - } - } + // V0 legacy round-trip tests deleted in Phase D step 9 — they were + // exercising deleted `StateTransitionValueConvert` methods. Outer-enum + // canonical round-trip in `json_convertible_tests` covers correctness. #[test] fn v0_should_create_from_created_data_contract() { @@ -430,3 +335,119 @@ mod test { assert_eq!(v0.user_fee_increase, 0); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::data_contract_create_transition::v0::DataContractCreateTransitionV0; + use crate::tests::fixtures::get_data_contract_fixture; + use platform_value::BinaryData; + use platform_version::version::PlatformVersion; + use platform_version::TryFromPlatformVersioned; + + pub(crate) fn fixture() -> DataContractCreateTransition { + let pv = PlatformVersion::latest(); + let created = get_data_contract_fixture(None, 0, pv.protocol_version); + let data_contract = created.data_contract().clone(); + let mut v0 = DataContractCreateTransitionV0::try_from_platform_versioned(data_contract, pv) + .expect("v0 from contract"); + v0.identity_nonce = 5; + v0.user_fee_increase = 3; + v0.signature_public_key_id = 1; + v0.signature = BinaryData::new(vec![0xab; 65]); + DataContractCreateTransition::V0(v0) + } + + fn assert_v0_fields(t: &DataContractCreateTransition) { + let DataContractCreateTransition::V0(rec) = t; + assert_eq!(rec.identity_nonce, 5, "identity_nonce"); + assert_eq!(rec.user_fee_increase, 3, "user_fee_increase"); + assert_eq!(rec.signature_public_key_id, 1, "signature_public_key_id"); + assert_eq!(rec.signature, BinaryData::new(vec![0xab; 65]), "signature"); + } + + #[test] + fn json_round_trip_with_per_property_assertions() { + // JSON has a single `Number` type, so sized integer variants in the + // `document_schemas` Value tree (e.g. `U32(63)`, `I32(0)`) collapse to + // `U64` on round-trip — a fundamental serde_json limitation, not a bug. + // We compare under a normalization that projects both sides through the + // same lossy map. See `tests::utils::normalize_integer_variants_for_json_round_trip`. + use crate::serialization::{JsonConvertible, ValueConvertible}; + use crate::tests::utils::normalize_integer_variants_for_json_round_trip; + let original = fixture(); + let json = JsonConvertible::to_json(&original).expect("to_json"); + let recovered = + ::from_json(json).expect("from_json"); + let mut original_canon = ValueConvertible::to_object(&original).expect("to_object"); + let mut recovered_canon = ValueConvertible::to_object(&recovered).expect("to_object"); + normalize_integer_variants_for_json_round_trip(&mut original_canon); + normalize_integer_variants_for_json_round_trip(&mut recovered_canon); + assert_eq!(original_canon, recovered_canon); + assert_v0_fields(&recovered); + } + + #[test] + fn json_preserves_format_version_tag() { + use crate::serialization::JsonConvertible; + let json = JsonConvertible::to_json(&fixture()).expect("to_json"); + assert_eq!(json["$formatVersion"], "0"); + } + + #[test] + fn value_round_trip_with_envelope_wire_shape() { + use crate::serialization::ValueConvertible; + use platform_value::{platform_value, Value}; + let original = fixture(); + let value = ValueConvertible::to_object(&original).expect("to_object"); + // Tier 3 envelope-only: the inner `dataContract` is a fully-fledged + // versioned `DataContractInSerializationFormat` with embedded JSON + // Schemas, group / token / keyword maps, etc. — far too large to inline + // here. We assert the outer envelope shape (every non-`dataContract` + // field) with sized-int suffixes (`5u64` for `identityNonce` u64, + // `3u16` for `userFeeIncrease` u16, `1u32` for `signaturePublicKeyId` + // u32, `BinaryData` -> `Value::Bytes`), and check that the + // `dataContract` slot is a `Value::Map` (its full shape is exercised + // by the round-trip-equality assertion further down + the tests living + // alongside `DataContractInSerializationFormat`). + let envelope: std::collections::BTreeMap = match &value { + Value::Map(entries) => entries + .iter() + .filter_map(|(k, v)| match k { + Value::Text(s) if s != "dataContract" => Some((s.clone(), v.clone())), + _ => None, + }) + .collect(), + _ => panic!("value is not a Map"), + }; + let envelope_value: Value = envelope.into(); + // Note: assertion uses alphabetical key order — BTreeMap sorts. + assert_eq!( + envelope_value, + platform_value!({ + "$formatVersion": "0", + "identityNonce": 5u64, + "signature": Value::Bytes(vec![0xab; 65]), + "signaturePublicKeyId": 1u32, + "userFeeIncrease": 3u16, + }) + ); + // dataContract slot is present and is a Map (full inline shape skipped) + let has_data_contract = matches!( + &value, + Value::Map(entries) if entries.iter().any(|(k, v)| + matches!(k, Value::Text(s) if s == "dataContract") && + matches!(v, Value::Map(_)) + ) + ); + assert!(has_data_contract, "dataContract slot must be a Value::Map"); + let recovered = ::from_object(value) + .expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v0/json_conversion.rs deleted file mode 100644 index a73694f0c3d..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v0/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::data_contract_create_transition::DataContractCreateTransitionV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for DataContractCreateTransitionV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v0/mod.rs index d89d169b4f2..c2ab6fd448a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v0/mod.rs @@ -1,11 +1,7 @@ mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; mod state_transition_like; mod types; pub(crate) mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; #[cfg(feature = "json-conversion")] diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v0/value_conversion.rs deleted file mode 100644 index d6397dbbc14..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/v0/value_conversion.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_value::{IntegerReplacementType, ReplacementType, Value}; - -use crate::{data_contract::DataContract, ProtocolError}; - -use platform_version::TryIntoPlatformVersioned; -use platform_version::version::PlatformVersion; -use crate::data_contract::conversion::value::v0::DataContractValueConversionMethodsV0; -use crate::state_transition::{StateTransitionFieldTypes, StateTransitionValueConvert}; -use crate::state_transition::data_contract_create_transition::{DataContractCreateTransitionV0}; -use crate::state_transition::data_contract_create_transition::fields::*; -use crate::state_transition::state_transitions::common_fields::property_names::USER_FEE_INCREASE; -use crate::state_transition::state_transitions::contract::data_contract_create_transition::fields::{BINARY_FIELDS, IDENTIFIER_FIELDS, U32_FIELDS}; - -impl StateTransitionValueConvert<'_> for DataContractCreateTransitionV0 { - fn to_object(&self, skip_signature: bool) -> Result { - let mut object: Value = platform_value::to_value(self)?; - if skip_signature { - Self::signature_property_paths() - .into_iter() - .try_for_each(|path| { - object - .remove_values_matching_path(path) - .map_err(ProtocolError::ValueError) - .map(|_| ()) - })?; - } - Ok(object) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - let mut object: Value = platform_value::to_value(self)?; - if skip_signature { - Self::signature_property_paths() - .into_iter() - .try_for_each(|path| { - object - .remove_values_matching_path(path) - .map_err(ProtocolError::ValueError) - .map(|_| ()) - })?; - } - Ok(object) - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - Ok(DataContractCreateTransitionV0 { - signature: raw_object - .remove_optional_binary_data(SIGNATURE) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(), - signature_public_key_id: raw_object - .get_optional_integer(SIGNATURE_PUBLIC_KEY_ID) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(), - identity_nonce: raw_object - .get_optional_integer(IDENTITY_NONCE) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(), - data_contract: DataContract::from_value( - raw_object.remove(DATA_CONTRACT).map_err(|_| { - ProtocolError::DecodingError( - "data contract missing on state transition".to_string(), - ) - })?, - true, - platform_version, - )? - .try_into_platform_versioned(platform_version)?, - user_fee_increase: raw_object - .get_optional_integer(USER_FEE_INCREASE) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(), - }) - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - Ok(DataContractCreateTransitionV0 { - signature: raw_value_map - .remove_optional_binary_data(SIGNATURE) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(), - signature_public_key_id: raw_value_map - .remove_optional_integer(SIGNATURE_PUBLIC_KEY_ID) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(), - identity_nonce: raw_value_map - .remove_optional_integer(IDENTITY_NONCE) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(), - data_contract: DataContract::from_value( - raw_value_map - .remove(DATA_CONTRACT) - .ok_or(ProtocolError::DecodingError( - "data contract missing on state transition".to_string(), - ))?, - false, - platform_version, - )? - .try_into_platform_versioned(platform_version)?, - user_fee_increase: raw_value_map - .remove_optional_integer(USER_FEE_INCREASE) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(), - }) - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; - value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; - value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; - Ok(()) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/value_conversion.rs deleted file mode 100644 index b4ec7a648ff..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_create_transition/value_conversion.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::Value; - -use crate::ProtocolError; - -use crate::state_transition::data_contract_create_transition::{ - DataContractCreateTransition, DataContractCreateTransitionV0, -}; -use crate::state_transition::state_transitions::data_contract_create_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_version::version::{FeatureVersion, PlatformVersion}; - -impl StateTransitionValueConvert<'_> for DataContractCreateTransition { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - DataContractCreateTransition::V0(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - DataContractCreateTransition::V0(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - DataContractCreateTransition::V0(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - DataContractCreateTransition::V0(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok( - DataContractCreateTransitionV0::from_object(raw_object, platform_version)?.into(), - ), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown DataContractCreateTransition version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(DataContractCreateTransitionV0::from_value_map( - raw_value_map, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown DataContractCreateTransition version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => DataContractCreateTransitionV0::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown DataContractCreateTransition version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/json_conversion.rs deleted file mode 100644 index b1ee11537d3..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/json_conversion.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::state_transition::data_contract_update_transition::DataContractUpdateTransition; -use crate::state_transition::state_transitions::data_contract_update_transition::fields::*; -use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, -}; -use crate::ProtocolError; -use serde_json::Number; -use serde_json::Value as JsonValue; - -impl StateTransitionJsonConvert<'_> for DataContractUpdateTransition { - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - match self { - DataContractUpdateTransition::V0(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(0)), - ); - Ok(value) - } - } - } -} -// -// #[cfg(test)] -// mod test { -// use crate::state_transition::data_contract_update_transition::STATE_TRANSITION_PROTOCOL_VERSION; -// use crate::state_transition::JsonStateTransitionSerializationOptions; -// -// #[test] -// fn should_return_state_transition_in_json_format() { -// let data = get_test_data(); -// let mut json_object = data -// .state_transition -// .to_json(JsonStateTransitionSerializationOptions { -// skip_signature: false, -// into_validating_json: false, -// }) -// .expect("conversion to JSON shouldn't fail"); -// -// assert_eq!( -// 0, -// json_object -// .get_u64(STATE_TRANSITION_PROTOCOL_VERSION) -// .expect("the protocol version should be present") as u32 -// ); -// -// assert_eq!( -// 4, -// json_object -// .get_u64(TRANSITION_TYPE) -// .expect("the transition type should be present") as u8 -// ); -// assert_eq!( -// 0, -// json_object -// .get_u64(SIGNATURE_PUBLIC_KEY_ID) -// .expect("default public key id should be defined"), -// ); -// assert_eq!( -// "", -// json_object -// .remove_into::(SIGNATURE) -// .expect("default string value for signature should be present") -// ); -// } -// } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/mod.rs index 7d0871eaa25..9299113b8e0 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/mod.rs @@ -16,15 +16,11 @@ use serde::{Deserialize, Serialize}; pub mod accessors; mod fields; mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; pub mod methods; mod serialize; mod state_transition_estimated_fee_validation; mod state_transition_like; mod v0; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; pub use fields::*; @@ -231,140 +227,127 @@ mod test { assert!(!result.is_valid()); } - #[test] - #[cfg(feature = "json-conversion")] - fn should_convert_to_json() { - use crate::state_transition::JsonStateTransitionSerializationOptions; - use crate::state_transition::StateTransitionJsonConvert; - - let data = get_test_data(); - let json = ::to_json( - &data.state_transition, - JsonStateTransitionSerializationOptions { - skip_signature: false, - into_validating_json: false, - }, - ) - .expect("to_json should succeed"); - - assert!(json.is_object()); + // Legacy `StateTransitionValueConvert` / `StateTransitionJsonConvert` + // round-trip tests deleted in Phase D step 9. The canonical + // `JsonConvertible` / `ValueConvertible` round-trip is exercised on the + // outer enum derive (see `json_convertible_tests` below) — these tested + // methods that no longer exist. +} - // Verify protocol version is set - let version = json - .get(STATE_TRANSITION_PROTOCOL_VERSION) - .expect("should have version"); - assert_eq!(version.as_u64(), Some(0)); +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::data_contract_update_transition::v0::DataContractUpdateTransitionV0; + use crate::tests::fixtures::get_data_contract_fixture; + use platform_value::BinaryData; + use platform_version::version::PlatformVersion; + use platform_version::TryIntoPlatformVersioned; + + pub(crate) fn fixture() -> DataContractUpdateTransition { + let pv = PlatformVersion::latest(); + let created = get_data_contract_fixture(None, 0, pv.protocol_version); + let data_contract = created.data_contract().clone(); + let data_contract_format = data_contract + .try_into_platform_versioned(pv) + .expect("contract -> format"); + DataContractUpdateTransition::V0(DataContractUpdateTransitionV0 { + identity_contract_nonce: 8, + data_contract: data_contract_format, + user_fee_increase: 5, + signature_public_key_id: 1, + signature: BinaryData::new(vec![0xff; 65]), + }) } - #[test] - #[cfg(feature = "value-conversion")] - fn should_deserialize_from_object() { - use crate::state_transition::state_transitions::common_fields::property_names::IDENTITY_CONTRACT_NONCE; - use crate::state_transition::StateTransitionValueConvert; - - let data = get_test_data(); - - let mut obj = StateTransitionValueConvert::to_object(&data.state_transition, false) - .expect("to_object should succeed"); - - // The serde field name for identity_contract_nonce is "$identity-contract-nonce" - // but from_object expects "identityContractNonce". Fix up the field name. - if let Ok(nonce) = obj.remove("$identity-contract-nonce") { - obj.insert(IDENTITY_CONTRACT_NONCE.to_string(), nonce) - .expect("insert should succeed"); - } - - let restored = ::from_object( - obj, - PlatformVersion::first(), - ) - .expect("from_object should succeed"); - - assert_eq!(data.state_transition, restored); + fn assert_v0_fields(t: &DataContractUpdateTransition) { + let DataContractUpdateTransition::V0(rec) = t; + assert_eq!(rec.identity_contract_nonce, 8, "identity_contract_nonce"); + assert_eq!(rec.user_fee_increase, 5, "user_fee_increase"); + assert_eq!(rec.signature_public_key_id, 1, "signature_public_key_id"); + assert_eq!(rec.signature, BinaryData::new(vec![0xff; 65]), "signature"); } #[test] - #[cfg(feature = "value-conversion")] - fn should_deserialize_from_value_map() { - use crate::state_transition::state_transitions::common_fields::property_names::IDENTITY_CONTRACT_NONCE; - use crate::state_transition::StateTransitionValueConvert; - - let data = get_test_data(); - - let obj = StateTransitionValueConvert::to_object(&data.state_transition, false) - .expect("to_object should succeed"); - let mut map = obj - .into_btree_string_map() - .expect("should convert to btree map"); - - // Fix up the serde-renamed field to the expected key for from_value_map - if let Some(nonce) = map.remove("$identity-contract-nonce") { - map.insert(IDENTITY_CONTRACT_NONCE.to_string(), nonce); - } - - let restored = - ::from_value_map( - map, - PlatformVersion::first(), - ) - .expect("from_value_map should succeed"); - - assert_eq!(data.state_transition, restored); + fn json_round_trip_with_per_property_assertions() { + // JSON's single `Number` type erases sized-int variants in the + // `document_schemas` tree on round-trip. Compare under normalization; + // see `tests::utils::normalize_integer_variants_for_json_round_trip`. + use crate::serialization::{JsonConvertible, ValueConvertible}; + use crate::tests::utils::normalize_integer_variants_for_json_round_trip; + let original = fixture(); + let json = JsonConvertible::to_json(&original).expect("to_json"); + let recovered = + ::from_json(json).expect("from_json"); + let mut original_canon = ValueConvertible::to_object(&original).expect("to_object"); + let mut recovered_canon = ValueConvertible::to_object(&recovered).expect("to_object"); + normalize_integer_variants_for_json_round_trip(&mut original_canon); + normalize_integer_variants_for_json_round_trip(&mut recovered_canon); + assert_eq!(original_canon, recovered_canon); + assert_v0_fields(&recovered); } #[test] - #[cfg(feature = "value-conversion")] - fn v0_should_deserialize_from_object() { - use crate::state_transition::state_transitions::common_fields::property_names::IDENTITY_CONTRACT_NONCE; - use crate::state_transition::StateTransitionValueConvert; - - let data = get_test_data(); - match &data.state_transition { - DataContractUpdateTransition::V0(v0) => { - let mut obj = StateTransitionValueConvert::to_object(v0, false) - .expect("to_object should succeed"); - - // Fix up the serde-renamed field - if let Ok(nonce) = obj.remove("$identity-contract-nonce") { - obj.insert(IDENTITY_CONTRACT_NONCE.to_string(), nonce) - .expect("insert should succeed"); - } - - let restored = - DataContractUpdateTransitionV0::from_object(obj, PlatformVersion::first()) - .expect("from_object should succeed"); - - assert_eq!(*v0, restored); - } - } + fn json_preserves_format_version_tag() { + use crate::serialization::JsonConvertible; + let json = JsonConvertible::to_json(&fixture()).expect("to_json"); + assert_eq!(json["$formatVersion"], "0"); } #[test] - #[cfg(feature = "value-conversion")] - fn v0_should_deserialize_from_value_map() { - use crate::state_transition::state_transitions::common_fields::property_names::IDENTITY_CONTRACT_NONCE; - use crate::state_transition::StateTransitionValueConvert; - - let data = get_test_data(); - match &data.state_transition { - DataContractUpdateTransition::V0(v0) => { - let obj = StateTransitionValueConvert::to_object(v0, false) - .expect("to_object should succeed"); - let mut map = obj - .into_btree_string_map() - .expect("should convert to btree map"); - - // Fix up the serde-renamed field - if let Some(nonce) = map.remove("$identity-contract-nonce") { - map.insert(IDENTITY_CONTRACT_NONCE.to_string(), nonce); - } - - let restored = - DataContractUpdateTransitionV0::from_value_map(map, PlatformVersion::first()) - .expect("from_value_map should succeed"); - - assert_eq!(*v0, restored); - } - } + fn value_round_trip_with_envelope_wire_shape() { + use crate::serialization::ValueConvertible; + use platform_value::{platform_value, Value}; + let original = fixture(); + let value = ValueConvertible::to_object(&original).expect("to_object"); + // Tier 3 envelope-only: the inner `dataContract` is a fully-fledged + // versioned `DataContractInSerializationFormat` with embedded JSON + // Schemas, group / token / keyword maps, etc. — far too large to inline + // here. We assert the outer envelope shape (every non-`dataContract` + // field) with sized-int suffixes (`8u64` for `$identity-contract-nonce` + // u64 — yes, kebab-case-with-leading-dollar is the actual wire key, + // see `crate::state_transition::state_transitions::contract::property_names`, + // `5u16` for `userFeeIncrease` u16, `1u32` for `signaturePublicKeyId` + // u32, `BinaryData` -> `Value::Bytes`), and check that the + // `dataContract` slot is a `Value::Map` (its full shape is exercised + // by the round-trip-equality assertion further down + the tests living + // alongside `DataContractInSerializationFormat`). + let envelope: std::collections::BTreeMap = match &value { + Value::Map(entries) => entries + .iter() + .filter_map(|(k, v)| match k { + Value::Text(s) if s != "dataContract" => Some((s.clone(), v.clone())), + _ => None, + }) + .collect(), + _ => panic!("value is not a Map"), + }; + let envelope_value: Value = envelope.into(); + // Note: assertion uses alphabetical key order — BTreeMap sorts. + assert_eq!( + envelope_value, + platform_value!({ + "$formatVersion": "0", + "$identity-contract-nonce": 8u64, + "signature": Value::Bytes(vec![0xff; 65]), + "signaturePublicKeyId": 1u32, + "userFeeIncrease": 5u16, + }) + ); + let has_data_contract = matches!( + &value, + Value::Map(entries) if entries.iter().any(|(k, v)| + matches!(k, Value::Text(s) if s == "dataContract") && + matches!(v, Value::Map(_)) + ) + ); + assert!(has_data_contract, "dataContract slot must be a Value::Map"); + let recovered = ::from_object(value) + .expect("from_object"); + assert_eq!(original, recovered); } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v0/json_conversion.rs deleted file mode 100644 index 4858d182b26..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v0/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::data_contract_update_transition::DataContractUpdateTransitionV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for DataContractUpdateTransitionV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v0/mod.rs index 95f50c7ea9e..88be0127bf8 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v0/mod.rs @@ -1,11 +1,7 @@ mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; mod state_transition_like; mod types; pub(super) mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; #[cfg(feature = "json-conversion")] diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v0/value_conversion.rs deleted file mode 100644 index 986fe7dc0eb..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v0/value_conversion.rs +++ /dev/null @@ -1,132 +0,0 @@ -use crate::data_contract::conversion::value::v0::DataContractValueConversionMethodsV0; -use crate::data_contract::DataContract; -use crate::state_transition::data_contract_update_transition::fields::*; -use crate::state_transition::data_contract_update_transition::{ - DataContractUpdateTransitionV0, BINARY_FIELDS, IDENTIFIER_FIELDS, U32_FIELDS, -}; -use crate::state_transition::state_transitions::common_fields::property_names::{ - IDENTITY_CONTRACT_NONCE, USER_FEE_INCREASE, -}; -use crate::state_transition::StateTransitionFieldTypes; -use crate::state_transition::StateTransitionValueConvert; -use crate::ProtocolError; -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_value::{IntegerReplacementType, ReplacementType, Value}; -use platform_version::version::PlatformVersion; -use platform_version::TryIntoPlatformVersioned; -use std::collections::BTreeMap; - -impl StateTransitionValueConvert<'_> for DataContractUpdateTransitionV0 { - fn to_object(&self, skip_signature: bool) -> Result { - let mut object: Value = platform_value::to_value(self)?; - if skip_signature { - Self::signature_property_paths() - .into_iter() - .try_for_each(|path| { - object - .remove_values_matching_path(path) - .map_err(ProtocolError::ValueError) - .map(|_| ()) - })?; - } - Ok(object) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - let mut object: Value = platform_value::to_value(self)?; - if skip_signature { - Self::signature_property_paths() - .into_iter() - .try_for_each(|path| { - object - .remove_values_matching_path(path) - .map_err(ProtocolError::ValueError) - .map(|_| ()) - })?; - } - Ok(object) - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - Ok(DataContractUpdateTransitionV0 { - identity_contract_nonce: raw_object.remove_integer(IDENTITY_CONTRACT_NONCE).map_err( - |_| { - ProtocolError::DecodingError( - "identity contract nonce missing on data contract update state transition" - .to_string(), - ) - }, - )?, - signature: raw_object - .remove_optional_binary_data(SIGNATURE) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(), - signature_public_key_id: raw_object - .get_optional_integer(SIGNATURE_PUBLIC_KEY_ID) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(), - data_contract: DataContract::from_value( - raw_object.remove(DATA_CONTRACT).map_err(|_| { - ProtocolError::DecodingError( - "data contract missing on state transition".to_string(), - ) - })?, - true, - platform_version, - )? - .try_into_platform_versioned(platform_version)?, - user_fee_increase: raw_object - .get_optional_integer(USER_FEE_INCREASE) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(), - }) - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - Ok(DataContractUpdateTransitionV0 { - identity_contract_nonce: raw_value_map - .remove_integer(IDENTITY_CONTRACT_NONCE) - .map_err(|_| { - ProtocolError::DecodingError( - "identity contract nonce missing on data contract update state transition" - .to_string(), - ) - })?, - signature: raw_value_map - .remove_optional_binary_data(SIGNATURE) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(), - signature_public_key_id: raw_value_map - .remove_optional_integer(SIGNATURE_PUBLIC_KEY_ID) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(), - data_contract: DataContract::from_value( - raw_value_map - .remove(DATA_CONTRACT) - .ok_or(ProtocolError::DecodingError( - "data contract missing on state transition".to_string(), - ))?, - false, - platform_version, - )? - .try_into_platform_versioned(platform_version)?, - user_fee_increase: raw_value_map - .remove_optional_integer(USER_FEE_INCREASE) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(), - }) - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; - value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; - value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; - Ok(()) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/value_conversion.rs deleted file mode 100644 index 1ec80c0e69e..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/value_conversion.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::Value; - -use crate::ProtocolError; - -use crate::state_transition::data_contract_update_transition::{ - DataContractUpdateTransition, DataContractUpdateTransitionV0, -}; -use crate::state_transition::state_transitions::data_contract_update_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_version::version::{FeatureVersion, PlatformVersion}; - -impl StateTransitionValueConvert<'_> for DataContractUpdateTransition { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - DataContractUpdateTransition::V0(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - DataContractUpdateTransition::V0(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - DataContractUpdateTransition::V0(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - DataContractUpdateTransition::V0(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok( - DataContractUpdateTransitionV0::from_object(raw_object, platform_version)?.into(), - ), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown DataContractUpdateTransition version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(DataContractUpdateTransitionV0::from_value_map( - raw_value_map, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown DataContractUpdateTransition version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => DataContractUpdateTransitionV0::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown DataContractUpdateTransition version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/mod.rs index 96195200a04..eddb5f55dd7 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/mod.rs @@ -26,15 +26,43 @@ use serde_json::Value as JsonValue; #[cfg(feature = "value-conversion")] use std::collections::BTreeMap; +// Internal tagging with `$baseFormatVersion`. `DocumentBaseTransition` is +// `serde(flatten)`'d into every document leaf transition's V0 struct; the +// leaf wrappers use `tag = "$formatVersion"`, so a distinct key per nesting +// level prevents collision at the same flattened level. The flat-shape +// convention is preserved across every document and token transition, so +// every consumer reads `tx["$id"]`, `tx["$identityContractNonce"]`, etc. at +// the top level (no `"base"` envelope to traverse). +// +// `DocumentCreateTransitionV0` and `DocumentReplaceTransitionV0` have an +// extra constraint: they combine `serde(flatten) base` with `serde(flatten) +// data: BTreeMap` (a catchall) — under the auto-derived +// `Deserialize` the catchall would steal the base's discriminator and +// fields. Both leaves carry a manual `Deserialize` impl that explicitly +// routes `$baseFormatVersion` + the known base struct fields to `base` +// before letting the catchall claim what's left. See comments on +// those impls for detail. #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$baseFormatVersion") +)] pub enum DocumentBaseTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(DocumentBaseTransitionV0), #[display("V1({})", "_1")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "1"))] V1(DocumentBaseTransitionV1), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for DocumentBaseTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for DocumentBaseTransition {} + impl Default for DocumentBaseTransition { fn default() -> Self { DocumentBaseTransition::V0(DocumentBaseTransitionV0::default()) // since only v0 @@ -99,3 +127,76 @@ impl DocumentTransitionObjectLike for DocumentBaseTransition { Ok(self.to_value_map()?.into()) } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + fn fixture() -> DocumentBaseTransition { + DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([0xa1; 32]), + identity_contract_nonce: 7, + document_type_name: "user".to_string(), + data_contract_id: Identifier::new([0xb2; 32]), + }) + } + + // Note: `DocumentBaseTransition` derives `Serialize/Deserialize` without a + // `#[serde(tag = ...)]` attribute, so the enum uses serde's default + // externally-tagged shape: `{"V0": { ... }}`. + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = JsonConvertible::to_json(&original).expect("to_json"); + // `identityContractNonce` is `u64` — JSON has only one number type so + // the U64 distinction is erased on the wire (the value-path test below + // pins the typed variant). Identifiers render as base58 in JSON HR. + assert_eq!( + json, + json!({ + "$baseFormatVersion": "0", + "$id": "Bswb3UyeD1pUTaGiE6WvqwFpJZsQSEY1xhJePCDTHdvp", + "$identityContractNonce": 7, + "$type": "user", + "$dataContractId": "D2ZcUbtpG5sKq7XLeB4YnpNnTGSptKCxTddoNeydzJQq", + }) + ); + let recovered = + ::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = ValueConvertible::to_object(&original).expect("to_object"); + let id = Identifier::new([0xa1; 32]); + let data_contract_id = Identifier::new([0xb2; 32]); + // Externally-tagged enum: `{"V0": {}}`. `7u64` keeps the typed + // U64 variant for `identity_contract_nonce`. + assert_eq!( + value, + platform_value!({ + "$baseFormatVersion": "0", + "$id": id, + "$identityContractNonce": 7u64, + "$type": "user", + "$dataContractId": data_contract_id, + }) + ); + let recovered = + ::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v0/mod.rs index 98f56ee490f..cbbd2d40cc4 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v0/mod.rs @@ -26,6 +26,10 @@ use crate::state_transition::batch_transition::document_base_transition::propert use crate::{data_contract::DataContract, errors::ProtocolError}; #[derive(Debug, Clone, Encode, Decode, Default, PartialEq, Display)] +// `json_safe_fields` auto-injects `crate::serialization::json_safe_u64` on +// `identity_contract_nonce: IdentityNonce` (= u64). Large nonces serialize +// as JSON strings to avoid JS Number precision loss; native u64 in non-HR. +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), @@ -70,6 +74,13 @@ impl DocumentBaseTransitionV0 { } } +/// **KEEP-AS-EXCEPTION** in the JSON/Value canonical-trait migration — this +/// trait is context-aware: the `from_*` constructors need a `DataContract` +/// to type document properties, which `JsonConvertible`/`ValueConvertible` +/// can't carry. NOTE the to-side emits a flat LEGACY shape (`$version: "0"`, +/// no `$action`/`$baseFormatVersion` tags) that intentionally differs from +/// canonical `JsonConvertible::to_json` on the same transition — see the +/// wire-shape comparison tests in `document_create_transition/v0/mod.rs`. pub trait DocumentTransitionObjectLike { #[cfg(feature = "json-conversion")] /// Creates the Document Transition from JSON representation. The JSON representation contains diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1/mod.rs index 644d288c773..b3ddc865150 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1/mod.rs @@ -24,6 +24,8 @@ use platform_value::Value; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Encode, Decode, Default, PartialEq, Display)] +// See `DocumentBaseTransitionV0` for json_safe_fields rationale. +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_create_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_create_transition/mod.rs index 070db325a44..50007d098d0 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_create_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_create_transition/mod.rs @@ -18,12 +18,23 @@ use serde::{Deserialize, Serialize}; pub use v0::DocumentCreateTransitionV0; #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum DocumentCreateTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(DocumentCreateTransitionV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for DocumentCreateTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for DocumentCreateTransition {} + impl Default for DocumentCreateTransition { fn default() -> Self { DocumentCreateTransition::V0(DocumentCreateTransitionV0::default()) // since only v0 @@ -128,3 +139,103 @@ impl DocumentFromCreateTransition for Document { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use crate::state_transition::batch_transition::document_create_transition::v0::DocumentCreateTransitionV0; + use platform_value::{platform_value, Identifier, Value}; + use serde_json::json; + use std::collections::BTreeMap; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> DocumentCreateTransition { + let mut data = BTreeMap::new(); + data.insert("name".to_string(), Value::Text("alice".to_string())); + DocumentCreateTransition::V0(DocumentCreateTransitionV0 { + base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([0xc1; 32]), + identity_contract_nonce: 11, + document_type_name: "post".to_string(), + data_contract_id: Identifier::new([0xd2; 32]), + }), + entropy: [0xab; 32], + data, + prefunded_voting_balance: Some(("uniqueName".to_string(), 50_000)), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Outer leaf wrapper: `tag = "$formatVersion"`. Flattened + // `DocumentBaseTransition`: `tag = "$baseFormatVersion"`. Both + // discriminators sit at the top level (no envelope nesting). + // `$entropy: [u8; 32]` is auto-injected with `serde_bytes` by + // `#[json_safe_fields]` on the V0 struct -> base64 string in JSON + // (matches shielded transitions' byte-field convention, NOT a JSON + // array of numbers as before). `$identityContractNonce: u64` (= + // IdentityNonce) goes through `json_safe_u64`: small values stay + // as numbers; values above `MAX_SAFE_INTEGER` (2^53 - 1) become + // strings to avoid JS Number precision loss. The `data: + // BTreeMap` flatten promotes its keys to the top + // level (`name`). `$prefundedVotingBalance: Option<(String, u64)>` + // uses the explicit `json_safe_option_string_u64_tuple` helper — + // 2-element JSON array, with the u64 stringified when above the + // safe-integer threshold. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$id": Identifier::new([0xc1; 32]), + "$identityContractNonce": 11, + "$type": "post", + "$dataContractId": Identifier::new([0xd2; 32]), + "$entropy": "q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6s=", + "name": "alice", + "$prefundedVotingBalance": ["uniqueName", 50_000], + }) + ); + let recovered = DocumentCreateTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `11u64`: `IdentityNonce` is a `u64` alias; explicit suffix locks in + // the sized `Value::U64`. `[u8; 32]` via `serde_bytes` (auto-injected + // by `json_safe_fields`) → `Value::Bytes32` in non-HR (NOT + // `Array`). `50_000u64`: `Credits` is a `u64` alias. + // `Identifier`s interpolate via `Serialize` → `Value::Identifier`. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$id": Identifier::new([0xc1; 32]), + "$identityContractNonce": 11u64, + "$type": "post", + "$dataContractId": Identifier::new([0xd2; 32]), + "$entropy": platform_value::Value::Bytes32([0xab; 32]), + "name": "alice", + "$prefundedVotingBalance": ["uniqueName", 50_000u64], + }) + ); + let recovered = DocumentCreateTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_create_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_create_transition/v0/mod.rs index 2907e4256e2..8b7a528b7d0 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_create_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_create_transition/v0/mod.rs @@ -48,9 +48,22 @@ pub const BINARY_FIELDS: [&str; 1] = ["$entropy"]; pub use super::super::document_base_transition::IDENTIFIER_FIELDS; #[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Display)] +// `json_safe_fields`: +// - Auto-injects `crate::serialization::serde_bytes` on `entropy: [u8; 32]` +// → base64 string in JSON HR, raw bytes in non-HR. +// - The `data: BTreeMap` flatten catchall and `base: +// DocumentBaseTransition` flatten are skipped (serde(flatten) override). +// - `prefunded_voting_balance: Option<(String, Credits)>` carries an explicit +// `serde(with = json_safe_option_string_u64_tuple)` annotation below — the +// macro can't auto-route tuple-inside-Option fields, so the helper enforces +// JS-safe u64 stringification on the second tuple element manually. +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] +// `Deserialize` is implemented manually below — see comments on the impl. +// Auto-derived `Serialize` produces the desired flat shape correctly; only +// the deserialize side needs the manual key-routing logic. #[cfg_attr( feature = "serde-conversion", - derive(Serialize, Deserialize), + derive(Serialize), serde(rename_all = "camelCase") )] #[display("Base: {}, Entropy: {:?}, Data: {:?}", "base", "entropy", "data")] @@ -60,6 +73,10 @@ pub struct DocumentCreateTransitionV0 { pub base: DocumentBaseTransition, /// Entropy used to create a Document ID. + // `[u8; 32]` is auto-annotated by `#[json_safe_fields]` on this struct + // with `crate::serialization::serde_bytes` — base64 string in JSON HR, + // raw bytes in non-HR. Same convention as shielded transition byte + // fields (`anchor`, `binding_signature`, etc.). #[cfg_attr(feature = "serde-conversion", serde(rename = "$entropy"))] pub entropy: [u8; 32], @@ -68,7 +85,11 @@ pub struct DocumentCreateTransitionV0 { #[cfg_attr( feature = "serde-conversion", - serde(rename = "$prefundedVotingBalance") + serde( + default, + rename = "$prefundedVotingBalance", + with = "crate::serialization::json::safe_integer::json_safe_option_string_u64_tuple" + ) )] /// Pre funded balance (for unique index conflict resolution voting - the identity will put money /// aside that will be used by voters to vote) @@ -77,6 +98,92 @@ pub struct DocumentCreateTransitionV0 { pub prefunded_voting_balance: Option<(String, Credits)>, } +// Manual `Deserialize` impl: the auto-derived one cannot route fields +// correctly because this struct combines `#[serde(flatten)] base: +// DocumentBaseTransition` (an internally-tagged enum) with +// `#[serde(flatten)] data: BTreeMap` (a catchall). Under the +// auto-derive, the catchall claims the base's discriminator and struct +// fields before the base flatten gets a chance, leaving `base = +// Default::default()`. This impl reads the entire object into a `Value` +// map first, peels off the keys known to belong to the base, reconstructs +// the base from those, then routes the remaining keys (minus the explicit +// named fields `$entropy` / `$prefundedVotingBalance`) to `data`. +// +// **WARNING**: when adding a new field to `DocumentBaseTransitionV0` / +// `DocumentBaseTransitionV1`, add its serde rename to `BASE_FIELD_NAMES` +// below — otherwise it silently routes to the dynamic `data` map at +// runtime. +#[cfg(feature = "serde-conversion")] +impl<'de> Deserialize<'de> for DocumentCreateTransitionV0 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + + // Tag + every serde-renamed field of `DocumentBaseTransitionV0` / + // `DocumentBaseTransitionV1`. Keep in sync with the base structs. + const BASE_FIELD_NAMES: &[&str] = &[ + "$baseFormatVersion", + "$id", + "$identityContractNonce", + "$type", + "$dataContractId", + "$tokenPaymentInfo", + ]; + + let mut map: BTreeMap = BTreeMap::deserialize(deserializer)?; + + let mut base_pairs: Vec<(Value, Value)> = Vec::with_capacity(BASE_FIELD_NAMES.len()); + for key in BASE_FIELD_NAMES { + if let Some(value) = map.remove(*key) { + base_pairs.push((Value::Text((*key).to_string()), value)); + } + } + let base = platform_value::from_value::(Value::Map(base_pairs)) + .map_err(D::Error::custom)?; + + let entropy_value = map + .remove("$entropy") + .ok_or_else(|| D::Error::missing_field("$entropy"))?; + // `serde_bytes` (auto-injected by `json_safe_fields`) wants a base64 + // string in HR and raw bytes in non-HR. After collecting through + // `BTreeMap` the `Value` variant tells us which form + // we got: `Text` from JSON, `Bytes32` / `Bytes` from platform_value + // / bincode. The default `[u8; 32]::deserialize` only accepts an + // array shape, so we route the byte variants explicitly. + let entropy: [u8; 32] = match entropy_value { + Value::Text(s) => { + use base64::Engine; + let bytes = base64::engine::general_purpose::STANDARD + .decode(s.as_bytes()) + .map_err(|e| D::Error::custom(format!("invalid base64 in $entropy: {e}")))?; + bytes.try_into().map_err(|v: Vec| { + D::Error::custom(format!("$entropy: expected 32 bytes, got {}", v.len())) + })? + } + Value::Bytes32(arr) => arr, + Value::Bytes(b) => b.try_into().map_err(|v: Vec| { + D::Error::custom(format!("$entropy: expected 32 bytes, got {}", v.len())) + })?, + other => platform_value::from_value(other).map_err(D::Error::custom)?, + }; + + let prefunded_voting_balance: Option<(String, Credits)> = + match map.remove("$prefundedVotingBalance") { + Some(Value::Null) | None => None, + Some(other) => Some(platform_value::from_value(other).map_err(D::Error::custom)?), + }; + + Ok(DocumentCreateTransitionV0 { + base, + entropy, + data: map, + prefunded_voting_balance, + }) + } +} + impl DocumentCreateTransitionV0 { #[cfg(feature = "value-conversion")] pub(crate) fn from_value_map( @@ -406,6 +513,8 @@ impl DocumentFromCreateTransitionV0 for Document { mod test { use crate::data_contract::v0::DataContractV0; use crate::state_transition::batch_transition::document_create_transition::DocumentCreateTransition; + use base64::prelude::BASE64_STANDARD; + use base64::Engine; use platform_value::btreemap_extensions::BTreeValueMapHelper; use platform_value::{platform_value, BinaryData, Bytes32, Identifier}; use platform_version::version::LATEST_PLATFORM_VERSION; @@ -518,9 +627,16 @@ mod test { DocumentCreateTransition::from_object(raw_document, data_contract).unwrap(); let json_transition = transition.to_json().expect("no errors"); - assert_eq!(json_transition["V0"]["$id"], JsonValue::String(id.into())); + // Note: this is `DocumentTransitionObjectLike::to_json`, NOT + // `JsonConvertible::to_json`. The former is a custom path + // (`to_value_map` flattens base into the transition map manually); + // the latter uses serde's auto-derived `Serialize`. The two paths + // produce different wire shapes — base fields are flat at the top + // level in this path, nested under `"base"` in the JsonConvertible + // path. + assert_eq!(json_transition["$id"], JsonValue::String(id.into())); assert_eq!( - json_transition["V0"]["$dataContractId"], + json_transition["$dataContractId"], JsonValue::String(data_contract_id.into()) ); assert_eq!( @@ -545,17 +661,26 @@ mod test { let data_contract_id = vec![13_u8; 32]; let entropy = vec![11_u8; 32]; + // Binary / identifier fields use the canonical encoded forms + // (Identifier=bs58, BinaryData/byteArray=base64). The previous + // fixture relied on the `From for Value` array→bytes + // heuristic (Critical-2) to silently coerce JSON arrays into + // Value::Bytes; that heuristic has been removed. let raw_document = json!({ "$protocolVersion" : 0, "$version" : "0", - "$id" : id, + "$id" : bs58::encode(&id).into_string(), "$type" : "test", - "$dataContractId" : data_contract_id, + "$dataContractId" : bs58::encode(&data_contract_id).into_string(), "$identityContractNonce": 0u64, "revision" : 1, - "alphaBinary" : alpha_value, - "alphaIdentifier" : alpha_value, - "$entropy" : entropy, + // NOTE field naming is misleading: `alphaBinary` is the one with + // `contentMediaType: ...identifier` in the contract schema above, + // so it gets bs58-decoded. `alphaIdentifier` is plain byteArray + // and gets base64-decoded. + "alphaBinary" : bs58::encode(&alpha_value).into_string(), + "alphaIdentifier" : BASE64_STANDARD.encode(&alpha_value), + "$entropy" : BASE64_STANDARD.encode(&entropy), "$action": 0 , }); @@ -568,13 +693,18 @@ mod test { .into_btree_string_map() .unwrap(); - let v0 = object_transition.get("V0").expect("to get V0"); + // Same caveat as `convert_to_json_with_dynamic_binary_paths`: this + // is `DocumentTransitionObjectLike::to_object` (custom flat shape), + // not `ValueConvertible::to_object` (nested-base shape). let right_id = Identifier::from_bytes(&id).unwrap(); let right_data_contract_id = Identifier::from_bytes(&data_contract_id).unwrap(); - assert_eq!(v0["$id"], Value::Identifier(right_id.into_buffer())); assert_eq!( - v0["$dataContractId"], + object_transition["$id"], + Value::Identifier(right_id.into_buffer()) + ); + assert_eq!( + object_transition["$dataContractId"], Value::Identifier(right_data_contract_id.into_buffer()) ); diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_delete_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_delete_transition/mod.rs index 024177a223a..24ceccaded5 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_delete_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_delete_transition/mod.rs @@ -9,8 +9,97 @@ use serde::{Deserialize, Serialize}; pub use v0::*; #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum DocumentDeleteTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(DocumentDeleteTransitionV0), } + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for DocumentDeleteTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for DocumentDeleteTransition {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use crate::state_transition::batch_transition::document_delete_transition::v0::DocumentDeleteTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> DocumentDeleteTransition { + DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { + base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([0xc1; 32]), + identity_contract_nonce: 9, + document_type_name: "post".to_string(), + data_contract_id: Identifier::new([0xd2; 32]), + }), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Doubly-tagged externally enum: outer `V0` for + // `DocumentDeleteTransition`, inner `V0` for the flattened + // `base: DocumentBaseTransition`. `$identityContractNonce` is + // `u64`; JSON erases the size — see Value-path assertion for the + // sized variant. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$id": Identifier::new([0xc1; 32]), + "$identityContractNonce": 9, + "$type": "post", + "$dataContractId": Identifier::new([0xd2; 32]), + + }) + ); + let recovered = DocumentDeleteTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `9u64`: `IdentityNonce` is a `u64` alias; explicit suffix locks + // in `Value::U64`. `Identifier`s interpolate via `Serialize` -> + // `Value::Identifier`. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$id": Identifier::new([0xc1; 32]), + "$identityContractNonce": 9u64, + "$type": "post", + "$dataContractId": Identifier::new([0xd2; 32]), + + }) + ); + let recovered = DocumentDeleteTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_delete_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_delete_transition/v0/mod.rs index b8a4685c175..b6c760bf28a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_delete_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_delete_transition/v0/mod.rs @@ -6,11 +6,14 @@ use crate::state_transition::batch_transition::document_base_transition::Documen use bincode::{Decode, Encode}; use derive_more::Display; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; pub use super::super::document_base_transition::IDENTIFIER_FIELDS; +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Display)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_purchase_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_purchase_transition/mod.rs index 32563fe5876..b63157bba34 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_purchase_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_purchase_transition/mod.rs @@ -9,8 +9,101 @@ use serde::{Deserialize, Serialize}; pub use v0::*; #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum DocumentPurchaseTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(DocumentPurchaseTransitionV0), } + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for DocumentPurchaseTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for DocumentPurchaseTransition {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use crate::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::DocumentPurchaseTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> DocumentPurchaseTransition { + DocumentPurchaseTransition::V0(DocumentPurchaseTransitionV0 { + base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([0xc1; 32]), + identity_contract_nonce: 11, + document_type_name: "post".to_string(), + data_contract_id: Identifier::new([0xd2; 32]), + }), + revision: 3, + price: 999_000, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Doubly-tagged externally enum: outer `V0` for the variant; inner + // `V0` for the flattened `base`. `$identityContractNonce`, + // `$revision`, and `price` are `u64`; JSON erases the size. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$id": Identifier::new([0xc1; 32]), + "$identityContractNonce": 11, + "$type": "post", + "$dataContractId": Identifier::new([0xd2; 32]), + + "$revision": 3, + "price": 999_000, + }) + ); + let recovered = DocumentPurchaseTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `11u64`/`3u64`/`999_000u64`: `IdentityNonce`, `Revision`, and + // `Credits` are all `u64` aliases — explicit suffixes lock in + // `Value::U64`. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$id": Identifier::new([0xc1; 32]), + "$identityContractNonce": 11u64, + "$type": "post", + "$dataContractId": Identifier::new([0xd2; 32]), + + "$revision": 3u64, + "price": 999_000u64, + }) + ); + let recovered = DocumentPurchaseTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_purchase_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_purchase_transition/v0/mod.rs index 4938ab1ee5e..d18bd1a9206 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_purchase_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_purchase_transition/v0/mod.rs @@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize}; pub use super::super::document_base_transition::IDENTIFIER_FIELDS; #[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Display)] +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_replace_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_replace_transition/mod.rs index 7edb19e02e9..b3af3e56b0a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_replace_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_replace_transition/mod.rs @@ -16,12 +16,23 @@ use serde::{Deserialize, Serialize}; pub use v0::*; #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum DocumentReplaceTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(DocumentReplaceTransitionV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for DocumentReplaceTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for DocumentReplaceTransition {} + /// document from replace transition pub trait DocumentFromReplaceTransition { /// Attempts to create a new `Document` from the given `DocumentReplaceTransition` reference, incorporating `owner_id`, creation metadata, and additional blockchain-related information. @@ -178,3 +189,87 @@ impl DocumentFromReplaceTransition for Document { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use crate::state_transition::batch_transition::document_replace_transition::v0::DocumentReplaceTransitionV0; + use platform_value::{platform_value, Identifier, Value}; + use serde_json::json; + use std::collections::BTreeMap; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> DocumentReplaceTransition { + let mut data = BTreeMap::new(); + data.insert("name".to_string(), Value::Text("alice".to_string())); + DocumentReplaceTransition::V0(DocumentReplaceTransitionV0 { + base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([0xc1; 32]), + identity_contract_nonce: 11, + document_type_name: "post".to_string(), + data_contract_id: Identifier::new([0xd2; 32]), + }), + revision: 5, + data, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Doubly-tagged externally enum: outer `V0` for the variant; inner + // `V0` for the flattened `base`. `data` is `#[serde(flatten)]` — + // its keys (`name`) become top-level. `$identityContractNonce` + // and `$revision` are `u64`; JSON erases the size. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$id": Identifier::new([0xc1; 32]), + "$identityContractNonce": 11, + "$type": "post", + "$dataContractId": Identifier::new([0xd2; 32]), + + "$revision": 5, + "name": "alice", + }) + ); + let recovered = DocumentReplaceTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `11u64`/`5u64`: `IdentityNonce` and `Revision` are `u64` aliases. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$id": Identifier::new([0xc1; 32]), + "$identityContractNonce": 11u64, + "$type": "post", + "$dataContractId": Identifier::new([0xd2; 32]), + + "$revision": 5u64, + "name": "alice", + }) + ); + let recovered = DocumentReplaceTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_replace_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_replace_transition/v0/mod.rs index 0a968bc3b25..0c2d1f95054 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_replace_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_replace_transition/v0/mod.rs @@ -26,9 +26,13 @@ mod property_names { } #[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Display)] +// Auto-injects `json_safe_u64` on `revision: Revision` (= u64). +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] +// `Deserialize` is implemented manually below — see comments. Same +// catchall-vs-base-flatten conflict as `DocumentCreateTransitionV0`. #[cfg_attr( feature = "serde-conversion", - derive(Serialize, Deserialize), + derive(Serialize), serde(rename_all = "camelCase") )] #[display("Base: {}, Revision: {}, Data: {:?}", "base", "revision", "data")] @@ -41,6 +45,58 @@ pub struct DocumentReplaceTransitionV0 { pub data: BTreeMap, } +// Manual `Deserialize` impl — see the equivalent on +// `DocumentCreateTransitionV0` for rationale and the `BASE_FIELD_NAMES` +// maintenance warning. +#[cfg(feature = "serde-conversion")] +impl<'de> Deserialize<'de> for DocumentReplaceTransitionV0 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + + const BASE_FIELD_NAMES: &[&str] = &[ + "$baseFormatVersion", + "$id", + "$identityContractNonce", + "$type", + "$dataContractId", + "$tokenPaymentInfo", + ]; + + let mut map: BTreeMap = BTreeMap::deserialize(deserializer)?; + + let mut base_pairs: Vec<(Value, Value)> = Vec::with_capacity(BASE_FIELD_NAMES.len()); + for key in BASE_FIELD_NAMES { + if let Some(value) = map.remove(*key) { + base_pairs.push((Value::Text((*key).to_string()), value)); + } + } + let base = platform_value::from_value::(Value::Map(base_pairs)) + .map_err(D::Error::custom)?; + + let revision_value = map + .remove("$revision") + .ok_or_else(|| D::Error::missing_field("$revision"))?; + // `json_safe_u64` stringifies u64 values above `MAX_SAFE_INTEGER` in + // JSON HR — accept both numeric and string forms here so the manual + // Deserialize doesn't reject large revisions. + let revision: Revision = match revision_value { + Value::Text(s) => s + .parse() + .map_err(|e| D::Error::custom(format!("invalid u64 string in $revision: {e}")))?, + other => platform_value::from_value(other).map_err(D::Error::custom)?, + }; + + Ok(DocumentReplaceTransitionV0 { + base, + revision, + data: map, + }) + } +} + /// document from replace transition v0 pub trait DocumentFromReplaceTransitionV0 { /// Attempts to create a new `Document` from the given `DocumentReplaceTransitionV0` reference. This operation is typically used to replace or update an existing document with new information. diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transfer_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transfer_transition/mod.rs index e66ec5fa714..c48178ab83e 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transfer_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transfer_transition/mod.rs @@ -9,8 +9,99 @@ use serde::{Deserialize, Serialize}; pub use v0::*; #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum DocumentTransferTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(DocumentTransferTransitionV0), } + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for DocumentTransferTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for DocumentTransferTransition {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use crate::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::DocumentTransferTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> DocumentTransferTransition { + DocumentTransferTransition::V0(DocumentTransferTransitionV0 { + base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([0xc1; 32]), + identity_contract_nonce: 11, + document_type_name: "post".to_string(), + data_contract_id: Identifier::new([0xd2; 32]), + }), + revision: 4, + recipient_owner_id: Identifier::new([0xee; 32]), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Doubly-tagged externally enum: outer `V0` for the variant; inner + // `V0` for the flattened `base`. `$identityContractNonce` and + // `$revision` are `u64`; JSON erases the size. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$id": Identifier::new([0xc1; 32]), + "$identityContractNonce": 11, + "$type": "post", + "$dataContractId": Identifier::new([0xd2; 32]), + + "$revision": 4, + "recipientOwnerId": Identifier::new([0xee; 32]), + }) + ); + let recovered = DocumentTransferTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `11u64`/`4u64`: `IdentityNonce` and `Revision` are `u64` aliases. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$id": Identifier::new([0xc1; 32]), + "$identityContractNonce": 11u64, + "$type": "post", + "$dataContractId": Identifier::new([0xd2; 32]), + + "$revision": 4u64, + "recipientOwnerId": Identifier::new([0xee; 32]), + }) + ); + let recovered = DocumentTransferTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transfer_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transfer_transition/v0/mod.rs index 2d8d5056ed1..488291f2f56 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transfer_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transfer_transition/v0/mod.rs @@ -19,6 +19,7 @@ mod property_names { } #[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Display)] +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs index 3597106f192..1e7886aa067 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs @@ -18,7 +18,19 @@ use crate::state_transition::batch_transition::document_replace_transition::v0:: use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; #[derive(Debug, Clone, Encode, Decode, From, PartialEq, Display)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + // System-field discriminator `$action` (consistent with the `$`-prefix + // convention for all serde-injected keys). Cannot use `$type` here + // because the flattened `DocumentBaseTransition` already exposes + // `document_type_name` as `$type` in JSON (the long-standing DPP + // document-type field). The variant names (`create`, `replace`, + // `delete`, `transfer`, `updatePrice`, `purchase`) read naturally as + // actions, matching the existing `PROPERTY_ACTION = "$action"` + // constant on the parent batch transition. + serde(tag = "$action", rename_all = "camelCase") +)] pub enum DocumentTransition { #[display("CreateDocumentTransition({})", "_0")] Create(DocumentCreateTransition), @@ -39,6 +51,121 @@ pub enum DocumentTransition { Purchase(DocumentPurchaseTransition), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for DocumentTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for DocumentTransition {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::batched_transition::{ + document_create_transition, document_delete_transition, document_purchase_transition, + document_replace_transition, document_transfer_transition, + document_update_price_transition, + }; + + /// Wrapping helper — drives a single `DocumentTransition::*` variant through + /// JSON and Value round-trips and asserts the outer wire shape carries + /// `"$type": ` with the inner-leaf fields merged in (internally + /// tagged). Each leaf already has its own per-property assertion test, so + /// here we only verify the umbrella adds the discriminator without altering + /// the rest of the shape. + fn assert_umbrella_round_trip(transition: DocumentTransition, expected_type: &str) { + use crate::serialization::{JsonConvertible, ValueConvertible}; + + let json = transition.to_json().expect("to_json"); + let json_obj = json.as_object().expect("json object"); + assert_eq!( + json_obj.get("$action").and_then(|v| v.as_str()), + Some(expected_type), + "json `type` discriminator mismatch" + ); + let recovered_json = DocumentTransition::from_json(json).expect("from_json"); + assert_eq!(transition, recovered_json); + + let value = transition.to_object().expect("to_object"); + let value_map = value.as_map().expect("value map"); + let type_kv = value_map + .iter() + .find(|(k, _)| matches!(k, platform_value::Value::Text(s) if s == "$action")) + .expect("type key present"); + assert_eq!( + type_kv.1, + platform_value::Value::Text(expected_type.to_string()), + "value `type` discriminator mismatch" + ); + let recovered_value = DocumentTransition::from_object(value).expect("from_object"); + assert_eq!(transition, recovered_value); + } + + #[test] + fn umbrella_create() { + assert_umbrella_round_trip( + DocumentTransition::Create( + document_create_transition::json_convertible_tests::fixture(), + ), + "create", + ); + } + + #[test] + fn umbrella_replace() { + assert_umbrella_round_trip( + DocumentTransition::Replace( + document_replace_transition::json_convertible_tests::fixture(), + ), + "replace", + ); + } + + #[test] + fn umbrella_delete() { + assert_umbrella_round_trip( + DocumentTransition::Delete( + document_delete_transition::json_convertible_tests::fixture(), + ), + "delete", + ); + } + + #[test] + fn umbrella_transfer() { + assert_umbrella_round_trip( + DocumentTransition::Transfer( + document_transfer_transition::json_convertible_tests::fixture(), + ), + "transfer", + ); + } + + #[test] + fn umbrella_update_price() { + assert_umbrella_round_trip( + DocumentTransition::UpdatePrice( + document_update_price_transition::json_convertible_tests::fixture(), + ), + "updatePrice", + ); + } + + #[test] + fn umbrella_purchase() { + assert_umbrella_round_trip( + DocumentTransition::Purchase( + document_purchase_transition::json_convertible_tests::fixture(), + ), + "purchase", + ); + } +} + impl BatchTransitionResolversV0 for DocumentTransition { fn as_transition_create(&self) -> Option<&DocumentCreateTransition> { if let Self::Create(ref t) = self { diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_update_price_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_update_price_transition/mod.rs index f9e99c6c584..7847b3fcc44 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_update_price_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_update_price_transition/mod.rs @@ -9,8 +9,100 @@ use serde::{Deserialize, Serialize}; pub use v0::*; #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum DocumentUpdatePriceTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(DocumentUpdatePriceTransitionV0), } + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for DocumentUpdatePriceTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for DocumentUpdatePriceTransition {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use crate::state_transition::batch_transition::batched_transition::document_update_price_transition::v0::DocumentUpdatePriceTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> DocumentUpdatePriceTransition { + DocumentUpdatePriceTransition::V0(DocumentUpdatePriceTransitionV0 { + base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([0xc1; 32]), + identity_contract_nonce: 11, + document_type_name: "post".to_string(), + data_contract_id: Identifier::new([0xd2; 32]), + }), + revision: 6, + price: 555_000, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Doubly-tagged externally enum: outer `V0` for the variant; inner + // `V0` for the flattened `base`. `$identityContractNonce`, + // `$revision`, and `$price` are `u64`; JSON erases the size. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$id": Identifier::new([0xc1; 32]), + "$identityContractNonce": 11, + "$type": "post", + "$dataContractId": Identifier::new([0xd2; 32]), + + "$revision": 6, + "$price": 555_000, + }) + ); + let recovered = DocumentUpdatePriceTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `11u64`/`6u64`/`555_000u64`: `IdentityNonce`, `Revision`, and + // `Credits` are all `u64` aliases. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$id": Identifier::new([0xc1; 32]), + "$identityContractNonce": 11u64, + "$type": "post", + "$dataContractId": Identifier::new([0xd2; 32]), + + "$revision": 6u64, + "$price": 555_000u64, + }) + ); + let recovered = DocumentUpdatePriceTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_update_price_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_update_price_transition/v0/mod.rs index b80bf312061..fe5ca2a4c26 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_update_price_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_update_price_transition/v0/mod.rs @@ -18,6 +18,7 @@ mod property_names { } #[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Display)] +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs index 8efa31c2dad..9f60de1413c 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs @@ -38,15 +38,25 @@ pub use document_delete_transition::DocumentDeleteTransition; pub use document_purchase_transition::DocumentPurchaseTransition; pub use document_replace_transition::DocumentReplaceTransition; pub use document_transfer_transition::DocumentTransferTransition; -use document_transition::DocumentTransition; +pub use document_transition::DocumentTransition; pub use document_update_price_transition::DocumentUpdatePriceTransition; use platform_value::Identifier; -use token_transition::TokenTransition; +pub use token_transition::TokenTransition; pub const PROPERTY_ACTION: &str = "$action"; #[derive(Debug, Clone, Encode, Decode, From, PartialEq, Display)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + // Internal tagging with the system-field key `$transition` — distinct + // from the inner umbrellas' `$type` discriminator so both flatten into + // the same wire shape without collision. Per the wasm-dpp2 convention + // (no `data` wrapper), the inner umbrella's fields appear at the top + // level alongside `$transition`. Resulting wire shape: + // { "$transition": "document", "$type": "create", "$formatVersion": "0", ... } + serde(tag = "$transition", rename_all = "camelCase") +)] pub enum BatchedTransition { #[display("DocumentTransition({})", "_0")] Document(DocumentTransition), @@ -54,6 +64,76 @@ pub enum BatchedTransition { Token(TokenTransition), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for BatchedTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for BatchedTransition {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::batched_transition::{ + document_create_transition, token_burn_transition, + }; + use document_transition::DocumentTransition; + use token_transition::TokenTransition; + + /// Internally tagged with `$transition` — wire shape is + /// `{"$transition": "", "$type": "", ...inner fields}`. + /// Both discriminators sit at the top level (no envelope nesting). + fn assert_umbrella_round_trip(transition: BatchedTransition, expected_transition: &str) { + use crate::serialization::{JsonConvertible, ValueConvertible}; + + let json = transition.to_json().expect("to_json"); + let json_obj = json.as_object().expect("json object"); + assert_eq!( + json_obj.get("$transition").and_then(|v| v.as_str()), + Some(expected_transition), + "json `$transition` discriminator mismatch" + ); + assert!( + json_obj.get("$action").and_then(|v| v.as_str()).is_some(), + "json inner `$action` discriminator missing" + ); + let recovered_json = BatchedTransition::from_json(json).expect("from_json"); + assert_eq!(transition, recovered_json); + + let value = transition.to_object().expect("to_object"); + let value_map = value.as_map().expect("value map"); + let kv = value_map + .iter() + .find(|(k, _)| matches!(k, platform_value::Value::Text(s) if s == "$transition")) + .expect("$transition key present"); + assert_eq!( + kv.1, + platform_value::Value::Text(expected_transition.to_string()), + "value `$transition` discriminator mismatch" + ); + let recovered_value = BatchedTransition::from_object(value).expect("from_object"); + assert_eq!(transition, recovered_value); + } + + #[test] + fn umbrella_document() { + let inner = DocumentTransition::Create( + document_create_transition::json_convertible_tests::fixture(), + ); + assert_umbrella_round_trip(BatchedTransition::Document(inner), "document"); + } + + #[test] + fn umbrella_token() { + let inner = TokenTransition::Burn(token_burn_transition::json_convertible_tests::fixture()); + assert_umbrella_round_trip(BatchedTransition::Token(inner), "token"); + } +} + #[derive(Debug, From, Clone, Copy, PartialEq, Display)] pub enum BatchedTransitionRef<'a> { #[display("DocumentTransition({})", "_0")] diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_base_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_base_transition/mod.rs index 3f6ab447498..6fe2cd1549c 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_base_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_base_transition/mod.rs @@ -21,13 +21,33 @@ use serde_json::Value as JsonValue; #[cfg(feature = "value-conversion")] use std::collections::BTreeMap; +// Internal tagging with `$baseFormatVersion`. `TokenBaseTransition` is +// `serde(flatten)`'d into every token leaf transition's V0 struct (e.g. +// `TokenBurnTransitionV0::base`); the leaf wrappers themselves use +// `tag = "$formatVersion"`, so a distinct key is required at the same +// flattened level to avoid colliding with the leaf's discriminator. +// The pair (`$formatVersion` on the leaf wrapper, `$baseFormatVersion` on +// the flattened base) keeps the entire transition wire shape flat — +// matching the convention every consumer reads (`tx["$id"]`, +// `tx["$identity-contract-nonce"]`, etc.). #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$baseFormatVersion") +)] pub enum TokenBaseTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(TokenBaseTransitionV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenBaseTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenBaseTransition {} + impl Default for TokenBaseTransition { fn default() -> Self { TokenBaseTransition::V0(TokenBaseTransitionV0::default()) // since only v0 @@ -92,3 +112,85 @@ impl DocumentTransitionObjectLike for TokenBaseTransition { Ok(self.to_value_map()?.into()) } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(super) fn fixture() -> TokenBaseTransition { + TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 13, + token_contract_position: 2, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + // `TokenBaseTransition` has two `to_json`/`from_json`-style impls + // (one from `DocumentTransitionObjectLike`, one from + // `JsonConvertible`); use fully-qualified syntax to disambiguate. + let json = ::to_json(&original).expect("to_json"); + // Externally-tagged enum: outer `V0`. Note the hyphenated rename + // `$identity-contract-nonce` (not camelCase) is intentional here — + // it's the explicit `serde(rename = "$identity-contract-nonce")` on + // the field. `$tokenContractPosition` is `u16`; JSON erases that + // size — see the Value-path assertion for `2u16`. + // `using_group_info` is `Option` flattened; + // when `None`, it contributes no keys to the wire shape. + assert_eq!( + json, + json!({ + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13, + "$tokenContractPosition": 2, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + }) + ); + let recovered = + ::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + // Same disambiguation as the JSON test above; both `to_object` and + // `from_object` are provided by two overlapping traits. + let value = + ::to_object(&original).expect("to_object"); + // `13u64`: `IdentityNonce` is a `u64` alias. `2u16`: + // `token_contract_position` is `u16`; explicit suffix locks in + // `Value::U16`. `Identifier`s interpolate via `Serialize` -> + // `Value::Identifier`. + assert_eq!( + value, + platform_value!({ + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13u64, + "$tokenContractPosition": 2u16, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + }) + ); + let recovered = + ::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_base_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_base_transition/v0/mod.rs index 0c7e123f3c0..5930e5c2957 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_base_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_base_transition/v0/mod.rs @@ -28,6 +28,9 @@ use crate::tokens::errors::TokenError; use crate::{data_contract::DataContract, errors::ProtocolError}; #[derive(Debug, Clone, Encode, Decode, Default, PartialEq, Display)] +// Auto-injects `json_safe_u64` on `identity_contract_nonce: IdentityNonce` +// (= u64) so JSON HR stringifies large values (JS Number precision safety). +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/mod.rs index 09035e402fc..bd0d92f7bdc 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/mod.rs @@ -9,14 +9,108 @@ use serde::{Deserialize, Serialize}; pub use v0::TokenBurnTransitionV0; #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum TokenBurnTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(TokenBurnTransitionV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenBurnTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenBurnTransition {} + impl Default for TokenBurnTransition { fn default() -> Self { TokenBurnTransition::V0(TokenBurnTransitionV0::default()) // since only v0 } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::batched_transition::token_burn_transition::v0::TokenBurnTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> TokenBurnTransition { + TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 13, + token_contract_position: 2, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + burn_amount: 100, + public_note: Some("burning".to_string()), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Doubly-tagged externally enum: outer `V0` for the variant; inner + // `V0` for the flattened token base. `burnAmount` is `u64`; JSON + // erases the size. Hyphenated `$identity-contract-nonce` is the + // explicit serde rename on `TokenBaseTransitionV0::identity_contract_nonce`. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13, + "$tokenContractPosition": 2, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "burnAmount": 100, + "publicNote": "burning", + }) + ); + let recovered = TokenBurnTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `13u64`: `IdentityNonce` is `u64`. `2u16`: token_contract_position + // is `u16`. `100u64`: burn_amount is `u64`. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13u64, + "$tokenContractPosition": 2u16, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "burnAmount": 100u64, + "publicNote": "burning", + }) + ); + let recovered = TokenBurnTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/v0/mod.rs index aea0d49117b..3296b4fef8a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/v0/mod.rs @@ -13,6 +13,7 @@ mod property_names { pub use super::super::document_base_transition::IDENTIFIER_FIELDS; #[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Display)] +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_transition/mod.rs index f34a6133690..1f623cc249b 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_transition/mod.rs @@ -9,14 +9,111 @@ use serde::{Deserialize, Serialize}; pub use v0::TokenClaimTransitionV0; #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum TokenClaimTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(TokenClaimTransitionV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenClaimTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenClaimTransition {} + impl Default for TokenClaimTransition { fn default() -> Self { TokenClaimTransition::V0(TokenClaimTransitionV0::default()) // since only v0 } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::batched_transition::token_claim_transition::v0::TokenClaimTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> TokenClaimTransition { + TokenClaimTransition::V0(TokenClaimTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 13, + token_contract_position: 2, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + distribution_type: TokenDistributionType::PreProgrammed, + public_note: Some("claim".to_string()), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `TokenDistributionType` is a unit-only enum without explicit + // rename — externally-tagged unit variant serializes as the bare + // variant name `"PreProgrammed"`. Field name itself is + // `distributionType` via `rename_all = "camelCase"` on the v0 + // struct. Hyphenated `$identity-contract-nonce` is the explicit + // rename on the inner token base. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13, + "$tokenContractPosition": 2, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "distributionType": "PreProgrammed", + "publicNote": "claim", + }) + ); + let recovered = TokenClaimTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `13u64`/`2u16`: identity_contract_nonce is `u64`, + // token_contract_position is `u16`. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13u64, + "$tokenContractPosition": 2u16, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "distributionType": "PreProgrammed", + "publicNote": "claim", + }) + ); + let recovered = TokenClaimTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_transition/v0/mod.rs index f4b5fff2f9f..11831c506af 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_transition/v0/mod.rs @@ -3,12 +3,15 @@ pub mod v0_methods; /// The Identifier fields in [`TokenClaimTransition`] pub use super::super::document_base_transition::IDENTIFIER_FIELDS; use crate::data_contract::associated_token::token_distribution_key::TokenDistributionType; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; use bincode::{Decode, Encode}; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; use std::fmt; +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, Default, Encode, Decode, PartialEq)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/mod.rs index dad38e44b16..004a7d50f38 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/mod.rs @@ -9,15 +9,118 @@ use serde::{Deserialize, Serialize}; pub use v0::TokenConfigUpdateTransitionV0; #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum TokenConfigUpdateTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(TokenConfigUpdateTransitionV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenConfigUpdateTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenConfigUpdateTransition {} + impl Default for TokenConfigUpdateTransition { fn default() -> Self { TokenConfigUpdateTransition::V0(TokenConfigUpdateTransitionV0::default()) // since only v0 } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::batched_transition::token_config_update_transition::v0::TokenConfigUpdateTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. The fixture uses the + /// `TokenConfigurationNoChange` unit variant of + /// `TokenConfigurationChangeItem` so the inline wire shape stays small; + /// the richer variants are covered by `TokenConfigurationChangeItem`'s + /// own tests. + pub(crate) fn fixture() -> TokenConfigUpdateTransition { + TokenConfigUpdateTransition::V0(TokenConfigUpdateTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 13, + token_contract_position: 2, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + update_token_configuration_item: + TokenConfigurationChangeItem::TokenConfigurationNoChange, + public_note: Some("config update".to_string()), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `TokenConfigurationChangeItem` is now internally tagged, so its unit + // variant `TokenConfigurationNoChange` serializes to the object + // `{"$type":"tokenConfigurationNoChange"}` (not a bare string). + // `updateTokenConfigurationItem` and `publicNote` come from the parent + // struct's camelCase rule. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13, + "$tokenContractPosition": 2, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "updateTokenConfigurationItem": { "$type": "tokenConfigurationNoChange" }, + "publicNote": "config update", + }) + ); + let recovered = TokenConfigUpdateTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `13u64`/`2u16`: identity_contract_nonce is `u64`, + // token_contract_position is `u16`. The unit-variant + // `TokenConfigurationNoChange` is now an internally-tagged map + // `{"$type":"tokenConfigurationNoChange"}`, matching its JSON form. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13u64, + "$tokenContractPosition": 2u16, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "updateTokenConfigurationItem": { "$type": "tokenConfigurationNoChange" }, + "publicNote": "config update", + }) + ); + let recovered = TokenConfigUpdateTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0/mod.rs index c43e5196ec8..9091ee88562 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0/mod.rs @@ -3,12 +3,15 @@ pub mod v0_methods; /// The Identifier fields in [`TokenConfigUpdateTransition`] pub use super::super::document_base_transition::IDENTIFIER_FIELDS; use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; use bincode::{Decode, Encode}; use derive_more::Display; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Display)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/mod.rs index 16be828a1bc..7b731be892f 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/mod.rs @@ -9,15 +9,107 @@ use serde::{Deserialize, Serialize}; pub use v0::TokenDestroyFrozenFundsTransitionV0; #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum TokenDestroyFrozenFundsTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(TokenDestroyFrozenFundsTransitionV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenDestroyFrozenFundsTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenDestroyFrozenFundsTransition {} + impl Default for TokenDestroyFrozenFundsTransition { fn default() -> Self { TokenDestroyFrozenFundsTransition::V0(TokenDestroyFrozenFundsTransitionV0::default()) // since only v0 } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::batched_transition::token_destroy_frozen_funds_transition::v0::TokenDestroyFrozenFundsTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> TokenDestroyFrozenFundsTransition { + TokenDestroyFrozenFundsTransition::V0(TokenDestroyFrozenFundsTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 13, + token_contract_position: 2, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + frozen_identity_id: Identifier::new([0xc3; 32]), + public_note: Some("destroy".to_string()), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `frozenIdentityId` / `publicNote` come from `rename_all = + // "camelCase"` on the v0 struct (no explicit per-field rename). + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13, + "$tokenContractPosition": 2, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "frozenIdentityId": Identifier::new([0xc3; 32]), + "publicNote": "destroy", + }) + ); + let recovered = TokenDestroyFrozenFundsTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `13u64`/`2u16`: identity_contract_nonce is `u64`, + // token_contract_position is `u16`. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13u64, + "$tokenContractPosition": 2u16, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "frozenIdentityId": Identifier::new([0xc3; 32]), + "publicNote": "destroy", + }) + ); + let recovered = TokenDestroyFrozenFundsTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/v0/mod.rs index 9d22c501e0c..b33f2e5f0bf 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/v0/mod.rs @@ -1,5 +1,7 @@ pub mod v0_methods; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; use bincode::{Decode, Encode}; use derive_more::Display; @@ -7,6 +9,7 @@ use platform_value::Identifier; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Display)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_transition/mod.rs index 86368c5eeb8..1583ba5370b 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_transition/mod.rs @@ -17,7 +17,11 @@ pub use v0::TokenDirectPurchaseTransitionV0; /// This transition type is used when a user intends to directly purchase tokens /// by specifying the desired amount and the maximum total price they are willing to pay. #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum TokenDirectPurchaseTransition { /// Version 0 of the token direct purchase transition. /// @@ -26,12 +30,101 @@ pub enum TokenDirectPurchaseTransition { /// If the price in the contract is lower than the agreed price, the lower /// price is used. #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(TokenDirectPurchaseTransitionV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenDirectPurchaseTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenDirectPurchaseTransition {} + impl Default for TokenDirectPurchaseTransition { fn default() -> Self { TokenDirectPurchaseTransition::V0(TokenDirectPurchaseTransitionV0::default()) // since only v0 } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::batched_transition::token_direct_purchase_transition::v0::TokenDirectPurchaseTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> TokenDirectPurchaseTransition { + TokenDirectPurchaseTransition::V0(TokenDirectPurchaseTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 13, + token_contract_position: 2, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + token_count: 100, + total_agreed_price: 999_000, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `tokenCount` / `totalAgreedPrice` come from `rename_all = + // "camelCase"` on the v0 struct. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13, + "$tokenContractPosition": 2, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "tokenCount": 100, + "totalAgreedPrice": 999_000, + }) + ); + let recovered = TokenDirectPurchaseTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `100u64`/`999_000u64`: `TokenAmount` and `Credits` are `u64` + // aliases. `13u64`/`2u16`: identity_contract_nonce is `u64`, + // token_contract_position is `u16`. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13u64, + "$tokenContractPosition": 2u16, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "tokenCount": 100u64, + "totalAgreedPrice": 999_000u64, + }) + ); + let recovered = TokenDirectPurchaseTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_transition/v0/mod.rs index eeaad8b8286..dbaa2fffa47 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_transition/v0/mod.rs @@ -12,6 +12,9 @@ use std::fmt; pub use super::super::document_base_transition::IDENTIFIER_FIELDS; #[derive(Debug, Clone, Default, Encode, Decode, PartialEq)] +// Auto-injects `json_safe_u64` on `token_count: TokenAmount` and +// `total_agreed_price: Credits` (both u64). +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/mod.rs index b38fc418371..529a19443d1 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/mod.rs @@ -9,15 +9,110 @@ use serde::{Deserialize, Serialize}; pub use v0::TokenEmergencyActionTransitionV0; #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum TokenEmergencyActionTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(TokenEmergencyActionTransitionV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenEmergencyActionTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenEmergencyActionTransition {} + impl Default for TokenEmergencyActionTransition { fn default() -> Self { TokenEmergencyActionTransition::V0(TokenEmergencyActionTransitionV0::default()) // since only v0 } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::batched_transition::token_emergency_action_transition::v0::TokenEmergencyActionTransitionV0; + use crate::tokens::emergency_action::TokenEmergencyAction; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> TokenEmergencyActionTransition { + TokenEmergencyActionTransition::V0(TokenEmergencyActionTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 13, + token_contract_position: 2, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + emergency_action: TokenEmergencyAction::Pause, + public_note: Some("pause".to_string()), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `TokenEmergencyAction` has `rename_all = "camelCase"` so unit + // variant `Pause` serializes as the bare string `"pause"`. + // `emergencyAction` / `publicNote` come from `rename_all = + // "camelCase"` on the v0 struct. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13, + "$tokenContractPosition": 2, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "emergencyAction": "pause", + "publicNote": "pause", + }) + ); + let recovered = TokenEmergencyActionTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `13u64`/`2u16`: identity_contract_nonce is `u64`, + // token_contract_position is `u16`. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13u64, + "$tokenContractPosition": 2u16, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "emergencyAction": "pause", + "publicNote": "pause", + }) + ); + let recovered = TokenEmergencyActionTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/v0/mod.rs index 154e432fdf8..58ced445bb9 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/v0/mod.rs @@ -1,5 +1,7 @@ pub mod v0_methods; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; use crate::tokens::emergency_action::TokenEmergencyAction; use bincode::{Decode, Encode}; @@ -7,6 +9,7 @@ use bincode::{Decode, Encode}; use serde::{Deserialize, Serialize}; use std::fmt; +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, Default, Encode, Decode, PartialEq)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/mod.rs index 63b80bfd20a..26b68dca2a7 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/mod.rs @@ -9,14 +9,108 @@ use serde::{Deserialize, Serialize}; pub use v0::TokenFreezeTransitionV0; #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum TokenFreezeTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(TokenFreezeTransitionV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenFreezeTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenFreezeTransition {} + impl Default for TokenFreezeTransition { fn default() -> Self { TokenFreezeTransition::V0(TokenFreezeTransitionV0::default()) // since only v0 } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::batched_transition::token_freeze_transition::v0::TokenFreezeTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> TokenFreezeTransition { + TokenFreezeTransition::V0(TokenFreezeTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 13, + token_contract_position: 2, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + identity_to_freeze_id: Identifier::new([0xc3; 32]), + public_note: Some("freeze".to_string()), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Doubly-tagged externally enum: outer `V0` for the variant; inner + // `V0` for the flattened token base. `frozenIdentityId` is the + // explicit serde rename on `identity_to_freeze_id`. `publicNote` is + // produced by `rename_all = "camelCase"`. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13, + "$tokenContractPosition": 2, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "frozenIdentityId": Identifier::new([0xc3; 32]), + "publicNote": "freeze", + }) + ); + let recovered = TokenFreezeTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `13u64`/`2u16`: identity_contract_nonce is `u64`, + // token_contract_position is `u16`. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13u64, + "$tokenContractPosition": 2u16, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "frozenIdentityId": Identifier::new([0xc3; 32]), + "publicNote": "freeze", + }) + ); + let recovered = TokenFreezeTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/v0/mod.rs index 5572d3a35c6..3bae77be515 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/v0/mod.rs @@ -1,5 +1,7 @@ pub mod v0_methods; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; use bincode::{Decode, Encode}; use platform_value::Identifier; @@ -10,6 +12,7 @@ use std::fmt; /// The Identifier fields in [`TokenFreezeTransition`] pub use super::super::document_base_transition::IDENTIFIER_FIELDS; +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, Default, Encode, Decode, PartialEq)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/mod.rs index 4d3bb99ba00..488a5f46d58 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/mod.rs @@ -9,14 +9,112 @@ use serde::{Deserialize, Serialize}; pub use v0::TokenMintTransitionV0; #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum TokenMintTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(TokenMintTransitionV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenMintTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenMintTransition {} + impl Default for TokenMintTransition { fn default() -> Self { TokenMintTransition::V0(TokenMintTransitionV0::default()) // since only v0 } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::batched_transition::token_mint_transition::v0::TokenMintTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> TokenMintTransition { + TokenMintTransition::V0(TokenMintTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 13, + token_contract_position: 2, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + issued_to_identity_id: Some(Identifier::new([0xc3; 32])), + amount: 5_000, + public_note: Some("minting".to_string()), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Doubly-tagged externally enum: outer `V0` for the variant; inner + // `V0` for the flattened token base. `issuedToIdentityId` is the + // explicit serde rename on `issued_to_identity_id`; `amount`/`publicNote` + // come from `rename_all = "camelCase"`. `amount` is `u64`; JSON + // erases the size — see Value-path assertion below. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13, + "$tokenContractPosition": 2, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "issuedToIdentityId": Identifier::new([0xc3; 32]), + "amount": 5_000, + "publicNote": "minting", + }) + ); + let recovered = TokenMintTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `13u64`/`2u16`/`5_000u64`: explicit suffixes lock in the sized + // variants (`Value::U64` / `Value::U16`) that JSON would erase. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13u64, + "$tokenContractPosition": 2u16, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "issuedToIdentityId": Identifier::new([0xc3; 32]), + "amount": 5_000u64, + "publicNote": "minting", + }) + ); + let recovered = TokenMintTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/v0/mod.rs index 17eb42fd13d..360a59d3454 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/v0/mod.rs @@ -15,6 +15,8 @@ mod property_names { pub use super::super::document_base_transition::IDENTIFIER_FIELDS; #[derive(Debug, Clone, Default, Encode, Decode, PartialEq)] +// Auto-injects `json_safe_u64` on `amount: u64`. +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/mod.rs index 0138ecf7dec..2404380b2dc 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/mod.rs @@ -22,7 +22,11 @@ pub use v0::TokenSetPriceForDirectPurchaseTransitionV0; /// Versioning enables forward compatibility by allowing future enhancements or changes /// without breaking existing clients. #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum TokenSetPriceForDirectPurchaseTransition { /// Version 0 of the token set price for direct purchase transition. /// @@ -34,9 +38,16 @@ pub enum TokenSetPriceForDirectPurchaseTransition { /// Group actions with multisig are supported in this version, /// enabling shared control over token pricing among multiple authorized identities. #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(TokenSetPriceForDirectPurchaseTransitionV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenSetPriceForDirectPurchaseTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenSetPriceForDirectPurchaseTransition {} + impl Default for TokenSetPriceForDirectPurchaseTransition { fn default() -> Self { TokenSetPriceForDirectPurchaseTransition::V0( @@ -44,3 +55,93 @@ impl Default for TokenSetPriceForDirectPurchaseTransition { ) // since only v0 } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::batched_transition::token_set_price_for_direct_purchase_transition::v0::TokenSetPriceForDirectPurchaseTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. `price: None` here exercises + /// the "clear price (no longer purchasable)" wire shape; nested + /// `TokenPricingSchedule` shapes are covered by that type's own tests. + pub(crate) fn fixture() -> TokenSetPriceForDirectPurchaseTransition { + TokenSetPriceForDirectPurchaseTransition::V0(TokenSetPriceForDirectPurchaseTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 13, + token_contract_position: 2, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + price: None, + public_note: Some("clear".to_string()), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Doubly-tagged externally enum: outer `V0` for the variant; inner + // `V0` for the flattened token base. The v0 struct has a stale + // `serde(rename = "issuedToIdentityId")` on the `price` field + // (copy-paste from the mint transition); that rename is the actual + // wire key for `price` and round-trips correctly. `Option<...>::None` + // serializes as `null`. `publicNote` comes from the camelCase rule. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13, + "$tokenContractPosition": 2, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "issuedToIdentityId": serde_json::Value::Null, + "publicNote": "clear", + }) + ); + let recovered = + TokenSetPriceForDirectPurchaseTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `13u64`/`2u16`: identity_contract_nonce is `u64`, + // token_contract_position is `u16`. `Value::Null` for the `None` + // price. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13u64, + "$tokenContractPosition": 2u16, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "issuedToIdentityId": platform_value::Value::Null, + "publicNote": "clear", + }) + ); + let recovered = + TokenSetPriceForDirectPurchaseTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/mod.rs index 3b27d470ee9..b04050ef21f 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/mod.rs @@ -2,6 +2,8 @@ pub mod v0_methods; /// The Identifier fields in [`TokenSetPriceForDirectPurchaseTransition`] pub use super::super::document_base_transition::IDENTIFIER_FIELDS; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; use crate::tokens::token_pricing_schedule::TokenPricingSchedule; use bincode::{Decode, Encode}; @@ -9,6 +11,7 @@ use bincode::{Decode, Encode}; use serde::{Deserialize, Serialize}; use std::fmt; +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, Default, Encode, Decode, PartialEq)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transfer_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transfer_transition/mod.rs index dfc6ce33ef4..bf71e260ad6 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transfer_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transfer_transition/mod.rs @@ -9,8 +9,110 @@ use serde::{Deserialize, Serialize}; pub use v0::*; #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum TokenTransferTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(TokenTransferTransitionV0), } + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenTransferTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenTransferTransition {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::batched_transition::token_transfer_transition::v0::TokenTransferTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> TokenTransferTransition { + TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 14, + token_contract_position: 3, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + amount: 250, + recipient_id: Identifier::new([0xc3; 32]), + public_note: Some("transfer note".to_string()), + shared_encrypted_note: None, + private_encrypted_note: None, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Doubly-tagged externally: outer `V0` for the variant; inner `V0` + // for the flattened token base. `$amount` is `u64`; JSON erases the + // size. Base fields are flattened into the outer object. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 14, + "$tokenContractPosition": 3, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "$amount": 250, + "recipientId": Identifier::new([0xc3; 32]), + "publicNote": "transfer note", + "sharedEncryptedNote": null, + "privateEncryptedNote": null, + }) + ); + let recovered = TokenTransferTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `14u64`: `IdentityNonce` is `u64`. `3u16`: token_contract_position + // is `u16`. `250u64`: amount is `u64`. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 14u64, + "$tokenContractPosition": 3u16, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "$amount": 250u64, + "recipientId": Identifier::new([0xc3; 32]), + "publicNote": "transfer note", + "sharedEncryptedNote": null, + "privateEncryptedNote": null, + }) + ); + let recovered = TokenTransferTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transfer_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transfer_transition/v0/mod.rs index 29b68479a21..27d783aed9a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transfer_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transfer_transition/v0/mod.rs @@ -16,6 +16,15 @@ mod property_names { } #[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Display)] +// `json_safe_fields` auto-injects: +// - `json_safe_u64` on `amount: u64` (JS-safe stringification when large) +// - `json_safe_option_encrypted_note` on `shared_encrypted_note` and +// `private_encrypted_note` — both are `Option<(u32, u32, Vec)>` via +// the `SharedEncryptedNote` / `PrivateEncryptedNote` aliases registered +// in the macro's `ENCRYPTED_NOTE_ALIASES` list. Wire shape: 3-element +// array `[u32, u32, ""]` in JSON HR; raw bytes for the third +// element in non-HR. +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs index 04f92fd9c14..0c6ab3152f9 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs @@ -46,7 +46,16 @@ pub const TOKEN_HISTORY_ID_BYTES: [u8; 32] = [ ]; #[derive(Debug, Clone, Encode, Decode, From, PartialEq, Display)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + // System-field discriminator `$action` — see note on `DocumentTransition` + // for why we don't use `$type` here. Token transitions don't actually + // collide on `$type` (their base struct has no `$type` field), but + // staying on `$action` keeps both umbrellas symmetric so consumers can + // discriminate inner shape with the same key regardless of `$transition`. + serde(tag = "$action", rename_all = "camelCase") +)] pub enum TokenTransition { #[display("TokenBurnTransition({})", "_0")] Burn(TokenBurnTransition), @@ -82,6 +91,159 @@ pub enum TokenTransition { SetPriceForDirectPurchase(TokenSetPriceForDirectPurchaseTransition), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenTransition {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::batched_transition::{ + token_burn_transition, token_claim_transition, token_config_update_transition, + token_destroy_frozen_funds_transition, token_direct_purchase_transition, + token_emergency_action_transition, token_freeze_transition, token_mint_transition, + token_set_price_for_direct_purchase_transition, token_transfer_transition, + token_unfreeze_transition, + }; + + /// Wrapping helper — drives a single `TokenTransition::*` variant through + /// JSON and Value round-trips and asserts the outer wire shape carries + /// `"$type": ` with the inner-leaf fields merged in (internally + /// tagged). Each leaf already has its own per-property assertion test. + fn assert_umbrella_round_trip(transition: TokenTransition, expected_type: &str) { + use crate::serialization::{JsonConvertible, ValueConvertible}; + + let json = transition.to_json().expect("to_json"); + let json_obj = json.as_object().expect("json object"); + assert_eq!( + json_obj.get("$action").and_then(|v| v.as_str()), + Some(expected_type), + "json `type` discriminator mismatch" + ); + let recovered_json = TokenTransition::from_json(json).expect("from_json"); + assert_eq!(transition, recovered_json); + + let value = transition.to_object().expect("to_object"); + let value_map = value.as_map().expect("value map"); + let type_kv = value_map + .iter() + .find(|(k, _)| matches!(k, platform_value::Value::Text(s) if s == "$action")) + .expect("type key present"); + assert_eq!( + type_kv.1, + platform_value::Value::Text(expected_type.to_string()), + "value `type` discriminator mismatch" + ); + let recovered_value = TokenTransition::from_object(value).expect("from_object"); + assert_eq!(transition, recovered_value); + } + + #[test] + fn umbrella_burn() { + assert_umbrella_round_trip( + TokenTransition::Burn(token_burn_transition::json_convertible_tests::fixture()), + "burn", + ); + } + + #[test] + fn umbrella_mint() { + assert_umbrella_round_trip( + TokenTransition::Mint(token_mint_transition::json_convertible_tests::fixture()), + "mint", + ); + } + + #[test] + fn umbrella_transfer() { + assert_umbrella_round_trip( + TokenTransition::Transfer(token_transfer_transition::json_convertible_tests::fixture()), + "transfer", + ); + } + + #[test] + fn umbrella_freeze() { + assert_umbrella_round_trip( + TokenTransition::Freeze(token_freeze_transition::json_convertible_tests::fixture()), + "freeze", + ); + } + + #[test] + fn umbrella_unfreeze() { + assert_umbrella_round_trip( + TokenTransition::Unfreeze(token_unfreeze_transition::json_convertible_tests::fixture()), + "unfreeze", + ); + } + + #[test] + fn umbrella_destroy_frozen_funds() { + assert_umbrella_round_trip( + TokenTransition::DestroyFrozenFunds( + token_destroy_frozen_funds_transition::json_convertible_tests::fixture(), + ), + "destroyFrozenFunds", + ); + } + + #[test] + fn umbrella_claim() { + assert_umbrella_round_trip( + TokenTransition::Claim(token_claim_transition::json_convertible_tests::fixture()), + "claim", + ); + } + + #[test] + fn umbrella_emergency_action() { + assert_umbrella_round_trip( + TokenTransition::EmergencyAction( + token_emergency_action_transition::json_convertible_tests::fixture(), + ), + "emergencyAction", + ); + } + + #[test] + fn umbrella_config_update() { + assert_umbrella_round_trip( + TokenTransition::ConfigUpdate( + token_config_update_transition::json_convertible_tests::fixture(), + ), + "configUpdate", + ); + } + + #[test] + fn umbrella_direct_purchase() { + assert_umbrella_round_trip( + TokenTransition::DirectPurchase( + token_direct_purchase_transition::json_convertible_tests::fixture(), + ), + "directPurchase", + ); + } + + #[test] + fn umbrella_set_price_for_direct_purchase() { + assert_umbrella_round_trip( + TokenTransition::SetPriceForDirectPurchase( + token_set_price_for_direct_purchase_transition::json_convertible_tests::fixture(), + ), + "setPriceForDirectPurchase", + ); + } +} + impl BatchTransitionResolversV0 for TokenTransition { fn as_transition_create(&self) -> Option<&DocumentCreateTransition> { None diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/mod.rs index 107feb83da6..ca16df71ced 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/mod.rs @@ -9,14 +9,108 @@ use serde::{Deserialize, Serialize}; pub use v0::TokenUnfreezeTransitionV0; #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] pub enum TokenUnfreezeTransition { #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(TokenUnfreezeTransitionV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenUnfreezeTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenUnfreezeTransition {} + impl Default for TokenUnfreezeTransition { fn default() -> Self { TokenUnfreezeTransition::V0(TokenUnfreezeTransitionV0::default()) // since only v0 } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::batched_transition::token_unfreeze_transition::v0::TokenUnfreezeTransitionV0; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> TokenUnfreezeTransition { + TokenUnfreezeTransition::V0(TokenUnfreezeTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 13, + token_contract_position: 2, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + frozen_identity_id: Identifier::new([0xc3; 32]), + public_note: Some("unfreeze".to_string()), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Doubly-tagged externally enum: outer `V0` for the variant; inner + // `V0` for the flattened token base. `frozenIdentityId` is the + // explicit serde rename on `frozen_identity_id`. `publicNote` is + // produced by `rename_all = "camelCase"`. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13, + "$tokenContractPosition": 2, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "frozenIdentityId": Identifier::new([0xc3; 32]), + "publicNote": "unfreeze", + }) + ); + let recovered = TokenUnfreezeTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `13u64`/`2u16`: identity_contract_nonce is `u64`, + // token_contract_position is `u16`. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$identity-contract-nonce": 13u64, + "$tokenContractPosition": 2u16, + "$dataContractId": Identifier::new([0xa1; 32]), + "$tokenId": Identifier::new([0xb2; 32]), + + "frozenIdentityId": Identifier::new([0xc3; 32]), + "publicNote": "unfreeze", + }) + ); + let recovered = TokenUnfreezeTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/v0/mod.rs index 6e29702c504..2c6a3d1c90b 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/v0/mod.rs @@ -1,5 +1,7 @@ pub mod v0_methods; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; use bincode::{Decode, Encode}; use platform_value::Identifier; @@ -10,6 +12,7 @@ use std::fmt; /// The Identifier fields in [`TokenUnfreezeTransition`] pub use super::super::document_base_transition::IDENTIFIER_FIELDS; +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, Default, Encode, Decode, PartialEq)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/json_conversion.rs deleted file mode 100644 index 71e854b45ec..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/json_conversion.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::state_transition::batch_transition::BatchTransition; -use crate::state_transition::state_transitions::batch_transition::fields::*; -use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, -}; -use crate::ProtocolError; -use serde_json::Number; -use serde_json::Value as JsonValue; - -impl StateTransitionJsonConvert<'_> for BatchTransition { - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - match self { - BatchTransition::V0(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(0)), - ); - Ok(value) - } - BatchTransition::V1(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(1)), - ); - Ok(value) - } - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs index 13123abfbda..78fd45d2a26 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs @@ -39,8 +39,6 @@ pub mod accessors; pub mod batched_transition; pub mod fields; mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; pub mod methods; pub mod resolvers; mod state_transition_estimated_fee_validation; @@ -49,8 +47,6 @@ mod v0; mod v1; #[cfg(feature = "validation")] mod validation; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; use crate::state_transition::data_contract_update_transition::{ @@ -91,6 +87,12 @@ pub enum BatchTransition { V1(BatchTransitionV1), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for BatchTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for BatchTransition {} + impl StateTransitionFieldTypes for BatchTransition { fn binary_property_paths() -> Vec<&'static str> { vec![SIGNATURE] @@ -105,6 +107,78 @@ impl StateTransitionFieldTypes for BatchTransition { } } +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use platform_value::{platform_value, BinaryData, Identifier}; + use serde_json::json; + + pub(crate) fn fixture() -> BatchTransition { + BatchTransition::V0(BatchTransitionV0 { + owner_id: Identifier::new([0xc0; 32]), + transitions: vec![], // empty transitions list — sub-types tested separately + user_fee_increase: 23, + signature_public_key_id: 4, + signature: BinaryData::new(vec![0xd0; 65]), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `BatchTransition` is internally tagged `$formatVersion`; V0 fields use + // camelCase. `userFeeIncrease` is `u16`, `signaturePublicKeyId` is `u32` + // — JSON has only one number type, so sized variants are erased on the + // wire (the value-path test below pins the typed variants). `Identifier` + // is base58 in JSON HR; `BinaryData` is base64. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "ownerId": "DyRkUpQxYG2VnP2SkMdQs5BTsVPeKCpSHLxzByc2Sxvj", + "transitions": [], + "userFeeIncrease": 23, + "signaturePublicKeyId": 4, + "signature": "0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NA=", + }) + ); + let recovered = BatchTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + use platform_value::Value; + let original = fixture(); + let value = original.to_object().expect("to_object"); + let owner_id = Identifier::new([0xc0; 32]); + // `userFeeIncrease` is `u16` (UserFeeIncrease alias), `signaturePublicKeyId` + // is `u32` (KeyID alias) — explicit suffixes lock in the typed variants. + // `BinaryData` is `Value::Bytes` in non-HR. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "ownerId": owner_id, + "transitions": Value::Array(vec![]), + "userFeeIncrease": 23u16, + "signaturePublicKeyId": 4u32, + "signature": Value::Bytes(vec![0xd0; 65]), + }) + ); + let recovered = BatchTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} + // TODO: Make a DocumentType method pub fn get_security_level_requirement(v: &Value, default: SecurityLevel) -> SecurityLevel { let maybe_security_level: Option = v diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/json_conversion.rs deleted file mode 100644 index 035a37ad021..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::batch_transition::BatchTransitionV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for BatchTransitionV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/mod.rs index 81a48c5cfa9..bb0ba1533c9 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/mod.rs @@ -1,11 +1,7 @@ mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; mod state_transition_like; mod types; mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; use crate::identity::KeyID; @@ -16,10 +12,13 @@ use bincode::{Decode, Encode}; use platform_serialization_derive::PlatformSignable; use crate::prelude::UserFeeIncrease; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use platform_value::{BinaryData, Identifier}; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, PartialEq, Encode, Decode, PlatformSignable)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/value_conversion.rs deleted file mode 100644 index dd33e917732..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/value_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::batch_transition::BatchTransitionV0; -use crate::state_transition::StateTransitionValueConvert; - -impl StateTransitionValueConvert<'_> for BatchTransitionV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/json_conversion.rs deleted file mode 100644 index 38ae2ffaee0..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::batch_transition::BatchTransitionV1; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for BatchTransitionV1 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/mod.rs index c06177d05ba..fbda254cfe0 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/mod.rs @@ -1,12 +1,8 @@ mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; mod state_transition_like; mod types; mod v0_methods; mod v1_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; use crate::identity::KeyID; @@ -17,10 +13,13 @@ use bincode::{Decode, Encode}; use platform_serialization_derive::PlatformSignable; use crate::prelude::UserFeeIncrease; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use platform_value::{BinaryData, Identifier}; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, PartialEq, Encode, Decode, PlatformSignable)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/value_conversion.rs deleted file mode 100644 index b2e4b9775d9..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/value_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::batch_transition::BatchTransitionV1; -use crate::state_transition::StateTransitionValueConvert; - -impl StateTransitionValueConvert<'_> for BatchTransitionV1 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/value_conversion.rs deleted file mode 100644 index 55beda05bdf..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/value_conversion.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::Value; - -use crate::ProtocolError; - -use crate::state_transition::batch_transition::{ - BatchTransition, BatchTransitionV0, BatchTransitionV1, -}; -use crate::state_transition::state_transitions::batch_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_version::version::{FeatureVersion, PlatformVersion}; - -impl StateTransitionValueConvert<'_> for BatchTransition { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - BatchTransition::V0(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - BatchTransition::V1(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - BatchTransition::V0(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - BatchTransition::V1(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - BatchTransition::V0(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - BatchTransition::V1(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - BatchTransition::V0(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - BatchTransition::V1(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(BatchTransitionV0::from_object(raw_object, platform_version)?.into()), - 1 => Ok(BatchTransitionV1::from_object(raw_object, platform_version)?.into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown contract_create_state_transition default_current_version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(BatchTransitionV0::from_value_map(raw_value_map, platform_version)?.into()), - 1 => Ok(BatchTransitionV1::from_value_map(raw_value_map, platform_version)?.into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown contract_create_state_transition default_current_version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => BatchTransitionV0::clean_value(value), - 1 => BatchTransitionV1::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown DataContractCreateTransition version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/json_conversion.rs deleted file mode 100644 index ea01c4fc8d4..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/json_conversion.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::state_transition::identity_create_from_addresses_transition::IdentityCreateFromAddressesTransition; -use crate::state_transition::state_transitions::identity_create_from_addresses_transition::fields::*; -use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, -}; -use crate::ProtocolError; -use serde_json::Number; -use serde_json::Value as JsonValue; - -impl StateTransitionJsonConvert<'_> for IdentityCreateFromAddressesTransition { - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - match self { - IdentityCreateFromAddressesTransition::V0(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(0)), - ); - Ok(value) - } - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/mod.rs index b595c8f3d87..aa427159896 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/mod.rs @@ -1,17 +1,15 @@ pub mod accessors; mod fields; -#[cfg(feature = "json-conversion")] -mod json_conversion; pub mod methods; mod state_transition_estimated_fee_validation; mod state_transition_fee_strategy; mod state_transition_like; mod state_transition_validation; pub mod v0; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; #[cfg(feature = "value-conversion")] use crate::serialization::ValueConvertible; use crate::state_transition::identity_create_from_addresses_transition::v0::IdentityCreateFromAddressesTransitionV0; @@ -77,6 +75,9 @@ impl IdentityCreateFromAddressesTransition { } } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl JsonConvertible for IdentityCreateFromAddressesTransition {} + impl OptionallyAssetLockProved for IdentityCreateFromAddressesTransition {} impl StateTransitionFieldTypes for IdentityCreateFromAddressesTransition { @@ -92,3 +93,172 @@ impl StateTransitionFieldTypes for IdentityCreateFromAddressesTransition { vec![] } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::address_funds::{AddressFundsFeeStrategyStep, AddressWitness, PlatformAddress}; + use crate::identity::{KeyType, Purpose, SecurityLevel}; + use crate::state_transition::identity_create_from_addresses_transition::v0::IdentityCreateFromAddressesTransitionV0; + use crate::state_transition::public_key_in_creation::v0::IdentityPublicKeyInCreationV0; + use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; + use platform_value::{platform_value, BinaryData, Value}; + use serde_json::json; + use std::collections::BTreeMap; + + /// Fixture with NON-DEFAULT values for every field so wire-shape + /// assertions actually exercise data preservation. + pub(crate) fn fixture() -> IdentityCreateFromAddressesTransition { + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([0x11; 20]), (7u32, 1_000_000u64)); + inputs.insert(PlatformAddress::P2sh([0x22; 20]), (3u32, 500_000u64)); + + let public_keys = vec![IdentityPublicKeyInCreation::V0( + IdentityPublicKeyInCreationV0 { + id: 5, + key_type: KeyType::ECDSA_SECP256K1, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::MASTER, + contract_bounds: None, + read_only: false, + data: BinaryData::new(vec![0xab; 33]), + signature: BinaryData::new(vec![0xcd; 65]), + }, + )]; + + let v0 = IdentityCreateFromAddressesTransitionV0 { + public_keys, + inputs, + output: Some((PlatformAddress::P2pkh([0x33; 20]), 250_000)), + fee_strategy: vec![AddressFundsFeeStrategyStep::DeductFromInput(0)], + user_fee_increase: 42, + input_witnesses: vec![ + AddressWitness::P2pkh { + signature: BinaryData::new(vec![0xee; 65]), + }, + AddressWitness::P2sh { + redeem_script: BinaryData::new(vec![0xff; 30]), + signatures: vec![BinaryData::new(vec![0x12; 65])], + }, + ], + }; + IdentityCreateFromAddressesTransition::V0(v0) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int fields lose their size on the JSON wire (single Number type): + // - public key `id` is u32, `type`/`purpose`/`securityLevel` are u8 enums, + // - `inputs[].nonce` is u32, `inputs[].amount` / `output.amount` are u64, + // - `feeStrategy[].index` is u16, `userFeeIncrease` is u16. + // The Value-path test below locks the typed variants. `BinaryData` is + // base64 in JSON, `Value::Bytes` in non-HR. `PlatformAddress` is hex + // string in JSON (1 type byte + 20 hash bytes), raw bytes in Value. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "publicKeys": [ + { + "$formatVersion": "0", + "id": 5, + "type": 0, + "purpose": 0, + "securityLevel": 0, + "contractBounds": null, + "readOnly": false, + "data": "q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6ur", + "signature": "zc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc0=", + }, + ], + "inputs": [ + {"address": "001111111111111111111111111111111111111111", "nonce": 7, "amount": 1_000_000}, + {"address": "012222222222222222222222222222222222222222", "nonce": 3, "amount": 500_000}, + ], + "output": {"address": "003333333333333333333333333333333333333333", "amount": 250_000}, + "feeStrategy": [{"$type": "deductFromInput", "index": 0}], + "userFeeIncrease": 42, + "inputWitnesses": [ + { + "$type": "p2pkh", + "signature": "7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u4=", + }, + { + "$type": "p2sh", + "signatures": [ + "EhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhI=", + ], + "redeemScript": "////////////////////////////////////////", + }, + ], + }) + ); + let recovered = IdentityCreateFromAddressesTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // PlatformAddress emits 21-byte raw bytes (1 type byte + 20-byte hash) in + // non-HR. KeyType/Purpose/SecurityLevel are #[repr(u8)] with u8 wire. + // `id` is u32 (KeyID), `nonce` is u32 (AddressNonce), `amount` is u64 + // (Credits), `index` is u16, `userFeeIncrease` is u16. + let mut p2pkh11_bytes = vec![0x00u8]; + p2pkh11_bytes.extend_from_slice(&[0x11u8; 20]); + let mut p2sh22_bytes = vec![0x01u8]; + p2sh22_bytes.extend_from_slice(&[0x22u8; 20]); + let mut p2pkh33_bytes = vec![0x00u8]; + p2pkh33_bytes.extend_from_slice(&[0x33u8; 20]); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "publicKeys": [ + { + "$formatVersion": "0", + "id": 5u32, + "type": 0u8, + "purpose": 0u8, + "securityLevel": 0u8, + "contractBounds": Value::Null, + "readOnly": false, + "data": Value::Bytes(vec![0xab; 33]), + "signature": Value::Bytes(vec![0xcd; 65]), + }, + ], + "inputs": [ + {"address": Value::Bytes(p2pkh11_bytes), "nonce": 7u32, "amount": 1_000_000u64}, + {"address": Value::Bytes(p2sh22_bytes), "nonce": 3u32, "amount": 500_000u64}, + ], + "output": {"address": Value::Bytes(p2pkh33_bytes), "amount": 250_000u64}, + "feeStrategy": [{"$type": "deductFromInput", "index": 0u16}], + "userFeeIncrease": 42u16, + "inputWitnesses": [ + { + "$type": "p2pkh", + "signature": Value::Bytes(vec![0xee; 65]), + }, + { + "$type": "p2sh", + "signatures": [Value::Bytes(vec![0x12; 65])], + "redeemScript": Value::Bytes(vec![0xff; 30]), + }, + ], + }) + ); + let recovered = + IdentityCreateFromAddressesTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/json_conversion.rs deleted file mode 100644 index 587ff896fe4..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::identity_create_from_addresses_transition::v0::IdentityCreateFromAddressesTransitionV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for IdentityCreateFromAddressesTransitionV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/mod.rs index 2d32a138cbc..a023e9aae62 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/mod.rs @@ -1,11 +1,7 @@ -#[cfg(feature = "json-conversion")] -mod json_conversion; mod state_transition_like; mod state_transition_validation; mod types; pub(super) mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; use std::collections::BTreeMap; @@ -16,12 +12,15 @@ use platform_serialization_derive::PlatformSignable; use crate::address_funds::{AddressFundsFeeStrategy, AddressWitness, PlatformAddress}; use crate::fee::Credits; use crate::prelude::{AddressNonce, UserFeeIncrease}; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreationSignable; use crate::ProtocolError; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, PartialEq, Encode, Decode, PlatformSignable)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/value_conversion.rs deleted file mode 100644 index 619a29432cc..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/v0/value_conversion.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::btreemap_extensions::BTreeValueRemoveInnerValueFromMapHelper; -use platform_value::{IntegerReplacementType, ReplacementType, Value}; - -use crate::{state_transition::StateTransitionFieldTypes, ProtocolError}; - -use crate::address_funds::{AddressWitness, PlatformAddress}; -use crate::fee::Credits; -use crate::prelude::AddressNonce; -use crate::state_transition::identity_create_from_addresses_transition::accessors::IdentityCreateFromAddressesTransitionAccessorsV0; -use crate::state_transition::identity_create_from_addresses_transition::fields::*; -use crate::state_transition::identity_create_from_addresses_transition::v0::IdentityCreateFromAddressesTransitionV0; -use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; -use crate::state_transition::StateTransitionValueConvert; - -use crate::identity::property_names::PUBLIC_KEYS; -use platform_version::version::PlatformVersion; - -impl StateTransitionValueConvert<'_> for IdentityCreateFromAddressesTransitionV0 { - fn from_object( - raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let mut state_transition = Self::default(); - - let mut transition_map = raw_object - .into_btree_string_map() - .map_err(ProtocolError::ValueError)?; - - // Parse public keys - if let Some(keys_value_array) = transition_map - .remove_optional_inner_value_array::>(PUBLIC_KEYS) - .map_err(ProtocolError::ValueError)? - { - let keys = keys_value_array - .into_iter() - .map(|val| IdentityPublicKeyInCreation::from_object(val, platform_version)) - .collect::, ProtocolError>>()?; - state_transition.set_public_keys(keys); - } - - // Parse inputs - if let Some(inputs_value) = transition_map.remove(INPUTS) { - let inputs: BTreeMap = - platform_value::from_value(inputs_value)?; - state_transition.inputs = inputs; - } - - // Parse user fee increase - if let Some(user_fee_increase_value) = transition_map.remove(USER_FEE_INCREASE) { - state_transition.user_fee_increase = - platform_value::from_value(user_fee_increase_value)?; - } - - // Parse input witnesses - if let Some(witnesses_value) = transition_map - .remove_optional_inner_value_array::>(INPUT_WITNESSES) - .map_err(ProtocolError::ValueError)? - { - let witnesses = witnesses_value - .into_iter() - .map(platform_value::from_value) - .collect::, _>>()?; - state_transition.input_witnesses = witnesses; - } - - Ok(state_transition) - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; - value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; - value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; - Ok(()) - } - - fn from_value_map( - raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let value: Value = raw_value_map.into(); - Self::from_object(value, platform_version) - } - - fn to_object(&self, skip_signature: bool) -> Result { - let mut value: Value = platform_value::to_value(self)?; - - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - - let mut public_keys: Vec = vec![]; - for key in self.public_keys.iter() { - public_keys.push(key.to_object(skip_signature)?); - } - - value.insert(PUBLIC_KEYS.to_owned(), Value::Array(public_keys))?; - - Ok(value) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - let mut value: Value = platform_value::to_value(self)?; - - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - - let mut public_keys: Vec = vec![]; - for key in self.public_keys.iter() { - public_keys.push(key.to_cleaned_object(skip_signature)?); - } - - value.insert(PUBLIC_KEYS.to_owned(), Value::Array(public_keys))?; - - Ok(value) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/value_conversion.rs deleted file mode 100644 index da6daa0716e..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_from_addresses_transition/value_conversion.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::Value; - -use crate::ProtocolError; - -use crate::state_transition::identity_create_from_addresses_transition::v0::IdentityCreateFromAddressesTransitionV0; -use crate::state_transition::identity_create_from_addresses_transition::IdentityCreateFromAddressesTransition; -use crate::state_transition::state_transitions::identity_create_from_addresses_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_version::version::{FeatureVersion, PlatformVersion}; - -impl StateTransitionValueConvert<'_> for IdentityCreateFromAddressesTransition { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreateFromAddressesTransition::V0(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreateFromAddressesTransition::V0(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreateFromAddressesTransition::V0(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreateFromAddressesTransition::V0(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(IdentityCreateFromAddressesTransitionV0::from_object( - raw_object, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityCreateFromAddressesTransition version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(IdentityCreateFromAddressesTransitionV0::from_value_map( - raw_value_map, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityCreateFromAddressesTransition version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => IdentityCreateFromAddressesTransitionV0::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityCreateFromAddressesTransition version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/json_conversion.rs deleted file mode 100644 index d8ea2797b0b..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/json_conversion.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::state_transition::identity_create_transition::IdentityCreateTransition; -use crate::state_transition::state_transitions::identity_create_transition::fields::*; -use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, -}; -use crate::ProtocolError; -use serde_json::Number; -use serde_json::Value as JsonValue; - -impl StateTransitionJsonConvert<'_> for IdentityCreateTransition { - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - match self { - IdentityCreateTransition::V0(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(0)), - ); - Ok(value) - } - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/mod.rs index 245c3469c83..50f1ae9938f 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/mod.rs @@ -1,14 +1,10 @@ pub mod accessors; mod fields; -#[cfg(feature = "json-conversion")] -mod json_conversion; pub mod methods; pub mod proved; mod state_transition_estimated_fee_validation; mod state_transition_like; pub mod v0; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; #[cfg(feature = "json-conversion")] @@ -212,3 +208,119 @@ mod test { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + + use crate::tests::fixtures::instant_asset_lock_proof_fixture; + use platform_value::BinaryData; + + // Tier 4: `instant_asset_lock_proof_fixture` produces NON-DETERMINISTIC bytes + // (transaction / instantLock include random per-run content). The full inline + // wire shape would change between runs, so wire-shape assertions stay envelope- + // only on the asset_lock_proof field, with deterministic siblings asserted + // literally. + pub(crate) fn fixture() -> IdentityCreateTransition { + let asset_lock_proof = instant_asset_lock_proof_fixture(None, None); + // identity_id is `serde(skip)` and reconstructed from the proof on deserialize + // (see IdentityCreateTransitionV0::try_from(IdentityCreateTransitionV0Inner)). + // Match what `create_identifier()` would produce so round-trip is identity. + let identity_id = asset_lock_proof + .create_identifier() + .expect("identity_id from proof"); + IdentityCreateTransition::V0(IdentityCreateTransitionV0 { + public_keys: vec![], + asset_lock_proof, + user_fee_increase: 7, + signature: BinaryData::new(vec![0xa1; 65]), + identity_id, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Envelope assertions: top-level keys + deterministic primitives. + // `assetLockProof` is non-deterministic (random tx bytes); only its + // discriminator is checked. + let obj = json.as_object().expect("json is an object"); + assert_eq!(obj.get("$formatVersion"), Some(&serde_json::json!("0"))); + assert_eq!(obj.get("publicKeys"), Some(&serde_json::json!([]))); + // `userFeeIncrease` is `u16` (UserFeeIncrease) in the source type. JSON has + // only one number type, so the size is erased on the wire — the value-path + // assertion below uses `7u16` to lock in the typed variant. + assert_eq!(obj.get("userFeeIncrease"), Some(&serde_json::json!(7))); + // 65-byte signature serialized as base64 (BinaryData) + assert_eq!( + obj.get("signature"), + Some(&serde_json::json!( + "oaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaE=" + )) + ); + // assetLockProof envelope + let proof = obj + .get("assetLockProof") + .and_then(|v| v.as_object()) + .expect("assetLockProof is an object"); + assert_eq!(proof.get("$type"), Some(&serde_json::json!("instant"))); + assert_eq!(proof.get("outputIndex"), Some(&serde_json::json!(0))); + assert!(proof.get("instantLock").is_some_and(|v| v.is_string())); + assert!(proof.get("transaction").is_some_and(|v| v.is_string())); + let recovered = IdentityCreateTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Envelope: keys + deterministic primitive variants (sized). + let map = value.as_map().expect("value is a map"); + let get = |key: &str| { + map.iter() + .find(|(k, _)| k.as_text() == Some(key)) + .map(|(_, v)| v) + }; + assert_eq!( + get("$formatVersion"), + Some(&platform_value::Value::Text("0".to_string())) + ); + assert_eq!( + get("publicKeys"), + Some(&platform_value::Value::Array(vec![])) + ); + // `7u16`: UserFeeIncrease is `u16`, so the value-path preserves U16. + assert_eq!(get("userFeeIncrease"), Some(&platform_value::Value::U16(7))); + assert_eq!( + get("signature"), + Some(&platform_value::Value::Bytes(vec![0xa1; 65])) + ); + let proof = get("assetLockProof") + .and_then(|v| v.as_map()) + .expect("assetLockProof is a map"); + let pget = |key: &str| { + proof + .iter() + .find(|(k, _)| k.as_text() == Some(key)) + .map(|(_, v)| v) + }; + assert_eq!( + pget("$type"), + Some(&platform_value::Value::Text("instant".to_string())) + ); + assert_eq!(pget("outputIndex"), Some(&platform_value::Value::U32(0))); + assert!(pget("instantLock").is_some_and(|v| matches!(v, platform_value::Value::Bytes(_)))); + assert!(pget("transaction").is_some_and(|v| matches!(v, platform_value::Value::Bytes(_)))); + let recovered = IdentityCreateTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/json_conversion.rs deleted file mode 100644 index f3f037fe269..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::identity_create_transition::v0::IdentityCreateTransitionV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for IdentityCreateTransitionV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/mod.rs index 5108230d276..11b90ce3c09 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/mod.rs @@ -1,11 +1,7 @@ -#[cfg(feature = "json-conversion")] -mod json_conversion; mod proved; mod state_transition_like; mod types; pub(super) mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; #[cfg(feature = "json-conversion")] @@ -224,30 +220,10 @@ mod test { assert!(t.public_keys().is_empty()); } - #[test] - fn test_to_object_produces_value() { - use crate::state_transition::StateTransitionValueConvert; - let t = make_create_v0(); - let obj = t.to_object(false).expect("to_object should work"); - assert!(obj.is_map()); - } - - #[test] - fn test_value_conversion_skip_signature() { - use crate::state_transition::StateTransitionValueConvert; - let t = make_create_v0(); - let obj = t.to_object(true).expect("to_object should work"); - let map = obj.into_btree_string_map().expect("should be a map"); - assert!(!map.contains_key("signature")); - } - - #[test] - fn test_to_cleaned_object() { - use crate::state_transition::StateTransitionValueConvert; - let t = make_create_v0(); - let obj = t.to_cleaned_object(false).expect("should work"); - assert!(obj.is_map()); - } + // Legacy `StateTransitionValueConvert` round-trip tests deleted in + // Phase D step 9. The canonical `JsonConvertible` / `ValueConvertible` + // round-trip is exercised via the outer enum derive — these tested + // methods that no longer exist. fn chain_proof() -> AssetLockProof { use crate::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/value_conversion.rs deleted file mode 100644 index 4a5ffe4047c..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/v0/value_conversion.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::collections::BTreeMap; -use std::convert::TryFrom; - -use platform_value::btreemap_extensions::{ - BTreeValueMapHelper, BTreeValueRemoveInnerValueFromMapHelper, -}; -use platform_value::{IntegerReplacementType, ReplacementType, Value}; - -use crate::{state_transition::StateTransitionFieldTypes, ProtocolError}; - -use crate::prelude::AssetLockProof; - -use crate::identity::state_transition::AssetLockProved; -use crate::state_transition::identity_create_transition::accessors::IdentityCreateTransitionAccessorsV0; -use crate::state_transition::identity_create_transition::fields::*; -use crate::state_transition::identity_create_transition::v0::IdentityCreateTransitionV0; -use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; -use crate::state_transition::{StateTransitionSingleSigned, StateTransitionValueConvert}; - -use platform_version::version::PlatformVersion; - -impl StateTransitionValueConvert<'_> for IdentityCreateTransitionV0 { - fn from_object( - raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let mut state_transition = Self::default(); - - let mut transition_map = raw_object - .into_btree_string_map() - .map_err(ProtocolError::ValueError)?; - if let Some(keys_value_array) = transition_map - .remove_optional_inner_value_array::>(PUBLIC_KEYS) - .map_err(ProtocolError::ValueError)? - { - let keys = keys_value_array - .into_iter() - .map(|val| IdentityPublicKeyInCreation::from_object(val, platform_version)) - .collect::, ProtocolError>>()?; - state_transition.set_public_keys(keys); - } - - if let Some(proof) = transition_map.get(ASSET_LOCK_PROOF) { - state_transition.set_asset_lock_proof(AssetLockProof::try_from(proof)?)?; - } - - if let Some(signature) = transition_map.get_optional_binary_data(SIGNATURE)? { - state_transition.set_signature(signature); - } - - Ok(state_transition) - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; - value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; - value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; - Ok(()) - } - - fn from_value_map( - raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let value: Value = raw_value_map.into(); - Self::from_object(value, platform_version) - } - - fn to_object(&self, skip_signature: bool) -> Result { - let mut value: Value = platform_value::to_value(self)?; - - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - - let mut public_keys: Vec = vec![]; - for key in self.public_keys.iter() { - public_keys.push(key.to_object(skip_signature)?); - } - - value.insert(PUBLIC_KEYS.to_owned(), Value::Array(public_keys))?; - - Ok(value) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - let mut value: Value = platform_value::to_value(self)?; - - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - - let mut public_keys: Vec = vec![]; - for key in self.public_keys.iter() { - public_keys.push(key.to_cleaned_object(skip_signature)?); - } - - value.insert(PUBLIC_KEYS.to_owned(), Value::Array(public_keys))?; - - Ok(value) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/value_conversion.rs deleted file mode 100644 index 8b2b462a7c2..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_create_transition/value_conversion.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::Value; - -use crate::ProtocolError; - -use crate::state_transition::identity_create_transition::v0::IdentityCreateTransitionV0; -use crate::state_transition::identity_create_transition::IdentityCreateTransition; -use crate::state_transition::state_transitions::identity_create_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_version::version::{FeatureVersion, PlatformVersion}; - -impl StateTransitionValueConvert<'_> for IdentityCreateTransition { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreateTransition::V0(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreateTransition::V0(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreateTransition::V0(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreateTransition::V0(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(IdentityCreateTransitionV0::from_object(raw_object, platform_version)?.into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityCreateTransition version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok( - IdentityCreateTransitionV0::from_value_map(raw_value_map, platform_version)?.into(), - ), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityCreateTransition version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => IdentityCreateTransitionV0::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityCreateTransition version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/json_conversion.rs deleted file mode 100644 index cb76b4a2161..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/json_conversion.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::state_transition::identity_credit_transfer_to_addresses_transition::IdentityCreditTransferToAddressesTransition; -use crate::state_transition::state_transitions::identity_credit_transfer_to_addresses_transition::fields::*; -use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, -}; -use crate::ProtocolError; -use serde_json::Number; -use serde_json::Value as JsonValue; - -impl StateTransitionJsonConvert<'_> for IdentityCreditTransferToAddressesTransition { - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - match self { - IdentityCreditTransferToAddressesTransition::V0(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(0)), - ); - Ok(value) - } - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/mod.rs index 56663ab4391..714db181a71 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/mod.rs @@ -1,17 +1,15 @@ pub mod accessors; pub mod fields; mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; pub mod methods; mod state_transition_estimated_fee_validation; mod state_transition_like; mod state_transition_validation; pub mod v0; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; #[cfg(feature = "value-conversion")] use crate::serialization::ValueConvertible; use crate::state_transition::identity_credit_transfer_to_addresses_transition::fields::property_names::RECIPIENT_ID; @@ -80,6 +78,9 @@ impl IdentityCreditTransferToAddressesTransition { } } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl JsonConvertible for IdentityCreditTransferToAddressesTransition {} + impl OptionallyAssetLockProved for IdentityCreditTransferToAddressesTransition {} impl StateTransitionFieldTypes for IdentityCreditTransferToAddressesTransition { @@ -95,3 +96,98 @@ impl StateTransitionFieldTypes for IdentityCreditTransferToAddressesTransition { vec![] } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::address_funds::PlatformAddress; + use crate::state_transition::identity_credit_transfer_to_addresses_transition::v0::IdentityCreditTransferToAddressesTransitionV0; + use platform_value::{platform_value, BinaryData, Identifier, Value}; + use serde_json::json; + use std::collections::BTreeMap; + + pub(crate) fn fixture() -> IdentityCreditTransferToAddressesTransition { + let mut recipient_addresses = BTreeMap::new(); + recipient_addresses.insert(PlatformAddress::P2pkh([0x88; 20]), 50_000u64); + recipient_addresses.insert(PlatformAddress::P2sh([0x99; 20]), 25_000u64); + + let v0 = IdentityCreditTransferToAddressesTransitionV0 { + identity_id: Identifier::new([0xaa; 32]), + recipient_addresses, + nonce: 13, + user_fee_increase: 5, + signature_public_key_id: 2, + signature: BinaryData::new(vec![0xbb; 65]), + }; + IdentityCreditTransferToAddressesTransition::V0(v0) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int fields lose their size on the JSON wire (single Number type): + // - `nonce` is u64 (IdentityNonce), `userFeeIncrease` is u16, + // - `signaturePublicKeyId` is u32 (KeyID), + // - `recipientAddresses[].amount` is u64 (Credits). + // The Value-path test below locks the typed variants. `Identifier` is + // base58 in JSON HR. `BinaryData` is base64. `PlatformAddress` is hex + // (1 type byte + 20 hash bytes). + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "identityId": "CVDFLCAjXhVWiPXH9nTCTpCgVzmDVoiPzNJYuccr1dqB", + "recipientAddresses": [ + {"address": "008888888888888888888888888888888888888888", "amount": 50_000}, + {"address": "019999999999999999999999999999999999999999", "amount": 25_000}, + ], + "nonce": 13, + "userFeeIncrease": 5, + "signaturePublicKeyId": 2, + "signature": "u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7s=", + }) + ); + let recovered = + IdentityCreditTransferToAddressesTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // PlatformAddress is 21-byte raw bytes (1 type byte + 20-byte hash) in + // non-HR. `nonce` is u64, `userFeeIncrease` u16, `signaturePublicKeyId` u32. + let identity_id = Identifier::new([0xaa; 32]); + let mut p2pkh88 = vec![0x00u8]; + p2pkh88.extend_from_slice(&[0x88u8; 20]); + let mut p2sh99 = vec![0x01u8]; + p2sh99.extend_from_slice(&[0x99u8; 20]); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "identityId": identity_id, + "recipientAddresses": [ + {"address": Value::Bytes(p2pkh88), "amount": 50_000u64}, + {"address": Value::Bytes(p2sh99), "amount": 25_000u64}, + ], + "nonce": 13u64, + "userFeeIncrease": 5u16, + "signaturePublicKeyId": 2u32, + "signature": Value::Bytes(vec![0xbb; 65]), + }) + ); + let recovered = + IdentityCreditTransferToAddressesTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/json_conversion.rs deleted file mode 100644 index 1bd99a6afec..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::identity_credit_transfer_to_addresses_transition::v0::IdentityCreditTransferToAddressesTransitionV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for IdentityCreditTransferToAddressesTransitionV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/mod.rs index b1f611057ff..9f25c87f49e 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/mod.rs @@ -1,12 +1,8 @@ mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; mod state_transition_like; mod state_transition_validation; mod types; pub(super) mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; use crate::address_funds::PlatformAddress; diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/value_conversion.rs deleted file mode 100644 index 8ac59a6df5c..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/value_conversion.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::{IntegerReplacementType, ReplacementType, Value}; - -use crate::{state_transition::StateTransitionFieldTypes, ProtocolError}; - -use crate::state_transition::identity_credit_transfer_to_addresses_transition::fields::*; -use crate::state_transition::identity_credit_transfer_to_addresses_transition::v0::IdentityCreditTransferToAddressesTransitionV0; -use crate::state_transition::StateTransitionValueConvert; - -use platform_version::version::PlatformVersion; - -impl StateTransitionValueConvert<'_> for IdentityCreditTransferToAddressesTransitionV0 { - fn from_object( - raw_object: Value, - _platform_version: &PlatformVersion, - ) -> Result { - platform_value::from_value(raw_object).map_err(ProtocolError::ValueError) - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; - value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; - value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; - Ok(()) - } - - fn from_value_map( - raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let value: Value = raw_value_map.into(); - Self::from_object(value, platform_version) - } - - fn to_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - // Override to_canonical_cleaned_object to manage add_public_keys individually - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - self.to_cleaned_object(skip_signature) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/value_conversion.rs deleted file mode 100644 index 02b520e7b3d..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/value_conversion.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::Value; - -use crate::ProtocolError; - -use crate::state_transition::identity_credit_transfer_to_addresses_transition::v0::IdentityCreditTransferToAddressesTransitionV0; -use crate::state_transition::identity_credit_transfer_to_addresses_transition::IdentityCreditTransferToAddressesTransition; -use crate::state_transition::state_transitions::identity_credit_transfer_to_addresses_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_version::version::{FeatureVersion, PlatformVersion}; - -impl StateTransitionValueConvert<'_> for IdentityCreditTransferToAddressesTransition { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreditTransferToAddressesTransition::V0(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreditTransferToAddressesTransition::V0(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreditTransferToAddressesTransition::V0(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreditTransferToAddressesTransition::V0(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(IdentityCreditTransferToAddressesTransitionV0::from_object( - raw_object, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityCreditTransferToAddressesTransition version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok( - IdentityCreditTransferToAddressesTransitionV0::from_value_map( - raw_value_map, - platform_version, - )? - .into(), - ), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityCreditTransferToAddressesTransition version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => IdentityCreditTransferToAddressesTransitionV0::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityCreditTransferToAddressesTransition version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/json_conversion.rs deleted file mode 100644 index 09f62ade27b..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/json_conversion.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::state_transition::identity_credit_transfer_transition::IdentityCreditTransferTransition; -use crate::state_transition::state_transitions::identity_credit_transfer_transition::fields::*; -use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, -}; -use crate::ProtocolError; -use serde_json::Number; -use serde_json::Value as JsonValue; - -impl StateTransitionJsonConvert<'_> for IdentityCreditTransferTransition { - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - match self { - IdentityCreditTransferTransition::V0(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(0)), - ); - Ok(value) - } - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/mod.rs index 9f15a4999fb..ccb686e8aa6 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/mod.rs @@ -1,14 +1,10 @@ pub mod accessors; pub mod fields; mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; pub mod methods; mod state_transition_estimated_fee_validation; mod state_transition_like; pub mod v0; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; #[cfg(feature = "json-conversion")] @@ -107,10 +103,10 @@ mod test { use crate::state_transition::{ StateTransitionEstimatedFeeValidation, StateTransitionHasUserFeeIncrease, StateTransitionIdentityEstimatedFeeValidation, StateTransitionLike, StateTransitionOwned, - StateTransitionSingleSigned, StateTransitionType, StateTransitionValueConvert, + StateTransitionSingleSigned, StateTransitionType, }; use crate::version::LATEST_PLATFORM_VERSION; - use platform_value::{BinaryData, Identifier, Value}; + use platform_value::{BinaryData, Identifier}; fn make_transfer() -> IdentityCreditTransferTransition { IdentityCreditTransferTransition::V0(IdentityCreditTransferTransitionV0 { @@ -212,77 +208,11 @@ mod test { assert!(bin_paths.is_empty()); } - #[test] - fn test_value_conversion_roundtrip() { - let transition = make_transfer(); - let obj = StateTransitionValueConvert::to_object(&transition, false) - .expect("to_object should work"); - let restored = - ::from_object( - obj, - LATEST_PLATFORM_VERSION, - ) - .expect("from_object should work"); - assert_eq!(transition, restored); - } - - #[test] - fn test_from_value_map_roundtrip() { - let transition = make_transfer(); - let obj = StateTransitionValueConvert::to_object(&transition, false) - .expect("to_object should work"); - let map = obj.into_btree_string_map().expect("should convert to map"); - let restored = - ::from_value_map( - map, - LATEST_PLATFORM_VERSION, - ) - .expect("from_value_map should work"); - assert_eq!(transition, restored); - } - - #[test] - fn test_to_cleaned_object() { - let transition = make_transfer(); - let obj = StateTransitionValueConvert::to_cleaned_object(&transition, false) - .expect("should work"); - assert!(obj.is_map()); - } - - #[test] - fn test_to_canonical_cleaned_object() { - let transition = make_transfer(); - let obj = StateTransitionValueConvert::to_canonical_cleaned_object(&transition, false) - .expect("should work"); - assert!(obj.is_map()); - } - - #[test] - fn test_to_object_skip_signature() { - let transition = make_transfer(); - let obj = StateTransitionValueConvert::to_object(&transition, true).expect("should work"); - let map = obj.into_btree_string_map().expect("should be a map"); - assert!(!map.contains_key("signature")); - } - - #[test] - fn test_clean_value_unknown_version() { - let mut value = Value::from([("$stateTransitionProtocolVersion", Value::U8(255))]); - let result = ::clean_value( - &mut value, - ); - assert!(result.is_err()); - } - - #[test] - fn test_from_object_unknown_version() { - let value = Value::from([("$stateTransitionProtocolVersion", Value::U16(255))]); - let result = ::from_object( - value, - LATEST_PLATFORM_VERSION, - ); - assert!(result.is_err()); - } + // Legacy `StateTransitionValueConvert` round-trip and + // unknown-version tests deleted in Phase D step 9. The canonical + // `JsonConvertible` / `ValueConvertible` round-trip is exercised on + // the outer enum derive (see `json_convertible_tests` below) — these + // tested methods that no longer exist. #[test] fn test_estimated_fee_validation_sufficient() { @@ -323,3 +253,78 @@ mod test { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + + use platform_value::{platform_value, BinaryData, Identifier}; + use serde_json::json; + + pub(crate) fn fixture() -> IdentityCreditTransferTransition { + IdentityCreditTransferTransition::V0(IdentityCreditTransferTransitionV0 { + identity_id: Identifier::new([0x11; 32]), + recipient_id: Identifier::new([0x22; 32]), + amount: 1_234_567, + nonce: 42, + user_fee_increase: 7, + signature_public_key_id: 3, + signature: BinaryData::new(vec![0xa1; 65]), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int fields whose wire encoding loses size info (JSON has only one + // number type): `nonce` (u64), `userFeeIncrease` (u16), + // `signaturePublicKeyId` (u32). The value-path assertion below uses the + // explicit suffixes to lock in the typed variants. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "identityId": Identifier::new([0x11; 32]), + "recipientId": Identifier::new([0x22; 32]), + "amount": 1_234_567, + "nonce": 42, + "userFeeIncrease": 7, + "signaturePublicKeyId": 3, + "signature": "oaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaE=", + }) + ); + let recovered = IdentityCreditTransferTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Explicit suffixes lock in sized variants: `nonce` u64, `userFeeIncrease` + // u16, `signaturePublicKeyId` u32 (KeyID alias). + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "identityId": Identifier::new([0x11; 32]), + "recipientId": Identifier::new([0x22; 32]), + "amount": 1_234_567u64, + "nonce": 42u64, + "userFeeIncrease": 7u16, + "signaturePublicKeyId": 3u32, + "signature": BinaryData::new(vec![0xa1; 65]), + }) + ); + let recovered = IdentityCreditTransferTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/json_conversion.rs deleted file mode 100644 index 35e47a44aba..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::identity_credit_transfer_transition::v0::IdentityCreditTransferTransitionV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for IdentityCreditTransferTransitionV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/mod.rs index 9a62843b924..7e12c7d0d42 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/mod.rs @@ -1,11 +1,7 @@ mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; mod state_transition_like; mod types; pub(super) mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; use crate::identity::KeyID; @@ -177,62 +173,10 @@ mod test { } } - #[test] - fn test_value_conversion_roundtrip_v0() { - use crate::state_transition::StateTransitionValueConvert; - use crate::version::LATEST_PLATFORM_VERSION; - let transition = make_transfer_v0(); - let obj = transition.to_object(false).expect("to_object should work"); - let restored = - IdentityCreditTransferTransitionV0::from_object(obj, LATEST_PLATFORM_VERSION) - .expect("from_object should work"); - assert_eq!(transition, restored); - } - - #[test] - fn test_value_conversion_skip_signature_v0() { - use crate::state_transition::StateTransitionValueConvert; - let transition = make_transfer_v0(); - let obj = transition.to_object(true).expect("to_object should work"); - // The signature field should have been removed - let map = obj.into_btree_string_map().expect("should be a map"); - assert!(!map.contains_key("signature")); - } - - #[test] - fn test_to_cleaned_object_v0() { - use crate::state_transition::StateTransitionValueConvert; - let transition = make_transfer_v0(); - let obj = transition - .to_cleaned_object(false) - .expect("to_cleaned_object should work"); - assert!(obj.is_map()); - } - - #[test] - fn test_to_canonical_cleaned_object_v0() { - use crate::state_transition::StateTransitionValueConvert; - let transition = make_transfer_v0(); - let obj = transition - .to_canonical_cleaned_object(false) - .expect("should work"); - assert!(obj.is_map()); - } - - #[test] - fn test_from_value_map_v0() { - use crate::state_transition::StateTransitionValueConvert; - use crate::version::LATEST_PLATFORM_VERSION; - let transition = make_transfer_v0(); - let obj = transition.to_object(false).expect("to_object should work"); - let map = obj - .into_btree_string_map() - .expect("should convert to btree map"); - let restored = - IdentityCreditTransferTransitionV0::from_value_map(map, LATEST_PLATFORM_VERSION) - .expect("from_value_map should work"); - assert_eq!(transition, restored); - } + // Legacy `StateTransitionValueConvert` round-trip tests on the V0 + // inner struct deleted in Phase D step 9. The canonical + // `JsonConvertible` / `ValueConvertible` round-trip is exercised via + // the outer enum derive — these tested methods that no longer exist. #[test] fn test_default_v0() { @@ -242,24 +186,6 @@ mod test { assert_eq!(transition.user_fee_increase, 0); } - #[test] - fn test_to_cleaned_object_skip_signature_removes_signature() { - use crate::state_transition::StateTransitionValueConvert; - let t = make_transfer_v0(); - let obj = t.to_cleaned_object(true).expect("should work"); - let map = obj.into_btree_string_map().expect("should be map"); - assert!(!map.contains_key("signature")); - } - - #[test] - fn test_to_canonical_cleaned_object_skip_signature_removes_signature() { - use crate::state_transition::StateTransitionValueConvert; - let t = make_transfer_v0(); - let obj = t.to_canonical_cleaned_object(true).expect("should work"); - let map = obj.into_btree_string_map().expect("should be map"); - assert!(!map.contains_key("signature")); - } - #[test] fn test_modified_data_ids_and_unique_identifiers() { use crate::state_transition::StateTransitionLike; @@ -271,24 +197,4 @@ mod test { let ids = t.unique_identifiers(); assert_eq!(ids.len(), 1); } - - #[test] - fn test_value_conversion_preserves_fields() { - use crate::state_transition::StateTransitionValueConvert; - use crate::version::LATEST_PLATFORM_VERSION; - let t = make_transfer_v0(); - let obj = t.to_object(false).expect("to_object"); - let map = obj.clone().into_btree_string_map().expect("should be map"); - assert!(map.contains_key("identityId")); - assert!(map.contains_key("recipientId")); - assert!(map.contains_key("amount")); - let restored = - IdentityCreditTransferTransitionV0::from_object(obj, LATEST_PLATFORM_VERSION) - .expect("from_object"); - assert_eq!(t.amount, restored.amount); - assert_eq!(t.identity_id, restored.identity_id); - assert_eq!(t.recipient_id, restored.recipient_id); - assert_eq!(t.nonce, restored.nonce); - assert_eq!(t.user_fee_increase, restored.user_fee_increase); - } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/value_conversion.rs deleted file mode 100644 index 3734da5c3d6..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/v0/value_conversion.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::{IntegerReplacementType, ReplacementType, Value}; - -use crate::{state_transition::StateTransitionFieldTypes, ProtocolError}; - -use crate::state_transition::identity_credit_transfer_transition::fields::*; -use crate::state_transition::identity_credit_transfer_transition::v0::IdentityCreditTransferTransitionV0; -use crate::state_transition::StateTransitionValueConvert; - -use platform_version::version::PlatformVersion; - -impl StateTransitionValueConvert<'_> for IdentityCreditTransferTransitionV0 { - fn from_object( - raw_object: Value, - _platform_version: &PlatformVersion, - ) -> Result { - platform_value::from_value(raw_object).map_err(ProtocolError::ValueError) - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; - value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; - value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; - Ok(()) - } - - fn from_value_map( - raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let value: Value = raw_value_map.into(); - Self::from_object(value, platform_version) - } - - fn to_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - // Override to_canonical_cleaned_object to manage add_public_keys individually - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - self.to_cleaned_object(skip_signature) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/value_conversion.rs deleted file mode 100644 index 3f1e808288a..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_transition/value_conversion.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::Value; - -use crate::ProtocolError; - -use crate::state_transition::identity_credit_transfer_transition::v0::IdentityCreditTransferTransitionV0; -use crate::state_transition::identity_credit_transfer_transition::IdentityCreditTransferTransition; -use crate::state_transition::state_transitions::identity_credit_transfer_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_version::version::{FeatureVersion, PlatformVersion}; - -impl StateTransitionValueConvert<'_> for IdentityCreditTransferTransition { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreditTransferTransition::V0(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreditTransferTransition::V0(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreditTransferTransition::V0(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreditTransferTransition::V0(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok( - IdentityCreditTransferTransitionV0::from_object(raw_object, platform_version)? - .into(), - ), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityCreditTransferTransition version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(IdentityCreditTransferTransitionV0::from_value_map( - raw_value_map, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityCreditTransferTransition version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => IdentityCreditTransferTransitionV0::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityCreditTransferTransition version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/json_conversion.rs deleted file mode 100644 index ad4ff4e6ea3..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/json_conversion.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; -use crate::state_transition::state_transitions::identity_credit_withdrawal_transition::fields::*; -use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, -}; -use crate::ProtocolError; -use serde_json::Number; -use serde_json::Value as JsonValue; - -impl StateTransitionJsonConvert<'_> for IdentityCreditWithdrawalTransition { - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - match self { - IdentityCreditWithdrawalTransition::V0(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(0)), - ); - Ok(value) - } - IdentityCreditWithdrawalTransition::V1(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(1)), - ); - Ok(value) - } - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/mod.rs index 48f9e1915b1..2bfe6acc69d 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/mod.rs @@ -7,15 +7,11 @@ use crate::state_transition::identity_credit_withdrawal_transition::v0::Identity pub mod accessors; pub mod fields; mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; pub mod methods; mod state_transition_estimated_fee_validation; mod state_transition_like; pub mod v0; pub mod v1; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; use crate::state_transition::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0Signable; @@ -140,11 +136,11 @@ mod test { use crate::state_transition::{ StateTransitionEstimatedFeeValidation, StateTransitionHasUserFeeIncrease, StateTransitionIdentityEstimatedFeeValidation, StateTransitionLike, StateTransitionOwned, - StateTransitionSingleSigned, StateTransitionType, StateTransitionValueConvert, + StateTransitionSingleSigned, StateTransitionType, }; use crate::version::LATEST_PLATFORM_VERSION; use crate::withdrawal::Pooling; - use platform_value::{BinaryData, Identifier, Value}; + use platform_value::{BinaryData, Identifier}; fn make_withdrawal_v0() -> IdentityCreditWithdrawalTransition { IdentityCreditWithdrawalTransition::V0(IdentityCreditWithdrawalTransitionV0 { @@ -279,66 +275,11 @@ mod test { assert_eq!(bin.len(), 2); } - #[test] - fn test_value_conversion_roundtrip_v0() { - let t = make_withdrawal_v0(); - let obj = StateTransitionValueConvert::to_object(&t, false).expect("should work"); - let restored = - ::from_object( - obj, - LATEST_PLATFORM_VERSION, - ) - .expect("should work"); - assert_eq!(t, restored); - } - - #[test] - fn test_value_conversion_roundtrip_v1() { - let t = make_withdrawal_v1(); - let obj = StateTransitionValueConvert::to_object(&t, false).expect("should work"); - let restored = - ::from_object( - obj, - LATEST_PLATFORM_VERSION, - ) - .expect("should work"); - assert_eq!(t, restored); - } - - #[test] - fn test_from_value_map_v0() { - let t = make_withdrawal_v0(); - let obj = StateTransitionValueConvert::to_object(&t, false).expect("should work"); - let map = obj.into_btree_string_map().expect("should be map"); - let restored = - ::from_value_map( - map, - LATEST_PLATFORM_VERSION, - ) - .expect("should work"); - assert_eq!(t, restored); - } - - #[test] - fn test_from_object_unknown_version() { - let value = Value::from([("$stateTransitionProtocolVersion", Value::U16(255))]); - let result = - ::from_object( - value, - LATEST_PLATFORM_VERSION, - ); - assert!(result.is_err()); - } - - #[test] - fn test_clean_value_unknown_version() { - let mut value = Value::from([("$stateTransitionProtocolVersion", Value::U8(255))]); - let result = - ::clean_value( - &mut value, - ); - assert!(result.is_err()); - } + // Legacy `StateTransitionValueConvert` round-trip and + // unknown-version tests deleted in Phase D step 9. The canonical + // `JsonConvertible` / `ValueConvertible` round-trip is exercised on + // the outer enum derive (see `json_convertible_tests` below) — these + // tested methods that no longer exist. #[test] fn test_estimated_fee_sufficient() { @@ -369,3 +310,219 @@ mod test { assert!(MIN_CORE_FEE_PER_BYTE == 1); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + + use crate::identity::core_script::CoreScript; + use crate::withdrawal::Pooling; + use platform_value::{platform_value, BinaryData, Identifier}; + use serde_json::json; + + pub(crate) fn fixture() -> IdentityCreditWithdrawalTransition { + IdentityCreditWithdrawalTransition::V0(IdentityCreditWithdrawalTransitionV0 { + identity_id: Identifier::new([0x33; 32]), + amount: 9_876_543, + core_fee_per_byte: 5, + pooling: Pooling::Never, + output_script: CoreScript::from_bytes(vec![0x76, 0xa9, 0x14]), + nonce: 11, + user_fee_increase: 2, + signature_public_key_id: 4, + signature: BinaryData::new(vec![0xb2; 65]), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int fields whose JSON wire encoding loses size info: + // `coreFeePerByte` (u32), `nonce` (u64), `userFeeIncrease` (u16), + // `signaturePublicKeyId` (u32). The value-path assertion below uses + // explicit suffixes to lock in the typed variants. + // `pooling` uses a custom `pooling_serde` that emits the camelCase name + // string in HR and the u8 discriminant in non-HR. + // `outputScript` is base64 in HR (CoreScript Serialize) and bytes in non-HR. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "identityId": Identifier::new([0x33; 32]), + "amount": 9_876_543u64, + "coreFeePerByte": 5u32, + "pooling": "never", + "outputScript": "dqkU", + "nonce": 11u64, + "userFeeIncrease": 2u16, + "signaturePublicKeyId": 4u32, + "signature": "srKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrI=", + }) + ); + let recovered = IdentityCreditWithdrawalTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Explicit suffixes lock in sized variants: `amount` u64, `coreFeePerByte` + // u32, `nonce` u64 (IdentityNonce), `userFeeIncrease` u16, + // `signaturePublicKeyId` u32 (KeyID). + // `pooling`: `pooling_serde` emits the u8 discriminant on the non-HR + // path (`Pooling::Never as u8 == 0`). + // `outputScript`: CoreScript serializes as Bytes on the non-HR path. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "identityId": Identifier::new([0x33; 32]), + "amount": 9_876_543u64, + "coreFeePerByte": 5u32, + "pooling": 0u8, + "outputScript": BinaryData::new(vec![0x76, 0xa9, 0x14]), + "nonce": 11u64, + "userFeeIncrease": 2u16, + "signaturePublicKeyId": 4u32, + "signature": BinaryData::new(vec![0xb2; 65]), + }) + ); + let recovered = + IdentityCreditWithdrawalTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + // --- V1 with output_script: None (restored coverage from Phase D step 9) --- + // + // V1 has `output_script: Option` where None means "send to the + // address set by core" (vs V0's mandatory script). These tests round-trip + // the None case through both JSON and Value paths, replacing the + // `make_withdrawal_v1_no_script` fixture deleted in step 9. + pub(crate) fn fixture_v1_no_script() -> IdentityCreditWithdrawalTransition { + IdentityCreditWithdrawalTransition::V1(IdentityCreditWithdrawalTransitionV1 { + identity_id: Identifier::new([0x44; 32]), + amount: 1_234_567, + core_fee_per_byte: 7, + pooling: Pooling::Standard, + output_script: None, + nonce: 13, + user_fee_increase: 3, + signature_public_key_id: 5, + signature: BinaryData::new(vec![0xa3; 65]), + }) + } + + #[test] + fn json_round_trip_v1_with_none_output_script() { + use crate::serialization::JsonConvertible; + let original = fixture_v1_no_script(); + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({ + "$formatVersion": "1", + "identityId": Identifier::new([0x44; 32]), + "amount": 1_234_567u64, + "coreFeePerByte": 7u32, + "pooling": "standard", + "outputScript": null, + "nonce": 13u64, + "userFeeIncrease": 3u16, + "signaturePublicKeyId": 5u32, + "signature": "o6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6M=", + }) + ); + let recovered = IdentityCreditWithdrawalTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_v1_with_none_output_script() { + use crate::serialization::ValueConvertible; + let original = fixture_v1_no_script(); + let value = original.to_object().expect("to_object"); + // Pooling::Standard = 2 (discriminant; Never=0, IfAvailable=1, Standard=2) + assert_eq!( + value, + platform_value!({ + "$formatVersion": "1", + "identityId": Identifier::new([0x44; 32]), + "amount": 1_234_567u64, + "coreFeePerByte": 7u32, + "pooling": 2u8, + "outputScript": platform_value::Value::Null, + "nonce": 13u64, + "userFeeIncrease": 3u16, + "signaturePublicKeyId": 5u32, + "signature": BinaryData::new(vec![0xa3; 65]), + }) + ); + let recovered = + IdentityCreditWithdrawalTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + // --- Unknown `$formatVersion` error coverage --- + // + // Representative test for the canonical `serde(tag = "$formatVersion")` + // dispatch error path. We assert that an unknown version (`"99"`) is + // rejected by both `from_json` and `from_object` paths. This pattern + // is unified across every `$formatVersion`-tagged outer enum in the + // codebase, so one representative test on a multi-variant enum + // (V0 + V1) demonstrates the contract; per-enum coverage would be + // mechanical noise. + #[test] + fn from_json_rejects_unknown_format_version() { + use crate::serialization::JsonConvertible; + let bad = json!({ + "$formatVersion": "99", + "identityId": Identifier::new([0x33; 32]), + "amount": 1u64, + "coreFeePerByte": 1u32, + "pooling": "never", + "outputScript": "AA==", + "nonce": 1u64, + "userFeeIncrease": 1u16, + "signaturePublicKeyId": 1u32, + "signature": "AA==", + }); + let result = IdentityCreditWithdrawalTransition::from_json(bad); + assert!( + result.is_err(), + "from_json should reject an unknown $formatVersion (got: {:?})", + result.as_ref().map(|_| "Ok") + ); + } + + #[test] + fn from_object_rejects_unknown_format_version() { + use crate::serialization::ValueConvertible; + let bad = platform_value!({ + "$formatVersion": "99", + "identityId": Identifier::new([0x33; 32]), + "amount": 1u64, + "coreFeePerByte": 1u32, + "pooling": 0u8, + "outputScript": BinaryData::new(vec![]), + "nonce": 1u64, + "userFeeIncrease": 1u16, + "signaturePublicKeyId": 1u32, + "signature": BinaryData::new(vec![]), + }); + let result = IdentityCreditWithdrawalTransition::from_object(bad); + assert!( + result.is_err(), + "from_object should reject an unknown $formatVersion (got: {:?})", + result.as_ref().map(|_| "Ok") + ); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/json_conversion.rs deleted file mode 100644 index 1a3029aab99..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for IdentityCreditWithdrawalTransitionV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/mod.rs index f35c75a7592..fb9a519c6da 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/mod.rs @@ -1,10 +1,6 @@ mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; mod state_transition_like; mod types; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; #[cfg(feature = "json-conversion")] @@ -383,99 +379,8 @@ mod test { } } - #[test] - fn test_value_conversion_roundtrip_v0() { - use crate::state_transition::StateTransitionValueConvert; - use crate::version::LATEST_PLATFORM_VERSION; - let t = make_withdrawal_v0(); - let obj = t.to_object(false).expect("to_object should work"); - let restored = - super::IdentityCreditWithdrawalTransitionV0::from_object(obj, LATEST_PLATFORM_VERSION) - .expect("from_object should work"); - assert_eq!(t, restored); - } - - #[test] - fn test_to_cleaned_object_v0() { - use crate::state_transition::StateTransitionValueConvert; - let t = make_withdrawal_v0(); - let obj = t.to_cleaned_object(false).expect("should work"); - assert!(obj.is_map()); - } - - #[test] - fn test_to_canonical_cleaned_object_v0() { - use crate::state_transition::StateTransitionValueConvert; - let t = make_withdrawal_v0(); - let obj = t.to_canonical_cleaned_object(false).expect("should work"); - assert!(obj.is_map()); - } - - #[test] - fn test_from_value_map_v0() { - use crate::state_transition::StateTransitionValueConvert; - use crate::version::LATEST_PLATFORM_VERSION; - let t = make_withdrawal_v0(); - let obj = t.to_object(false).expect("to_object should work"); - let map = obj.into_btree_string_map().expect("should be a map"); - let restored = super::IdentityCreditWithdrawalTransitionV0::from_value_map( - map, - LATEST_PLATFORM_VERSION, - ) - .expect("should work"); - assert_eq!(t, restored); - } - - #[test] - fn test_to_object_skip_signature_removes_signature() { - use crate::state_transition::StateTransitionValueConvert; - let t = make_withdrawal_v0(); - let obj = t.to_object(true).expect("should work"); - let map = obj.into_btree_string_map().expect("should be a map"); - assert!(!map.contains_key("signature")); - } - - #[test] - fn test_to_cleaned_object_skip_signature() { - use crate::state_transition::StateTransitionValueConvert; - let t = make_withdrawal_v0(); - let obj = t.to_cleaned_object(true).expect("should work"); - let map = obj.into_btree_string_map().expect("should be a map"); - assert!(!map.contains_key("signature")); - } - - #[test] - fn test_to_canonical_cleaned_object_skip_signature() { - use crate::state_transition::StateTransitionValueConvert; - let t = make_withdrawal_v0(); - let obj = t.to_canonical_cleaned_object(true).expect("should work"); - let map = obj.into_btree_string_map().expect("should be a map"); - assert!(!map.contains_key("signature")); - } - - #[test] - fn test_pooling_roundtrip_never() { - use crate::state_transition::StateTransitionValueConvert; - use crate::version::LATEST_PLATFORM_VERSION; - let mut t = make_withdrawal_v0(); - t.pooling = Pooling::Never; - let obj = t.to_object(false).expect("to_object"); - let restored = - super::IdentityCreditWithdrawalTransitionV0::from_object(obj, LATEST_PLATFORM_VERSION) - .expect("from_object"); - assert_eq!(restored.pooling, Pooling::Never); - } - - #[test] - fn test_pooling_roundtrip_standard() { - use crate::state_transition::StateTransitionValueConvert; - use crate::version::LATEST_PLATFORM_VERSION; - let mut t = make_withdrawal_v0(); - t.pooling = Pooling::Standard; - let obj = t.to_object(false).expect("to_object"); - let restored = - super::IdentityCreditWithdrawalTransitionV0::from_object(obj, LATEST_PLATFORM_VERSION) - .expect("from_object"); - assert_eq!(restored.pooling, Pooling::Standard); - } + // Legacy `StateTransitionValueConvert` round-trip / pooling tests on + // the V0 inner struct deleted in Phase D step 9. The canonical + // `JsonConvertible` / `ValueConvertible` round-trip is exercised on + // the outer enum derive — these tested methods that no longer exist. } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/value_conversion.rs deleted file mode 100644 index 777e24cb519..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v0/value_conversion.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::{IntegerReplacementType, ReplacementType, Value}; - -use crate::{state_transition::StateTransitionFieldTypes, ProtocolError}; - -use crate::state_transition::identity_credit_withdrawal_transition::fields::*; -use crate::state_transition::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0; -use crate::state_transition::StateTransitionValueConvert; - -use platform_version::version::PlatformVersion; - -impl StateTransitionValueConvert<'_> for IdentityCreditWithdrawalTransitionV0 { - fn from_object( - raw_object: Value, - _platform_version: &PlatformVersion, - ) -> Result { - platform_value::from_value(raw_object).map_err(ProtocolError::ValueError) - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; - value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; - value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; - Ok(()) - } - - fn from_value_map( - raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let value: Value = raw_value_map.into(); - Self::from_object(value, platform_version) - } - - fn to_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - // Override to_canonical_cleaned_object to manage add_public_keys individually - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - self.to_cleaned_object(skip_signature) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/json_conversion.rs deleted file mode 100644 index 9c2525ee4b3..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::identity_credit_withdrawal_transition::v1::IdentityCreditWithdrawalTransitionV1; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for IdentityCreditWithdrawalTransitionV1 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/mod.rs index 7364334be17..b445d14700c 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/mod.rs @@ -1,11 +1,7 @@ mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; mod state_transition_like; mod types; mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; #[cfg(feature = "json-conversion")] @@ -57,7 +53,6 @@ mod test { use crate::state_transition::{ StateTransitionHasUserFeeIncrease, StateTransitionIdentitySigned, StateTransitionLike, StateTransitionOwned, StateTransitionSingleSigned, StateTransitionType, - StateTransitionValueConvert, }; use platform_value::BinaryData; @@ -75,20 +70,6 @@ mod test { } } - fn make_withdrawal_v1_no_script() -> IdentityCreditWithdrawalTransitionV1 { - IdentityCreditWithdrawalTransitionV1 { - identity_id: Identifier::random(), - amount: 200_000, - core_fee_per_byte: 1, - pooling: Pooling::Standard, - output_script: None, - nonce: 10, - user_fee_increase: 0, - signature_public_key_id: 2, - signature: [0u8; 65].to_vec().into(), - } - } - #[test] fn test_default() { let t = IdentityCreditWithdrawalTransitionV1::default(); @@ -159,77 +140,11 @@ mod test { } } - #[test] - fn test_value_conversion_roundtrip_with_script() { - use crate::version::LATEST_PLATFORM_VERSION; - let t = make_withdrawal_v1(); - let obj = t.to_object(false).expect("to_object should work"); - let restored = - IdentityCreditWithdrawalTransitionV1::from_object(obj, LATEST_PLATFORM_VERSION) - .expect("from_object should work"); - assert_eq!(t, restored); - } - - #[test] - fn test_value_conversion_roundtrip_without_script() { - use crate::version::LATEST_PLATFORM_VERSION; - let t = make_withdrawal_v1_no_script(); - let obj = t.to_object(false).expect("to_object should work"); - let restored = - IdentityCreditWithdrawalTransitionV1::from_object(obj, LATEST_PLATFORM_VERSION) - .expect("from_object should work"); - assert_eq!(t, restored); - } - - #[test] - fn test_from_value_map() { - use crate::version::LATEST_PLATFORM_VERSION; - let t = make_withdrawal_v1(); - let obj = t.to_object(false).expect("to_object should work"); - let map = obj.into_btree_string_map().expect("should be map"); - let restored = - IdentityCreditWithdrawalTransitionV1::from_value_map(map, LATEST_PLATFORM_VERSION) - .expect("should work"); - assert_eq!(t, restored); - } - - #[test] - fn test_to_cleaned_object() { - let t = make_withdrawal_v1(); - let obj = t.to_cleaned_object(false).expect("should work"); - assert!(obj.is_map()); - } - - #[test] - fn test_to_canonical_cleaned_object() { - let t = make_withdrawal_v1(); - let obj = t.to_canonical_cleaned_object(false).expect("should work"); - assert!(obj.is_map()); - } - - #[test] - fn test_to_object_skip_signature() { - let t = make_withdrawal_v1(); - let obj = t.to_object(true).expect("should work"); - let map = obj.into_btree_string_map().expect("should be map"); - assert!(!map.contains_key("signature")); - } - - #[test] - fn test_to_cleaned_object_skip_signature() { - let t = make_withdrawal_v1(); - let obj = t.to_cleaned_object(true).expect("should work"); - let map = obj.into_btree_string_map().expect("should be map"); - assert!(!map.contains_key("signature")); - } - - #[test] - fn test_to_canonical_cleaned_object_skip_signature() { - let t = make_withdrawal_v1(); - let obj = t.to_canonical_cleaned_object(true).expect("should work"); - let map = obj.into_btree_string_map().expect("should be map"); - assert!(!map.contains_key("signature")); - } + // Legacy `StateTransitionValueConvert` round-trip / cleaned-object / + // skip-signature tests on the V1 inner struct deleted in Phase D + // step 9. The canonical `JsonConvertible` / `ValueConvertible` + // round-trip is exercised on the outer enum derive — these tested + // methods that no longer exist. #[test] fn test_unique_identifier_includes_nonce() { @@ -256,17 +171,4 @@ mod test { let t = make_withdrawal_v1(); assert_eq!(t.owner_id(), t.identity_id); } - - #[test] - fn test_value_conversion_script_none_roundtrip_map() { - use crate::version::LATEST_PLATFORM_VERSION; - let t = make_withdrawal_v1_no_script(); - let obj = t.to_object(false).expect("to_object"); - let map = obj.into_btree_string_map().expect("should be map"); - let restored = - IdentityCreditWithdrawalTransitionV1::from_value_map(map, LATEST_PLATFORM_VERSION) - .expect("from_value_map"); - assert!(restored.output_script.is_none()); - assert_eq!(t, restored); - } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/value_conversion.rs deleted file mode 100644 index e276d30319d..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/v1/value_conversion.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::{IntegerReplacementType, ReplacementType, Value}; - -use crate::{state_transition::StateTransitionFieldTypes, ProtocolError}; - -use crate::state_transition::identity_credit_withdrawal_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use crate::state_transition::identity_credit_withdrawal_transition::v1::IdentityCreditWithdrawalTransitionV1; -use platform_version::version::PlatformVersion; - -impl StateTransitionValueConvert<'_> for IdentityCreditWithdrawalTransitionV1 { - fn from_object( - raw_object: Value, - _platform_version: &PlatformVersion, - ) -> Result { - platform_value::from_value(raw_object).map_err(ProtocolError::ValueError) - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; - value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; - value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; - Ok(()) - } - - fn from_value_map( - raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let value: Value = raw_value_map.into(); - Self::from_object(value, platform_version) - } - - fn to_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - // Override to_canonical_cleaned_object to manage add_public_keys individually - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - self.to_cleaned_object(skip_signature) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/value_conversion.rs deleted file mode 100644 index c92b8d51b07..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/value_conversion.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::Value; - -use crate::ProtocolError; - -use crate::state_transition::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0; -use crate::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; -use crate::state_transition::state_transitions::identity_credit_withdrawal_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use crate::state_transition::identity_credit_withdrawal_transition::v1::IdentityCreditWithdrawalTransitionV1; -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_version::version::{FeatureVersion, PlatformVersion}; - -impl StateTransitionValueConvert<'_> for IdentityCreditWithdrawalTransition { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreditWithdrawalTransition::V0(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - IdentityCreditWithdrawalTransition::V1(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreditWithdrawalTransition::V0(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - IdentityCreditWithdrawalTransition::V1(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreditWithdrawalTransition::V0(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - IdentityCreditWithdrawalTransition::V1(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityCreditWithdrawalTransition::V0(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - IdentityCreditWithdrawalTransition::V1(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(1))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(IdentityCreditWithdrawalTransitionV0::from_object( - raw_object, - platform_version, - )? - .into()), - 1 => Ok(IdentityCreditWithdrawalTransitionV1::from_object( - raw_object, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityCreditWithdrawalTransition version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(IdentityCreditWithdrawalTransitionV0::from_value_map( - raw_value_map, - platform_version, - )? - .into()), - 1 => Ok(IdentityCreditWithdrawalTransitionV1::from_value_map( - raw_value_map, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityCreditWithdrawalTransition version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => IdentityCreditWithdrawalTransitionV0::clean_value(value), - 1 => IdentityCreditWithdrawalTransitionV1::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityCreditWithdrawalTransition version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/json_conversion.rs deleted file mode 100644 index 48d407ce973..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/json_conversion.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::state_transition::identity_topup_from_addresses_transition::IdentityTopUpFromAddressesTransition; -use crate::state_transition::state_transitions::identity_topup_from_addresses_transition::fields::*; -use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, -}; -use crate::ProtocolError; -use serde_json::Number; -use serde_json::Value as JsonValue; - -impl StateTransitionJsonConvert<'_> for IdentityTopUpFromAddressesTransition { - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - match self { - IdentityTopUpFromAddressesTransition::V0(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(0)), - ); - Ok(value) - } - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/mod.rs index ef33b3e8011..92380ff61be 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/mod.rs @@ -1,17 +1,15 @@ pub mod accessors; pub mod fields; -#[cfg(feature = "json-conversion")] -mod json_conversion; pub mod methods; mod state_transition_estimated_fee_validation; mod state_transition_fee_strategy; mod state_transition_like; mod state_transition_validation; pub mod v0; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; #[cfg(feature = "value-conversion")] use crate::serialization::ValueConvertible; use fields::*; @@ -75,6 +73,9 @@ impl IdentityTopUpFromAddressesTransition { } } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl JsonConvertible for IdentityTopUpFromAddressesTransition {} + impl StateTransitionFieldTypes for IdentityTopUpFromAddressesTransition { fn signature_property_paths() -> Vec<&'static str> { vec![SIGNATURE] @@ -88,3 +89,103 @@ impl StateTransitionFieldTypes for IdentityTopUpFromAddressesTransition { vec![] } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::address_funds::{AddressFundsFeeStrategyStep, AddressWitness, PlatformAddress}; + use crate::state_transition::identity_topup_from_addresses_transition::v0::IdentityTopUpFromAddressesTransitionV0; + use platform_value::{platform_value, BinaryData, Identifier, Value}; + use serde_json::json; + use std::collections::BTreeMap; + + pub(crate) fn fixture() -> IdentityTopUpFromAddressesTransition { + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([0x44; 20]), (9u32, 750_000u64)); + + let v0 = IdentityTopUpFromAddressesTransitionV0 { + inputs, + output: Some((PlatformAddress::P2sh([0x55; 20]), 100_000)), + identity_id: Identifier::new([0x66; 32]), + fee_strategy: vec![AddressFundsFeeStrategyStep::DeductFromInput(0)], + user_fee_increase: 7, + input_witnesses: vec![AddressWitness::P2pkh { + signature: BinaryData::new(vec![0x77; 65]), + }], + }; + IdentityTopUpFromAddressesTransition::V0(v0) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int fields lose their size on the JSON wire (single Number type): + // - `inputs[].nonce` is u32, `inputs[].amount` / `output.amount` are u64, + // - `feeStrategy[].index` is u16, `userFeeIncrease` is u16. + // The Value-path test below locks the typed variants. `Identifier` is + // base58 in JSON HR. `BinaryData` is base64. `PlatformAddress` is hex + // (1 type byte + 20 hash bytes). + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "inputs": [ + {"address": "004444444444444444444444444444444444444444", "nonce": 9, "amount": 750_000}, + ], + "output": {"address": "015555555555555555555555555555555555555555", "amount": 100_000}, + "identityId": "7tj9biW3KRJ7EEWmVUGigHiouCTXhV2dzcyvwma7Cyu7", + "feeStrategy": [{"$type": "deductFromInput", "index": 0}], + "userFeeIncrease": 7, + "inputWitnesses": [ + { + "$type": "p2pkh", + "signature": "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3c=", + }, + ], + }) + ); + let recovered = IdentityTopUpFromAddressesTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + let identity_id = Identifier::new([0x66; 32]); + let mut p2pkh44 = vec![0x00u8]; + p2pkh44.extend_from_slice(&[0x44u8; 20]); + let mut p2sh55 = vec![0x01u8]; + p2sh55.extend_from_slice(&[0x55u8; 20]); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "inputs": [ + {"address": Value::Bytes(p2pkh44), "nonce": 9u32, "amount": 750_000u64}, + ], + "output": {"address": Value::Bytes(p2sh55), "amount": 100_000u64}, + "identityId": identity_id, + "feeStrategy": [{"$type": "deductFromInput", "index": 0u16}], + "userFeeIncrease": 7u16, + "inputWitnesses": [ + { + "$type": "p2pkh", + "signature": Value::Bytes(vec![0x77; 65]), + }, + ], + }) + ); + let recovered = + IdentityTopUpFromAddressesTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/json_conversion.rs deleted file mode 100644 index 2f804f2e7c7..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::identity_topup_from_addresses_transition::v0::IdentityTopUpFromAddressesTransitionV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for IdentityTopUpFromAddressesTransitionV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/mod.rs index 9b81be9dc55..a25069be018 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/mod.rs @@ -1,11 +1,7 @@ -#[cfg(feature = "json-conversion")] -mod json_conversion; mod state_transition_like; mod state_transition_validation; mod types; pub(super) mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; use bincode::{Decode, Encode}; @@ -15,11 +11,14 @@ use std::collections::BTreeMap; use crate::address_funds::{AddressFundsFeeStrategy, AddressWitness, PlatformAddress}; use crate::fee::Credits; use crate::prelude::{AddressNonce, Identifier, UserFeeIncrease}; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; use crate::ProtocolError; +#[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Debug, Clone, Encode, Decode, PlatformSignable, PartialEq)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/value_conversion.rs deleted file mode 100644 index 76fd1046831..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/v0/value_conversion.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::{IntegerReplacementType, ReplacementType, Value}; - -use crate::{state_transition::StateTransitionFieldTypes, ProtocolError}; - -use crate::state_transition::identity_topup_from_addresses_transition::fields::*; -use crate::state_transition::identity_topup_from_addresses_transition::v0::IdentityTopUpFromAddressesTransitionV0; -use crate::state_transition::StateTransitionValueConvert; - -use platform_version::version::PlatformVersion; - -impl StateTransitionValueConvert<'_> for IdentityTopUpFromAddressesTransitionV0 { - fn from_object( - raw_object: Value, - _platform_version: &PlatformVersion, - ) -> Result { - platform_value::from_value(raw_object).map_err(ProtocolError::ValueError) - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; - value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; - value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; - Ok(()) - } - - fn from_value_map( - raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let value: Value = raw_value_map.into(); - Self::from_object(value, platform_version) - } - - fn to_object(&self, skip_signature: bool) -> Result { - let mut value: Value = platform_value::to_value(self)?; - - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - - Ok(value) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - let mut value: Value = platform_value::to_value(self)?; - - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - - Ok(value) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/value_conversion.rs deleted file mode 100644 index adbbdba80b5..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_from_addresses_transition/value_conversion.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::Value; - -use crate::ProtocolError; - -use crate::state_transition::identity_topup_from_addresses_transition::v0::IdentityTopUpFromAddressesTransitionV0; -use crate::state_transition::identity_topup_from_addresses_transition::IdentityTopUpFromAddressesTransition; -use crate::state_transition::state_transitions::identity_topup_from_addresses_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_version::version::{FeatureVersion, PlatformVersion}; - -impl StateTransitionValueConvert<'_> for IdentityTopUpFromAddressesTransition { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - IdentityTopUpFromAddressesTransition::V0(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - IdentityTopUpFromAddressesTransition::V0(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityTopUpFromAddressesTransition::V0(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityTopUpFromAddressesTransition::V0(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(IdentityTopUpFromAddressesTransitionV0::from_object( - raw_object, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityTopUpFromAddressesTransition version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(IdentityTopUpFromAddressesTransitionV0::from_value_map( - raw_value_map, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityTopUpFromAddressesTransition version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => IdentityTopUpFromAddressesTransitionV0::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityTopUpFromAddressesTransition version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/json_conversion.rs deleted file mode 100644 index 1ac3c96fa8c..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/json_conversion.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::state_transition::identity_topup_transition::IdentityTopUpTransition; -use crate::state_transition::state_transitions::identity_topup_transition::fields::*; -use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, -}; -use crate::ProtocolError; -use serde_json::Number; -use serde_json::Value as JsonValue; - -impl StateTransitionJsonConvert<'_> for IdentityTopUpTransition { - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - match self { - IdentityTopUpTransition::V0(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(0)), - ); - Ok(value) - } - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/mod.rs index f669693080a..74da8481583 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/mod.rs @@ -1,14 +1,10 @@ pub mod accessors; pub mod fields; -#[cfg(feature = "json-conversion")] -mod json_conversion; pub mod methods; pub mod proved; mod state_transition_estimated_fee_validation; mod state_transition_like; pub mod v0; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; #[cfg(feature = "json-conversion")] @@ -101,10 +97,10 @@ mod test { use crate::state_transition::{ StateTransitionEstimatedFeeValidation, StateTransitionHasUserFeeIncrease, StateTransitionLike, StateTransitionOwned, StateTransitionSingleSigned, - StateTransitionType, StateTransitionValueConvert, + StateTransitionType, }; use crate::version::LATEST_PLATFORM_VERSION; - use platform_value::{BinaryData, Identifier, Value}; + use platform_value::{BinaryData, Identifier}; fn make_topup() -> IdentityTopUpTransition { IdentityTopUpTransition::V0(IdentityTopUpTransitionV0 { @@ -183,23 +179,8 @@ mod test { assert!(fee > 0); } - #[test] - fn test_from_object_unknown_version() { - let value = Value::from([("$stateTransitionProtocolVersion", Value::U16(255))]); - let result = ::from_object( - value, - LATEST_PLATFORM_VERSION, - ); - assert!(result.is_err()); - } - - #[test] - fn test_clean_value_unknown_version() { - let mut value = Value::from([("$stateTransitionProtocolVersion", Value::U8(255))]); - let result = - ::clean_value(&mut value); - assert!(result.is_err()); - } + // Legacy `StateTransitionValueConvert` unknown-version tests deleted + // in Phase D step 9 — they tested methods that no longer exist. #[test] fn test_into_from_v0() { @@ -210,3 +191,107 @@ mod test { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + + use crate::tests::fixtures::instant_asset_lock_proof_fixture; + use platform_value::{BinaryData, Identifier}; + + // Tier 4: `instant_asset_lock_proof_fixture` produces NON-DETERMINISTIC bytes + // (random transaction / instantLock per run), so wire-shape assertions on the + // asset_lock_proof field stay envelope-only — the deterministic siblings + // (identity_id, user_fee_increase, signature) get full literal assertions. + pub(crate) fn fixture() -> IdentityTopUpTransition { + IdentityTopUpTransition::V0(IdentityTopUpTransitionV0 { + asset_lock_proof: instant_asset_lock_proof_fixture(None, None), + identity_id: Identifier::new([0x44; 32]), + user_fee_increase: 9, + signature: BinaryData::new(vec![0xc3; 65]), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + let obj = json.as_object().expect("json is an object"); + assert_eq!(obj.get("$formatVersion"), Some(&serde_json::json!("0"))); + assert_eq!( + obj.get("identityId"), + Some(&serde_json::json!(Identifier::new([0x44; 32]))) + ); + // `userFeeIncrease` is `u16` (UserFeeIncrease) in the source type. JSON + // erases the size on the wire — the value-path assertion uses `9u16`. + assert_eq!(obj.get("userFeeIncrease"), Some(&serde_json::json!(9))); + // 65-byte signature serialized as base64 (BinaryData) + assert_eq!( + obj.get("signature"), + Some(&serde_json::json!( + "w8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8M=" + )) + ); + let proof = obj + .get("assetLockProof") + .and_then(|v| v.as_object()) + .expect("assetLockProof is an object"); + assert_eq!(proof.get("$type"), Some(&serde_json::json!("instant"))); + assert_eq!(proof.get("outputIndex"), Some(&serde_json::json!(0))); + assert!(proof.get("instantLock").is_some_and(|v| v.is_string())); + assert!(proof.get("transaction").is_some_and(|v| v.is_string())); + let recovered = IdentityTopUpTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + let map = value.as_map().expect("value is a map"); + let get = |key: &str| { + map.iter() + .find(|(k, _)| k.as_text() == Some(key)) + .map(|(_, v)| v) + }; + assert_eq!( + get("$formatVersion"), + Some(&platform_value::Value::Text("0".to_string())) + ); + assert_eq!( + get("identityId"), + Some(&platform_value::Value::Identifier([0x44; 32])) + ); + // `9u16`: UserFeeIncrease is `u16`; value-path preserves U16. + assert_eq!(get("userFeeIncrease"), Some(&platform_value::Value::U16(9))); + assert_eq!( + get("signature"), + Some(&platform_value::Value::Bytes(vec![0xc3; 65])) + ); + let proof = get("assetLockProof") + .and_then(|v| v.as_map()) + .expect("assetLockProof is a map"); + let pget = |key: &str| { + proof + .iter() + .find(|(k, _)| k.as_text() == Some(key)) + .map(|(_, v)| v) + }; + assert_eq!( + pget("$type"), + Some(&platform_value::Value::Text("instant".to_string())) + ); + assert_eq!(pget("outputIndex"), Some(&platform_value::Value::U32(0))); + assert!(pget("instantLock").is_some_and(|v| matches!(v, platform_value::Value::Bytes(_)))); + assert!(pget("transaction").is_some_and(|v| matches!(v, platform_value::Value::Bytes(_)))); + let recovered = IdentityTopUpTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/json_conversion.rs deleted file mode 100644 index 69e4dbb2922..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::identity_topup_transition::v0::IdentityTopUpTransitionV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for IdentityTopUpTransitionV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/mod.rs index 28034c9d9eb..b01df1d247a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/mod.rs @@ -1,11 +1,7 @@ -#[cfg(feature = "json-conversion")] -mod json_conversion; mod proved; mod state_transition_like; mod types; pub(super) mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; #[cfg(feature = "json-conversion")] diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/value_conversion.rs deleted file mode 100644 index d6b51ddf416..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/v0/value_conversion.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::collections::BTreeMap; -use std::convert::TryFrom; - -use platform_value::{IntegerReplacementType, ReplacementType, Value}; - -use crate::{prelude::Identifier, state_transition::StateTransitionFieldTypes, ProtocolError}; - -use crate::prelude::AssetLockProof; - -use crate::state_transition::identity_topup_transition::fields::*; -use crate::state_transition::identity_topup_transition::v0::IdentityTopUpTransitionV0; -use crate::state_transition::StateTransitionValueConvert; - -use crate::state_transition::state_transitions::common_fields::property_names::USER_FEE_INCREASE; -use platform_version::version::PlatformVersion; - -impl StateTransitionValueConvert<'_> for IdentityTopUpTransitionV0 { - fn from_object( - raw_object: Value, - _platform_version: &PlatformVersion, - ) -> Result { - let signature = raw_object - .get_optional_binary_data(SIGNATURE) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(); - let identity_id = Identifier::from( - raw_object - .get_hash256(IDENTITY_ID) - .map_err(ProtocolError::ValueError)?, - ); - - let raw_asset_lock_proof = raw_object - .get_value(ASSET_LOCK_PROOF) - .map_err(ProtocolError::ValueError)?; - let asset_lock_proof = AssetLockProof::try_from(raw_asset_lock_proof)?; - - let user_fee_increase = raw_object - .get_optional_integer(USER_FEE_INCREASE) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(); - - Ok(IdentityTopUpTransitionV0 { - signature, - identity_id, - asset_lock_proof, - user_fee_increase, - }) - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; - value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; - value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; - Ok(()) - } - - fn from_value_map( - raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let value: Value = raw_value_map.into(); - Self::from_object(value, platform_version) - } - - fn to_object(&self, skip_signature: bool) -> Result { - let mut value: Value = platform_value::to_value(self)?; - - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - - Ok(value) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - let mut value: Value = platform_value::to_value(self)?; - - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - - Ok(value) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/value_conversion.rs deleted file mode 100644 index a419084e52b..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_topup_transition/value_conversion.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::Value; - -use crate::ProtocolError; - -use crate::state_transition::identity_topup_transition::v0::IdentityTopUpTransitionV0; -use crate::state_transition::identity_topup_transition::IdentityTopUpTransition; -use crate::state_transition::state_transitions::identity_topup_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_version::version::{FeatureVersion, PlatformVersion}; - -impl StateTransitionValueConvert<'_> for IdentityTopUpTransition { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - IdentityTopUpTransition::V0(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - IdentityTopUpTransition::V0(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityTopUpTransition::V0(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityTopUpTransition::V0(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(IdentityTopUpTransitionV0::from_object(raw_object, platform_version)?.into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityTopUpTransition version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok( - IdentityTopUpTransitionV0::from_value_map(raw_value_map, platform_version)?.into(), - ), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityTopUpTransition version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => IdentityTopUpTransitionV0::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityTopUpTransition version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/json_conversion.rs deleted file mode 100644 index cce061e0073..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/json_conversion.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::state_transition::identity_update_transition::IdentityUpdateTransition; -use crate::state_transition::state_transitions::identity_update_transition::fields::*; -use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, -}; -use crate::ProtocolError; -use serde_json::Number; -use serde_json::Value as JsonValue; - -impl StateTransitionJsonConvert<'_> for IdentityUpdateTransition { - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - match self { - IdentityUpdateTransition::V0(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(0)), - ); - Ok(value) - } - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/mod.rs index 8e8f5e68f83..26321a944e2 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/mod.rs @@ -1,15 +1,11 @@ pub mod accessors; pub mod fields; mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; pub mod methods; mod state_transition_estimated_fee_validation; mod state_transition_like; pub mod v0; mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; #[cfg(feature = "json-conversion")] @@ -110,10 +106,10 @@ mod test { use crate::state_transition::{ StateTransitionEstimatedFeeValidation, StateTransitionHasUserFeeIncrease, StateTransitionIdentityEstimatedFeeValidation, StateTransitionLike, StateTransitionOwned, - StateTransitionSingleSigned, StateTransitionType, StateTransitionValueConvert, + StateTransitionSingleSigned, StateTransitionType, }; use crate::version::LATEST_PLATFORM_VERSION; - use platform_value::{BinaryData, Identifier, Value}; + use platform_value::{BinaryData, Identifier}; fn make_update() -> IdentityUpdateTransition { IdentityUpdateTransition::V0(IdentityUpdateTransitionV0 { @@ -231,55 +227,97 @@ mod test { assert!(!result.is_valid()); } - #[test] - fn test_value_conversion_roundtrip() { - let t = make_update(); - let obj = StateTransitionValueConvert::to_object(&t, false).expect("should work"); - let restored = ::from_object( - obj, - LATEST_PLATFORM_VERSION, - ) - .expect("should work"); - assert_eq!(t, restored); - } + // Legacy `StateTransitionValueConvert` round-trip and + // unknown-version tests deleted in Phase D step 9. The canonical + // `JsonConvertible` / `ValueConvertible` round-trip is exercised on + // the outer enum derive (see `json_convertible_tests` below) — these + // tested methods that no longer exist. #[test] - fn test_from_value_map() { - let t = make_update(); - let obj = StateTransitionValueConvert::to_object(&t, false).expect("should work"); - let map = obj.into_btree_string_map().expect("should be map"); - let restored = ::from_value_map( - map, - LATEST_PLATFORM_VERSION, - ) - .expect("should work"); - assert_eq!(t, restored); + fn test_into_from_v0() { + let v0 = IdentityUpdateTransitionV0::default(); + let t: IdentityUpdateTransition = v0.clone().into(); + match t { + IdentityUpdateTransition::V0(inner) => assert_eq!(inner, v0), + } } +} - #[test] - fn test_from_object_unknown_version() { - let value = Value::from([("$stateTransitionProtocolVersion", Value::U16(255))]); - let result = ::from_object( - value, - LATEST_PLATFORM_VERSION, - ); - assert!(result.is_err()); +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + + use platform_value::{platform_value, BinaryData, Identifier}; + use serde_json::json; + + pub(crate) fn fixture() -> IdentityUpdateTransition { + IdentityUpdateTransition::V0(IdentityUpdateTransitionV0 { + identity_id: Identifier::new([0x55; 32]), + revision: 3, + nonce: 17, + add_public_keys: vec![], + disable_public_keys: vec![1, 2, 3], + user_fee_increase: 4, + signature_public_key_id: 6, + signature: BinaryData::new(vec![0xd4; 65]), + }) } #[test] - fn test_clean_value_unknown_version() { - let mut value = Value::from([("$stateTransitionProtocolVersion", Value::U8(255))]); - let result = - ::clean_value(&mut value); - assert!(result.is_err()); + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int fields whose JSON wire encoding loses size info: + // `revision` (u64), `nonce` (u64), `userFeeIncrease` (u16), + // `signaturePublicKeyId` (u32), `disablePublicKeys` items (u32 KeyIDs). + // The value-path assertion below uses explicit suffixes to lock variants. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "identityId": Identifier::new([0x55; 32]), + "revision": 3, + "nonce": 17, + "addPublicKeys": [], + "disablePublicKeys": [1, 2, 3], + "userFeeIncrease": 4, + "signaturePublicKeyId": 6, + "signature": "1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NQ=", + }) + ); + let recovered = IdentityUpdateTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); } #[test] - fn test_into_from_v0() { - let v0 = IdentityUpdateTransitionV0::default(); - let t: IdentityUpdateTransition = v0.clone().into(); - match t { - IdentityUpdateTransition::V0(inner) => assert_eq!(inner, v0), - } + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Explicit suffixes lock in sized variants: `revision` u64, `nonce` u64, + // `userFeeIncrease` u16, `signaturePublicKeyId` u32 (KeyID), + // `disablePublicKeys` items u32 (KeyID). + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "identityId": Identifier::new([0x55; 32]), + "revision": 3u64, + "nonce": 17u64, + "addPublicKeys": Vec::::new(), + "disablePublicKeys": [1u32, 2u32, 3u32], + "userFeeIncrease": 4u16, + "signaturePublicKeyId": 6u32, + "signature": BinaryData::new(vec![0xd4; 65]), + }) + ); + let recovered = IdentityUpdateTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/v0/json_conversion.rs deleted file mode 100644 index b88a90f2371..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/v0/json_conversion.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::state_transition::identity_update_transition::v0::IdentityUpdateTransitionV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for IdentityUpdateTransitionV0 {} - -#[cfg(test)] -mod test { - use crate::identity::accessors::IdentityGettersV0; - use crate::state_transition::identity_update_transition::fields::property_names::*; - use crate::state_transition::identity_update_transition::fields::*; - use crate::state_transition::identity_update_transition::v0::IdentityUpdateTransitionV0; - use crate::state_transition::identity_update_transition::IdentityUpdateTransition; - use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, - }; - use crate::tests::fixtures::identity_v0_fixture; - use crate::tests::utils::generate_random_identifier_struct; - use assert_matches::assert_matches; - use platform_value::BinaryData; - use serde_json::Value as JsonValue; - - #[test] - fn conversion_to_json_object() { - let public_key = identity_v0_fixture().public_keys()[&0].to_owned(); - let buffer = [0u8; 33]; - let transition: IdentityUpdateTransition = IdentityUpdateTransitionV0 { - identity_id: generate_random_identifier_struct(), - revision: 0, - nonce: 1, - add_public_keys: vec![public_key.into()], - disable_public_keys: vec![], - user_fee_increase: 0, - signature_public_key_id: 0, - signature: BinaryData::new(buffer.to_vec()), - } - .into(); - - let result = transition - .to_json(JsonStateTransitionSerializationOptions { - skip_signature: false, - into_validating_json: false, - }) - .expect("conversion to json shouldn't fail"); - assert_matches!(result[IDENTITY_ID], JsonValue::String(_)); - assert_matches!(result[SIGNATURE], JsonValue::String(_)); - assert_matches!(result[ADD_PUBLIC_KEYS][0]["data"], JsonValue::String(_)); - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/v0/mod.rs index 8dc1da98ead..fdf350d6ea3 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/v0/mod.rs @@ -1,11 +1,7 @@ mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; mod state_transition_like; mod types; pub(super) mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; #[cfg(feature = "json-conversion")] @@ -93,7 +89,6 @@ mod test { use crate::state_transition::{ StateTransitionHasUserFeeIncrease, StateTransitionIdentitySigned, StateTransitionLike, StateTransitionOwned, StateTransitionSingleSigned, StateTransitionType, - StateTransitionValueConvert, }; use platform_value::BinaryData; @@ -179,62 +174,11 @@ mod test { } } - #[test] - fn test_value_conversion_roundtrip() { - let t = make_update_v0(); - let obj = t.to_object(false).expect("to_object should work"); - let restored = - IdentityUpdateTransitionV0::from_object(obj, crate::version::PlatformVersion::latest()) - .expect("from_object should work"); - assert_eq!(t, restored); - } - - #[test] - fn test_to_object_skip_signature() { - let t = make_update_v0(); - let obj = t.to_object(true).expect("should work"); - let map = obj.into_btree_string_map().expect("should be map"); - assert!(!map.contains_key("signature")); - } - - #[test] - fn test_to_cleaned_object() { - let t = make_update_v0(); - let obj = t.to_cleaned_object(false).expect("should work"); - assert!(obj.is_map()); - } - - #[test] - fn test_to_cleaned_object_removes_empty_arrays() { - let t = IdentityUpdateTransitionV0 { - identity_id: Identifier::random(), - revision: 1, - nonce: 1, - add_public_keys: vec![], - disable_public_keys: vec![], - user_fee_increase: 0, - signature_public_key_id: 0, - signature: vec![].into(), - }; - let obj = t.to_cleaned_object(false).expect("should work"); - let map = obj.into_btree_string_map().expect("should be map"); - // Empty arrays should be removed - assert!(!map.contains_key("addPublicKeys")); - assert!(!map.contains_key("disablePublicKeys")); - } - - #[test] - fn test_from_value_map() { - let t = make_update_v0(); - let obj = t.to_object(false).expect("should work"); - let map = obj.into_btree_string_map().expect("should be map"); - let restored = IdentityUpdateTransitionV0::from_value_map( - map, - crate::version::PlatformVersion::latest(), - ) - .expect("should work"); - assert_eq!(t, restored); - } + // Legacy `StateTransitionValueConvert` round-trip / cleaned-object / + // skip-signature tests on the V0 inner struct deleted in Phase D + // step 9. The canonical `JsonConvertible` / `ValueConvertible` + // round-trip is exercised on the outer enum derive — these tested + // methods that no longer exist. #[test] fn test_get_list_empty() { diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/v0/value_conversion.rs deleted file mode 100644 index 34ad3fad146..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/v0/value_conversion.rs +++ /dev/null @@ -1,126 +0,0 @@ -use platform_value::{IntegerReplacementType, ReplacementType, Value}; - -use crate::{state_transition::StateTransitionFieldTypes, ProtocolError}; - -use crate::state_transition::identity_update_transition::fields::*; -use crate::state_transition::identity_update_transition::v0::{ - remove_integer_list_or_default, IdentityUpdateTransitionV0, -}; -use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; -use crate::state_transition::StateTransitionValueConvert; - -use crate::state_transition::state_transitions::common_fields::property_names::{ - NONCE, USER_FEE_INCREASE, -}; -use platform_version::version::PlatformVersion; - -impl StateTransitionValueConvert<'_> for IdentityUpdateTransitionV0 { - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let signature = raw_object - .get_binary_data(SIGNATURE) - .map_err(ProtocolError::ValueError)?; - let signature_public_key_id = raw_object - .get_integer(SIGNATURE_PUBLIC_KEY_ID) - .map_err(ProtocolError::ValueError)?; - let identity_id = raw_object - .get_identifier(IDENTITY_ID) - .map_err(ProtocolError::ValueError)?; - - let revision = raw_object - .get_integer(REVISION) - .map_err(ProtocolError::ValueError)?; - let nonce = raw_object - .get_integer(NONCE) - .map_err(ProtocolError::ValueError)?; - let user_fee_increase = raw_object - .get_optional_integer(USER_FEE_INCREASE) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default(); - let add_public_keys = raw_object - .remove_optional_array(property_names::ADD_PUBLIC_KEYS) - .map_err(ProtocolError::ValueError)? - .unwrap_or_default() - .into_iter() - .map(|value| IdentityPublicKeyInCreation::from_object(value, platform_version)) - .collect::, ProtocolError>>()?; - let disable_public_keys = - remove_integer_list_or_default(&mut raw_object, property_names::DISABLE_PUBLIC_KEYS)?; - - Ok(IdentityUpdateTransitionV0 { - signature, - signature_public_key_id, - identity_id, - revision, - nonce, - add_public_keys, - disable_public_keys, - user_fee_increase, - }) - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; - value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; - value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; - Ok(()) - } - - fn to_object(&self, skip_signature: bool) -> Result { - let mut value: Value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - - let mut add_public_keys: Vec = vec![]; - for key in self.add_public_keys.iter() { - add_public_keys.push(key.to_object(skip_signature)?); - } - - if !add_public_keys.is_empty() { - value.insert_at_end( - property_names::ADD_PUBLIC_KEYS.to_owned(), - Value::Array(add_public_keys), - )?; - } - - Ok(value) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - let mut value: Value = platform_value::to_value(self)?; - - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - - if !self.add_public_keys.is_empty() { - let mut add_public_keys: Vec = vec![]; - for key in self.add_public_keys.iter() { - add_public_keys.push(key.to_cleaned_object(skip_signature)?); - } - - value.insert( - property_names::ADD_PUBLIC_KEYS.to_owned(), - Value::Array(add_public_keys), - )?; - } - - value.remove_optional_value_if_empty_array(property_names::ADD_PUBLIC_KEYS)?; - - value.remove_optional_value_if_empty_array(property_names::DISABLE_PUBLIC_KEYS)?; - - Ok(value) - } - - // Override to_canonical_cleaned_object to manage add_public_keys individually - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - self.to_cleaned_object(skip_signature) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/value_conversion.rs deleted file mode 100644 index 173aa94b714..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_update_transition/value_conversion.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::Value; - -use crate::ProtocolError; - -use crate::state_transition::identity_update_transition::v0::IdentityUpdateTransitionV0; -use crate::state_transition::identity_update_transition::IdentityUpdateTransition; -use crate::state_transition::state_transitions::identity_update_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_version::version::{FeatureVersion, PlatformVersion}; - -impl StateTransitionValueConvert<'_> for IdentityUpdateTransition { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - IdentityUpdateTransition::V0(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - IdentityUpdateTransition::V0(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityUpdateTransition::V0(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityUpdateTransition::V0(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(IdentityUpdateTransitionV0::from_object(raw_object, platform_version)?.into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityUpdateTransition version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok( - IdentityUpdateTransitionV0::from_value_map(raw_value_map, platform_version)?.into(), - ), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityUpdateTransition version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => IdentityUpdateTransitionV0::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityUpdateTransition version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/json_conversion.rs deleted file mode 100644 index a1c5a4af7e2..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/json_conversion.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::state_transition::masternode_vote_transition::MasternodeVoteTransition; -use crate::state_transition::state_transitions::masternode_vote_transition::fields::*; -use crate::state_transition::{ - JsonStateTransitionSerializationOptions, StateTransitionJsonConvert, -}; -use crate::ProtocolError; -use serde_json::Number; -use serde_json::Value as JsonValue; - -impl StateTransitionJsonConvert<'_> for MasternodeVoteTransition { - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - match self { - MasternodeVoteTransition::V0(transition) => { - let mut value = transition.to_json(options)?; - let map_value = value.as_object_mut().expect("expected an object"); - map_value.insert( - STATE_TRANSITION_PROTOCOL_VERSION.to_string(), - JsonValue::Number(Number::from(0)), - ); - Ok(value) - } - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/mod.rs index 4af1d81d4ab..573a45ce0c6 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/mod.rs @@ -1,14 +1,10 @@ pub mod accessors; pub mod fields; mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; pub mod methods; mod state_transition_estimated_fee_validation; mod state_transition_like; pub mod v0; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; #[cfg(feature = "json-conversion")] @@ -105,7 +101,7 @@ mod test { use crate::serialization::{PlatformDeserializable, PlatformSerializable}; use crate::state_transition::{ StateTransitionEstimatedFeeValidation, StateTransitionLike, StateTransitionOwned, - StateTransitionSingleSigned, StateTransitionType, StateTransitionValueConvert, + StateTransitionSingleSigned, StateTransitionType, }; use crate::version::LATEST_PLATFORM_VERSION; use crate::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; @@ -114,7 +110,7 @@ mod test { use crate::voting::votes::resource_vote::v0::ResourceVoteV0; use crate::voting::votes::resource_vote::ResourceVote; use crate::voting::votes::Vote; - use platform_value::{BinaryData, Identifier, Value}; + use platform_value::{BinaryData, Identifier}; fn make_vote() -> MasternodeVoteTransition { MasternodeVoteTransition::V0(MasternodeVoteTransitionV0 { @@ -208,47 +204,139 @@ mod test { assert!(fee > 0); } + // Legacy `StateTransitionValueConvert` round-trip and + // unknown-version tests deleted in Phase D step 9. The canonical + // `JsonConvertible` / `ValueConvertible` round-trip is exercised on + // the outer enum derive (see `json_convertible_tests` below) — these + // tested methods that no longer exist. + #[test] - fn test_value_conversion_roundtrip() { - let t = make_vote(); - let obj = StateTransitionValueConvert::to_object(&t, false).expect("should work"); - let restored = ::from_object( - obj, - LATEST_PLATFORM_VERSION, - ) - .expect("should work"); - assert_eq!(t, restored); + fn test_into_from_v0() { + let v0 = MasternodeVoteTransitionV0::default(); + let t: MasternodeVoteTransition = v0.clone().into(); + match t { + MasternodeVoteTransition::V0(inner) => assert_eq!(inner, v0), + } } +} - #[test] - fn test_from_value_map() { - let t = make_vote(); - let obj = StateTransitionValueConvert::to_object(&t, false).expect("should work"); - let map = obj.into_btree_string_map().expect("should be map"); - let restored = ::from_value_map( - map, - LATEST_PLATFORM_VERSION, - ) - .expect("should work"); - assert_eq!(t, restored); +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + + use crate::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; + use crate::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; + use crate::voting::vote_polls::VotePoll; + use crate::voting::votes::resource_vote::v0::ResourceVoteV0; + use crate::voting::votes::resource_vote::ResourceVote; + use crate::voting::votes::Vote; + use platform_value::{platform_value, BinaryData, Identifier, Value}; + use serde_json::json; + + fn fixture_vote() -> Vote { + Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { + vote_poll: VotePoll::ContestedDocumentResourceVotePoll( + ContestedDocumentResourceVotePoll { + contract_id: Identifier::new([0x12; 32]), + document_type_name: "domain".to_string(), + index_name: "parentNameAndLabel".to_string(), + index_values: vec![Value::Text("dash".to_string())], + }, + ), + resource_vote_choice: ResourceVoteChoice::TowardsIdentity(Identifier::new([0x34; 32])), + })) + } + + pub(crate) fn fixture() -> MasternodeVoteTransition { + MasternodeVoteTransition::V0(MasternodeVoteTransitionV0 { + pro_tx_hash: Identifier::new([0x66; 32]), + voter_identity_id: Identifier::new([0x77; 32]), + vote: fixture_vote(), + nonce: 99, + signature_public_key_id: 8, + signature: BinaryData::new(vec![0xe5; 65]), + }) } #[test] - fn test_from_object_unknown_version() { - let value = Value::from([("$stateTransitionProtocolVersion", Value::U16(255))]); - let result = ::from_object( - value, - LATEST_PLATFORM_VERSION, + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int fields whose JSON wire encoding loses size info: + // `nonce` (u64 IdentityNonce), `signaturePublicKeyId` (u32 KeyID). + // The `vote` field is externally tagged (`type`/`data`) at every level + // (Vote enum, ResourceVote `$formatVersion`, VotePoll, ResourceVoteChoice). + // The value-path assertion below uses explicit suffixes for size lock-in. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "proTxHash": Identifier::new([0x66; 32]), + "voterIdentityId": Identifier::new([0x77; 32]), + "vote": { + "$type": "resourceVote", + "$formatVersion": "0", + "votePoll": { + "$type": "contestedDocumentResourceVotePoll", + "contractId": Identifier::new([0x12; 32]), + "documentTypeName": "domain", + "indexName": "parentNameAndLabel", + "indexValues": ["dash"], + }, + "resourceVoteChoice": { + "$type": "towardsIdentity", + "identity": Identifier::new([0x34; 32]), + }, + }, + "nonce": 99, + "signaturePublicKeyId": 8, + "signature": "5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eU=", + }) ); - assert!(result.is_err()); + let recovered = MasternodeVoteTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); } #[test] - fn test_into_from_v0() { - let v0 = MasternodeVoteTransitionV0::default(); - let t: MasternodeVoteTransition = v0.clone().into(); - match t { - MasternodeVoteTransition::V0(inner) => assert_eq!(inner, v0), - } + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Explicit suffixes lock in sized variants: `nonce` u64 (IdentityNonce), + // `signaturePublicKeyId` u32 (KeyID). + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "proTxHash": Identifier::new([0x66; 32]), + "voterIdentityId": Identifier::new([0x77; 32]), + "vote": { + "$type": "resourceVote", + "$formatVersion": "0", + "votePoll": { + "$type": "contestedDocumentResourceVotePoll", + "contractId": Identifier::new([0x12; 32]), + "documentTypeName": "domain", + "indexName": "parentNameAndLabel", + "indexValues": ["dash"], + }, + "resourceVoteChoice": { + "$type": "towardsIdentity", + "identity": Identifier::new([0x34; 32]), + }, + }, + "nonce": 99u64, + "signaturePublicKeyId": 8u32, + "signature": BinaryData::new(vec![0xe5; 65]), + }) + ); + let recovered = MasternodeVoteTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/v0/json_conversion.rs deleted file mode 100644 index e0663be9a9d..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/v0/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for MasternodeVoteTransitionV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/v0/mod.rs index 65c35bf6d08..80bf2bae25d 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/v0/mod.rs @@ -1,11 +1,7 @@ mod identity_signed; -#[cfg(feature = "json-conversion")] -mod json_conversion; mod state_transition_like; mod types; pub(super) mod v0_methods; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; use crate::identity::KeyID; @@ -197,43 +193,9 @@ mod test { } } - #[test] - fn test_value_conversion_roundtrip_v0() { - use crate::state_transition::StateTransitionValueConvert; - use crate::version::LATEST_PLATFORM_VERSION; - let t = make_vote_v0(); - let obj = t.to_object(false).expect("to_object should work"); - let restored = MasternodeVoteTransitionV0::from_object(obj, LATEST_PLATFORM_VERSION) - .expect("from_object should work"); - assert_eq!(t, restored); - } - - #[test] - fn test_from_value_map_v0() { - use crate::state_transition::StateTransitionValueConvert; - use crate::version::LATEST_PLATFORM_VERSION; - let t = make_vote_v0(); - let obj = t.to_object(false).expect("should work"); - let map = obj.into_btree_string_map().expect("should be map"); - let restored = MasternodeVoteTransitionV0::from_value_map(map, LATEST_PLATFORM_VERSION) - .expect("should work"); - assert_eq!(t, restored); - } - - #[test] - fn test_to_cleaned_object_v0() { - use crate::state_transition::StateTransitionValueConvert; - let t = make_vote_v0(); - let obj = t.to_cleaned_object(false).expect("should work"); - assert!(obj.is_map()); - } - - #[test] - fn test_to_object_skip_signature_v0() { - use crate::state_transition::StateTransitionValueConvert; - let t = make_vote_v0(); - let obj = t.to_object(true).expect("should work"); - let map = obj.into_btree_string_map().expect("should be map"); - assert!(!map.contains_key("signature")); - } + // Legacy `StateTransitionValueConvert` round-trip / cleaned-object / + // skip-signature tests on the V0 inner struct deleted in Phase D + // step 9. The canonical `JsonConvertible` / `ValueConvertible` + // round-trip is exercised on the outer enum derive — these tested + // methods that no longer exist. } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/v0/value_conversion.rs deleted file mode 100644 index cd73aba394f..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/v0/value_conversion.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::{IntegerReplacementType, ReplacementType, Value}; - -use crate::{state_transition::StateTransitionFieldTypes, ProtocolError}; - -use crate::state_transition::masternode_vote_transition::fields::*; -use crate::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; -use crate::state_transition::StateTransitionValueConvert; - -use platform_version::version::PlatformVersion; - -impl StateTransitionValueConvert<'_> for MasternodeVoteTransitionV0 { - fn from_object( - raw_object: Value, - _platform_version: &PlatformVersion, - ) -> Result { - platform_value::from_value(raw_object).map_err(ProtocolError::ValueError) - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - value.replace_at_paths(IDENTIFIER_FIELDS, ReplacementType::Identifier)?; - value.replace_at_paths(BINARY_FIELDS, ReplacementType::BinaryBytes)?; - value.replace_integer_type_at_paths(U32_FIELDS, IntegerReplacementType::U32)?; - Ok(()) - } - - fn from_value_map( - raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let value: Value = raw_value_map.into(); - Self::from_object(value, platform_version) - } - - fn to_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - let mut value = platform_value::to_value(self)?; - if skip_signature { - value - .remove_values_matching_paths(Self::signature_property_paths()) - .map_err(ProtocolError::ValueError)?; - } - Ok(value) - } - - // Override to_canonical_cleaned_object to manage add_public_keys individually - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - self.to_cleaned_object(skip_signature) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/value_conversion.rs deleted file mode 100644 index 472a906ae9f..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/masternode_vote_transition/value_conversion.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::collections::BTreeMap; - -use platform_value::Value; - -use crate::ProtocolError; - -use crate::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; -use crate::state_transition::masternode_vote_transition::MasternodeVoteTransition; -use crate::state_transition::state_transitions::masternode_vote_transition::fields::*; -use crate::state_transition::StateTransitionValueConvert; - -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_version::version::{FeatureVersion, PlatformVersion}; - -impl StateTransitionValueConvert<'_> for MasternodeVoteTransition { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - MasternodeVoteTransition::V0(transition) => { - let mut value = transition.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - MasternodeVoteTransition::V0(transition) => { - let mut value = transition.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - MasternodeVoteTransition::V0(transition) => { - let mut value = transition.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - MasternodeVoteTransition::V0(transition) => { - let mut value = transition.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok(MasternodeVoteTransitionV0::from_object(raw_object, platform_version)?.into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown MasternodeVoteTransition version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok( - MasternodeVoteTransitionV0::from_value_map(raw_value_map, platform_version)?.into(), - ), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown MasternodeVoteTransition version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => MasternodeVoteTransitionV0::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown MasternodeVoteTransition version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/json_conversion.rs deleted file mode 100644 index c98ecc86fad..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for IdentityPublicKeyInCreation {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/mod.rs index 66405e80237..98ac687de63 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/mod.rs @@ -16,13 +16,9 @@ use serde::{Deserialize, Serialize}; pub mod accessors; mod fields; -#[cfg(feature = "json-conversion")] -mod json_conversion; mod methods; mod types; pub mod v0; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; #[cfg_attr( @@ -380,78 +376,11 @@ mod test { assert_eq!(hash.len(), 20); } - #[test] - fn test_value_conversion_roundtrip() { - use crate::state_transition::StateTransitionValueConvert; - let key = make_master_key(0); - let v = StateTransitionValueConvert::to_object(&key, false).expect("to_object"); - assert!(v.is_map()); - let restored = ::from_object( - v, - LATEST_PLATFORM_VERSION, - ) - .expect("from_object"); - assert_eq!(key, restored); - } - - #[test] - fn test_value_conversion_unknown_version() { - use crate::state_transition::StateTransitionValueConvert; - use platform_value::Value; - let v = Value::from([("$version", Value::U16(255))]); - let result = ::from_object( - v, - LATEST_PLATFORM_VERSION, - ); - assert!(result.is_err()); - } - - #[test] - fn test_clean_value_unknown_version() { - use crate::state_transition::StateTransitionValueConvert; - use platform_value::Value; - let mut v = Value::from([("$version", Value::U8(255))]); - let result = - ::clean_value(&mut v); - assert!(result.is_err()); - } - - #[test] - fn test_to_canonical_object_inserts_version() { - use crate::state_transition::StateTransitionValueConvert; - let key = make_master_key(0); - let v = StateTransitionValueConvert::to_canonical_object(&key, false) - .expect("to_canonical_object"); - let map = v - .into_btree_string_map() - .expect("canonical object should be a map"); - assert!(map.contains_key("$version")); - } - - #[test] - fn test_to_canonical_cleaned_object_inserts_version() { - use crate::state_transition::StateTransitionValueConvert; - let key = make_master_key(0); - let v = StateTransitionValueConvert::to_canonical_cleaned_object(&key, false) - .expect("to_canonical_cleaned_object"); - let map = v.into_btree_string_map().expect("should be a map"); - assert!(map.contains_key("$version")); - } - - #[test] - fn test_from_value_map_roundtrip() { - use crate::state_transition::StateTransitionValueConvert; - let key = make_master_key(0); - let v = StateTransitionValueConvert::to_object(&key, false).expect("to_object"); - let map = v.into_btree_string_map().expect("should be a map"); - let restored = - ::from_value_map( - map, - LATEST_PLATFORM_VERSION, - ) - .expect("from_value_map"); - assert_eq!(key, restored); - } + // Legacy `StateTransitionValueConvert` round-trip / canonical / + // unknown-version tests deleted in Phase D step 9. The canonical + // `JsonConvertible` / `ValueConvertible` round-trip is exercised on + // the outer enum derive (see `json_convertible_tests` below) — these + // tested methods that no longer exist. #[test] fn test_default_versioned_unknown() { @@ -484,3 +413,83 @@ mod test { assert_eq!(dup_ids.len(), 1, "one duplicate expected"); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + + use crate::identity::{KeyType, Purpose, SecurityLevel}; + use platform_value::{platform_value, BinaryData}; + use serde_json::json; + + fn fixture() -> IdentityPublicKeyInCreation { + IdentityPublicKeyInCreation::V0(IdentityPublicKeyInCreationV0 { + id: 7, + key_type: KeyType::ECDSA_SECP256K1, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + read_only: true, + data: BinaryData::new(vec![0x88; 33]), + signature: BinaryData::new(vec![0x99; 65]), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int fields whose JSON wire encoding loses size info: + // `id` (u32 KeyID), `type`/`purpose`/`securityLevel` (u8 repr enums). + // The value-path assertion below uses explicit suffixes for size lock-in. + // Note `key_type` is renamed to `"type"` in the wire shape. + // `securityLevel` = 2 because `SecurityLevel::HIGH as u8 == 2`. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "id": 7, + "type": 0, + "purpose": 0, + "securityLevel": 2, + "contractBounds": serde_json::Value::Null, + "readOnly": true, + "data": "iIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI", + "signature": "mZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZk=", + }) + ); + let recovered = IdentityPublicKeyInCreation::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Explicit suffixes lock in sized variants: `id` u32 (KeyID), + // `type`/`purpose`/`securityLevel` u8 (#[repr(u8)] enums). + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "id": 7u32, + "type": 0u8, + "purpose": 0u8, + "securityLevel": 2u8, + "contractBounds": platform_value::Value::Null, + "readOnly": true, + "data": BinaryData::new(vec![0x88; 33]), + "signature": BinaryData::new(vec![0x99; 65]), + }) + ); + let recovered = IdentityPublicKeyInCreation::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/v0/json_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/v0/json_conversion.rs deleted file mode 100644 index 4f9e56bee02..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/v0/json_conversion.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::state_transition::public_key_in_creation::v0::IdentityPublicKeyInCreationV0; -use crate::state_transition::StateTransitionJsonConvert; - -impl StateTransitionJsonConvert<'_> for IdentityPublicKeyInCreationV0 {} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/v0/mod.rs index fc4ef336d45..35d406cffe5 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/v0/mod.rs @@ -1,8 +1,4 @@ -#[cfg(feature = "json-conversion")] -mod json_conversion; mod types; -#[cfg(feature = "value-conversion")] -mod value_conversion; mod version; use crate::identity::{IdentityPublicKey, KeyID, KeyType, Purpose, SecurityLevel}; @@ -145,126 +141,6 @@ impl IdentityPublicKeyInCreationMethodsV0 for IdentityPublicKeyInCreationV0 { } } -// -// #[cfg(feature = "value-conversion")] -// pub fn from_object(mut raw_object: Value) -> Result { -// raw_object.try_into().map_err(ProtocolError::ValueError) -// } -// -// -// -// -// pub fn from_raw_json_object(raw_object: JsonValue) -> Result { -// let identity_public_key: Self = serde_json::from_value(raw_object)?; -// Ok(identity_public_key) -// } -// -// pub fn from_json_object(raw_object: JsonValue) -> Result { -// let mut value: Value = raw_object.into(); -// value.replace_at_paths(BINARY_DATA_FIELDS, ReplacementType::BinaryBytes)?; -// value.try_into().map_err(ProtocolError::ValueError) -// } -// -// /// Return raw data, with all binary fields represented as arrays -// pub fn to_raw_object(&self, skip_signature: bool) -> Result { -// let mut value = self.to_object()?; -// -// if skip_signature || self.signature.is_empty() { -// value -// .remove("signature") -// .map_err(ProtocolError::ValueError)?; -// } -// -// Ok(value) -// } -// -// /// Return raw data, with all binary fields represented as arrays -// pub fn to_raw_cleaned_object(&self, skip_signature: bool) -> Result { -// let mut value = self.to_cleaned_object()?; -// -// if skip_signature || self.signature.is_empty() { -// value -// .remove("signature") -// .map_err(ProtocolError::ValueError)?; -// } -// -// Ok(value) -// } -// -// /// Return raw data, with all binary fields represented as arrays -// pub fn to_raw_json_object(&self, skip_signature: bool) -> Result { -// let mut value = serde_json::to_value(self)?; -// -// if skip_signature { -// if let JsonValue::Object(ref mut o) = value { -// o.remove("signature"); -// } -// } -// -// Ok(value) -// } -// -// -// -// pub fn to_ecdsa_array(&self) -> Result<[u8; 33], InvalidVectorSizeError> { -// vec::vec_to_array::<33>(self.data.as_slice()) -// } -// -// -// -// -// -// #[cfg(feature = "state-transition-cbor-conversion")] -// pub fn from_cbor_value(cbor_value: &CborValue) -> Result { -// let key_value_map = cbor_value.as_map().ok_or_else(|| { -// ProtocolError::DecodingError(String::from( -// "Expected identity public key to be a key value map", -// )) -// })?; -// -// let id = key_value_map.as_u16("id", "A key must have an uint16 id")?; -// let key_type = key_value_map.as_u8("type", "Identity public key must have a type")?; -// let purpose = key_value_map.as_u8("purpose", "Identity public key must have a purpose")?; -// let security_level = key_value_map.as_u8( -// "securityLevel", -// "Identity public key must have a securityLevel", -// )?; -// let readonly = -// key_value_map.as_bool("readOnly", "Identity public key must have a readOnly")?; -// let public_key_bytes = -// key_value_map.as_bytes("data", "Identity public key must have a data")?; -// let signature_bytes = key_value_map.as_bytes("signature", "").unwrap_or_default(); -// -// Ok(Self { -// id: id.into(), -// purpose: purpose.try_into()?, -// security_level: security_level.try_into()?, -// key_type: key_type.try_into()?, -// data: BinaryData::from(public_key_bytes), -// read_only: readonly, -// signature: BinaryData::from(signature_bytes), -// }) -// } -// -// #[cfg(feature = "state-transition-cbor-conversion")] -// pub fn to_cbor_value(&self) -> CborValue { -// let mut pk_map = CborCanonicalMap::new(); -// -// pk_map.insert("id", self.id); -// pk_map.insert("data", self.data.as_slice()); -// pk_map.insert("type", self.key_type); -// pk_map.insert("purpose", self.purpose); -// pk_map.insert("readOnly", self.read_only); -// pk_map.insert("securityLevel", self.security_level); -// -// if !self.signature.is_empty() { -// pk_map.insert("signature", self.signature.as_slice()) -// } -// -// pk_map.to_value_sorted() -// } -// } - impl From for IdentityPublicKey { fn from(val: IdentityPublicKeyInCreationV0) -> Self { IdentityPublicKeyV0 { diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/v0/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/v0/value_conversion.rs deleted file mode 100644 index 68cf0b4fc29..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/v0/value_conversion.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::state_transition::public_key_in_creation::v0::IdentityPublicKeyInCreationV0; -use crate::state_transition::StateTransitionValueConvert; - -impl StateTransitionValueConvert<'_> for IdentityPublicKeyInCreationV0 { - // this might be faster (todo: check) - // fn from_value_map(mut value_map: BTreeMap) -> Result where Self: Sized { - // Ok(Self { - // id: value_map - // .get_integer("id") - // .map_err(ProtocolError::ValueError)?, - // purpose: value_map - // .get_integer::("purpose") - // .map_err(ProtocolError::ValueError)? - // .try_into()?, - // security_level: value_map - // .get_integer::("securityLevel") - // .map_err(ProtocolError::ValueError)? - // .try_into()?, - // key_type: value_map - // .get_integer::("keyType") - // .map_err(ProtocolError::ValueError)? - // .try_into()?, - // data: value_map - // .remove_binary_data("data") - // .map_err(ProtocolError::ValueError)?, - // read_only: value_map - // .get_bool("readOnly") - // .map_err(ProtocolError::ValueError)?, - // signature: value_map - // .remove_binary_data("signature") - // .map_err(ProtocolError::ValueError)?, - // }) - // } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/value_conversion.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/value_conversion.rs deleted file mode 100644 index 7d21b75aab2..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/public_key_in_creation/value_conversion.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::state_transition::batch_transition::fields::property_names::STATE_TRANSITION_PROTOCOL_VERSION; -use crate::state_transition::public_key_in_creation::v0::IdentityPublicKeyInCreationV0; -use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; -use crate::state_transition::StateTransitionValueConvert; -use crate::ProtocolError; -use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; -use platform_value::Value; -use platform_version::version::{FeatureVersion, PlatformVersion}; -use std::collections::BTreeMap; - -impl StateTransitionValueConvert<'_> for IdentityPublicKeyInCreation { - fn to_object(&self, skip_signature: bool) -> Result { - match self { - IdentityPublicKeyInCreation::V0(public_key) => { - let mut value = public_key.to_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_object(&self, skip_signature: bool) -> Result { - match self { - IdentityPublicKeyInCreation::V0(public_key) => { - let mut value = public_key.to_canonical_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityPublicKeyInCreation::V0(public_key) => { - let mut value = public_key.to_canonical_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - match self { - IdentityPublicKeyInCreation::V0(public_key) => { - let mut value = public_key.to_cleaned_object(skip_signature)?; - value.insert(STATE_TRANSITION_PROTOCOL_VERSION.to_string(), Value::U16(0))?; - Ok(value) - } - } - } - - fn from_object( - mut raw_object: Value, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_object - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .contract_create_state_transition - .default_current_version - }); - - match version { - 0 => Ok( - IdentityPublicKeyInCreationV0::from_object(raw_object, platform_version)?.into(), - ), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityPublicKeyInCreation version {n}" - ))), - } - } - - fn from_value_map( - mut raw_value_map: BTreeMap, - platform_version: &PlatformVersion, - ) -> Result { - let version: FeatureVersion = raw_value_map - .remove_optional_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)? - .unwrap_or({ - platform_version - .dpp - .state_transition_serialization_versions - .identity_public_key_in_creation - .default_current_version - }); - - match version { - 0 => Ok(IdentityPublicKeyInCreationV0::from_value_map( - raw_value_map, - platform_version, - )? - .into()), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityPublicKeyInCreation version {n}" - ))), - } - } - - fn clean_value(value: &mut Value) -> Result<(), ProtocolError> { - let version: u8 = value - .get_integer(STATE_TRANSITION_PROTOCOL_VERSION) - .map_err(ProtocolError::ValueError)?; - - match version { - 0 => IdentityPublicKeyInCreationV0::clean_value(value), - n => Err(ProtocolError::UnknownVersionError(format!( - "Unknown IdentityPublicKeyInCreation version {n}" - ))), - } - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/identity_create_from_shielded_pool_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/identity_create_from_shielded_pool_transition/mod.rs index 1e503a85ca3..0e11c927edb 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/identity_create_from_shielded_pool_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/identity_create_from_shielded_pool_transition/mod.rs @@ -104,6 +104,162 @@ impl StateTransitionFieldTypes for IdentityCreateFromShieldedPoolTransition { } } +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::address_funds::PlatformAddress; + use crate::identity::{KeyType, Purpose, SecurityLevel}; + use crate::shielded::SerializedAction; + use crate::state_transition::identity_create_from_shielded_pool_transition::v0::IdentityCreateFromShieldedPoolTransitionV0; + use crate::state_transition::public_key_in_creation::v0::IdentityPublicKeyInCreationV0; + use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; + use platform_value::{platform_value, BinaryData, Bytes32}; + use serde_json::json; + + fn fixture_action() -> SerializedAction { + SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + } + } + + fn fixture_public_key() -> IdentityPublicKeyInCreation { + IdentityPublicKeyInCreation::V0(IdentityPublicKeyInCreationV0 { + id: 7, + key_type: KeyType::ECDSA_SECP256K1, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + read_only: true, + data: BinaryData::new(vec![0x88; 33]), + signature: BinaryData::new(vec![0x99; 65]), + }) + } + + pub(crate) fn fixture() -> IdentityCreateFromShieldedPoolTransition { + let actions = vec![fixture_action()]; + // identity_id is derived from the nullifiers, so the wire literal below is + // deterministic for this fixture. + let identity_id = derive_identity_id_from_actions(&actions); + IdentityCreateFromShieldedPoolTransition::V0(IdentityCreateFromShieldedPoolTransitionV0 { + public_keys: vec![fixture_public_key()], + denomination: 10_000_000_000, + actions, + anchor: [0x77; 32], + proof: vec![0x88; 192], + binding_signature: [0x99; 64], + send_to_address_on_creation_failure: PlatformAddress::P2pkh([0xa1; 20]), + identity_id, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int fields whose JSON wire encoding loses size info: + // `denomination` (u64), `publicKeys[].id` (u32 KeyID), + // `publicKeys[].type`/`purpose`/`securityLevel` (u8 repr enums). + // PlatformAddress → hex string in HR / 21 bytes non-HR; `identityId` + // → base58 string (Identifier). The value-path assertion locks all + // sized variants via explicit suffixes. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "publicKeys": [{ + "$formatVersion": "0", + "id": 7, + "type": 0, + "purpose": 0, + "securityLevel": 2, + "contractBounds": serde_json::Value::Null, + "readOnly": true, + "data": "iIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI", + "signature": "mZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZk=", + }], + "denomination": 10_000_000_000u64, + "actions": [{ + "nullifier": "ERERERERERERERERERERERERERERERERERERERERERE=", + "rk": "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=", + "cmx": "MzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM=", + "encryptedNote": "RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE", + "cvNet": "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=", + "spendAuthSig": "ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZg==", + }], + "anchor": "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3c=", + "proof": "iIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI", + "bindingSignature": "mZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmQ==", + "sendToAddressOnCreationFailure": "00a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "identityId": "71Rhgo8fHdyu6FtYEGTHVfGQBB13vE8ehHZ2PWo3F8JS", + }) + ); + let recovered = + IdentityCreateFromShieldedPoolTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let identity_id = match &original { + IdentityCreateFromShieldedPoolTransition::V0(v0) => v0.identity_id, + }; + let value = original.to_object().expect("to_object"); + // Explicit suffixes lock in sized variants: `denomination` u64, + // `publicKeys[].id` u32, `type`/`purpose`/`securityLevel` u8. + // PlatformAddress non-HR → 21-byte `Value::Bytes` (P2pkh type byte + // 0x00); `identityId` → `Value::Identifier`. + let mut address_bytes = vec![0x00]; + address_bytes.extend(vec![0xa1; 20]); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "publicKeys": [{ + "$formatVersion": "0", + "id": 7u32, + "type": 0u8, + "purpose": 0u8, + "securityLevel": 2u8, + "contractBounds": platform_value::Value::Null, + "readOnly": true, + "data": BinaryData::new(vec![0x88; 33]), + "signature": BinaryData::new(vec![0x99; 65]), + }], + "denomination": 10_000_000_000u64, + "actions": [{ + "nullifier": Bytes32::new([0x11; 32]), + "rk": Bytes32::new([0x22; 32]), + "cmx": Bytes32::new([0x33; 32]), + "encryptedNote": platform_value::Value::Bytes(vec![0x44; 216]), + "cvNet": Bytes32::new([0x55; 32]), + "spendAuthSig": platform_value::Value::Bytes(vec![0x66; 64]), + }], + "anchor": Bytes32::new([0x77; 32]), + "proof": platform_value::Value::Bytes(vec![0x88; 192]), + "bindingSignature": platform_value::Value::Bytes(vec![0x99; 64]), + "sendToAddressOnCreationFailure": platform_value::Value::Bytes(address_bytes), + "identityId": identity_id, + }) + ); + let recovered = + IdentityCreateFromShieldedPoolTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs index f8ab580d66d..541deac47fa 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs @@ -78,3 +78,147 @@ impl StateTransitionFieldTypes for ShieldFromAssetLockTransition { vec![SIGNATURE, PROOF] } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::shielded::SerializedAction; + use crate::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; + use crate::tests::fixtures::instant_asset_lock_proof_fixture; + use platform_value::BinaryData; + + // Tier 4: `instant_asset_lock_proof_fixture` produces NON-DETERMINISTIC bytes + // (random transaction / instantLock per run), so the full inline wire shape + // would change between runs. We assert envelope only on `assetLockProof` and + // a structural `assert_eq!(original, recovered)` covers that field's + // round-trip; deterministic siblings get full literal assertions. + pub(crate) fn fixture() -> ShieldFromAssetLockTransition { + ShieldFromAssetLockTransition::V0(ShieldFromAssetLockTransitionV0 { + asset_lock_proof: instant_asset_lock_proof_fixture(None, None), + actions: vec![SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + }], + value_balance: 1_000_000, + anchor: [0x77; 32], + proof: vec![0x88; 192], + binding_signature: [0x99; 64], + surplus_output: None, + signature: BinaryData::new(vec![0xab; 65]), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Envelope assertions: top-level keys + deterministic primitives. + // `assetLockProof` is non-deterministic; only its discriminator is checked. + let obj = json.as_object().expect("json is an object"); + assert_eq!(obj.get("$formatVersion"), Some(&serde_json::json!("0"))); + // Single action with deterministic byte fields → base64 strings. + let actions = obj + .get("actions") + .and_then(|v| v.as_array()) + .expect("actions array"); + assert_eq!(actions.len(), 1); + let act0 = actions[0].as_object().expect("action[0]"); + assert_eq!( + act0.get("nullifier"), + Some(&serde_json::json!( + "ERERERERERERERERERERERERERERERERERERERERERE=" + )) + ); + assert_eq!( + act0.get("rk"), + Some(&serde_json::json!( + "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=" + )) + ); + // `valueBalance` is `u64` in source. JSON erases the size on the wire — + // value-path uses `1_000_000u64` to lock the variant. + assert_eq!(obj.get("valueBalance"), Some(&serde_json::json!(1_000_000))); + assert_eq!( + obj.get("anchor"), + Some(&serde_json::json!( + "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3c=" + )) + ); + assert_eq!( + obj.get("signature"), + Some(&serde_json::json!( + "q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6s=" + )) + ); + // assetLockProof envelope only. + let proof = obj + .get("assetLockProof") + .and_then(|v| v.as_object()) + .expect("assetLockProof is an object"); + assert_eq!(proof.get("$type"), Some(&serde_json::json!("instant"))); + assert_eq!(proof.get("outputIndex"), Some(&serde_json::json!(0))); + assert!(proof.get("instantLock").is_some_and(|v| v.is_string())); + assert!(proof.get("transaction").is_some_and(|v| v.is_string())); + let recovered = ShieldFromAssetLockTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Envelope assertions on the deterministic siblings; sized variants + // locked in via explicit suffix (`1_000_000u64` for `value_balance`). + let map = value.as_map().expect("value is a map"); + let get = |key: &str| { + map.iter() + .find(|(k, _)| k.as_text() == Some(key)) + .map(|(_, v)| v) + }; + assert_eq!( + get("$formatVersion"), + Some(&platform_value::Value::Text("0".to_string())) + ); + assert_eq!( + get("valueBalance"), + Some(&platform_value::Value::U64(1_000_000)) + ); + assert_eq!( + get("anchor"), + Some(&platform_value::Value::Bytes32([0x77; 32])) + ); + assert_eq!( + get("signature"), + Some(&platform_value::Value::Bytes(vec![0xab; 65])) + ); + let proof = get("assetLockProof") + .and_then(|v| v.as_map()) + .expect("assetLockProof is a map"); + let pget = |key: &str| { + proof + .iter() + .find(|(k, _)| k.as_text() == Some(key)) + .map(|(_, v)| v) + }; + assert_eq!( + pget("$type"), + Some(&platform_value::Value::Text("instant".to_string())) + ); + assert_eq!(pget("outputIndex"), Some(&platform_value::Value::U32(0))); + assert!(pget("instantLock").is_some_and(|v| matches!(v, platform_value::Value::Bytes(_)))); + assert!(pget("transaction").is_some_and(|v| matches!(v, platform_value::Value::Bytes(_)))); + let recovered = ShieldFromAssetLockTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/mod.rs index 5107a765363..d81ad112d31 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_transition/mod.rs @@ -71,3 +71,138 @@ impl StateTransitionFieldTypes for ShieldTransition { vec![] } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::address_funds::{AddressFundsFeeStrategyStep, AddressWitness, PlatformAddress}; + use crate::shielded::SerializedAction; + use crate::state_transition::shield_transition::v0::ShieldTransitionV0; + use platform_value::{platform_value, BinaryData, Bytes32}; + use serde_json::json; + use std::collections::BTreeMap; + + fn fixture_action() -> SerializedAction { + SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + } + } + + pub(crate) fn fixture() -> ShieldTransition { + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([0xa1; 20]), (3u32, 500_000u64)); + ShieldTransition::V0(ShieldTransitionV0 { + inputs, + actions: vec![fixture_action()], + amount: 250_000, + anchor: [0x77; 32], + proof: vec![0x88; 192], + binding_signature: [0x99; 64], + fee_strategy: vec![AddressFundsFeeStrategyStep::DeductFromInput(0)], + user_fee_increase: 5, + input_witnesses: vec![AddressWitness::P2pkh { + signature: BinaryData::new(vec![0xaa; 65]), + }], + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int fields whose JSON wire encoding loses size info: + // `inputs[].nonce` (u32 AddressNonce), `inputs[].amount` (u64), + // `amount` (u64), `feeStrategy[].index` (u16), + // `userFeeIncrease` (u16). PlatformAddress → hex string in HR / 21 bytes + // non-HR; AddressWitness uses externally-tagged `{type, signature}`. + // BTreeMap serializes as array of + // `{address, nonce, amount}` objects, NOT a JSON map. The value-path + // assertion locks all sized variants via explicit suffixes. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "inputs": [{ + "address": "00a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "nonce": 3, + "amount": 500_000, + }], + "actions": [{ + "nullifier": "ERERERERERERERERERERERERERERERERERERERERERE=", + "rk": "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=", + "cmx": "MzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM=", + "encryptedNote": "RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE", + "cvNet": "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=", + "spendAuthSig": "ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZg==", + }], + "amount": 250_000, + "anchor": "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3c=", + "proof": "iIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI", + "bindingSignature": "mZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmQ==", + "feeStrategy": [{"$type": "deductFromInput", "index": 0}], + "userFeeIncrease": 5, + "inputWitnesses": [{ + "$type": "p2pkh", + "signature": "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo=", + }], + }) + ); + let recovered = ShieldTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Explicit suffixes lock in sized variants: `inputs[].nonce` u32 + // (AddressNonce), `inputs[].amount` u64, `amount` u64, + // `feeStrategy[].index` u16, `userFeeIncrease` u16. + // PlatformAddress non-HR → 21-byte `Value::Bytes` (P2pkh type byte 0x00). + let mut address_bytes = vec![0x00]; + address_bytes.extend(vec![0xa1; 20]); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "inputs": [{ + "address": platform_value::Value::Bytes(address_bytes), + "nonce": 3u32, + "amount": 500_000u64, + }], + "actions": [{ + "nullifier": Bytes32::new([0x11; 32]), + "rk": Bytes32::new([0x22; 32]), + "cmx": Bytes32::new([0x33; 32]), + "encryptedNote": platform_value::Value::Bytes(vec![0x44; 216]), + "cvNet": Bytes32::new([0x55; 32]), + "spendAuthSig": platform_value::Value::Bytes(vec![0x66; 64]), + }], + "amount": 250_000u64, + "anchor": Bytes32::new([0x77; 32]), + "proof": platform_value::Value::Bytes(vec![0x88; 192]), + "bindingSignature": platform_value::Value::Bytes(vec![0x99; 64]), + "feeStrategy": [{"$type": "deductFromInput", "index": 0u16}], + "userFeeIncrease": 5u16, + "inputWitnesses": [{ + "$type": "p2pkh", + "signature": BinaryData::new(vec![0xaa; 65]), + }], + }) + ); + let recovered = ShieldTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/mod.rs index 3c8eb39c6f5..1b4943406d6 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_transfer_transition/mod.rs @@ -71,3 +71,99 @@ impl StateTransitionFieldTypes for ShieldedTransferTransition { vec![] } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::shielded_transfer_transition::v0::ShieldedTransferTransitionV0; + use platform_value::{platform_value, Bytes32}; + use serde_json::json; + + fn fixture_action() -> crate::shielded::SerializedAction { + crate::shielded::SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + } + } + + pub(crate) fn fixture() -> ShieldedTransferTransition { + ShieldedTransferTransition::V0(ShieldedTransferTransitionV0 { + actions: vec![fixture_action()], + value_balance: 100_000, + anchor: [0x77; 32], + proof: vec![0x88; 192], + binding_signature: [0x99; 64], + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int field whose JSON wire encoding loses size info: + // `valueBalance` (u64). Fixed-width byte arrays serialize as base64 in JSON + // (via `serde_bytes` on the `[u8; N]` fields); the value-path assertion + // below uses `Bytes32::new(...)` / explicit `Bytes(...)` to lock variants. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "actions": [{ + "nullifier": "ERERERERERERERERERERERERERERERERERERERERERE=", + "rk": "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=", + "cmx": "MzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM=", + "encryptedNote": "RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE", + "cvNet": "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=", + "spendAuthSig": "ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZg==", + }], + "valueBalance": 100_000, + "anchor": "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3c=", + "proof": "iIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI", + "bindingSignature": "mZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmQ==", + }) + ); + let recovered = ShieldedTransferTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Explicit suffix locks in `valueBalance` as u64. Fixed-size 32-byte arrays + // (`nullifier`, `rk`, `cmx`, `cvNet`, `anchor`) serialize as `Value::Bytes32` + // via `serde_bytes` const-generic; 64-byte / variable arrays serialize as + // generic `Value::Bytes`. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "actions": [{ + "nullifier": Bytes32::new([0x11; 32]), + "rk": Bytes32::new([0x22; 32]), + "cmx": Bytes32::new([0x33; 32]), + "encryptedNote": platform_value::Value::Bytes(vec![0x44; 216]), + "cvNet": Bytes32::new([0x55; 32]), + "spendAuthSig": platform_value::Value::Bytes(vec![0x66; 64]), + }], + "valueBalance": 100_000u64, + "anchor": Bytes32::new([0x77; 32]), + "proof": platform_value::Value::Bytes(vec![0x88; 192]), + "bindingSignature": platform_value::Value::Bytes(vec![0x99; 64]), + }) + ); + let recovered = ShieldedTransferTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/mod.rs index 2eb7b6b8f45..4cb092e86a5 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shielded_withdrawal_transition/mod.rs @@ -71,3 +71,107 @@ impl StateTransitionFieldTypes for ShieldedWithdrawalTransition { vec![] } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::identity::core_script::CoreScript; + use crate::shielded::SerializedAction; + use crate::state_transition::shielded_withdrawal_transition::v0::ShieldedWithdrawalTransitionV0; + use crate::withdrawal::Pooling; + use platform_value::{platform_value, BinaryData, Bytes32}; + use serde_json::json; + + pub(crate) fn fixture() -> ShieldedWithdrawalTransition { + ShieldedWithdrawalTransition::V0(ShieldedWithdrawalTransitionV0 { + actions: vec![SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + }], + unshielding_amount: 750_000, + anchor: [0x77; 32], + proof: vec![0x88; 192], + binding_signature: [0x99; 64], + core_fee_per_byte: 21, + pooling: Pooling::IfAvailable, + output_script: CoreScript::from_bytes(vec![0xaa, 0xbb]), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int fields whose JSON wire encoding loses size info: + // `unshieldingAmount` (u64), `coreFeePerByte` (u32). `pooling` uses + // `pooling_serde` which emits the camelCase name in HR and u8 in non-HR. + // `outputScript` is base64 in HR (CoreScript Serialize) and bytes in non-HR. + // SerializedAction 32-byte fields → base64 in HR, `Value::Bytes32` non-HR. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "actions": [{ + "nullifier": "ERERERERERERERERERERERERERERERERERERERERERE=", + "rk": "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=", + "cmx": "MzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM=", + "encryptedNote": "RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE", + "cvNet": "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=", + "spendAuthSig": "ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZg==", + }], + "unshieldingAmount": 750_000, + "anchor": "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3c=", + "proof": "iIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI", + "bindingSignature": "mZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmQ==", + "coreFeePerByte": 21, + "pooling": "ifAvailable", + "outputScript": "qrs=", + }) + ); + let recovered = ShieldedWithdrawalTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Explicit suffixes lock in sized variants: `unshieldingAmount` u64, + // `coreFeePerByte` u32. `pooling` non-HR path emits the u8 discriminant + // (`Pooling::IfAvailable as u8 == 1`). `outputScript` non-HR → bytes. + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "actions": [{ + "nullifier": Bytes32::new([0x11; 32]), + "rk": Bytes32::new([0x22; 32]), + "cmx": Bytes32::new([0x33; 32]), + "encryptedNote": platform_value::Value::Bytes(vec![0x44; 216]), + "cvNet": Bytes32::new([0x55; 32]), + "spendAuthSig": platform_value::Value::Bytes(vec![0x66; 64]), + }], + "unshieldingAmount": 750_000u64, + "anchor": Bytes32::new([0x77; 32]), + "proof": platform_value::Value::Bytes(vec![0x88; 192]), + "bindingSignature": platform_value::Value::Bytes(vec![0x99; 64]), + "coreFeePerByte": 21u32, + "pooling": 1u8, + "outputScript": BinaryData::new(vec![0xaa, 0xbb]), + }) + ); + let recovered = ShieldedWithdrawalTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/mod.rs index 02d8aac6c19..1a8f8c8085a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/unshield_transition/mod.rs @@ -71,3 +71,104 @@ impl StateTransitionFieldTypes for UnshieldTransition { vec![] } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::unshield_transition::v0::UnshieldTransitionV0; + use platform_value::{platform_value, Bytes32}; + use serde_json::json; + + fn fixture_action() -> crate::shielded::SerializedAction { + crate::shielded::SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + } + } + + pub(crate) fn fixture() -> UnshieldTransition { + UnshieldTransition::V0(UnshieldTransitionV0 { + output_address: crate::address_funds::PlatformAddress::P2pkh([0xa1; 20]), + actions: vec![fixture_action()], + unshielding_amount: 250_000, + anchor: [0x77; 32], + proof: vec![0x88; 192], + binding_signature: [0x99; 64], + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Sized-int field whose JSON wire encoding loses size info: + // `unshieldingAmount` (u64). `outputAddress` is a `PlatformAddress` which + // serializes as hex string in HR (1 byte type + 20 byte hash) and bytes + // non-HR. SerializedAction byte fields: 32-byte arrays are base64 (HR); + // value-path uses `Value::Bytes32`. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "outputAddress": "00a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "actions": [{ + "nullifier": "ERERERERERERERERERERERERERERERERERERERERERE=", + "rk": "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=", + "cmx": "MzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM=", + "encryptedNote": "RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE", + "cvNet": "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=", + "spendAuthSig": "ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZg==", + }], + "unshieldingAmount": 250_000, + "anchor": "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3c=", + "proof": "iIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI", + "bindingSignature": "mZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmQ==", + }) + ); + let recovered = UnshieldTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Explicit suffix locks `unshieldingAmount` as u64. `PlatformAddress` on the + // non-HR path serializes as raw bytes (21 = 1 type + 20 hash); for P2pkh the + // type byte is 0x00. Fixed-size 32-byte fields → `Value::Bytes32`. + let mut output_address_bytes = vec![0x00]; + output_address_bytes.extend(vec![0xa1; 20]); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "outputAddress": platform_value::Value::Bytes(output_address_bytes), + "actions": [{ + "nullifier": Bytes32::new([0x11; 32]), + "rk": Bytes32::new([0x22; 32]), + "cmx": Bytes32::new([0x33; 32]), + "encryptedNote": platform_value::Value::Bytes(vec![0x44; 216]), + "cvNet": Bytes32::new([0x55; 32]), + "spendAuthSig": platform_value::Value::Bytes(vec![0x66; 64]), + }], + "unshieldingAmount": 250_000u64, + "anchor": Bytes32::new([0x77; 32]), + "proof": platform_value::Value::Bytes(vec![0x88; 192]), + "bindingSignature": platform_value::Value::Bytes(vec![0x99; 64]), + }) + ); + let recovered = UnshieldTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/traits/mod.rs b/packages/rs-dpp/src/state_transition/traits/mod.rs index 5283be02f4c..b7b7b3ffc26 100644 --- a/packages/rs-dpp/src/state_transition/traits/mod.rs +++ b/packages/rs-dpp/src/state_transition/traits/mod.rs @@ -4,15 +4,11 @@ mod state_transition_field_types; mod state_transition_has_user_fee_increase; mod state_transition_identity_id_from_inputs; mod state_transition_identity_signed; -#[cfg(feature = "json-conversion")] -mod state_transition_json_convert; mod state_transition_like; mod state_transition_multi_signed; mod state_transition_owned; mod state_transition_single_signed; mod state_transition_structure_validation; -#[cfg(feature = "value-conversion")] -mod state_transition_value_convert; mod state_transition_versioned; mod state_transition_witness_validation; @@ -22,14 +18,10 @@ pub use state_transition_field_types::*; pub use state_transition_has_user_fee_increase::*; pub use state_transition_identity_id_from_inputs::*; pub use state_transition_identity_signed::*; -#[cfg(feature = "json-conversion")] -pub use state_transition_json_convert::*; pub use state_transition_like::*; pub use state_transition_multi_signed::*; pub use state_transition_owned::*; pub use state_transition_single_signed::*; pub use state_transition_structure_validation::*; -#[cfg(feature = "value-conversion")] -pub use state_transition_value_convert::*; pub use state_transition_versioned::*; pub use state_transition_witness_validation::*; diff --git a/packages/rs-dpp/src/state_transition/traits/state_transition_json_convert.rs b/packages/rs-dpp/src/state_transition/traits/state_transition_json_convert.rs deleted file mode 100644 index 73e8dad39a3..00000000000 --- a/packages/rs-dpp/src/state_transition/traits/state_transition_json_convert.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::state_transition::StateTransitionValueConvert; -use crate::ProtocolError; -use serde::Serialize; -use serde_json::Value as JsonValue; -use std::convert::TryInto; - -#[derive(Debug, Copy, Clone, Default)] -pub struct JsonStateTransitionSerializationOptions { - pub skip_signature: bool, - pub into_validating_json: bool, -} - -/// The trait contains methods related to conversion of StateTransition into different formats -pub trait StateTransitionJsonConvert<'a>: Serialize + StateTransitionValueConvert<'a> { - /// Returns the [`serde_json::Value`] instance that encodes: - /// - Identifiers - with base58 - /// - Binary data - with base64 - fn to_json( - &self, - options: JsonStateTransitionSerializationOptions, - ) -> Result { - if options.into_validating_json { - self.to_object(options.skip_signature)? - .try_into_validating_json() - .map_err(ProtocolError::ValueError) - } else { - self.to_object(options.skip_signature)? - .try_into() - .map_err(ProtocolError::ValueError) - } - } -} diff --git a/packages/rs-dpp/src/state_transition/traits/state_transition_value_convert.rs b/packages/rs-dpp/src/state_transition/traits/state_transition_value_convert.rs deleted file mode 100644 index afe985b12ca..00000000000 --- a/packages/rs-dpp/src/state_transition/traits/state_transition_value_convert.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::state_transition::{state_transition_helpers, StateTransitionFieldTypes}; -use crate::ProtocolError; -use platform_value::{Value, ValueMapHelper}; -use platform_version::version::PlatformVersion; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; - -/// The trait contains methods related to conversion of StateTransition into different formats -pub trait StateTransitionValueConvert<'a>: - Serialize + Deserialize<'a> + StateTransitionFieldTypes -{ - /// Returns the [`platform_value::Value`] instance that preserves the `Vec` representation - /// for Identifiers and binary data - fn to_object(&self, skip_signature: bool) -> Result { - let skip_signature_paths = if skip_signature { - Self::signature_property_paths() - } else { - vec![] - }; - state_transition_helpers::to_object(self, skip_signature_paths) - } - - /// Returns the [`platform_value::Value`] instance that preserves the `Vec` representation - /// for Identifiers and binary data - fn to_canonical_object(&self, skip_signature: bool) -> Result { - let skip_signature_paths = if skip_signature { - Self::signature_property_paths() - } else { - vec![] - }; - let mut object = state_transition_helpers::to_object(self, skip_signature_paths)?; - - object.as_map_mut_ref().unwrap().sort_by_keys(); - Ok(object) - } - - /// Returns the [`platform_value::Value`] instance that preserves the `Vec` representation - /// for Identifiers and binary data - fn to_canonical_cleaned_object(&self, skip_signature: bool) -> Result { - let skip_signature_paths = if skip_signature { - Self::signature_property_paths() - } else { - vec![] - }; - let mut object = state_transition_helpers::to_cleaned_object(self, skip_signature_paths)?; - - object.as_map_mut_ref().unwrap().sort_by_keys(); - Ok(object) - } - - fn to_cleaned_object(&self, skip_signature: bool) -> Result { - self.to_object(skip_signature) - } - fn from_object( - raw_object: Value, - _platform_version: &PlatformVersion, - ) -> Result - where - Self: Sized, - { - platform_value::from_value(raw_object).map_err(ProtocolError::ValueError) - } - - fn from_value_map( - raw_value_map: BTreeMap, - _platform_version: &PlatformVersion, - ) -> Result - where - Self: Sized, - { - platform_value::from_value(Value::Map( - raw_value_map - .into_iter() - .map(|(k, v)| (k.into(), v)) - .collect(), - )) - .map_err(ProtocolError::ValueError) - } - fn clean_value(_value: &mut Value) -> Result<(), ProtocolError> { - Ok(()) - } -} diff --git a/packages/rs-dpp/src/tests/fixtures/identity_create_transition_fixture.rs b/packages/rs-dpp/src/tests/fixtures/identity_create_transition_fixture.rs index e46fe4009c8..794a261e5e2 100644 --- a/packages/rs-dpp/src/tests/fixtures/identity_create_transition_fixture.rs +++ b/packages/rs-dpp/src/tests/fixtures/identity_create_transition_fixture.rs @@ -20,7 +20,7 @@ pub fn identity_create_transition_fixture(one_time_private_key: Option Value { platform_value!({ "protocolVersion": version::LATEST_VERSION, - "type": StateTransitionType::IdentityCreditWithdrawal as u8, + "$type": StateTransitionType::IdentityCreditWithdrawal as u8, "identityId": Identifier::from([1_u8; 32]), "amount": 1042u64, "coreFeePerByte": 3u32, @@ -28,7 +28,7 @@ pub fn identity_credit_withdrawal_transition_fixture_raw_object() -> Value { pub fn identity_credit_withdrawal_transition_fixture_json() -> JsonValue { json!({ "protocolVersion": version::LATEST_VERSION, - "type": StateTransitionType::IdentityCreditWithdrawal, + "$type": StateTransitionType::IdentityCreditWithdrawal, "identityId": encode(&[1_u8; 32], Encoding::Base58), "amount": 1042, "coreFeePerByte": 3, diff --git a/packages/rs-dpp/src/tests/fixtures/identity_topup_transition_fixture.rs b/packages/rs-dpp/src/tests/fixtures/identity_topup_transition_fixture.rs index 6573cd74d0c..375c8a89842 100644 --- a/packages/rs-dpp/src/tests/fixtures/identity_topup_transition_fixture.rs +++ b/packages/rs-dpp/src/tests/fixtures/identity_topup_transition_fixture.rs @@ -12,7 +12,7 @@ pub fn identity_topup_transition_fixture(one_time_private_key: Option Result { let value = json_document_to_json_value(path)?; - DataContract::from_json(value, full_validation, platform_version) + if full_validation { + DataContract::from_json(value, true, platform_version) + } else { + // Non-validating path: deserialize the platform-version-agnostic + // serialization format (which handles both V0/V1 wire shapes via + // `$formatVersion`), then dispatch on the caller-provided + // `platform_version` to pick the DataContract variant. We avoid + // `serde_json::from_value::` here because that path + // ignores the caller pv and uses the process-global current/latest. + let format: crate::data_contract::serialized_version::DataContractInSerializationFormat = + serde_json::from_value(value) + .map_err(|e| ProtocolError::DecodingError(e.to_string()))?; + DataContract::try_from_platform_versioned(format, false, &mut vec![], platform_version) + } } #[cfg(all( diff --git a/packages/rs-dpp/src/tests/utils/mod.rs b/packages/rs-dpp/src/tests/utils/mod.rs index 852a7d6f8b1..7fac28875fb 100644 --- a/packages/rs-dpp/src/tests/utils/mod.rs +++ b/packages/rs-dpp/src/tests/utils/mod.rs @@ -57,6 +57,62 @@ where map.push((key.into(), value.into())); } +/// Recursively collapse sized integer variants in a `Value` tree to the +/// shape JSON can preserve. +/// +/// JSON has a single `Number` type, so a round-trip through `serde_json::Value` +/// erases the distinction between `Value::U32` / `Value::U16` / `Value::U8` / +/// `Value::I32` / `Value::I16` / `Value::I8` and lands on `Value::U64` (for +/// non-negatives) or `Value::I64` (for negatives). This helper normalizes a +/// `Value` tree to the same projection so that `assert_eq!(canonical(original), +/// canonical(recovered))` is meaningful for JSON-round-trip tests of +/// schema-bearing types like `DataContract`'s `document_schemas`. +/// +/// The normalization is intentionally lossy on the same axis JSON itself is +/// lossy on. Use only in tests where you need to compare values modulo +/// sized-int distinction. +pub fn normalize_integer_variants_for_json_round_trip(value: &mut Value) { + match value { + Value::U8(v) => *value = Value::U64(*v as u64), + Value::U16(v) => *value = Value::U64(*v as u64), + Value::U32(v) => *value = Value::U64(*v as u64), + Value::I8(v) => { + *value = if *v < 0 { + Value::I64(*v as i64) + } else { + Value::U64(*v as u64) + } + } + Value::I16(v) => { + *value = if *v < 0 { + Value::I64(*v as i64) + } else { + Value::U64(*v as u64) + } + } + Value::I32(v) => { + *value = if *v < 0 { + Value::I64(*v as i64) + } else { + Value::U64(*v as u64) + } + } + Value::I64(v) if *v >= 0 => *value = Value::U64(*v as u64), + Value::Array(items) => { + for item in items.iter_mut() { + normalize_integer_variants_for_json_round_trip(item); + } + } + Value::Map(entries) => { + for (k, v) in entries.iter_mut() { + normalize_integer_variants_for_json_round_trip(k); + normalize_integer_variants_for_json_round_trip(v); + } + } + _ => {} + } +} + pub fn generate_random_identifier_struct() -> Identifier { let mut buffer = [0u8; 32]; getrandom::getrandom(&mut buffer).unwrap(); diff --git a/packages/rs-dpp/src/tokens/contract_info/mod.rs b/packages/rs-dpp/src/tokens/contract_info/mod.rs index fa4272c9b3a..ba96a52d24e 100644 --- a/packages/rs-dpp/src/tokens/contract_info/mod.rs +++ b/packages/rs-dpp/src/tokens/contract_info/mod.rs @@ -27,12 +27,22 @@ pub mod v0; #[cfg_attr( any(feature = "fixtures-and-mocks", feature = "serde-conversion"), derive(serde::Serialize, serde::Deserialize), - serde(untagged) + serde(tag = "$formatVersion") )] pub enum TokenContractInfo { + #[cfg_attr( + any(feature = "fixtures-and-mocks", feature = "serde-conversion"), + serde(rename = "0") + )] V0(TokenContractInfoV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenContractInfo {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenContractInfo {} + impl TokenContractInfo { pub fn new( contract_id: Identifier, @@ -56,3 +66,63 @@ impl TokenContractInfo { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + fn fixture() -> TokenContractInfo { + TokenContractInfo::V0(crate::tokens::contract_info::v0::TokenContractInfoV0 { + contract_id: Identifier::new([0xab; 32]), + token_contract_position: 7, + }) + } + + // `TokenContractInfo` uses the standard `tag = "$formatVersion"` convention. + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `Identifier` renders as base58 in JSON HR. `tokenContractPosition` is + // a `u16` (TokenContractPosition alias); JSON has only one number type + // so the U16 distinction is erased — the Value-path assertion below + // uses `7u16` to lock in the sized variant. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "contractId": "CZ8YUVdk7znjrUmnb5n7kgySk9yRAsQDYmyCxzfSky9t", + "tokenContractPosition": 7, + }) + ); + let recovered = TokenContractInfo::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + let contract_id = Identifier::new([0xab; 32]); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "contractId": contract_id, + "tokenContractPosition": 7u16, + }) + ); + let recovered = TokenContractInfo::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/tokens/emergency_action.rs b/packages/rs-dpp/src/tokens/emergency_action.rs index d2d65d5ef06..89e03f8a1b4 100644 --- a/packages/rs-dpp/src/tokens/emergency_action.rs +++ b/packages/rs-dpp/src/tokens/emergency_action.rs @@ -17,6 +17,12 @@ pub enum TokenEmergencyAction { Resume = 1, } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenEmergencyAction {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenEmergencyAction {} + impl TokenEmergencyAction { pub fn paused(&self) -> bool { matches!(self, TokenEmergencyAction::Pause) @@ -31,3 +37,59 @@ impl TokenEmergencyAction { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + // `TokenEmergencyAction` uses `#[serde(rename_all = "camelCase")]` over a + // unit-only enum, so it (de)serializes as a plain camelCase string in both + // JSON and platform_value. + + #[test] + fn json_round_trip_pause() { + use crate::serialization::JsonConvertible; + let original = TokenEmergencyAction::Pause; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!("pause")); + let recovered = TokenEmergencyAction::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_resume() { + use crate::serialization::JsonConvertible; + let original = TokenEmergencyAction::Resume; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!("resume")); + let recovered = TokenEmergencyAction::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_pause() { + use crate::serialization::ValueConvertible; + let original = TokenEmergencyAction::Pause; + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!("pause")); + let recovered = TokenEmergencyAction::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_resume() { + use crate::serialization::ValueConvertible; + let original = TokenEmergencyAction::Resume; + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!("resume")); + let recovered = TokenEmergencyAction::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/tokens/gas_fees_paid_by.rs b/packages/rs-dpp/src/tokens/gas_fees_paid_by.rs index cec7564da4f..e7987805a14 100644 --- a/packages/rs-dpp/src/tokens/gas_fees_paid_by.rs +++ b/packages/rs-dpp/src/tokens/gas_fees_paid_by.rs @@ -4,20 +4,11 @@ use crate::consensus::ConsensusError; use crate::ProtocolError; use bincode::{Decode, Encode}; use derive_more::Display; -#[cfg(any( - feature = "serde-conversion", - all(feature = "serde-conversion", feature = "serde-conversion"), -))] +#[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, Encode, Decode, Default, PartialEq, Display)] -#[cfg_attr( - any( - feature = "serde-conversion", - all(feature = "serde-conversion", feature = "serde-conversion"), - ), - derive(Serialize, Deserialize) -)] +#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] pub enum GasFeesPaidBy { /// The user pays the gas fees #[default] @@ -29,6 +20,12 @@ pub enum GasFeesPaidBy { PreferContractOwner = 2, } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for GasFeesPaidBy {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for GasFeesPaidBy {} + impl From for u8 { fn from(value: GasFeesPaidBy) -> Self { match value { @@ -73,3 +70,78 @@ impl TryFrom for GasFeesPaidBy { .try_into() } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + // `GasFeesPaidBy` is a unit-only enum without `rename_all`, so each variant + // (de)serializes as its PascalCase Rust name in both JSON and platform_value. + + #[test] + fn json_round_trip_document_owner() { + use crate::serialization::JsonConvertible; + let original = GasFeesPaidBy::DocumentOwner; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!("DocumentOwner")); + let recovered = GasFeesPaidBy::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_contract_owner() { + use crate::serialization::JsonConvertible; + let original = GasFeesPaidBy::ContractOwner; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!("ContractOwner")); + let recovered = GasFeesPaidBy::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_prefer_contract_owner() { + use crate::serialization::JsonConvertible; + let original = GasFeesPaidBy::PreferContractOwner; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!("PreferContractOwner")); + let recovered = GasFeesPaidBy::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_document_owner() { + use crate::serialization::ValueConvertible; + let original = GasFeesPaidBy::DocumentOwner; + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!("DocumentOwner")); + let recovered = GasFeesPaidBy::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_contract_owner() { + use crate::serialization::ValueConvertible; + let original = GasFeesPaidBy::ContractOwner; + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!("ContractOwner")); + let recovered = GasFeesPaidBy::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_prefer_contract_owner() { + use crate::serialization::ValueConvertible; + let original = GasFeesPaidBy::PreferContractOwner; + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!("PreferContractOwner")); + let recovered = GasFeesPaidBy::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/tokens/info/mod.rs b/packages/rs-dpp/src/tokens/info/mod.rs index 4baa1bb666d..f4bf7a61528 100644 --- a/packages/rs-dpp/src/tokens/info/mod.rs +++ b/packages/rs-dpp/src/tokens/info/mod.rs @@ -108,3 +108,45 @@ mod tests { assert_eq!(info, restored); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_identitytokeninfo { + use super::*; + use crate::tokens::info::v0::IdentityTokenInfoV0; + use platform_value::platform_value; + use serde_json::json; + + fn fixture() -> IdentityTokenInfo { + IdentityTokenInfo::V0(IdentityTokenInfoV0 { frozen: true }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Internally-tagged enum with `tag = "$formatVersion"`; `IdentityTokenInfoV0` + // has no `rename_all`, so the inner field stays as the raw `frozen`. + assert_eq!(json, json!({"$formatVersion": "0", "frozen": true})); + let recovered = IdentityTokenInfo::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({"$formatVersion": "0", "frozen": true}) + ); + let recovered = IdentityTokenInfo::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/tokens/status/mod.rs b/packages/rs-dpp/src/tokens/status/mod.rs index db28ed7be54..627352683be 100644 --- a/packages/rs-dpp/src/tokens/status/mod.rs +++ b/packages/rs-dpp/src/tokens/status/mod.rs @@ -75,3 +75,46 @@ mod tests { assert_eq!(status, restored); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_tokenstatus { + use super::*; + use crate::tokens::status::v0::TokenStatusV0; + use platform_value::platform_value; + use serde_json::json; + + fn fixture() -> TokenStatus { + TokenStatus::V0(TokenStatusV0 { paused: true }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Internally-tagged enum (`tag = "$formatVersion"`); `TokenStatusV0` has + // `rename_all = "camelCase"` but the only field (`paused`) is already + // a single-token name, so the wire key matches the source identifier. + assert_eq!(json, json!({"$formatVersion": "0", "paused": true})); + let recovered = TokenStatus::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({"$formatVersion": "0", "paused": true}) + ); + let recovered = TokenStatus::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/tokens/token_event.rs b/packages/rs-dpp/src/tokens/token_event.rs index 49a63b9b651..5982eb45520 100644 --- a/packages/rs-dpp/src/tokens/token_event.rs +++ b/packages/rs-dpp/src/tokens/token_event.rs @@ -60,11 +60,15 @@ pub type FrozenIdentifier = Identifier; #[derive( Debug, PartialEq, PartialOrd, Clone, Eq, Encode, Decode, PlatformDeserialize, PlatformSerialize, )] -#[cfg_attr( - feature = "serde-conversion", - derive(serde::Serialize, serde::Deserialize), - serde(tag = "type", content = "data", rename_all = "camelCase") -)] +// Custom `Serialize` / `Deserialize` below — `TokenEvent` is a flat enum +// with all-tuple variants. Internal tagging requires struct variants or +// newtype-of-named-struct, which doesn't apply to tuple shapes. The custom +// impl maps positional tuple fields to named JSON keys per variant, emits +// an internal `$type` discriminator (no `data` wrapper), and uses the +// `json_safe_u64` +// / `json_safe_option_encrypted_note` helpers for u64 + encrypted-note +// fields. Bincode `Encode` / `Decode` derives above are untouched — +// consensus binary path is unaffected. #[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] #[platform_serialize(unversioned)] pub enum TokenEvent { @@ -161,6 +165,403 @@ pub enum TokenEvent { #[cfg(feature = "json-conversion")] impl JsonConvertible for TokenEvent {} +#[cfg(feature = "serde-conversion")] +impl serde::Serialize for TokenEvent { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeMap; + + // Wrappers that route through `json_safe_u64` and the encrypted-note + // helper so large u64s stringify in JSON HR and Vec inside the + // tuple becomes base64. + struct SafeU64<'a>(&'a u64); + impl<'a> serde::Serialize for SafeU64<'a> { + fn serialize(&self, s: S) -> Result { + crate::serialization::json::safe_integer::json_safe_u64::serialize(self.0, s) + } + } + struct SafeOptEncNote<'a>(&'a Option<(u32, u32, Vec)>); + impl<'a> serde::Serialize for SafeOptEncNote<'a> { + fn serialize(&self, s: S) -> Result { + crate::serialization::json::safe_integer::json_safe_option_encrypted_note::serialize( + self.0, s, + ) + } + } + + match self { + TokenEvent::Mint(amount, recipient, note) => { + let mut m = serializer.serialize_map(Some(4))?; + m.serialize_entry("$type", "mint")?; + m.serialize_entry("amount", &SafeU64(amount))?; + m.serialize_entry("recipient", recipient)?; + m.serialize_entry("publicNote", note)?; + m.end() + } + TokenEvent::Burn(amount, from, note) => { + let mut m = serializer.serialize_map(Some(4))?; + m.serialize_entry("$type", "burn")?; + m.serialize_entry("amount", &SafeU64(amount))?; + m.serialize_entry("burnFromIdentifier", from)?; + m.serialize_entry("publicNote", note)?; + m.end() + } + TokenEvent::Freeze(frozen, note) => { + let mut m = serializer.serialize_map(Some(3))?; + m.serialize_entry("$type", "freeze")?; + m.serialize_entry("frozenIdentifier", frozen)?; + m.serialize_entry("publicNote", note)?; + m.end() + } + TokenEvent::Unfreeze(frozen, note) => { + let mut m = serializer.serialize_map(Some(3))?; + m.serialize_entry("$type", "unfreeze")?; + m.serialize_entry("frozenIdentifier", frozen)?; + m.serialize_entry("publicNote", note)?; + m.end() + } + TokenEvent::DestroyFrozenFunds(frozen, amount, note) => { + let mut m = serializer.serialize_map(Some(4))?; + m.serialize_entry("$type", "destroyFrozenFunds")?; + m.serialize_entry("frozenIdentifier", frozen)?; + m.serialize_entry("amount", &SafeU64(amount))?; + m.serialize_entry("publicNote", note)?; + m.end() + } + TokenEvent::Transfer(recipient, note, shared, private, amount) => { + let mut m = serializer.serialize_map(Some(6))?; + m.serialize_entry("$type", "transfer")?; + m.serialize_entry("recipient", recipient)?; + m.serialize_entry("publicNote", note)?; + m.serialize_entry("sharedEncryptedNote", &SafeOptEncNote(shared))?; + m.serialize_entry("privateEncryptedNote", &SafeOptEncNote(private))?; + m.serialize_entry("amount", &SafeU64(amount))?; + m.end() + } + TokenEvent::Claim(distribution_type, amount, note) => { + let mut m = serializer.serialize_map(Some(4))?; + m.serialize_entry("$type", "claim")?; + m.serialize_entry("distributionType", distribution_type)?; + m.serialize_entry("amount", &SafeU64(amount))?; + m.serialize_entry("publicNote", note)?; + m.end() + } + TokenEvent::EmergencyAction(action, note) => { + let mut m = serializer.serialize_map(Some(3))?; + m.serialize_entry("$type", "emergencyAction")?; + m.serialize_entry("action", action)?; + m.serialize_entry("publicNote", note)?; + m.end() + } + TokenEvent::ConfigUpdate(change, note) => { + let mut m = serializer.serialize_map(Some(3))?; + m.serialize_entry("$type", "configUpdate")?; + m.serialize_entry("configurationChange", change)?; + m.serialize_entry("publicNote", note)?; + m.end() + } + TokenEvent::ChangePriceForDirectPurchase(schedule, note) => { + let mut m = serializer.serialize_map(Some(3))?; + m.serialize_entry("$type", "changePriceForDirectPurchase")?; + m.serialize_entry("pricingSchedule", schedule)?; + m.serialize_entry("publicNote", note)?; + m.end() + } + TokenEvent::DirectPurchase(amount, credits) => { + let mut m = serializer.serialize_map(Some(3))?; + m.serialize_entry("$type", "directPurchase")?; + m.serialize_entry("amount", &SafeU64(amount))?; + m.serialize_entry("credits", &SafeU64(credits))?; + m.end() + } + } + } +} + +#[cfg(feature = "serde-conversion")] +impl<'de> serde::Deserialize<'de> for TokenEvent { + fn deserialize>(deserializer: D) -> Result { + use serde::de::{Error, IgnoredAny, MapAccess, Visitor}; + + // Newtype wrappers that route u64 / encrypted-note deserialization + // through the json_safe helpers (accept both numeric and string forms + // for u64; accept either tuple-with-base64 or tuple-with-bytes). + #[derive(serde::Deserialize)] + #[serde(transparent)] + struct U64Safe( + #[serde(with = "crate::serialization::json::safe_integer::json_safe_u64")] u64, + ); + #[derive(serde::Deserialize)] + #[serde(transparent)] + struct OptEncNote( + #[serde( + with = "crate::serialization::json::safe_integer::json_safe_option_encrypted_note" + )] + Option<(u32, u32, Vec)>, + ); + + struct V; + + impl<'de> Visitor<'de> for V { + type Value = TokenEvent; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("TokenEvent as a map with `$type` discriminator + variant fields") + } + + fn visit_map>(self, mut map: A) -> Result { + let mut ty: Option = None; + let mut amount: Option = None; + let mut credits: Option = None; + let mut recipient: Option = None; + let mut burn_from: Option = None; + let mut frozen: Option = None; + let mut public_note: Option = None; + let mut shared_note: Option<(u32, u32, Vec)> = None; + let mut private_note: Option<(u32, u32, Vec)> = None; + let mut distribution_type: Option = + None; + let mut action: Option = None; + let mut configuration_change: Option = None; + let mut pricing_schedule: Option = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "$type" => ty = Some(map.next_value()?), + "amount" => amount = Some(map.next_value::()?.0), + "credits" => credits = Some(map.next_value::()?.0), + "recipient" => recipient = Some(map.next_value()?), + "burnFromIdentifier" => burn_from = Some(map.next_value()?), + "frozenIdentifier" => frozen = Some(map.next_value()?), + "publicNote" => public_note = map.next_value()?, + "sharedEncryptedNote" => { + shared_note = map.next_value::()?.0; + } + "privateEncryptedNote" => { + private_note = map.next_value::()?.0; + } + "distributionType" => distribution_type = Some(map.next_value()?), + "action" => action = Some(map.next_value()?), + "configurationChange" => configuration_change = Some(map.next_value()?), + "pricingSchedule" => pricing_schedule = map.next_value()?, + _ => { + let _: IgnoredAny = map.next_value()?; + } + } + } + + let ty = ty.ok_or_else(|| A::Error::missing_field("$type"))?; + match ty.as_str() { + "mint" => Ok(TokenEvent::Mint( + amount.ok_or_else(|| A::Error::missing_field("amount"))?, + recipient.ok_or_else(|| A::Error::missing_field("recipient"))?, + public_note, + )), + "burn" => Ok(TokenEvent::Burn( + amount.ok_or_else(|| A::Error::missing_field("amount"))?, + burn_from.ok_or_else(|| A::Error::missing_field("burnFromIdentifier"))?, + public_note, + )), + "freeze" => Ok(TokenEvent::Freeze( + frozen.ok_or_else(|| A::Error::missing_field("frozenIdentifier"))?, + public_note, + )), + "unfreeze" => Ok(TokenEvent::Unfreeze( + frozen.ok_or_else(|| A::Error::missing_field("frozenIdentifier"))?, + public_note, + )), + "destroyFrozenFunds" => Ok(TokenEvent::DestroyFrozenFunds( + frozen.ok_or_else(|| A::Error::missing_field("frozenIdentifier"))?, + amount.ok_or_else(|| A::Error::missing_field("amount"))?, + public_note, + )), + "transfer" => Ok(TokenEvent::Transfer( + recipient.ok_or_else(|| A::Error::missing_field("recipient"))?, + public_note, + shared_note, + private_note, + amount.ok_or_else(|| A::Error::missing_field("amount"))?, + )), + "claim" => Ok(TokenEvent::Claim( + distribution_type + .ok_or_else(|| A::Error::missing_field("distributionType"))?, + amount.ok_or_else(|| A::Error::missing_field("amount"))?, + public_note, + )), + "emergencyAction" => Ok(TokenEvent::EmergencyAction( + action.ok_or_else(|| A::Error::missing_field("action"))?, + public_note, + )), + "configUpdate" => Ok(TokenEvent::ConfigUpdate( + configuration_change + .ok_or_else(|| A::Error::missing_field("configurationChange"))?, + public_note, + )), + "changePriceForDirectPurchase" => Ok(TokenEvent::ChangePriceForDirectPurchase( + pricing_schedule, + public_note, + )), + "directPurchase" => Ok(TokenEvent::DirectPurchase( + amount.ok_or_else(|| A::Error::missing_field("amount"))?, + credits.ok_or_else(|| A::Error::missing_field("credits"))?, + )), + other => Err(A::Error::unknown_variant( + other, + &[ + "mint", + "burn", + "freeze", + "unfreeze", + "destroyFrozenFunds", + "transfer", + "claim", + "emergencyAction", + "configUpdate", + "changePriceForDirectPurchase", + "directPurchase", + ], + )), + } + } + } + + deserializer.deserialize_map(V) + } +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + // `TokenEvent` has a custom `Serialize` / `Deserialize` impl emitting an + // internally-tagged flat shape: each variant maps positional tuple fields + // to named JSON keys (`amount` / `recipient` / `publicNote` / etc.). + // Round-trip covers a representative sample: `Mint` (3-tuple), `Freeze` + // (2-tuple including null note), `DirectPurchase` (2-tuple of u64 aliases). + + pub(crate) fn mint_fixture() -> TokenEvent { + TokenEvent::Mint( + 5_000, + Identifier::new([0xa1; 32]), + Some("genesis mint".to_string()), + ) + } + + #[test] + fn json_round_trip_mint() { + use crate::serialization::JsonConvertible; + let original = mint_fixture(); + let json = original.to_json().expect("to_json"); + // `TokenAmount` (u64) → `json_safe_u64` (number for small values, + // string above MAX_SAFE_INTEGER). `Identifier` → base58 string in HR. + assert_eq!( + json, + json!({ + "$type": "mint", + "amount": 5_000, + "recipient": "Bswb3UyeD1pUTaGiE6WvqwFpJZsQSEY1xhJePCDTHdvp", + "publicNote": "genesis mint", + }) + ); + let recovered = TokenEvent::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_freeze_no_note() { + use crate::serialization::JsonConvertible; + let original = TokenEvent::Freeze(Identifier::new([0xb2; 32]), None); + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({ + "$type": "freeze", + "frozenIdentifier": "D2ZcUbtpG5sKq7XLeB4YnpNnTGSptKCxTddoNeydzJQq", + "publicNote": null, + }) + ); + let recovered = TokenEvent::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_direct_purchase() { + use crate::serialization::JsonConvertible; + let original = TokenEvent::DirectPurchase(100, 5_000); + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({ + "$type": "directPurchase", + "amount": 100, + "credits": 5_000, + }) + ); + let recovered = TokenEvent::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_mint() { + use crate::serialization::ValueConvertible; + let original = mint_fixture(); + let value = original.to_object().expect("to_object"); + // `TokenAmount` is `u64` → `Value::U64`. Identifier → `Value::Identifier`. + assert_eq!( + value, + platform_value!({ + "$type": "mint", + "amount": 5_000u64, + "recipient": Identifier::new([0xa1; 32]), + "publicNote": "genesis mint", + }) + ); + let recovered = TokenEvent::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_freeze_no_note() { + use crate::serialization::ValueConvertible; + let original = TokenEvent::Freeze(Identifier::new([0xb2; 32]), None); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({ + "$type": "freeze", + "frozenIdentifier": Identifier::new([0xb2; 32]), + "publicNote": null, + }) + ); + let recovered = TokenEvent::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_direct_purchase() { + use crate::serialization::ValueConvertible; + let original = TokenEvent::DirectPurchase(100, 5_000); + let value = original.to_object().expect("to_object"); + // `TokenAmount` and `Credits` are both `u64`. + assert_eq!( + value, + platform_value!({ + "$type": "directPurchase", + "amount": 100u64, + "credits": 5_000u64, + }) + ); + let recovered = TokenEvent::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} + impl fmt::Display for TokenEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { diff --git a/packages/rs-dpp/src/tokens/token_payment_info/mod.rs b/packages/rs-dpp/src/tokens/token_payment_info/mod.rs index 1c10d21672f..9a37abb3777 100644 --- a/packages/rs-dpp/src/tokens/token_payment_info/mod.rs +++ b/packages/rs-dpp/src/tokens/token_payment_info/mod.rs @@ -56,10 +56,7 @@ use platform_value::btreemap_extensions::BTreeValueMapHelper; #[cfg(feature = "value-conversion")] use platform_value::Error; use platform_value::{Identifier, Value}; -#[cfg(any( - feature = "serde-conversion", - all(feature = "serde-conversion", feature = "serde-conversion"), -))] +#[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -79,10 +76,7 @@ pub mod v0; From, )] #[cfg_attr( - any( - feature = "serde-conversion", - all(feature = "serde-conversion", feature = "serde-conversion"), - ), + feature = "serde-conversion", derive(Serialize, Deserialize), serde(tag = "$formatVersion") )] @@ -97,16 +91,16 @@ pub mod v0; /// See [`v0::TokenPaymentInfoV0`] for the current set of fields and semantics. pub enum TokenPaymentInfo { #[display("V0({})", "_0")] - #[cfg_attr( - any( - feature = "serde-conversion", - all(feature = "serde-conversion", feature = "serde-conversion"), - ), - serde(rename = "0") - )] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(TokenPaymentInfoV0), } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenPaymentInfo {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenPaymentInfo {} + impl TokenPaymentInfoMethodsV0 for TokenPaymentInfo {} impl TokenPaymentInfoAccessorsV0 for TokenPaymentInfo { @@ -215,3 +209,75 @@ impl TryFrom for Value { platform_value::to_value(value) } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + fn fixture() -> TokenPaymentInfo { + TokenPaymentInfo::V0(TokenPaymentInfoV0 { + payment_token_contract_id: Some(Identifier::new([0x99; 32])), + token_contract_position: 3, + minimum_token_cost: Some(100), + maximum_token_cost: Some(1_000), + gas_fees_paid_by: GasFeesPaidBy::ContractOwner, + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Internally-tagged enum (`tag = "$formatVersion"`); inner V0 has + // `rename_all = "camelCase"`. `Identifier` -> base58 in JSON. + // `token_contract_position` is `TokenContractPosition` (= u16) and + // `minimum_token_cost` / `maximum_token_cost` are `TokenAmount` (= u64); + // JSON erases the size — see the value-path assertion for typed locks. + // `gas_fees_paid_by` is the unit enum `GasFeesPaidBy` and serializes + // as `"ContractOwner"` (no `rename_all`). + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "paymentTokenContractId": "BLbDu5FZUdSfLrGejhuaWw5iMJBo3j3TVRyPv9rfJyMA", + "tokenContractPosition": 3, + "minimumTokenCost": 100, + "maximumTokenCost": 1_000, + "gasFeesPaidBy": "ContractOwner", + }) + ); + let recovered = TokenPaymentInfo::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // `Identifier` flows as `Value::Identifier` when interpolated. + // `3u16` locks `Value::U16`; `100u64` / `1_000u64` lock `Value::U64`. + let payment_token_contract_id = Identifier::new([0x99; 32]); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "paymentTokenContractId": payment_token_contract_id, + "tokenContractPosition": 3u16, + "minimumTokenCost": 100u64, + "maximumTokenCost": 1_000u64, + "gasFeesPaidBy": "ContractOwner", + }) + ); + let recovered = TokenPaymentInfo::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/tokens/token_payment_info/v0/mod.rs b/packages/rs-dpp/src/tokens/token_payment_info/v0/mod.rs index 6ff89f0cb8d..b180bbbf9b0 100644 --- a/packages/rs-dpp/src/tokens/token_payment_info/v0/mod.rs +++ b/packages/rs-dpp/src/tokens/token_payment_info/v0/mod.rs @@ -9,19 +9,17 @@ use bincode::{Decode, Encode}; use derive_more::Display; use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper; use platform_value::{Identifier, Value}; -#[cfg(any( - feature = "serde-conversion", - all(feature = "serde-conversion", feature = "serde-conversion"), -))] +#[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; #[derive(Debug, Clone, Copy, Encode, Decode, Default, PartialEq, Display)] +// `json_safe_fields` auto-injects `json_safe_option_u64` on +// `Option` (= `Option`) fields so JSON encodes large +// values as strings — same convention as the rest of the wire shape. +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] #[cfg_attr( - any( - feature = "serde-conversion", - all(feature = "serde-conversion", feature = "serde-conversion"), - ), + feature = "serde-conversion", derive(Serialize, Deserialize), serde(rename_all = "camelCase") )] diff --git a/packages/rs-dpp/src/tokens/token_pricing_schedule.rs b/packages/rs-dpp/src/tokens/token_pricing_schedule.rs index 5af1f3ebb9a..b73b0ab5251 100644 --- a/packages/rs-dpp/src/tokens/token_pricing_schedule.rs +++ b/packages/rs-dpp/src/tokens/token_pricing_schedule.rs @@ -25,7 +25,11 @@ use std::fmt::{self, Display, Formatter}; PlatformSerialize, PlatformDeserialize, )] -#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(into = "TokenPricingScheduleRepr", from = "TokenPricingScheduleRepr") +)] pub enum TokenPricingSchedule { /// A single flat price in credits for all token amounts. /// @@ -44,6 +48,58 @@ pub enum TokenPricingSchedule { SetPrices(BTreeMap), } +// Internal-`$type` serde shape. The tuple-variant outer enum can neither +// auto-derive internal tagging nor annotate its variant-internal u64s, so this +// struct-variant helper does both: `json_safe_u64` / `json_safe_u64_u64_map` +// keep the `Credits` and `TokenAmount` values JS-safe (string above +// `Number.MAX_SAFE_INTEGER`) in human-readable JSON, with no effect on `Value` +// or the bincode consensus path (which round-trips the outer enum directly). +#[cfg(feature = "serde-conversion")] +#[derive(Serialize, Deserialize)] +#[serde(tag = "$type", rename_all = "camelCase")] +enum TokenPricingScheduleRepr { + SinglePrice { + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::serialization::json_safe_u64") + )] + price: Credits, + }, + SetPrices { + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::serialization::json::safe_integer_map::json_safe_u64_u64_map") + )] + prices: BTreeMap, + }, +} + +#[cfg(feature = "serde-conversion")] +impl From for TokenPricingScheduleRepr { + fn from(schedule: TokenPricingSchedule) -> Self { + match schedule { + TokenPricingSchedule::SinglePrice(price) => Self::SinglePrice { price }, + TokenPricingSchedule::SetPrices(prices) => Self::SetPrices { prices }, + } + } +} + +#[cfg(feature = "serde-conversion")] +impl From for TokenPricingSchedule { + fn from(repr: TokenPricingScheduleRepr) -> Self { + match repr { + TokenPricingScheduleRepr::SinglePrice { price } => Self::SinglePrice(price), + TokenPricingScheduleRepr::SetPrices { prices } => Self::SetPrices(prices), + } + } +} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenPricingSchedule {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenPricingSchedule {} + impl TokenPricingSchedule { pub fn minimum_purchase_amount_and_price(&self) -> (TokenAmount, Credits) { match self { @@ -76,6 +132,113 @@ impl Display for TokenPricingSchedule { } } +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::{platform_value, Value}; + use serde_json::json; + + // Internally `$type`-tagged: `SinglePrice(u64)` → `{"$type":"singlePrice", + // "price": }`, `SetPrices(BTreeMap)` → `{"$type":"setPrices", + // "prices": {: , ...}}`. `Credits`/`TokenAmount` u64s are JS-safe + // (number below 2^53, string above); JSON forces map keys to strings while + // platform_value preserves typed keys. + + #[test] + fn json_round_trip_single_price() { + use crate::serialization::JsonConvertible; + let original = TokenPricingSchedule::SinglePrice(1234); + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!({ "$type": "singlePrice", "price": 1234 })); + let recovered = TokenPricingSchedule::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_single_price_above_max_safe_integer_is_string() { + use crate::serialization::JsonConvertible; + // Above Number.MAX_SAFE_INTEGER (2^53): json_safe_u64 must stringify the + // Credits so JS consumers can't silently round it. (Raw u64 before the + // Repr fix; string after.) + let big: Credits = (1u64 << 53) + 1; + let original = TokenPricingSchedule::SinglePrice(big); + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({ "$type": "singlePrice", "price": big.to_string() }) + ); + let recovered = TokenPricingSchedule::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_set_prices() { + use crate::serialization::JsonConvertible; + let mut prices = BTreeMap::new(); + prices.insert(5u64, 50u64); + prices.insert(10u64, 80u64); + let original = TokenPricingSchedule::SetPrices(prices); + let json = original.to_json().expect("to_json"); + // JSON object keys must be strings — `serde_json` stringifies the + // u64 amount keys. + assert_eq!( + json, + json!({ "$type": "setPrices", "prices": { "5": 50, "10": 80 } }) + ); + let recovered = TokenPricingSchedule::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_single_price() { + use crate::serialization::ValueConvertible; + let original = TokenPricingSchedule::SinglePrice(1234); + let value = original.to_object().expect("to_object"); + // `Credits` is `u64` → `Value::U64` (non-HR: json_safe_u64 stays typed). + assert_eq!( + value, + platform_value!({ "$type": "singlePrice", "price": 1234u64 }) + ); + let recovered = TokenPricingSchedule::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_set_prices() { + use crate::serialization::ValueConvertible; + let mut prices = BTreeMap::new(); + prices.insert(5u64, 50u64); + prices.insert(10u64, 80u64); + let original = TokenPricingSchedule::SetPrices(prices); + let value = original.to_object().expect("to_object"); + // platform_value preserves typed map keys: `BTreeMap` → + // map of `(Value::U64, Value::U64)` pairs. Serialized `$type` first. + assert_eq!( + value, + Value::Map(vec![ + ( + Value::Text("$type".to_string()), + Value::Text("setPrices".to_string()), + ), + ( + Value::Text("prices".to_string()), + Value::Map(vec![ + (Value::U64(5), Value::U64(50)), + (Value::U64(10), Value::U64(80)), + ]), + ), + ]) + ); + let recovered = TokenPricingSchedule::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/rs-dpp/src/util/deserializer.rs b/packages/rs-dpp/src/util/deserializer.rs index 4ea03014dca..d84af78e3ee 100644 --- a/packages/rs-dpp/src/util/deserializer.rs +++ b/packages/rs-dpp/src/util/deserializer.rs @@ -53,34 +53,3 @@ pub fn split_cbor_feature_version( main_message_bytes, }) } - -pub mod serde_entropy { - use base64::prelude::BASE64_STANDARD; - use base64::Engine; - use std::convert::TryInto; - - use serde::{Deserialize, Deserializer, Serializer}; - - pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 32], D::Error> { - let data: String = Deserialize::deserialize(d)?; - BASE64_STANDARD - .decode(&data) - .map_err(|e| { - serde::de::Error::custom(format!("Unable to decode {}' with base64 - {}", data, e)) - })? - .try_into() - .map_err(|_| { - serde::de::Error::custom(format!( - "Unable to convert the '{:?}' into 32 bytes array", - data - )) - }) - } - - pub fn serialize(buffer: &[u8], serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&BASE64_STANDARD.encode(buffer)) - } -} diff --git a/packages/rs-dpp/src/voting/contender_structs/contender/mod.rs b/packages/rs-dpp/src/voting/contender_structs/contender/mod.rs index 2c8389588d1..a8e4e9c42d7 100644 --- a/packages/rs-dpp/src/voting/contender_structs/contender/mod.rs +++ b/packages/rs-dpp/src/voting/contender_structs/contender/mod.rs @@ -456,3 +456,78 @@ mod tests { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_contender_with_serialized_document { + use super::*; + use platform_value::{platform_value, Identifier, Value}; + use serde_json::json; + + /// Non-default values per field (real identity_id bytes, non-empty + /// serialized_document, non-zero tally) so the wire-shape assertion + /// catches silent zero-out / flip on round-trip. + fn fixture() -> ContenderWithSerializedDocument { + ContenderWithSerializedDocument::V0(ContenderWithSerializedDocumentV0 { + identity_id: Identifier::new([0xa1; 32]), + serialized_document: Some(vec![0xde, 0xad, 0xbe, 0xef]), + vote_tally: Some(42), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `Identifier` renders as base58 in JSON. `Vec` (from the default + // `serialize_seq` in serde_json) renders as an array of numbers — NOT + // bytes — so each element appears as `Number(...)`. `vote_tally` is + // `Option`; JSON erases the size — the value-path assertion uses + // `42u32` to lock in `Value::U32`. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "identityId": "Bswb3UyeD1pUTaGiE6WvqwFpJZsQSEY1xhJePCDTHdvp", + "serializedDocument": [222, 173, 190, 239], + "voteTally": 42, + }) + ); + let recovered = ContenderWithSerializedDocument::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // platform_value preserves typed variants: `Identifier` renders as + // `Value::Identifier`, `Vec` renders as `Value::Array([U8(...), ...])` + // (NOT `Value::Bytes`, because `Vec` uses the generic `serialize_seq` + // path), `Option` becomes `Value::U32`. + let id = Identifier::new([0xa1; 32]); + let bytes_array = Value::Array(vec![ + Value::U8(0xde), + Value::U8(0xad), + Value::U8(0xbe), + Value::U8(0xef), + ]); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "identityId": id, + "serializedDocument": bytes_array, + "voteTally": 42u32, + }) + ); + let recovered = ContenderWithSerializedDocument::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/voting/vote_choices/resource_vote_choice/mod.rs b/packages/rs-dpp/src/voting/vote_choices/resource_vote_choice/mod.rs index fcaaedbc381..9373a007d76 100644 --- a/packages/rs-dpp/src/voting/vote_choices/resource_vote_choice/mod.rs +++ b/packages/rs-dpp/src/voting/vote_choices/resource_vote_choice/mod.rs @@ -22,11 +22,13 @@ use std::fmt; /// the name. /// #[derive(Debug, Clone, Copy, Encode, Decode, Ord, Eq, PartialOrd, PartialEq, Default)] -#[cfg_attr( - feature = "serde-conversion", - derive(Serialize, Deserialize), - serde(tag = "type", content = "data", rename_all = "camelCase") -)] +// Custom `Serialize` / `Deserialize` below — `derive(Serialize, Deserialize)` +// can't produce the desired flat wire shape because the `TowardsIdentity` +// variant wraps `Identifier` (a tuple struct that serializes as a base58 +// string, not a map), so internal tagging doesn't apply. The custom impl +// emits a flat `{"$type": ..., "identity": ...}` shape with a synthesized +// `identity` field name. Bincode `Encode` / `Decode` derives are untouched +// (consensus binary format is unaffected). #[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] pub enum ResourceVoteChoice { TowardsIdentity(Identifier), @@ -35,6 +37,89 @@ pub enum ResourceVoteChoice { Lock, } +#[cfg(feature = "serde-conversion")] +impl Serialize for ResourceVoteChoice { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeMap; + match self { + ResourceVoteChoice::TowardsIdentity(id) => { + let mut m = serializer.serialize_map(Some(2))?; + m.serialize_entry("$type", "towardsIdentity")?; + m.serialize_entry("identity", id)?; + m.end() + } + ResourceVoteChoice::Abstain => { + let mut m = serializer.serialize_map(Some(1))?; + m.serialize_entry("$type", "abstain")?; + m.end() + } + ResourceVoteChoice::Lock => { + let mut m = serializer.serialize_map(Some(1))?; + m.serialize_entry("$type", "lock")?; + m.end() + } + } + } +} + +#[cfg(feature = "serde-conversion")] +impl<'de> Deserialize<'de> for ResourceVoteChoice { + fn deserialize>(deserializer: D) -> Result { + use serde::de::{self, MapAccess, Visitor}; + + struct V; + + impl<'de> Visitor<'de> for V { + type Value = ResourceVoteChoice; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("ResourceVoteChoice as a map with `type` discriminator") + } + + fn visit_map>(self, mut map: A) -> Result { + let mut variant: Option = None; + let mut identity: Option = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "$type" => { + if variant.is_some() { + return Err(de::Error::duplicate_field("$type")); + } + variant = Some(map.next_value()?); + } + "identity" => { + if identity.is_some() { + return Err(de::Error::duplicate_field("identity")); + } + identity = Some(map.next_value()?); + } + _ => { + let _: serde::de::IgnoredAny = map.next_value()?; + } + } + } + + let variant = variant.ok_or_else(|| de::Error::missing_field("$type"))?; + match variant.as_str() { + "towardsIdentity" => { + let id = identity.ok_or_else(|| de::Error::missing_field("identity"))?; + Ok(ResourceVoteChoice::TowardsIdentity(id)) + } + "abstain" => Ok(ResourceVoteChoice::Abstain), + "lock" => Ok(ResourceVoteChoice::Lock), + other => Err(de::Error::unknown_variant( + other, + &["towardsIdentity", "abstain", "lock"], + )), + } + } + } + + deserializer.deserialize_map(V) + } +} + impl fmt::Display for ResourceVoteChoice { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -104,3 +189,31 @@ impl TryFrom<(i32, Option>)> for ResourceVoteChoice { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_resourcevotechoice { + use super::*; + + #[test] + fn json_round_trip_resourcevotechoice() { + use crate::serialization::JsonConvertible; + let original = ResourceVoteChoice::default(); + let json = original.to_json().expect("to_json"); + let recovered = ResourceVoteChoice::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_resourcevotechoice() { + use crate::serialization::ValueConvertible; + let original = ResourceVoteChoice::default(); + let value = original.to_object().expect("to_object"); + let recovered = ResourceVoteChoice::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/voting/vote_choices/yes_no_abstain_vote_choice/mod.rs b/packages/rs-dpp/src/voting/vote_choices/yes_no_abstain_vote_choice/mod.rs index a5abf8d862e..03bcdf65786 100644 --- a/packages/rs-dpp/src/voting/vote_choices/yes_no_abstain_vote_choice/mod.rs +++ b/packages/rs-dpp/src/voting/vote_choices/yes_no_abstain_vote_choice/mod.rs @@ -14,3 +14,93 @@ pub enum YesNoAbstainVoteChoice { #[default] ABSTAIN, } + +// --- canonical conversion trait impls (unification pass 1) --- +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for YesNoAbstainVoteChoice {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for YesNoAbstainVoteChoice {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_yesnoabstainvotechoice { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + // `YesNoAbstainVoteChoice` is a unit-only enum with `rename_all = "camelCase"`, + // so each variant serializes as a plain string: `"yes"` / `"no"` / `"abstain"`. + + // Surprise wire shape: the variants are SCREAMING_CASE in source + // (`YES`/`NO`/`ABSTAIN`) and the type carries `rename_all = "camelCase"`. + // serde's camelCase rule lowercases the FIRST letter only, leaving the + // rest as-is — so the wire emits `"yES"` / `"nO"` / `"aBSTAIN"` rather + // than the lowercase-clean strings a casual reader would expect. These + // tests pin that behaviour so a future "looks-like-a-typo" rename to + // lowercase doesn't silently change the on-the-wire format. + + #[test] + fn json_round_trip_yes() { + use crate::serialization::JsonConvertible; + let original = YesNoAbstainVoteChoice::YES; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!("yES")); + let recovered = YesNoAbstainVoteChoice::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_no() { + use crate::serialization::JsonConvertible; + let original = YesNoAbstainVoteChoice::NO; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!("nO")); + let recovered = YesNoAbstainVoteChoice::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_abstain() { + use crate::serialization::JsonConvertible; + let original = YesNoAbstainVoteChoice::ABSTAIN; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!("aBSTAIN")); + let recovered = YesNoAbstainVoteChoice::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_yes() { + use crate::serialization::ValueConvertible; + let original = YesNoAbstainVoteChoice::YES; + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!("yES")); + let recovered = YesNoAbstainVoteChoice::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_no() { + use crate::serialization::ValueConvertible; + let original = YesNoAbstainVoteChoice::NO; + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!("nO")); + let recovered = YesNoAbstainVoteChoice::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_abstain() { + use crate::serialization::ValueConvertible; + let original = YesNoAbstainVoteChoice::ABSTAIN; + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!("aBSTAIN")); + let recovered = YesNoAbstainVoteChoice::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/voting/vote_info_storage/contested_document_vote_poll_winner_info/mod.rs b/packages/rs-dpp/src/voting/vote_info_storage/contested_document_vote_poll_winner_info/mod.rs index b027e492e4e..79825f00e0a 100644 --- a/packages/rs-dpp/src/voting/vote_info_storage/contested_document_vote_poll_winner_info/mod.rs +++ b/packages/rs-dpp/src/voting/vote_info_storage/contested_document_vote_poll_winner_info/mod.rs @@ -7,9 +7,14 @@ use platform_value::Identifier; use serde::{Deserialize, Serialize}; use std::fmt; -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Encode, Decode, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Encode, Decode)] +// Custom `Serialize` / `Deserialize` below — same pattern as +// `ResourceVoteChoice`. The `WonByIdentity` variant wraps `Identifier` +// (a tuple struct that serializes as a base58 string, not a map), so +// internal tagging doesn't apply natively. The custom impl emits a flat +// `{"$type": ..., "identity": ...}` shape with a synthesized `identity` +// field name. Bincode `Encode` / `Decode` derives are untouched. #[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] -#[serde(tag = "type", content = "data", rename_all = "camelCase")] pub enum ContestedDocumentVotePollWinnerInfo { #[default] NoWinner, @@ -17,6 +22,89 @@ pub enum ContestedDocumentVotePollWinnerInfo { Locked, } +impl Serialize for ContestedDocumentVotePollWinnerInfo { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeMap; + match self { + ContestedDocumentVotePollWinnerInfo::NoWinner => { + let mut m = serializer.serialize_map(Some(1))?; + m.serialize_entry("$type", "noWinner")?; + m.end() + } + ContestedDocumentVotePollWinnerInfo::WonByIdentity(id) => { + let mut m = serializer.serialize_map(Some(2))?; + m.serialize_entry("$type", "wonByIdentity")?; + m.serialize_entry("identity", id)?; + m.end() + } + ContestedDocumentVotePollWinnerInfo::Locked => { + let mut m = serializer.serialize_map(Some(1))?; + m.serialize_entry("$type", "locked")?; + m.end() + } + } + } +} + +impl<'de> Deserialize<'de> for ContestedDocumentVotePollWinnerInfo { + fn deserialize>(deserializer: D) -> Result { + use serde::de::{self, MapAccess, Visitor}; + + struct V; + + impl<'de> Visitor<'de> for V { + type Value = ContestedDocumentVotePollWinnerInfo; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str( + "ContestedDocumentVotePollWinnerInfo as a map with `type` discriminator", + ) + } + + fn visit_map>(self, mut map: A) -> Result { + let mut variant: Option = None; + let mut identity: Option = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "$type" => { + if variant.is_some() { + return Err(de::Error::duplicate_field("$type")); + } + variant = Some(map.next_value()?); + } + "identity" => { + if identity.is_some() { + return Err(de::Error::duplicate_field("identity")); + } + identity = Some(map.next_value()?); + } + _ => { + let _: serde::de::IgnoredAny = map.next_value()?; + } + } + } + + let variant = variant.ok_or_else(|| de::Error::missing_field("$type"))?; + match variant.as_str() { + "noWinner" => Ok(ContestedDocumentVotePollWinnerInfo::NoWinner), + "wonByIdentity" => { + let id = identity.ok_or_else(|| de::Error::missing_field("identity"))?; + Ok(ContestedDocumentVotePollWinnerInfo::WonByIdentity(id)) + } + "locked" => Ok(ContestedDocumentVotePollWinnerInfo::Locked), + other => Err(de::Error::unknown_variant( + other, + &["noWinner", "wonByIdentity", "locked"], + )), + } + } + } + + deserializer.deserialize_map(V) + } +} + impl fmt::Display for ContestedDocumentVotePollWinnerInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -108,3 +196,62 @@ mod tests { assert_eq!(back, value); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + /// Non-default variant (`WonByIdentity` with a non-zero identifier) so + /// the wire-shape assertion catches silent variant-flip / identifier-zero + /// on round-trip — the previous fixture used `Default` (`NoWinner`), + /// which carries no inner state. + fn fixture() -> ContestedDocumentVotePollWinnerInfo { + ContestedDocumentVotePollWinnerInfo::WonByIdentity(Identifier::new([0xab; 32])) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `ContestedDocumentVotePollWinnerInfo` has a custom Serialize/ + // Deserialize that emits a flat shape with a synthesized `identity` + // field. `Identifier` -> base58 string in JSON. + assert_eq!( + json, + json!({ + "$type": "wonByIdentity", + "identity": "CZ8YUVdk7znjrUmnb5n7kgySk9yRAsQDYmyCxzfSky9t", + }) + ); + let recovered = ContestedDocumentVotePollWinnerInfo::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // platform_value preserves typed `Identifier` variants — interpolate + // through the macro so Serialize emits `Value::Identifier`. + let id = Identifier::new([0xab; 32]); + assert_eq!( + value, + platform_value!({ + "$type": "wonByIdentity", + "identity": id, + }) + ); + let recovered = + ContestedDocumentVotePollWinnerInfo::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/voting/vote_polls/contested_document_resource_vote_poll/mod.rs b/packages/rs-dpp/src/voting/vote_polls/contested_document_resource_vote_poll/mod.rs index 797fde7f1b5..7047493cd93 100644 --- a/packages/rs-dpp/src/voting/vote_polls/contested_document_resource_vote_poll/mod.rs +++ b/packages/rs-dpp/src/voting/vote_polls/contested_document_resource_vote_poll/mod.rs @@ -76,3 +76,73 @@ impl ContestedDocumentResourceVotePoll { self.sha256_2_hash().map(Identifier::new) } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + /// Non-default values per field (real contract id, named type/index, two + /// index values) so the wire-shape assertion catches silent zero-out / + /// vec-truncate on round-trip. + fn fixture() -> ContestedDocumentResourceVotePoll { + ContestedDocumentResourceVotePoll { + contract_id: Identifier::new([0xc1; 32]), + document_type_name: "preorder".to_string(), + index_name: "parentNameAndLabel".to_string(), + index_values: vec![ + Value::Text("dash".to_string()), + Value::Text("alice".to_string()), + ], + } + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // This is a plain struct (no `#[serde(tag)]`), so there is no + // `$formatVersion` on the wire. `Identifier` -> base58 string. + // `Value::Text` inside the array -> JSON string. + assert_eq!( + json, + json!({ + "contractId": "E3M3d7sy8ZKivUGxBexL9wxE7ebqzGWFqkdeFMedCJFS", + "documentTypeName": "preorder", + "indexName": "parentNameAndLabel", + "indexValues": ["dash", "alice"], + }) + ); + let recovered = ContestedDocumentResourceVotePoll::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // Interpolate the `Identifier` via `platform_value!` so Serialize emits + // `Value::Identifier` (NOT `Value::Bytes32`). `index_values` is a + // `Vec` round-tripped element-wise. + let id = Identifier::new([0xc1; 32]); + assert_eq!( + value, + platform_value!({ + "contractId": id, + "documentTypeName": "preorder", + "indexName": "parentNameAndLabel", + "indexValues": ["dash", "alice"], + }) + ); + let recovered = ContestedDocumentResourceVotePoll::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/voting/vote_polls/mod.rs b/packages/rs-dpp/src/voting/vote_polls/mod.rs index 86d4cacb157..467a283e749 100644 --- a/packages/rs-dpp/src/voting/vote_polls/mod.rs +++ b/packages/rs-dpp/src/voting/vote_polls/mod.rs @@ -22,7 +22,9 @@ pub mod contested_document_resource_vote_poll; #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), - serde(tag = "type", content = "data", rename_all = "camelCase") + // Internally tagged with a `$type` discriminator; inner + // `ContestedDocumentResourceVotePoll` fields flatten at the same level. + serde(tag = "$type", rename_all = "camelCase") )] #[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] #[platform_serialize(unversioned)] @@ -66,3 +68,31 @@ impl VotePoll { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_votepoll { + use super::*; + + #[test] + fn json_round_trip_votepoll() { + use crate::serialization::JsonConvertible; + let original = VotePoll::default(); + let json = original.to_json().expect("to_json"); + let recovered = VotePoll::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_votepoll() { + use crate::serialization::ValueConvertible; + let original = VotePoll::default(); + let value = original.to_object().expect("to_object"); + let recovered = VotePoll::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/voting/votes/mod.rs b/packages/rs-dpp/src/voting/votes/mod.rs index 6078372cba5..9e4d42327be 100644 --- a/packages/rs-dpp/src/voting/votes/mod.rs +++ b/packages/rs-dpp/src/voting/votes/mod.rs @@ -18,7 +18,10 @@ use serde::{Deserialize, Serialize}; #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), - serde(tag = "type", content = "data", rename_all = "camelCase") + // Internal tagging with `$type` — system-field convention. Drops the + // `data` wrapper. Inner `ResourceVote` is `tag = "$formatVersion"` + // (different key, no collision). + serde(tag = "$type", rename_all = "camelCase") )] #[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] #[platform_serialize(limit = 15000, unversioned)] @@ -86,3 +89,31 @@ mod tests { assert_eq!(vote, restored); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_vote { + use super::*; + + #[test] + fn json_round_trip_vote() { + use crate::serialization::JsonConvertible; + let original = Vote::default(); + let json = original.to_json().expect("to_json"); + let recovered = Vote::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_vote() { + use crate::serialization::ValueConvertible; + let original = Vote::default(); + let value = original.to_object().expect("to_object"); + let recovered = Vote::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/voting/votes/resource_vote/mod.rs b/packages/rs-dpp/src/voting/votes/resource_vote/mod.rs index 3f2236c9421..ff57370eb67 100644 --- a/packages/rs-dpp/src/voting/votes/resource_vote/mod.rs +++ b/packages/rs-dpp/src/voting/votes/resource_vote/mod.rs @@ -34,3 +34,97 @@ impl Default for ResourceVote { Self::V0(ResourceVoteV0::default()) } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_resource_vote { + use super::*; + use crate::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; + use crate::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; + use crate::voting::vote_polls::VotePoll; + use platform_value::{platform_value, Identifier, Value}; + use serde_json::json; + + /// Non-default values per inner field (named contract / index / values + /// inside the poll, plus a `TowardsIdentity` choice with non-zero + /// identifier) so the wire-shape assertion catches silent zero-out / + /// variant flip on round-trip. + fn fixture() -> ResourceVote { + ResourceVote::V0(ResourceVoteV0 { + vote_poll: VotePoll::ContestedDocumentResourceVotePoll( + ContestedDocumentResourceVotePoll { + contract_id: Identifier::new([0xc1; 32]), + document_type_name: "preorder".to_string(), + index_name: "parentNameAndLabel".to_string(), + index_values: vec![Value::Text("dash".to_string())], + }, + ), + resource_vote_choice: ResourceVoteChoice::TowardsIdentity(Identifier::new([0xab; 32])), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // `VotePoll` uses internal tagging (`tag = "$type"`), so its variant + // body fields are flattened next to the `$type` discriminator. + // `ResourceVoteChoice` uses a custom Serialize/Deserialize that + // emits `{"$type": "towardsIdentity", "identity": }` for the + // newtype variant. Identifiers render as base58 strings in JSON. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "votePoll": { + "$type": "contestedDocumentResourceVotePoll", + "contractId": "E3M3d7sy8ZKivUGxBexL9wxE7ebqzGWFqkdeFMedCJFS", + "documentTypeName": "preorder", + "indexName": "parentNameAndLabel", + "indexValues": ["dash"], + }, + "resourceVoteChoice": { + "$type": "towardsIdentity", + "identity": "CZ8YUVdk7znjrUmnb5n7kgySk9yRAsQDYmyCxzfSky9t", + }, + }) + ); + let recovered = ResourceVote::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + // platform_value preserves typed `Identifier` variants. Interpolate + // through the macro so Serialize emits `Value::Identifier`. + let contract_id = Identifier::new([0xc1; 32]); + let voter_id = Identifier::new([0xab; 32]); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "votePoll": { + "$type": "contestedDocumentResourceVotePoll", + "contractId": contract_id, + "documentTypeName": "preorder", + "indexName": "parentNameAndLabel", + "indexValues": ["dash"], + }, + "resourceVoteChoice": { + "$type": "towardsIdentity", + "identity": voter_id, + }, + }) + ); + let recovered = ResourceVote::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/withdrawal/mod.rs b/packages/rs-dpp/src/withdrawal/mod.rs index 0d39b205992..2c64ff1cbed 100644 --- a/packages/rs-dpp/src/withdrawal/mod.rs +++ b/packages/rs-dpp/src/withdrawal/mod.rs @@ -5,6 +5,11 @@ mod document_try_into_asset_unlock_base_transaction_info; use bincode::{Decode, Encode}; use serde_repr::{Deserialize_repr, Serialize_repr}; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; + #[repr(u8)] #[derive( Serialize_repr, Deserialize_repr, PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, Default, @@ -16,6 +21,12 @@ pub enum Pooling { Standard = 2, } +#[cfg(feature = "json-conversion")] +impl JsonConvertible for Pooling {} + +#[cfg(feature = "value-conversion")] +impl ValueConvertible for Pooling {} + /// Transaction index type pub type WithdrawalTransactionIndex = u64; @@ -154,3 +165,82 @@ pub mod pooling_serde { } } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests_pooling { + use super::*; + use platform_value::platform_value; + use serde_json::json; + + // `Pooling` is `#[repr(u8)]` with `Serialize_repr` / `Deserialize_repr`, so + // the wire shape is the raw `u8` discriminant: `0` / `1` / `2`. JSON has + // only one number type, so `0u8` is erased to `Number(0)`; the value-path + // assertion uses explicit `0u8` etc. to lock in `Value::U8`. + + #[test] + fn json_round_trip_never() { + use crate::serialization::JsonConvertible; + let original = Pooling::Never; + let json = original.to_json().expect("to_json"); + // u8 size erased in JSON. + assert_eq!(json, json!(0)); + let recovered = Pooling::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_if_available() { + use crate::serialization::JsonConvertible; + let original = Pooling::IfAvailable; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!(1)); + let recovered = Pooling::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn json_round_trip_standard() { + use crate::serialization::JsonConvertible; + let original = Pooling::Standard; + let json = original.to_json().expect("to_json"); + assert_eq!(json, json!(2)); + let recovered = Pooling::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_never() { + use crate::serialization::ValueConvertible; + let original = Pooling::Never; + let value = original.to_object().expect("to_object"); + // `0u8` locks `Value::U8` (not I32 from a bare `0`). + assert_eq!(value, platform_value!(0u8)); + let recovered = Pooling::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_if_available() { + use crate::serialization::ValueConvertible; + let original = Pooling::IfAvailable; + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!(1u8)); + let recovered = Pooling::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_standard() { + use crate::serialization::ValueConvertible; + let original = Pooling::Standard; + let value = original.to_object().expect("to_object"); + assert_eq!(value, platform_value!(2u8)); + let recovered = Pooling::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs index ba541b1ccd7..ea93d56d8e4 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs @@ -4048,9 +4048,8 @@ mod tests { .expect("expected to load contract"); // Convert the contract back to Value so we can mutate its fields - let mut contract_value = data_contract - .to_value(PlatformVersion::latest()) - .expect("to_value failed"); + let mut contract_value = + dpp::platform_value::to_value(&data_contract).expect("to_value failed"); // Insert 21 keywords to exceed the max limit let mut excessive_keywords: Vec = vec![]; @@ -4131,9 +4130,8 @@ mod tests { .expect("expected to load contract"); // Convert to Value to mutate fields - let mut contract_value = data_contract - .to_value(PlatformVersion::latest()) - .expect("to_value failed"); + let mut contract_value = + dpp::platform_value::to_value(&data_contract).expect("to_value failed"); // Insert some duplicates let duplicated_keywords = vec!["keyword1", "keyword2", "keyword2"]; @@ -4216,9 +4214,8 @@ mod tests { .expect("expected to load contract"); // Convert to Value for mutation - let mut contract_value = data_contract - .to_value(PlatformVersion::latest()) - .expect("to_value failed"); + let mut contract_value = + dpp::platform_value::to_value(&data_contract).expect("to_value failed"); // Insert a keyword with length < 3 contract_value["keywords"] = Value::Array(vec![Value::Text("hi".to_string())]); @@ -4289,9 +4286,8 @@ mod tests { ) .expect("expected to load contract"); - let mut contract_value = data_contract - .to_value(platform_version) - .expect("to_value failed"); + let mut contract_value = + dpp::platform_value::to_value(&data_contract).expect("to_value failed"); // Create a 51-char keyword let too_long_keyword = "x".repeat(51); @@ -4363,9 +4359,8 @@ mod tests { .expect("expected to load contract"); // Convert to Value so we can adjust fields if needed - let mut contract_value = data_contract - .to_value(PlatformVersion::latest()) - .expect("to_value failed"); + let mut contract_value = + dpp::platform_value::to_value(&data_contract).expect("to_value failed"); // Insert a valid set of keywords: all distinct, fewer than 20 let valid_keywords = vec!["key1", "key2", "key3"]; @@ -4528,9 +4523,8 @@ mod tests { ) .expect("expected to load contract"); - let mut contract_value = data_contract - .to_value(platform_version) - .expect("to_value failed"); + let mut contract_value = + dpp::platform_value::to_value(&data_contract).expect("to_value failed"); // Inject `keywords` onto the `preorder` document type schema — the // wrong place for it. This should be rejected by the v1 meta @@ -4618,9 +4612,8 @@ mod tests { ) .expect("expected to load contract"); - let mut contract_value = data_contract - .to_value(PlatformVersion::latest()) - .expect("to_value failed"); + let mut contract_value = + dpp::platform_value::to_value(&data_contract).expect("to_value failed"); // Ensure the `keywords` array is not empty so that Drive will attempt // to create the description documents. diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs index f373bc03fb5..ce93db84a59 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs @@ -2426,7 +2426,7 @@ mod tests { ) .expect("load base contract"); - let mut val = base.to_value(platform_version).expect("to_value"); + let mut val = dpp::platform_value::to_value(&base).expect("to_value"); val["keywords"] = Value::Array( keywords @@ -2516,7 +2516,7 @@ mod tests { .unwrap() .unwrap(); - let mut val = fetched.contract.to_value(platform_version).unwrap(); + let mut val = dpp::platform_value::to_value(&fetched.contract).unwrap(); val["keywords"] = Value::Array( new_keywords @@ -2818,7 +2818,7 @@ mod tests { ) .expect("load base contract"); - let mut val = base.to_value(platform_version).expect("to_value"); + let mut val = dpp::platform_value::to_value(&base).expect("to_value"); val["description"] = Value::Text(description.to_string()); @@ -2903,7 +2903,7 @@ mod tests { .unwrap() .unwrap(); - let mut val = fetched.contract.to_value(platform_version).unwrap(); + let mut val = dpp::platform_value::to_value(&fetched.contract).unwrap(); val["description"] = Value::Text(new_description.to_string()); diff --git a/packages/rs-drive/src/drive/document/update/mod.rs b/packages/rs-drive/src/drive/document/update/mod.rs index c17b27333f1..9ffa42c3eb2 100644 --- a/packages/rs-drive/src/drive/document/update/mod.rs +++ b/packages/rs-drive/src/drive/document/update/mod.rs @@ -59,15 +59,14 @@ mod tests { use dpp::data_contract::conversion::value::v0::DataContractValueConversionMethodsV0; use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; use dpp::document::document_methods::DocumentMethodsV0; - use dpp::document::serialization_traits::{ - DocumentPlatformConversionMethodsV0, DocumentPlatformValueMethodsV0, - }; + use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; use dpp::document::specialized_document_factory::SpecializedDocumentFactory; use dpp::document::{Document, DocumentV0Getters, DocumentV0Setters}; use dpp::fee::default_costs::KnownCostItem::StorageDiskUsageCreditPerByte; use dpp::fee::default_costs::{CachedEpochIndexFeeVersions, EpochCosts}; use dpp::fee::fee_result::FeeResult; use dpp::platform_value; + use dpp::serialization::ValueConvertible; use dpp::tests::json_document::json_document_to_document; use dpp::version::fee::FeeVersion; use once_cell::sync::Lazy; @@ -76,6 +75,25 @@ mod tests { static EPOCH_CHANGE_FEE_VERSION_TEST: Lazy = Lazy::new(|| BTreeMap::from([(0, FeeVersion::first())])); + /// Build a `Document` from a legacy un-tagged `platform_value!` map by + /// inserting `$formatVersion: "0"` and routing through canonical + /// `ValueConvertible::from_object`. Replaces the deleted + /// `Document::from_platform_value` ingest path. + fn document_from_legacy_value(mut value: Value) -> Document { + if let Value::Map(ref mut entries) = value { + let has_tag = entries + .iter() + .any(|(k, _)| matches!(k, Value::Text(s) if s == "$formatVersion")); + if !has_tag { + entries.push(( + Value::Text("$formatVersion".to_string()), + Value::Text("0".to_string()), + )); + } + } + Document::from_object(value).expect("expected to make document from legacy value") + } + #[test] fn test_create_and_update_document_same_transaction() { let (drive, contract) = setup_dashpay("", true); @@ -637,8 +655,7 @@ mod tests { "$updatedAt": 1647535750329_u64, }); - let document = Document::from_platform_value(document_values, platform_version) - .expect("expected to make document"); + let document = document_from_legacy_value(document_values); let document_type = contract .document_type_for_name("indexedDocument") @@ -682,8 +699,7 @@ mod tests { "$updatedAt":1647535754556_u64, }); - let document = Document::from_platform_value(document_values, platform_version) - .expect("expected to make document"); + let document = document_from_legacy_value(document_values); drive .update_document_for_contract( @@ -960,8 +976,7 @@ mod tests { let value = platform_value::to_value(&person_0_original).expect("person into value"); - let document = - Document::from_platform_value(value, platform_version).expect("value to document"); + let document = document_from_legacy_value(value); let document_serialized = DocumentPlatformConversionMethodsV0::serialize( &document, @@ -1696,8 +1711,7 @@ mod tests { let value = platform_value::to_value(person).expect("person into value"); - let document = - Document::from_platform_value(value, platform_version).expect("value to document"); + let document = document_from_legacy_value(value); let storage_flags = Some(Cow::Owned(StorageFlags::SingleEpochOwned( 0, diff --git a/packages/rs-drive/src/query/drive_document_count_query/tests.rs b/packages/rs-drive/src/query/drive_document_count_query/tests.rs index ce21548ad2d..28a75d95c0b 100644 --- a/packages/rs-drive/src/query/drive_document_count_query/tests.rs +++ b/packages/rs-drive/src/query/drive_document_count_query/tests.rs @@ -1829,7 +1829,6 @@ fn test_count_tree_aggregation_with_empty_child_subtrees() { /// picker → tree-type selection (`ProvableCountTree`) → fast-path read. #[test] fn test_countable_allowing_offset_variant_end_to_end() { - use dpp::data_contract::conversion::json::DataContractJsonConversionMethodsV0; use dpp::data_contract::document_type::IndexCountability; let drive = setup_drive_with_initial_state_structure(None); @@ -1867,9 +1866,13 @@ fn test_countable_allowing_offset_variant_end_to_end() { } }); - let data_contract = - dpp::data_contract::DataContract::from_json(contract_json, false, platform_version) - .expect("expected to load contract with string-form countable"); + // Use canonical Deserialize (no schema validation — see + // `data_contract/conversion/serde/mod.rs` for the no-validation-by-default + // policy). The earlier `from_json(_, false, _)` legacy method was deleted + // when the `_versioned` family collapsed into canonical + `_validated`. + let _ = platform_version; + let data_contract: dpp::data_contract::DataContract = serde_json::from_value(contract_json) + .expect("expected to load contract with string-form countable"); let document_type = data_contract .document_type_for_name("person") diff --git a/packages/rs-drive/tests/query_tests.rs b/packages/rs-drive/tests/query_tests.rs index 77594d748ba..e2aab513220 100644 --- a/packages/rs-drive/tests/query_tests.rs +++ b/packages/rs-drive/tests/query_tests.rs @@ -65,7 +65,7 @@ use dpp::data_contract::config::v1::DataContractConfigSettersV1; use dpp::data_contract::conversion::value::v0::DataContractValueConversionMethodsV0; use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; use dpp::document::serialization_traits::{ - DocumentCborMethodsV0, DocumentPlatformConversionMethodsV0, DocumentPlatformValueMethodsV0, + DocumentCborMethodsV0, DocumentPlatformConversionMethodsV0, }; use dpp::document::{DocumentV0Getters, DocumentV0Setters}; use dpp::fee::default_costs::CachedEpochIndexFeeVersions; @@ -74,6 +74,7 @@ use dpp::platform_value; use dpp::platform_value::string_encoding::Encoding; #[cfg(feature = "server")] use dpp::prelude::DataContract; +use dpp::serialization::ValueConvertible; use dpp::tests::json_document::json_document_to_contract; #[cfg(feature = "server")] use dpp::util::cbor_serializer; @@ -95,6 +96,27 @@ use drive::query::{WhereClause, WhereOperator}; use drive::util::test_helpers; use drive::util::test_helpers::setup::setup_drive_with_initial_state_structure; +/// Build a `Document` from an un-tagged `platform_value::Value` (e.g. +/// produced by `platform_value::to_value` over a serde-derived domain +/// struct) by inserting `$formatVersion: "0"` and routing through +/// canonical `ValueConvertible::from_object`. Replaces the deleted +/// `Document::from_platform_value` ingest path. +#[cfg(feature = "server")] +fn document_from_legacy_value(mut value: Value) -> Document { + if let Value::Map(ref mut entries) = value { + let has_tag = entries + .iter() + .any(|(k, _)| matches!(k, Value::Text(s) if s == "$formatVersion")); + if !has_tag { + entries.push(( + Value::Text("$formatVersion".to_string()), + Value::Text("0".to_string()), + )); + } + } + Document::from_object(value).expect("expected document from legacy value") +} + #[cfg(feature = "server")] #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -566,8 +588,7 @@ fn test_serialization_and_deserialization() { for domain in domains { let value = platform_value::to_value(domain).expect("expected value"); - let mut document = - Document::from_platform_value(value, platform_version).expect("expected value"); + let mut document = document_from_legacy_value(value); document.set_revision(Some(1)); let serialized = ::serialize( &document, @@ -615,8 +636,7 @@ fn test_serialization_and_deserialization_with_null_values_should_fail_if_requir }; let value = platform_value::to_value(domain).expect("expected value"); - let mut document = - Document::from_platform_value(value, platform_version).expect("expected value"); + let mut document = document_from_legacy_value(value); document.set_revision(Some(1)); ::serialize( @@ -667,8 +687,7 @@ fn test_serialization_and_deserialization_with_null_values() { value .remove_optional_value("normalizedLabel") .expect("expected to remove null"); - let mut document = - Document::from_platform_value(value, platform_version).expect("expected value"); + let mut document = document_from_legacy_value(value); document.set_revision(Some(1)); let serialized = DocumentPlatformConversionMethodsV0::serialize( &document, @@ -822,8 +841,7 @@ pub fn add_domains_to_contract( .expect("expected to get document type"); for domain in domains { let value = platform_value::to_value(domain).expect("expected value"); - let document = - Document::from_platform_value(value, platform_version).expect("expected value"); + let document = document_from_legacy_value(value); let storage_flags = Some(Cow::Owned(StorageFlags::SingleEpoch(0))); @@ -865,8 +883,7 @@ pub fn add_withdrawals_to_contract( .expect("expected to get document type"); for domain in withdrawals { let value = platform_value::to_value(domain).expect("expected value"); - let document = - Document::from_platform_value(value, platform_version).expect("expected value"); + let document = document_from_legacy_value(value); let storage_flags = Some(Cow::Owned(StorageFlags::SingleEpoch(0))); @@ -6009,8 +6026,7 @@ mod tests { }; let value0 = platform_value::to_value(domain0).expect("serialized domain"); - let document0 = Document::from_platform_value(value0, platform_version) - .expect("document should be properly deserialized"); + let document0 = document_from_legacy_value(value0); let storage_flags = Some(Cow::Owned(StorageFlags::SingleEpoch(0))); diff --git a/packages/rs-drive/tests/supporting_files/contract/tokens/token-example-contract.json b/packages/rs-drive/tests/supporting_files/contract/tokens/token-example-contract.json index e785633f35d..ac5de3630ee 100644 --- a/packages/rs-drive/tests/supporting_files/contract/tokens/token-example-contract.json +++ b/packages/rs-drive/tests/supporting_files/contract/tokens/token-example-contract.json @@ -24,32 +24,32 @@ "keepsHistory": false, "freezeRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"type": "contractOwner"}, + "adminActionTakers": {"type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "unfreezeRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"type": "contractOwner"}, + "adminActionTakers": {"type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "destroyFrozenFundsRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"type": "contractOwner"}, + "adminActionTakers": {"type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "emergencyActionRules": { "$formatVersion": "0", - "authorizedToMakeChange": "ContractOwner", - "adminActionTakers": "ContractOwner", + "authorizedToMakeChange": {"type": "contractOwner"}, + "adminActionTakers": {"type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false diff --git a/packages/rs-platform-value/src/converter/serde_json.rs b/packages/rs-platform-value/src/converter/serde_json.rs index 8f121b8d63d..dd067d22dc5 100644 --- a/packages/rs-platform-value/src/converter/serde_json.rs +++ b/packages/rs-platform-value/src/converter/serde_json.rs @@ -41,7 +41,12 @@ impl Value { Value::I16(i) => JsonValue::Number(i.into()), Value::U8(i) => JsonValue::Number(i.into()), Value::I8(i) => JsonValue::Number(i.into()), - Value::Float(float) => JsonValue::Number(Number::from_f64(float).unwrap_or(0.into())), + // JSON cannot represent NaN/±∞ (`from_f64` returns None) — fail + // loudly instead of silently substituting 0, which would be + // indistinguishable from a real zero to JSON consumers. + Value::Float(float) => JsonValue::Number(Number::from_f64(float).ok_or_else(|| { + Error::Unsupported("non-finite float (NaN/±∞) is not representable in JSON".into()) + })?), Value::Text(string) => JsonValue::String(string), Value::Bool(value) => JsonValue::Bool(value), Value::Null => JsonValue::Null, @@ -137,7 +142,14 @@ impl Value { Value::I16(i) => JsonValue::Number((*i).into()), Value::U8(i) => JsonValue::Number((*i).into()), Value::I8(i) => JsonValue::Number((*i).into()), - Value::Float(float) => JsonValue::Number(Number::from_f64(*float).unwrap_or(0.into())), + // JSON cannot represent NaN/±∞ — fail loudly (see owned path above). + Value::Float(float) => { + JsonValue::Number(Number::from_f64(*float).ok_or_else(|| { + Error::Unsupported( + "non-finite float (NaN/±∞) is not representable in JSON".into(), + ) + })?) + } Value::Text(string) => JsonValue::String(string.clone()), Value::Bool(value) => JsonValue::Bool(*value), Value::Null => JsonValue::Null, @@ -219,27 +231,16 @@ impl From for Value { } JsonValue::String(string) => Self::Text(string), JsonValue::Array(array) => { - let u8_max = u8::MAX as u64; - //todo: hacky solution, to fix - let len = array.len(); - if len >= 10 - && array.iter().all(|v| { - let Some(int) = v.as_u64() else { - return false; - }; - int.le(&u8_max) - }) - { - //this is an array of bytes - Self::Bytes( - array - .into_iter() - .map(|v| v.as_u64().unwrap() as u8) - .collect(), - ) - } else { - Self::Array(array.into_iter().map(|v| v.into()).collect()) - } + // Critical-2 fix: faithful array → array conversion. The previous + // heuristic ("if len >= 10 and all elements ≤ 255 then call it + // bytes") was a JS-DPP-era workaround for clients that sent + // binary as JSON arrays of u8. With the canonical-trait + // unification (HR = base64 strings, non-HR = Value::Bytes; + // BinaryData/Identifier/Bytes* deserializers handle both), + // the heuristic is no longer needed and was actively corrupting + // genuine arrays of small integers (e.g., a document property + // typed as "list of small ints" of length 10+). + Self::Array(array.into_iter().map(|v| v.into()).collect()) } JsonValue::Object(map) => { Self::Map(map.into_iter().map(|(k, v)| (k.into(), v.into())).collect()) @@ -265,22 +266,8 @@ impl From<&JsonValue> for Value { } JsonValue::String(string) => Self::Text(string.clone()), JsonValue::Array(array) => { - let u8_max = u8::MAX as u64; - //todo: hacky solution, to fix - let len = array.len(); - if len >= 10 - && array.iter().all(|v| { - let Some(int) = v.as_u64() else { - return false; - }; - int.le(&u8_max) - }) - { - //this is an array of bytes - Self::Bytes(array.iter().map(|v| v.as_u64().unwrap() as u8).collect()) - } else { - Self::Array(array.iter().map(|v| v.into()).collect()) - } + // Critical-2 fix: see owned-form comment above. + Self::Array(array.iter().map(|v| v.into()).collect()) } JsonValue::Object(map) => Self::Map( map.into_iter() @@ -311,7 +298,10 @@ impl TryInto for Value { Value::Bytes20(bytes) => JsonValue::String(BASE64_STANDARD.encode(bytes.as_slice())), Value::Bytes32(bytes) => JsonValue::String(BASE64_STANDARD.encode(bytes.as_slice())), Value::Bytes36(bytes) => JsonValue::String(BASE64_STANDARD.encode(bytes.as_slice())), - Value::Float(float) => JsonValue::Number(Number::from_f64(float).unwrap_or(0.into())), + // JSON cannot represent NaN/±∞ — fail loudly (see validating path above). + Value::Float(float) => JsonValue::Number(Number::from_f64(float).ok_or_else(|| { + Error::Unsupported("non-finite float (NaN/±∞) is not representable in JSON".into()) + })?), Value::Text(string) => JsonValue::String(string), Value::Bool(value) => JsonValue::Bool(value), Value::Null => JsonValue::Null, @@ -720,19 +710,41 @@ mod tests { assert!(val.is_map()); } - // --- byte-array heuristic tests --- + // ----------------------------------------------------------------------- + // From for Value — array conversion is faithful + // + // The previous `len >= 10 && all u8-range` heuristic that silently + // reclassified JSON arrays as `Value::Bytes` was removed. JSON arrays now + // always become `Value::Array(...)` regardless of length / content + // shape. Binary fields should flow through canonical encodings + // (base64 strings in JSON, decoded by the receiver's Deserialize impl). + // ----------------------------------------------------------------------- #[test] - fn from_json_array_10_u8_range_becomes_bytes() { - // Exactly 10 elements, all in u8 range -> Bytes + fn from_json_array_10_u8_range_stays_array_not_bytes() { + // Previously: heuristic silently coerced this to Value::Bytes. + // Now: faithful array → array conversion. let arr: Vec = (0u64..10).map(|i| json!(i)).collect(); let val: Value = JsonValue::Array(arr).into(); - assert_eq!(val, Value::Bytes(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); + assert_eq!( + val, + Value::Array(vec![ + Value::U64(0), + Value::U64(1), + Value::U64(2), + Value::U64(3), + Value::U64(4), + Value::U64(5), + Value::U64(6), + Value::U64(7), + Value::U64(8), + Value::U64(9), + ]) + ); } #[test] fn from_json_array_9_u8_range_stays_array() { - // Only 9 elements -> stays as Array even though all are u8-range let arr: Vec = (0u64..9).map(|i| json!(i)).collect(); let val: Value = JsonValue::Array(arr).into(); assert!(matches!(val, Value::Array(_))); @@ -740,7 +752,6 @@ mod tests { #[test] fn from_json_array_mixed_types_stays_array() { - // 10+ elements but mixed types -> stays as Array let mut arr: Vec = (0u64..10).map(|i| json!(i)).collect(); arr.push(json!("not_a_number")); let val: Value = JsonValue::Array(arr).into(); @@ -749,30 +760,47 @@ mod tests { #[test] fn from_json_array_large_values_stays_array() { - // 10+ elements but values exceed u8 range -> stays as Array let arr: Vec = (0u64..12).map(|i| json!(i * 100)).collect(); let val: Value = JsonValue::Array(arr).into(); - // Some values like 1100 exceed u8::MAX (255), so not all u8-range assert!(matches!(val, Value::Array(_))); } #[test] - fn from_json_array_all_255_becomes_bytes() { - // 10 elements all at u8::MAX + fn from_json_array_all_255_stays_array_not_bytes() { + // Previously: heuristic coerced this to Value::Bytes. + // Now: faithful array → array. let arr: Vec = vec![json!(255); 10]; let val: Value = JsonValue::Array(arr).into(); - assert_eq!(val, Value::Bytes(vec![255; 10])); + assert_eq!(val, Value::Array(vec![Value::U64(255); 10])); } #[test] fn from_json_array_with_negative_stays_array() { - // Negative numbers are not in u8 range let mut arr: Vec = (0u64..9).map(|i| json!(i)).collect(); arr.push(json!(-1)); let val: Value = JsonValue::Array(arr).into(); assert!(matches!(val, Value::Array(_))); } + #[test] + fn from_json_long_byte_like_array_stays_array_not_bytes() { + // Round-trip pin: a 1000-element JSON array of small ints (e.g., a + // document property typed as `array of integers`) previously got + // silently corrupted into Value::Bytes(1000 bytes). The roundtrip + // back via `try_into::` would then emit a base64 string + // instead of the original array. After the Critical-2 fix, the + // conversion is faithful in both directions. + let original: JsonValue = JsonValue::Array(vec![json!(7u64); 1000]); + let value: Value = original.clone().into(); + assert!( + matches!(value, Value::Array(_)), + "1000-element u8-range JSON array must stay an array, not silently \ + become bytes" + ); + let recovered: JsonValue = value.try_into().unwrap(); + assert_eq!(original, recovered, "round-trip JSON array → Value → JSON"); + } + // ----------------------------------------------------------------------- // From<&JsonValue> for Value — reference variant // ----------------------------------------------------------------------- @@ -785,11 +813,14 @@ mod tests { } #[test] - fn from_json_ref_array_becomes_bytes() { + fn from_json_ref_array_stays_array_not_bytes() { + // Mirrors `from_json_array_10_u8_range_stays_array_not_bytes` + // for the borrowed `From<&JsonValue>` variant — same Critical-2 + // fix applies to both impls. let arr: Vec = (0u64..15).map(|i| json!(i)).collect(); let jv = JsonValue::Array(arr); let val: Value = (&jv).into(); - assert!(matches!(val, Value::Bytes(_))); + assert!(matches!(val, Value::Array(_))); } #[test] @@ -1116,9 +1147,32 @@ mod tests { } #[test] - fn validating_json_float_nan_becomes_zero() { - // NaN cannot be represented in JSON Number, falls back to 0 - let result = Value::Float(f64::NAN).try_into_validating_json().unwrap(); - assert_eq!(result, json!(0)); + fn non_finite_floats_error_in_all_three_json_converters() { + // JSON (RFC 8259) cannot represent NaN/±∞. The converters used to + // silently substitute 0 (`Number::from_f64(..).unwrap_or(0)`), which + // made `Value::Float(f64::NAN)` indistinguishable from a real zero to + // every JSON consumer. All three Value→JSON paths now fail loudly + // instead — consistent with the loud IntegerSizeError the same + // functions return for out-of-range integers. + for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { + // owned validating path + assert!(matches!( + Value::Float(bad).try_into_validating_json(), + Err(Error::Unsupported(_)) + )); + // by-ref validating path + assert!(matches!( + Value::Float(bad).try_to_validating_json(), + Err(Error::Unsupported(_)) + )); + // plain TryInto path + let plain: Result = Value::Float(bad).try_into(); + assert!(matches!(plain, Err(Error::Unsupported(_)))); + } + // Finite floats still convert. + assert_eq!( + Value::Float(1.5).try_into_validating_json().unwrap(), + json!(1.5) + ); } } diff --git a/packages/rs-platform-value/src/types/bytes_32.rs b/packages/rs-platform-value/src/types/bytes_32.rs index ba5024489ac..3e9a1f63aa6 100644 --- a/packages/rs-platform-value/src/types/bytes_32.rs +++ b/packages/rs-platform-value/src/types/bytes_32.rs @@ -91,6 +91,12 @@ impl<'de> Deserialize<'de> for Bytes32 { where D: serde::Deserializer<'de>, { + // Both visitors accept strings AND bytes because serde's ContentDeserializer + // (used for internally tagged enums like `#[serde(tag = "$version")]`) defaults + // `is_human_readable` to `true` regardless of the parent deserializer's setting. + // This means bytes can arrive through the string path and vice versa. Mirrors + // the pattern used by `BinaryData` and `Identifier`. + if deserializer.is_human_readable() { struct StringVisitor; @@ -98,7 +104,7 @@ impl<'de> Deserialize<'de> for Bytes32 { type Value = Bytes32; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a base64-encoded string with length 44") + formatter.write_str("a base64-encoded string with length 44 or 32-byte array") } fn visit_str(self, v: &str) -> Result @@ -115,6 +121,18 @@ impl<'de> Deserialize<'de> for Bytes32 { array.copy_from_slice(&bytes); Ok(Bytes32(array)) } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + if v.len() != 32 { + return Err(E::invalid_length(v.len(), &self)); + } + let mut array = [0u8; 32]; + array.copy_from_slice(v); + Ok(Bytes32(array)) + } } deserializer.deserialize_string(StringVisitor) @@ -125,20 +143,35 @@ impl<'de> Deserialize<'de> for Bytes32 { type Value = Bytes32; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a byte array with length 32") + formatter.write_str("a 32-byte array or base64-encoded string") } fn visit_bytes(self, v: &[u8]) -> Result where E: serde::de::Error, { - let mut bytes = [0u8; 32]; if v.len() != 32 { return Err(E::invalid_length(v.len(), &self)); } + let mut bytes = [0u8; 32]; bytes.copy_from_slice(v); Ok(Bytes32(bytes)) } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let bytes = BASE64_STANDARD + .decode(v) + .map_err(|e| E::custom(format!("expected base 64 for bytes32: {}", e)))?; + if bytes.len() != 32 { + return Err(E::invalid_length(bytes.len(), &self)); + } + let mut array = [0u8; 32]; + array.copy_from_slice(&bytes); + Ok(Bytes32(array)) + } } deserializer.deserialize_bytes(BytesVisitor) diff --git a/packages/rs-platform-value/src/value_serialization/mod.rs b/packages/rs-platform-value/src/value_serialization/mod.rs index 97e3519fb58..4ca6aa4a78a 100644 --- a/packages/rs-platform-value/src/value_serialization/mod.rs +++ b/packages/rs-platform-value/src/value_serialization/mod.rs @@ -47,16 +47,18 @@ pub mod ser; /// # Errors /// /// This conversion can fail if `T`'s implementation of `Serialize` decides to -/// fail, or if `T` contains a map with non-string keys. +/// fail. Unlike `serde_json::to_value`, `platform_value::Value::Map` accepts +/// non-string keys (any `Value` is a valid map key), so maps with vector or +/// numeric keys serialize without error: /// /// ``` /// use std::collections::BTreeMap; /// -/// // The keys in this map are vectors, not strings. /// let mut map = BTreeMap::new(); -/// map.insert(vec![32, 64], "x86"); +/// map.insert(vec![32u8, 64], "x86"); /// -/// println!("{}", platform_value::to_value(map).unwrap_err()); +/// let v = platform_value::to_value(map).unwrap(); +/// assert!(v.is_map()); /// ``` pub fn to_value(value: T) -> Result where diff --git a/packages/rs-platform-value/src/value_serialization/ser.rs b/packages/rs-platform-value/src/value_serialization/ser.rs index cb4598cab8c..146cb6d1084 100644 --- a/packages/rs-platform-value/src/value_serialization/ser.rs +++ b/packages/rs-platform-value/src/value_serialization/ser.rs @@ -3,7 +3,7 @@ use crate::value_map::ValueMap; use crate::{to_value, Value}; use base64::prelude::BASE64_STANDARD; use base64::Engine; -use serde::ser::{Impossible, Serialize}; +use serde::ser::Serialize; use std::fmt::Display; // We only use our own error type; no need for From conversions provided by the @@ -357,7 +357,7 @@ pub struct SerializeTupleVariant { pub enum SerializeMap { Map { map: ValueMap, - next_key: Option, + next_key: Option, }, } @@ -463,9 +463,15 @@ impl serde::ser::SerializeMap for SerializeMap { where T: ?Sized + Serialize, { + // Route keys through the regular Serializer (HR=false) so typed keys + // — e.g. `Value::Bytes32` for `BTreeMap` — survive the + // round-trip. The previous design routed keys through a dedicated + // string-only `MapKeySerializer` (HR=true), which forced hash-typed + // keys to be hex strings on serialize while the deserialize side + // (HR=false) expected bytes — non-round-trippable. match self { SerializeMap::Map { next_key, .. } => { - *next_key = Some(tri!(key.serialize(MapKeySerializer))); + *next_key = Some(tri!(to_value(key))); Ok(()) } } @@ -481,7 +487,7 @@ impl serde::ser::SerializeMap for SerializeMap { // Panic because this indicates a bug in the program rather than an // expected failure. let key = key.expect("serialize_value called before serialize_key"); - map.push((Value::Text(key), tri!(to_value(value)))); + map.push((key, tri!(to_value(value)))); Ok(()) } } @@ -494,191 +500,13 @@ impl serde::ser::SerializeMap for SerializeMap { } } -struct MapKeySerializer; - -fn key_must_be_a_string() -> Error { - Error::KeyMustBeAString -} - -impl serde::Serializer for MapKeySerializer { - type Ok = String; - type Error = Error; - - type SerializeSeq = Impossible; - type SerializeTuple = Impossible; - type SerializeTupleStruct = Impossible; - type SerializeTupleVariant = Impossible; - type SerializeMap = Impossible; - type SerializeStruct = Impossible; - type SerializeStructVariant = Impossible; - - #[inline] - fn serialize_unit_variant( - self, - _name: &'static str, - _variant_index: u32, - variant: &'static str, - ) -> Result { - Ok(variant.to_owned()) - } - - #[inline] - fn serialize_newtype_struct(self, _name: &'static str, value: &T) -> Result - where - T: ?Sized + Serialize, - { - value.serialize(self) - } - - fn serialize_bool(self, _value: bool) -> Result { - Err(key_must_be_a_string()) - } - - fn serialize_i8(self, value: i8) -> Result { - Ok(value.to_string()) - } - - fn serialize_i16(self, value: i16) -> Result { - Ok(value.to_string()) - } - - fn serialize_i32(self, value: i32) -> Result { - Ok(value.to_string()) - } - - fn serialize_i64(self, value: i64) -> Result { - Ok(value.to_string()) - } - - fn serialize_u8(self, value: u8) -> Result { - Ok(value.to_string()) - } - - fn serialize_u16(self, value: u16) -> Result { - Ok(value.to_string()) - } - - fn serialize_u32(self, value: u32) -> Result { - Ok(value.to_string()) - } - - fn serialize_u64(self, value: u64) -> Result { - Ok(value.to_string()) - } - - fn serialize_f32(self, _value: f32) -> Result { - Err(key_must_be_a_string()) - } - - fn serialize_f64(self, _value: f64) -> Result { - Err(key_must_be_a_string()) - } - - #[inline] - fn serialize_char(self, value: char) -> Result { - Ok({ - let mut s = String::new(); - s.push(value); - s - }) - } - - #[inline] - fn serialize_str(self, value: &str) -> Result { - Ok(value.to_owned()) - } - - fn serialize_bytes(self, _value: &[u8]) -> Result { - Err(key_must_be_a_string()) - } - - fn serialize_unit(self) -> Result { - Err(key_must_be_a_string()) - } - - fn serialize_unit_struct(self, _name: &'static str) -> Result { - Err(key_must_be_a_string()) - } - - fn serialize_newtype_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - _value: &T, - ) -> Result - where - T: ?Sized + Serialize, - { - Err(key_must_be_a_string()) - } - - fn serialize_none(self) -> Result { - Err(key_must_be_a_string()) - } - - fn serialize_some(self, _value: &T) -> Result - where - T: ?Sized + Serialize, - { - Err(key_must_be_a_string()) - } - - fn serialize_seq(self, _len: Option) -> Result { - Err(key_must_be_a_string()) - } - - fn serialize_tuple(self, _len: usize) -> Result { - Err(key_must_be_a_string()) - } - - fn serialize_tuple_struct( - self, - _name: &'static str, - _len: usize, - ) -> Result { - Err(key_must_be_a_string()) - } - - fn serialize_tuple_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - _len: usize, - ) -> Result { - Err(key_must_be_a_string()) - } - - fn serialize_map(self, _len: Option) -> Result { - Err(key_must_be_a_string()) - } - - fn serialize_struct( - self, - _name: &'static str, - _len: usize, - ) -> Result { - Err(key_must_be_a_string()) - } - - fn serialize_struct_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - _len: usize, - ) -> Result { - Err(key_must_be_a_string()) - } - - fn collect_str(self, value: &T) -> Result - where - T: ?Sized + Display, - { - Ok(value.to_string()) - } -} +// `MapKeySerializer` was removed: keys now flow through the regular +// `Serializer` so typed keys (e.g. `Value::Bytes32` for `BTreeMap`) +// round-trip symmetrically with the deserialize side. The previous +// string-only serializer artificially forced every key to `Value::Text`, +// causing an HR-asymmetry with the deserialize path. The +// `Error::KeyMustBeAString` variant is left in the error enum for SemVer +// stability but is no longer produced by this crate. impl serde::ser::SerializeStruct for SerializeMap { type Ok = Value; @@ -1146,15 +974,24 @@ mod tests { } // --------------------------------------------------------------- - // MapKeySerializer error cases + // Map key types — platform_value allows any Value variant as a key. + // This is unlike serde_json (which mandates string keys for JSON + // compatibility); platform_value's richer Value type means a + // `BTreeMap` round-trips with `Value::Bytes32` keys. // --------------------------------------------------------------- #[test] - fn map_key_bool_errors() { + fn map_key_bool_now_supported() { let mut map = std::collections::HashMap::new(); map.insert(true, "value"); - let result = to_value(map); - assert!(result.is_err()); + let result = to_value(map).expect("bool keys are now allowed"); + match result { + Value::Map(entries) => { + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].0, Value::Bool(true)); + } + _ => panic!("expected Map"), + } } #[test] @@ -1173,6 +1010,35 @@ mod tests { assert!(result.is_ok()); } + #[test] + fn map_key_bytes_round_trips_as_bytes32() { + // The motivating use case: BTreeMap in dashpay/platform. + // Hash types serialize via `serialize_bytes` (HR=false). With typed + // map keys, the result must be `Value::Bytes32`, not a stringified + // hex form — symmetric with the deserialize side which expects bytes. + use serde::{ser::SerializeMap as _, Serializer}; + + // Drive serialize_bytes directly — the cheapest way to exercise + // the path without pulling in a full Hash type. + let mut s = Serializer.serialize_map(Some(1)).unwrap(); + struct BytesKey([u8; 32]); + impl serde::Serialize for BytesKey { + fn serialize(&self, s: S) -> Result { + s.serialize_bytes(&self.0) + } + } + s.serialize_entry(&BytesKey([0xab; 32]), &7u32).unwrap(); + let val = serde::ser::SerializeMap::end(s).unwrap(); + match val { + Value::Map(entries) => { + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].0, Value::Bytes32([0xab; 32])); + assert_eq!(entries[0].1, Value::U32(7)); + } + _ => panic!("expected Value::Map"), + } + } + // --------------------------------------------------------------- // Round-trip tests: Rust -> Value -> Rust // --------------------------------------------------------------- diff --git a/packages/rs-platform-value/tests/coverage_tests.rs b/packages/rs-platform-value/tests/coverage_tests.rs index 3c805d9464f..cae884280c5 100644 --- a/packages/rs-platform-value/tests/coverage_tests.rs +++ b/packages/rs-platform-value/tests/coverage_tests.rs @@ -1957,11 +1957,13 @@ mod json_converter_tests { } #[test] - fn json_to_value_large_u8_array_becomes_bytes() { - // Arrays >= 10 elements of u8 should become bytes + fn json_to_value_large_u8_array_stays_array() { + // Per Critical-2 (removed silent array→bytes coercion), JSON integer + // arrays of any size stay as arrays. Callers that need bytes use the + // explicit `replace_at_paths(_, ReplacementType::BinaryBytes)` path. let j = serde_json::json!([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); let v: Value = j.into(); - assert!(v.is_bytes()); + assert!(v.is_array()); } #[test] diff --git a/packages/rs-platform-wallet-ffi/src/document.rs b/packages/rs-platform-wallet-ffi/src/document.rs index de5c06de938..32509bc3356 100644 --- a/packages/rs-platform-wallet-ffi/src/document.rs +++ b/packages/rs-platform-wallet-ffi/src/document.rs @@ -5,10 +5,9 @@ use std::os::raw::c_char; use std::ptr; use std::slice; -use dpp::document::serialization_traits::DocumentJsonMethodsV0; use dpp::document::{Document, DocumentV0Getters}; use dpp::prelude::Identifier; -use dpp::version::PlatformVersion; +use dpp::serialization::ValueConvertible; use platform_wallet::PlatformWalletError; use rs_sdk_ffi::{SignerHandle, VTableSigner}; @@ -34,12 +33,12 @@ use crate::{unwrap_option_or_return, unwrap_result_or_return}; /// On success the confirmed document's 32-byte id is written to /// `out_document_id`, and a NUL-terminated, owned UTF-8 JSON string of /// the confirmed document is written to `*out_document_json`. The JSON -/// is the canonical query-side representation — produced via DPP's -/// `Document::to_json_with_identifiers_using_bytes`, so it carries the -/// system fields (`$id`/`$ownerId`, set timestamps, `$revision`) with -/// identifiers rendered as base58 strings and only the document's -/// populated fields present (DPP-normalized + default-filled). Swift -/// persists this body verbatim so the local cache matches what a +/// is the canonical query-side representation — the same bytes a DOC-01 +/// list query (`dash_sdk_document_search`) returns, produced by +/// re-serializing DPP's canonical value form (`to_object()`) through +/// `serde_json`: `$id`/`$ownerId`/`$creatorId` as base58 strings, binary +/// properties as base64, `$formatVersion` and unset system fields present. +/// Swift persists this body verbatim so the local cache matches what a /// DOC-01 query would return, rather than the user's form input. /// Ownership of the JSON transfers to the caller, who MUST release it /// with `platform_wallet_string_free`. On any error `*out_document_json` @@ -102,27 +101,10 @@ pub unsafe extern "C" fn platform_wallet_create_document_with_signer( signer, ) .await?; - // Serialize the confirmed document to its canonical - // query-side JSON. `to_json_with_identifiers_using_bytes` - // renders `$id`/`$ownerId` (and `$creatorId`) as base58 - // strings and emits only the populated system fields — the - // shape a DOC-01 query display expects. The trait's - // `platform_version` argument is unused by the V0 impl, so - // `latest()` (the same convention the other FFI entry points - // in this crate use) is safe here. - let platform_version = PlatformVersion::latest(); - let json_value = confirmed - .to_json_with_identifiers_using_bytes(platform_version) - .map_err(|e| { - PlatformWalletError::InvalidIdentityData(format!( - "Failed to convert confirmed document to JSON: {e}" - )) - })?; - let json_string = serde_json::to_string(&json_value).map_err(|e| { - PlatformWalletError::InvalidIdentityData(format!( - "Failed to serialize confirmed document JSON: {e}" - )) - })?; + // Serialize the confirmed document to its canonical query-side + // JSON — the same representation a DOC-01 list query returns — + // so the persisted body matches what a later query yields. + let json_string = confirmed_document_to_json(&confirmed)?; Ok::<_, PlatformWalletError>((confirmed.id(), json_string)) }); result @@ -145,25 +127,27 @@ pub unsafe extern "C" fn platform_wallet_create_document_with_signer( PlatformWalletFFIResult::ok() } -/// Serialize a confirmed `Document` to its canonical query-side JSON -/// string, the same representation the create path returns. +/// Serialize a confirmed `Document` to its canonical query-side JSON string — +/// byte-for-byte the representation `dash_sdk_document_search` (the DOC-01 list +/// query) returns. It re-serializes the canonical value form (`to_object()`) +/// through `serde_json`, so `$id`/`$ownerId`/`$creatorId` render as base58 +/// strings, binary properties as base64, and `$formatVersion` plus unset system +/// fields (as `null`) are present. Swift persists this verbatim so the local +/// cache matches what a subsequent query returns. /// -/// `to_json_with_identifiers_using_bytes` renders `$id`/`$ownerId` -/// (and `$creatorId`) as base58 strings and emits only the populated -/// system fields — the shape a DOC-01 query display expects. Swift -/// persists this verbatim so the local cache matches the on-chain -/// document. The trait's `platform_version` argument is unused by the -/// V0 impl, so `latest()` (the convention the other entry points in -/// this crate use) is safe here. +/// Note: `dash_sdk_document_get_info` (single-document fetch) emits a different, +/// per-field shape (bytes as hex); this parity is with the list-query path only. fn confirmed_document_to_json(document: &Document) -> Result { - let platform_version = PlatformVersion::latest(); - let json_value = document - .to_json_with_identifiers_using_bytes(platform_version) - .map_err(|e| { - PlatformWalletError::InvalidIdentityData(format!( - "Failed to convert confirmed document to JSON: {e}" - )) - })?; + let document_value = document.to_object().map_err(|e| { + PlatformWalletError::InvalidIdentityData(format!( + "Failed to convert confirmed document to a value: {e}" + )) + })?; + let json_value = serde_json::to_value(&document_value).map_err(|e| { + PlatformWalletError::InvalidIdentityData(format!( + "Failed to convert confirmed document to JSON: {e}" + )) + })?; serde_json::to_string(&json_value).map_err(|e| { PlatformWalletError::InvalidIdentityData(format!( "Failed to serialize confirmed document JSON: {e}" @@ -526,3 +510,64 @@ pub unsafe extern "C" fn platform_wallet_document_purchase( *out_document_json = json_cstring.into_raw(); PlatformWalletFFIResult::ok() } + +#[cfg(test)] +mod tests { + use super::*; + use dpp::document::DocumentV0; + use dpp::platform_value::Value; + use std::collections::BTreeMap; + + // The confirmed-document JSON handed to Swift must be the same canonical + // shape the DOC-01 list query (`dash_sdk_document_search`) returns: + // `$formatVersion` present, identifiers as base58 strings, and binary + // properties as base64 strings (not u8-arrays). Guards against reverting to + // the legacy `to_json_with_identifiers_using_bytes` representation. + #[test] + fn confirmed_document_json_matches_canonical_query_shape() { + let mut properties = BTreeMap::new(); + properties.insert( + "blob".to_string(), + Value::Bytes(vec![0xde, 0xad, 0xbe, 0xef]), + ); + + let document = Document::V0(DocumentV0 { + id: Identifier::from([1u8; 32]), + owner_id: Identifier::from([2u8; 32]), + properties, + revision: Some(1), + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + }); + + let json: serde_json::Value = + serde_json::from_str(&confirmed_document_to_json(&document).unwrap()).unwrap(); + + // `$formatVersion` present (the legacy shape omitted it). + assert_eq!(json["$formatVersion"], serde_json::json!("0")); + // Identifiers as base58 strings. + assert!( + json["$id"].is_string(), + "$id must be a base58 string, got {:?}", + json["$id"] + ); + // The key differentiator from the legacy shape: binary property as a + // base64 string (not a u8-array). 0xdeadbeef -> "3q2+7w==". + assert_eq!(json["blob"], serde_json::json!("3q2+7w==")); + // Unset system fields are present as null (the legacy shape omitted them). + assert!( + json.get("$createdAt") + .is_some_and(serde_json::Value::is_null), + "unset $createdAt must be present and null, got {:?}", + json.get("$createdAt") + ); + } +} diff --git a/packages/rs-scripts/src/bin/register_contract.rs b/packages/rs-scripts/src/bin/register_contract.rs index 1cabc6c26ca..7040ab565f8 100644 --- a/packages/rs-scripts/src/bin/register_contract.rs +++ b/packages/rs-scripts/src/bin/register_contract.rs @@ -23,7 +23,6 @@ use dash_sdk::platform::Fetch; use dash_sdk::{Sdk, SdkBuilder}; use dpp::dashcore::Network; use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters}; -use dpp::data_contract::conversion::json::DataContractJsonConversionMethodsV0; use dpp::identity::accessors::IdentityGettersV0; use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use dpp::identity::signer::Signer; @@ -31,7 +30,6 @@ use dpp::identity::{Identity, IdentityPublicKey, KeyType, Purpose, SecurityLevel use dpp::platform_value::string_encoding::Encoding; use dpp::platform_value::Identifier; use dpp::prelude::DataContract; -use platform_version::version::PlatformVersion; use rs_dapi_client::AddressList; use rs_sdk_trusted_context_provider::TrustedHttpContextProvider; use simple_signer::single_key_signer::SingleKeySigner; @@ -104,7 +102,6 @@ async fn run() -> Result<(), String> { let args = Args::parse(); let network = parse_network(&args.network)?; - let platform_version = PlatformVersion::latest(); let identity_id = Identifier::from_string(&args.identity_id, Encoding::Base58) .map_err(|e| format!("invalid --identity (expected base58): {e}"))?; @@ -121,13 +118,15 @@ async fn run() -> Result<(), String> { ) })?; - // full_validation = false: the file may carry an `id` / + // Non-validating deserialization: the file may carry an `id` / // `ownerId` from a fixture that we're about to overwrite, and // strict id-vs-owner checks would reject otherwise-valid // contracts. The schema itself is still parsed and shape- // checked. The on-chain `validate_basic_structure` runs server- - // side during state-transition validation anyway. - let mut data_contract = DataContract::from_json(json_value, false, platform_version) + // side during state-transition validation anyway. Uses the + // canonical (no-validation) `serde_json::from_value::` + // path — the validating opt-in is `DataContract::from_json`. + let mut data_contract: DataContract = serde_json::from_value(json_value) .map_err(|e| format!("failed to build DataContract from JSON: {e}"))?; // Set owner so the SDK's PutContract path fetches the right diff --git a/packages/rs-sdk-ffi/src/data_contract/queries/fetch_json.rs b/packages/rs-sdk-ffi/src/data_contract/queries/fetch_json.rs index a2d40927558..162a2c91329 100644 --- a/packages/rs-sdk-ffi/src/data_contract/queries/fetch_json.rs +++ b/packages/rs-sdk-ffi/src/data_contract/queries/fetch_json.rs @@ -1,7 +1,6 @@ use crate::error::{DashSDKError, DashSDKErrorCode, FFIError}; use crate::sdk::SDKWrapper; use crate::types::{DashSDKResult, SDKHandle}; -use dash_sdk::dpp::data_contract::conversion::json::DataContractJsonConversionMethodsV0; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::platform::{DataContract, Fetch, Identifier}; use std::ffi::{CStr, CString}; @@ -50,11 +49,9 @@ pub unsafe extern "C" fn dash_sdk_data_contract_fetch_json( match result { Ok(Some(contract)) => { - // Get the platform version - let platform_version = wrapper.sdk.version(); - - // Convert to JSON - match contract.to_json(platform_version) { + // Convert to JSON via canonical serde (manual Serialize on the + // outer DataContract enum threads the active platform version). + match serde_json::to_value(&contract) { Ok(json_value) => match serde_json::to_string(&json_value) { Ok(json_string) => match CString::new(json_string) { Ok(c_str) => { diff --git a/packages/rs-sdk-ffi/src/data_contract/queries/fetch_with_serialization.rs b/packages/rs-sdk-ffi/src/data_contract/queries/fetch_with_serialization.rs index 414ae29e395..9ca5467a88a 100644 --- a/packages/rs-sdk-ffi/src/data_contract/queries/fetch_with_serialization.rs +++ b/packages/rs-sdk-ffi/src/data_contract/queries/fetch_with_serialization.rs @@ -1,8 +1,9 @@ use crate::sdk::SDKWrapper; use crate::{DashSDKError, DashSDKErrorCode, DataContractHandle, FFIError, SDKHandle}; -use dash_sdk::dpp::data_contract::conversion::json::DataContractJsonConversionMethodsV0; +use dash_sdk::dpp::data_contract::serialized_version::DataContractInSerializationFormat; use dash_sdk::dpp::data_contract::DataContractWithSerialization; use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use dash_sdk::dpp::version::TryIntoPlatformVersioned; use dash_sdk::platform::{Fetch, Identifier}; use std::ffi::{CStr, CString}; use std::os::raw::c_char; @@ -104,14 +105,30 @@ pub unsafe extern "C" fn dash_sdk_data_contract_fetch_with_serialization( match result { Ok(Some((contract, serialization))) => { - let platform_version = wrapper.sdk.version(); - // Always create a handle since we have the contract let handle = Some(Box::into_raw(Box::new(contract.clone())) as *mut DataContractHandle); // Prepare JSON if requested let json = if return_json { - match contract.to_json(platform_version) { + // Serialize at the SDK's network protocol version (not the + // process-global current/latest) so the JSON matches the + // proof-verified fetch. Mirrors DataContract's own serde with + // the version pinned to `wrapper.sdk.version()`. + let platform_version = wrapper.sdk.version(); + let format: DataContractInSerializationFormat = + match contract.try_into_platform_versioned(platform_version) { + Ok(format) => format, + Err(e) => { + return DashSDKDataContractFetchResult::error(DashSDKError::new( + DashSDKErrorCode::SerializationError, + format!( + "Failed to convert contract to its serialization format: {}", + e + ), + )) + } + }; + match serde_json::to_value(&format) { Ok(json_value) => match serde_json::to_string(&json_value) { Ok(json_string) => match CString::new(json_string) { Ok(c_str) => Some(c_str.into_raw()), diff --git a/packages/rs-sdk-ffi/src/document/queries/search.rs b/packages/rs-sdk-ffi/src/document/queries/search.rs index a6b8b940a11..0fe78bc9a8e 100644 --- a/packages/rs-sdk-ffi/src/document/queries/search.rs +++ b/packages/rs-sdk-ffi/src/document/queries/search.rs @@ -3,10 +3,10 @@ use std::ffi::{CStr, CString}; use std::os::raw::c_char; -use dash_sdk::dpp::document::serialization_traits::DocumentPlatformValueMethodsV0; use dash_sdk::dpp::document::Document; use dash_sdk::dpp::platform_value::Value; use dash_sdk::dpp::prelude::DataContract; +use dash_sdk::dpp::serialization::ValueConvertible; use dash_sdk::drive::query::{OrderClause, WhereClause, WhereOperator}; use dash_sdk::platform::{DocumentQuery, FetchMany}; use serde::{Deserialize, Serialize}; diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift index 6ef4a0f4d20..99538388434 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift @@ -2565,11 +2565,13 @@ extension ManagedPlatformWallet { /// JSON once Platform confirms the transition. /// /// The returned JSON is DPP's canonical representation of the - /// confirmed document (system fields `$id`/`$ownerId`/timestamps/ - /// `$revision` with identifiers as base58 strings, only populated - /// fields present) — what a DOC-01 query would return. Callers - /// persist this verbatim so the local cache matches the on-chain - /// document rather than the user's raw form input. + /// confirmed document — the same bytes a DOC-01 list query + /// (`dash_sdk_document_search`) returns: `$formatVersion` present, + /// `$id`/`$ownerId` as base58 strings, binary properties as base64, + /// and unset system fields as `null`. Callers persist this verbatim + /// so the local cache matches the on-chain document rather than the + /// user's raw form input. (A single-document `documentGet` fetch + /// uses a different, per-field shape.) /// /// Routes through `IdentityWallet::create_document_with_signer` /// (via `platform_wallet_create_document_with_signer`), the diff --git a/packages/wasm-dpp/src/data_contract/data_contract.rs b/packages/wasm-dpp/src/data_contract/data_contract.rs index 682a050869a..1dcdb700a36 100644 --- a/packages/wasm-dpp/src/data_contract/data_contract.rs +++ b/packages/wasm-dpp/src/data_contract/data_contract.rs @@ -13,7 +13,6 @@ use dpp::platform_value::{platform_value, Value}; use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters}; use dpp::data_contract::accessors::v1::DataContractV1Getters; use dpp::data_contract::config::DataContractConfig; -use dpp::data_contract::conversion::json::DataContractJsonConversionMethodsV0; use dpp::data_contract::conversion::value::v0::DataContractValueConversionMethodsV0; use dpp::data_contract::created_data_contract::CreatedDataContract; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; @@ -102,13 +101,11 @@ impl DataContractWasm { let platform_version = PlatformVersion::first(); - DataContract::from_value( - raw_parameters.with_serde_to_platform_value()?, - !skip_validation, - platform_version, - ) - .with_js_error() - .map(Into::into) + let value = raw_parameters.with_serde_to_platform_value()?; + let full_validation = !skip_validation; + DataContract::from_value(value, full_validation, platform_version) + .with_js_error() + .map(Into::into) } #[wasm_bindgen(js_name=getId)] @@ -326,9 +323,9 @@ impl DataContractWasm { #[wasm_bindgen(js_name=toObject)] pub fn to_object(&self) -> Result { - let platform_version = PlatformVersion::first(); - - let value = self.inner.to_value(platform_version).with_js_error()?; + let value = dpp::platform_value::to_value(&self.inner) + .map_err(ProtocolError::ValueError) + .with_js_error()?; let serializer = serde_wasm_bindgen::Serializer::json_compatible(); @@ -370,9 +367,9 @@ impl DataContractWasm { #[wasm_bindgen(js_name=toJSON)] pub fn to_json(&self) -> Result { - let platform_version = PlatformVersion::first(); - - let json = self.inner.to_json(platform_version).with_js_error()?; + let json = serde_json::to_value(&self.inner) + .map_err(|e| ProtocolError::EncodingError(e.to_string())) + .with_js_error()?; let serializer = serde_wasm_bindgen::Serializer::json_compatible(); with_js_error!(json.serialize(&serializer)) } diff --git a/packages/wasm-dpp/src/data_contract/state_transition/data_contract_create_transition/mod.rs b/packages/wasm-dpp/src/data_contract/state_transition/data_contract_create_transition/mod.rs index 73bc6556c2b..197f652ac31 100644 --- a/packages/wasm-dpp/src/data_contract/state_transition/data_contract_create_transition/mod.rs +++ b/packages/wasm-dpp/src/data_contract/state_transition/data_contract_create_transition/mod.rs @@ -15,7 +15,9 @@ use dpp::state_transition::{ StateTransitionIdentitySigned, StateTransitionOwned, StateTransitionSingleSigned, }; -use dpp::state_transition::{StateTransition, StateTransitionValueConvert}; +use dpp::serialization::ValueConvertible; +use dpp::state_transition::StateTransition; +use dpp::state_transition::StateTransitionFieldTypes; use dpp::version::PlatformVersion; use dpp::{state_transition::StateTransitionLike, ProtocolError}; use serde::Serialize; @@ -47,14 +49,32 @@ impl From for DataContractCreateTransition { impl DataContractCreateTransitionWasm { #[wasm_bindgen(constructor)] pub fn new(value: JsValue) -> Result { - let platform_value = PlatformVersion::first(); - - DataContractCreateTransition::from_object( - value.with_serde_to_platform_value()?, - platform_value, - ) - .map(Into::into) - .with_js_error() + use dpp::platform_value::Value; + let mut raw = value.with_serde_to_platform_value()?; + // Canonical `ValueConvertible::from_object` is a strict serde + // deserialization: it dispatches on the enum's `$formatVersion` tag and + // requires every field. Legacy JS clients construct a transition from + // its essential inputs (data contract + identity nonce) and sign it + // afterwards, so default the format tag plus the protocol-managed and + // signature fields they omit — preserving the lenient construction the + // pre-canonical path provided. + if let Value::Map(ref mut entries) = raw { + let mut ensure = |key: &str, default: Value| { + if !entries + .iter() + .any(|(k, _)| matches!(k, Value::Text(s) if s == key)) + { + entries.push((Value::Text(key.to_string()), default)); + } + }; + ensure("$formatVersion", Value::Text("0".to_string())); + ensure("userFeeIncrease", Value::U16(0)); + ensure("signaturePublicKeyId", Value::U32(0)); + ensure("signature", Value::Bytes(vec![])); + } + DataContractCreateTransition::from_object(raw) + .map(Into::into) + .with_js_error() } #[wasm_bindgen(js_name=getDataContract)] @@ -187,10 +207,17 @@ impl DataContractCreateTransitionWasm { #[wasm_bindgen(js_name=toObject)] pub fn to_object(&self, skip_signature: Option) -> Result { - let serde_object = self - .0 - .to_cleaned_object(skip_signature.unwrap_or(false)) - .map_err(from_protocol_error)?; + let mut serde_object = self.0.to_object().map_err(from_protocol_error)?; + + if skip_signature.unwrap_or(false) { + for path in + ::signature_property_paths() + { + serde_object + .remove_values_matching_path(path) + .map_err(|e| from_protocol_error(dpp::ProtocolError::ValueError(e)))?; + } + } let serializer = serde_wasm_bindgen::Serializer::json_compatible(); diff --git a/packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs b/packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs index 9ce90d5c004..dfcddef8c94 100644 --- a/packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs +++ b/packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs @@ -3,11 +3,13 @@ // pub use validation::*; use dpp::consensus::ConsensusError; +use dpp::serialization::ValueConvertible; use dpp::serialization::{PlatformDeserializable, PlatformSerializable}; use dpp::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; +use dpp::state_transition::StateTransition; +use dpp::state_transition::StateTransitionFieldTypes; use dpp::state_transition::StateTransitionHasUserFeeIncrease; -use dpp::state_transition::{StateTransition, StateTransitionValueConvert}; use dpp::state_transition::{ StateTransitionIdentitySigned, StateTransitionOwned, StateTransitionSingleSigned, }; @@ -48,14 +50,32 @@ impl From for DataContractUpdateTransition { impl DataContractUpdateTransitionWasm { #[wasm_bindgen(constructor)] pub fn new(raw_parameters: JsValue) -> Result { - let platform_version = PlatformVersion::first(); - - DataContractUpdateTransition::from_object( - raw_parameters.with_serde_to_platform_value()?, - platform_version, - ) - .map(Into::into) - .with_js_error() + use dpp::platform_value::Value; + let mut raw = raw_parameters.with_serde_to_platform_value()?; + // Canonical `ValueConvertible::from_object` is a strict serde + // deserialization: it dispatches on the enum's `$formatVersion` tag and + // requires every field. Legacy JS clients construct a transition from + // its essential inputs (data contract + identity contract nonce) and + // sign it afterwards, so default the format tag plus the + // protocol-managed and signature fields they omit — preserving the + // lenient construction the pre-canonical path provided. + if let Value::Map(ref mut entries) = raw { + let mut ensure = |key: &str, default: Value| { + if !entries + .iter() + .any(|(k, _)| matches!(k, Value::Text(s) if s == key)) + { + entries.push((Value::Text(key.to_string()), default)); + } + }; + ensure("$formatVersion", Value::Text("0".to_string())); + ensure("userFeeIncrease", Value::U16(0)); + ensure("signaturePublicKeyId", Value::U32(0)); + ensure("signature", Value::Bytes(vec![])); + } + DataContractUpdateTransition::from_object(raw) + .map(Into::into) + .with_js_error() } #[wasm_bindgen(js_name=getDataContract)] @@ -191,10 +211,17 @@ impl DataContractUpdateTransitionWasm { #[wasm_bindgen(js_name=toObject)] pub fn to_object(&self, skip_signature: Option) -> Result { - let serde_object = self - .0 - .to_cleaned_object(skip_signature.unwrap_or(false)) - .map_err(from_protocol_error)?; + let mut serde_object = self.0.to_object().map_err(from_protocol_error)?; + + if skip_signature.unwrap_or(false) { + for path in + ::signature_property_paths() + { + serde_object + .remove_values_matching_path(path) + .map_err(|e| from_protocol_error(ProtocolError::ValueError(e)))?; + } + } serde_object .serialize(&serde_wasm_bindgen::Serializer::json_compatible()) diff --git a/packages/wasm-dpp/src/document/extended_document.rs b/packages/wasm-dpp/src/document/extended_document.rs index f3ff6b0399c..bfe92af63f9 100644 --- a/packages/wasm-dpp/src/document/extended_document.rs +++ b/packages/wasm-dpp/src/document/extended_document.rs @@ -1,13 +1,9 @@ -use dpp::document::{ - DocumentV0Getters, DocumentV0Setters, ExtendedDocument, EXTENDED_DOCUMENT_IDENTIFIER_FIELDS, -}; +use dpp::document::{DocumentV0Getters, DocumentV0Setters, ExtendedDocument}; use serde_json::Value as JsonValue; use dpp::platform_value::{Bytes32, Value}; use dpp::prelude::{Identifier, Revision, TimestampMillis}; -use dpp::util::json_value::JsonValueExt; - use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::document::serialization_traits::ExtendedDocumentPlatformConversionMethodsV0; use dpp::platform_value::converter::serde_json::BTreeValueJsonConverter; @@ -21,14 +17,12 @@ use crate::buffer::Buffer; use crate::data_contract::DataContractWasm; #[allow(deprecated)] // BinaryType is unused in unused code below use crate::document::BinaryType; -use crate::document::{ConversionOptions, DocumentWasm}; +use crate::document::DocumentWasm; use crate::errors::RustConversionError; use crate::identifier::{identifier_from_js_value, IdentifierWrapper}; -use crate::lodash::lodash_set; use crate::metadata::MetadataWasm; use crate::utils::{with_serde_to_platform_value, IntoWasm, ToSerdeJSONExt, WithJsError}; use crate::validation::ValidationResultWasm; -use crate::with_js_error; #[wasm_bindgen(js_name=ExtendedDocument)] #[derive(Debug, Clone, Serialize, Deserialize)] @@ -187,6 +181,21 @@ impl ExtendedDocumentWasm { Ok(js_value) } + #[wasm_bindgen(js_name=toObject)] + pub fn to_object(&self) -> Result { + // Canonical object shape: every `$`-prefixed system field (id, ownerId, + // type, dataContractId, revision, timestamps/block-heights, creatorId) + // plus the flattened document properties, with identifiers and binary + // data rendered as `Uint8Array`. Serializing the non-human-readable + // `to_map_value()` turns every identifier/bytes node — including nested + // document properties — into a `Uint8Array` in one pass, so there is no + // need to walk the document type's identifier/binary paths. + let map = self.0.to_map_value().with_js_error()?; + let serializer = + serde_wasm_bindgen::Serializer::json_compatible().serialize_bytes_as_arrays(false); + Ok(map.serialize(&serializer)?) + } + #[wasm_bindgen(js_name=set)] pub fn set(&mut self, path: String, js_value_to_set: JsValue) -> Result<(), JsValue> { let value: Value = js_value_to_set.with_serde_to_platform_value()?; @@ -273,58 +282,6 @@ impl ExtendedDocumentWasm { Ok(()) } - #[wasm_bindgen(js_name=toObject)] - pub fn to_object(&self, options: &JsValue) -> Result { - let options: ConversionOptions = if !options.is_undefined() && options.is_object() { - let raw_options = options.with_serde_to_json_value()?; - serde_json::from_value(raw_options).with_js_error()? - } else { - Default::default() - }; - let mut value = self.0.to_json_object_for_validation().with_js_error()?; - - let document_type = self.0.document_type().with_js_error()?; - - let identifier_paths = document_type.identifier_paths(); - let binary_paths = document_type.binary_paths(); - - let serializer = serde_wasm_bindgen::Serializer::json_compatible(); - let js_value = value.serialize(&serializer)?; - - for path in identifier_paths - .iter() - .map(|s| s.as_str()) - .chain(EXTENDED_DOCUMENT_IDENTIFIER_FIELDS) - { - if let Ok(bytes) = value.remove_value_at_path_into::>(path) { - let buffer = Buffer::from_bytes_owned(bytes); - if !options.skip_identifiers_conversion { - lodash_set(&js_value, path, buffer.into()); - } else { - let id = IdentifierWrapper::new(buffer.into()); - lodash_set(&js_value, path, id.into()); - } - } - } - - for path in binary_paths { - if let Ok(bytes) = value.remove_value_at_path_into::>(path) { - let buffer = Buffer::from_bytes(&bytes); - lodash_set(&js_value, path, buffer.into()); - } - } - - Ok(js_value) - } - - #[wasm_bindgen(js_name=toJSON)] - pub fn to_json(&self) -> Result { - let value = self.0.to_json(PlatformVersion::first()).with_js_error()?; - let serializer = serde_wasm_bindgen::Serializer::json_compatible(); - - with_js_error!(value.serialize(&serializer)) - } - #[wasm_bindgen(js_name=toBuffer)] pub fn to_buffer(&self) -> Result { let bytes: Vec = self diff --git a/packages/wasm-dpp/src/document/mod.rs b/packages/wasm-dpp/src/document/mod.rs index 78da69239fc..f34a0e0b414 100644 --- a/packages/wasm-dpp/src/document/mod.rs +++ b/packages/wasm-dpp/src/document/mod.rs @@ -11,7 +11,9 @@ use crate::data_contract::DataContractWasm; use crate::identifier::IdentifierWrapper; use crate::utils::WithJsError; -use crate::utils::{with_serde_to_json_value, ToSerdeJSONExt}; +use crate::utils::{ + json_value_to_platform_value_lenient, with_serde_to_json_value, ToSerdeJSONExt, +}; use dpp::document::document_methods::DocumentMethodsV0; use dpp::document::DocumentV0Getters; @@ -39,7 +41,6 @@ use dpp::{platform_value, ProtocolError}; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; -use dpp::document::serialization_traits::DocumentPlatformValueMethodsV0; use dpp::version::PlatformVersion; use serde_json::Value as JsonValue; @@ -71,7 +72,8 @@ impl DocumentWasm { js_data_contract: &DataContractWasm, js_document_type_name: JsValue, ) -> Result { - let mut raw_document: Value = with_serde_to_json_value(&js_raw_document)?.into(); + let mut raw_document: Value = + json_value_to_platform_value_lenient(with_serde_to_json_value(&js_raw_document)?); let document_type_name = js_document_type_name .as_string() @@ -99,8 +101,23 @@ impl DocumentWasm { .with_js_error()?; // The binary paths are not being converted, because they always should be a `Buffer`. `Buffer` is always an Array - let document = Document::from_platform_value(raw_document, PlatformVersion::first()) - .with_js_error()?; + // Phase D step 8 slice B replaced legacy `Document::from_platform_value` + // with canonical `ValueConvertible::from_object`, which requires a + // `$formatVersion` tag. Insert it for un-tagged JS-supplied input + // before delegating. + if let Value::Map(ref mut entries) = raw_document { + let has_tag = entries + .iter() + .any(|(k, _)| matches!(k, Value::Text(s) if s == "$formatVersion")); + if !has_tag { + entries.push(( + Value::Text("$formatVersion".to_string()), + Value::Text("0".to_string()), + )); + } + } + use dpp::serialization::ValueConvertible; + let document = Document::from_object(raw_document).with_js_error()?; Ok(document.into()) } diff --git a/packages/wasm-dpp/src/identity/state_transition/asset_lock_proof/chain/chain_asset_lock_proof.rs b/packages/wasm-dpp/src/identity/state_transition/asset_lock_proof/chain/chain_asset_lock_proof.rs index 4176b09af57..b01cd588b87 100644 --- a/packages/wasm-dpp/src/identity/state_transition/asset_lock_proof/chain/chain_asset_lock_proof.rs +++ b/packages/wasm-dpp/src/identity/state_transition/asset_lock_proof/chain/chain_asset_lock_proof.rs @@ -1,6 +1,7 @@ use dpp::dashcore::consensus::Encodable; use dpp::dashcore::OutPoint; use dpp::identity::state_transition::asset_lock_proof::AssetLockProofType; +use dpp::serialization::ValueConvertible; use serde::{Deserialize, Serialize}; use std::convert::TryInto; use wasm_bindgen::prelude::*; @@ -117,7 +118,9 @@ impl ChainAssetLockProofWasm { #[wasm_bindgen(js_name=toObject)] pub fn to_object(&self) -> Result { - let asset_lock_value = self.0.to_cleaned_object().with_js_error()?; + // `to_cleaned_object` was a pure delegation to `to_object` in rs-dpp; + // the canonical `ValueConvertible::to_object` produces the same Value. + let asset_lock_value = self.0.to_object().with_js_error()?; let serializer = serde_wasm_bindgen::Serializer::json_compatible(); let js_object = with_js_error!(asset_lock_value.serialize(&serializer))?; diff --git a/packages/wasm-dpp/src/identity/state_transition/asset_lock_proof/instant/instant_asset_lock_proof.rs b/packages/wasm-dpp/src/identity/state_transition/asset_lock_proof/instant/instant_asset_lock_proof.rs index 244bc89e2ff..38953ad03de 100644 --- a/packages/wasm-dpp/src/identity/state_transition/asset_lock_proof/instant/instant_asset_lock_proof.rs +++ b/packages/wasm-dpp/src/identity/state_transition/asset_lock_proof/instant/instant_asset_lock_proof.rs @@ -5,6 +5,7 @@ use dpp::dashcore::{ use dpp::dashcore::consensus::Encodable; use dpp::identity::state_transition::asset_lock_proof::AssetLockProofType; +use dpp::serialization::ValueConvertible; use serde::{Deserialize, Serialize}; use std::convert::TryInto; use wasm_bindgen::prelude::*; @@ -117,7 +118,10 @@ impl InstantAssetLockProofWasm { #[wasm_bindgen(js_name=toObject)] pub fn to_object(&self) -> Result { - let asset_lock_value = self.0.to_cleaned_object().with_js_error()?; + // `to_cleaned_object` was a pure delegation to `to_object` in + // rs-dpp; the canonical `ValueConvertible::to_object` produces the + // same Value (no `disabledAt` field on InstantAssetLockProof to clean). + let asset_lock_value = self.0.to_object().with_js_error()?; let serializer = serde_wasm_bindgen::Serializer::json_compatible(); let js_object = with_js_error!(asset_lock_value.serialize(&serializer))?; diff --git a/packages/wasm-dpp/src/utils.rs b/packages/wasm-dpp/src/utils.rs index 20794d3a6d4..665f8fbc70f 100644 --- a/packages/wasm-dpp/src/utils.rs +++ b/packages/wasm-dpp/src/utils.rs @@ -105,7 +105,57 @@ pub fn with_serde_to_json_value(data: &JsValue) -> Result { } pub fn with_serde_to_platform_value(data: &JsValue) -> Result { - Ok(with_serde_to_json_value(data)?.into()) + Ok(json_value_to_platform_value_lenient( + with_serde_to_json_value(data)?, + )) +} + +/// Converts a `serde_json::Value` into a `platform_value::Value`, restoring the +/// legacy "array of u8 → `Value::Bytes`" coercion that `From for Value` +/// used to perform before the Critical-2 faithful-conversion change in +/// rs-platform-value. +/// +/// The JS boundary of this (deprecated) crate stringifies JS `Buffer`s into plain +/// number arrays via [`stringify`], then relied on that coercion to reconstruct +/// binary document fields (`byteArrayField`, identifiers, …) as `Value::Bytes`. +/// rs-platform-value is now intentionally faithful (array → `Value::Array`), so we +/// re-apply the heuristic here — scoped to the legacy crate's JS input only — +/// rather than reintroduce the footgun globally. +pub(crate) fn json_value_to_platform_value_lenient(value: JsonValue) -> Value { + match value { + JsonValue::Array(array) => { + let u8_max = u8::MAX as u64; + // Matches the pre-Critical-2 heuristic exactly: length >= 10 and every + // element a u64 that fits in a byte ⇒ treat as a byte array. + if array.len() >= 10 + && array + .iter() + .all(|v| v.as_u64().map(|int| int <= u8_max).unwrap_or(false)) + { + Value::Bytes( + array + .into_iter() + .map(|v| v.as_u64().expect("checked above") as u8) + .collect(), + ) + } else { + Value::Array( + array + .into_iter() + .map(json_value_to_platform_value_lenient) + .collect(), + ) + } + } + JsonValue::Object(map) => Value::Map( + map.into_iter() + .map(|(k, v)| (k.into(), json_value_to_platform_value_lenient(v))) + .collect(), + ), + // Null / Bool / Number / String are unaffected by the heuristic and convert + // identically through the canonical impl. + other => other.into(), + } } pub fn with_serde_into(data: &JsValue) -> Result diff --git a/packages/wasm-dpp/test/integration/document/Document.spec.js b/packages/wasm-dpp/test/integration/document/Document.spec.js index cf153f3f0e1..68d50698217 100644 --- a/packages/wasm-dpp/test/integration/document/Document.spec.js +++ b/packages/wasm-dpp/test/integration/document/Document.spec.js @@ -32,66 +32,6 @@ describe('ExtendedDocument', () => { document.setMetadata(metadataFixture); }); - describe.skip('#toJSON', () => { - it('should return json document - Rust', () => { - const result = document.toJSON(); - - expect(result).to.deep.equal({ - $protocolVersion: document.getProtocolVersion(), - $dataContractId: dataContract.getId().toString(), - $id: document.getId().toString(), - $ownerId: document.getOwnerId().toString(), - $revision: 1, - $type: 'withByteArrays', - byteArrayField: document.get('byteArrayField').toString('base64'), - identifierField: document.get('identifierField').toString(), - }); - }); - }); - - describe('#toObject', () => { - it('should return raw document - Rust', () => { - const result = document.toObject(); - - expect(result).to.deep.equal({ - $createdAt: null, // TODO: it should be omitted - $createdAtBlockHeight: null, - $createdAtCoreBlockHeight: null, - $creatorId: null, - $updatedAt: null, - $updatedAtBlockHeight: null, - $updatedAtCoreBlockHeight: null, - $transferredAt: null, - $transferredAtBlockHeight: null, - $transferredAtCoreBlockHeight: null, - $dataContractId: dataContract.getId().toBuffer(), - $id: document.getId().toBuffer(), - $ownerId: document.getOwnerId().toBuffer(), - $revision: 1, - $type: 'withByteArrays', - byteArrayField: document.get('byteArrayField'), - identifierField: document.get('identifierField').toBuffer(), - }); - }); - - it('should return raw document with Identifiers - Rust', () => { - const result = document.toObject({ skipIdentifiersConversion: true }); - - expect(result.$dataContractId).to.be.an.instanceOf(Identifier); - expect(result.$id).to.be.an.instanceOf(Identifier); - expect(result.$ownerId).to.be.an.instanceOf(Identifier); - expect(result.identifierField).to.be.an.instanceOf(Identifier); - - expect(result.$dataContractId.toBuffer()).to.deep.equal(dataContract.getId().toBuffer()); - expect(result.$id.toBuffer()).to.deep.equal(document.getId().toBuffer()); - expect(result.$ownerId.toBuffer()).to.deep.equal(document.getOwnerId().toBuffer()); - expect(result.identifierField.toBuffer()).to.deep.equal(document.get('identifierField').toBuffer()); - expect(BigInt(result.$revision)).to.deep.equal(document.getRevision()); - expect(result.$type).to.deep.equal(document.getType()); - expect(result.byteArrayField).to.deep.equal(document.get('byteArrayField')); - }); - }); - describe('#setMetadata', () => { it('should set metadata - Rust', () => { const otherMetadata = new Metadata(BigInt(43), 1, BigInt(100), 2); diff --git a/packages/wasm-dpp/test/unit/dataContract/DataContract.spec.js b/packages/wasm-dpp/test/unit/dataContract/DataContract.spec.js index 38b139b54f1..1c3df94c386 100644 --- a/packages/wasm-dpp/test/unit/dataContract/DataContract.spec.js +++ b/packages/wasm-dpp/test/unit/dataContract/DataContract.spec.js @@ -218,7 +218,7 @@ describe('DataContract', () => { const result = dataContract.toJSON(); expect(result).to.deep.equal({ - $formatVersion: '0', + $formatVersion: '1', config: { $formatVersion: '0', canBeDeleted: false, @@ -235,6 +235,16 @@ describe('DataContract', () => { ownerId: bs58.encode(ownerId), schemaDefs: null, documentSchemas, + createdAt: null, + updatedAt: null, + createdAtBlockHeight: null, + updatedAtBlockHeight: null, + createdAtEpoch: null, + updatedAtEpoch: null, + description: null, + keywords: [], + groups: {}, + tokens: {}, }); }); @@ -248,7 +258,7 @@ describe('DataContract', () => { const result = dataContract.toJSON(); expect(result).to.deep.equal({ - $formatVersion: '0', + $formatVersion: '1', config: { $formatVersion: '0', canBeDeleted: false, @@ -265,6 +275,16 @@ describe('DataContract', () => { ownerId: bs58.encode(ownerId), documentSchemas, schemaDefs: $defs, + createdAt: null, + updatedAt: null, + createdAtBlockHeight: null, + updatedAtBlockHeight: null, + createdAtEpoch: null, + updatedAtEpoch: null, + description: null, + keywords: [], + groups: {}, + tokens: {}, }); }); }); diff --git a/packages/wasm-dpp/test/unit/dataContract/stateTransition/DataContractCreateTransition/DataContractCreateTransition.spec.js b/packages/wasm-dpp/test/unit/dataContract/stateTransition/DataContractCreateTransition/DataContractCreateTransition.spec.js index 2fa931c11ab..0ea73fcab0e 100644 --- a/packages/wasm-dpp/test/unit/dataContract/stateTransition/DataContractCreateTransition/DataContractCreateTransition.spec.js +++ b/packages/wasm-dpp/test/unit/dataContract/stateTransition/DataContractCreateTransition/DataContractCreateTransition.spec.js @@ -72,7 +72,7 @@ describe('DataContractCreateTransition', () => { it('should return serialized State Transition', () => { const result = stateTransition.toBuffer(); expect(result).to.be.instanceOf(Buffer); - expect(result).to.have.lengthOf(2359); + expect(result).to.have.lengthOf(2370); }); it('should be able to restore contract config from bytes', () => { diff --git a/packages/wasm-dpp/test/unit/dataContract/stateTransition/DataContractUpdateTransition/DataContractUpdateTransition.spec.js b/packages/wasm-dpp/test/unit/dataContract/stateTransition/DataContractUpdateTransition/DataContractUpdateTransition.spec.js index 444f695113c..d53fa0dc500 100644 --- a/packages/wasm-dpp/test/unit/dataContract/stateTransition/DataContractUpdateTransition/DataContractUpdateTransition.spec.js +++ b/packages/wasm-dpp/test/unit/dataContract/stateTransition/DataContractUpdateTransition/DataContractUpdateTransition.spec.js @@ -20,7 +20,8 @@ describe('DataContractUpdateTransition', () => { stateTransition = new DataContractUpdateTransition({ protocolVersion: 1, dataContract: dataContract.toObject(), - identityContractNonce: 1, + // Canonical serde wire name for the identity-contract-nonce field. + '$identity-contract-nonce': 1, }); }); @@ -65,7 +66,7 @@ describe('DataContractUpdateTransition', () => { it('should return serialized State Transition', () => { const result = stateTransition.toBuffer(); expect(result).to.be.instanceOf(Buffer); - expect(result).to.have.lengthOf(2359); + expect(result).to.have.lengthOf(2370); }); it('should be able to restore contract config from bytes', () => { diff --git a/packages/wasm-dpp/test/unit/document/Document.spec.js b/packages/wasm-dpp/test/unit/document/Document.spec.js index 7e2a3afbaa2..8aad26908ff 100644 --- a/packages/wasm-dpp/test/unit/document/Document.spec.js +++ b/packages/wasm-dpp/test/unit/document/Document.spec.js @@ -397,28 +397,6 @@ describe('Document', () => { }); }); - describe('#toJSON', () => { - it('should return Document as plain JS object', () => { - const jsonDocument = { - ...rawDocument, - $dataContractId: document.getDataContractId().toString(), - $dataContract: document.getDataContract().toJSON(), - $id: document.getId().toString(), - $ownerId: document.getOwnerId().toString(), - }; - - expect(document.toJSON()).to.deep.equal(jsonDocument); - }); - }); - - describe('#toObject', () => { - it('should return Document as object', () => { - const result = document.toObject(); - - expect(rawDocumentWithBuffers).to.deep.equal(result); - }); - }); - describe('#toBuffer', () => { it('should return serialized Document', () => { const buffer = document.toBuffer(); diff --git a/packages/wasm-dpp/test/unit/document/DocumentFactory.spec.js b/packages/wasm-dpp/test/unit/document/DocumentFactory.spec.js index 2ed208c04ee..3fc811169f5 100644 --- a/packages/wasm-dpp/test/unit/document/DocumentFactory.spec.js +++ b/packages/wasm-dpp/test/unit/document/DocumentFactory.spec.js @@ -49,7 +49,6 @@ describe('DocumentFactory', () => { ownerId = documents[0].getOwnerId(); ([, , , document] = documents); - rawDocument = document.toObject(); factory = new DocumentFactory(1, { generate: () => crypto.randomBytes(32) }); }); @@ -62,7 +61,6 @@ describe('DocumentFactory', () => { // the mocked DataContract validator always returned true. const [niceDocument] = documents; - const newRawDocument = niceDocument.toObject(); const contractId = bs58.decode('FQco85WbwNgb5ix8QQAH6wurMcgEC5ENSCv5ixG9cj12'); const name = 'Cutie'; @@ -74,13 +72,13 @@ describe('DocumentFactory', () => { const newDocument = factory.create( dataContract, ownerIdJs, - newRawDocument.$type, + niceDocument.getType(), { name }, ); expect(newDocument).to.be.an.instanceOf(ExtendedDocument); - expect(newDocument.getType()).to.equal(newRawDocument.$type); + expect(newDocument.getType()).to.equal(niceDocument.getType()); expect(newDocument.get('name')).to.equal(name); diff --git a/packages/wasm-dpp/test/unit/document/errors/InvalidDocumentError.spec.js b/packages/wasm-dpp/test/unit/document/errors/InvalidDocumentError.spec.js index 1406cf83584..2919e038102 100644 --- a/packages/wasm-dpp/test/unit/document/errors/InvalidDocumentError.spec.js +++ b/packages/wasm-dpp/test/unit/document/errors/InvalidDocumentError.spec.js @@ -1,15 +1,16 @@ const { InvalidDocumentError } = require('../../../..'); -const getDocumentsFixture = require('../../../../lib/test/fixtures/getDocumentsFixture'); describe('InvalidDocumentError', () => { let rawDocument; let error; - beforeEach(async () => { + beforeEach(() => { error = new Error('Some error'); - const [document] = await getDocumentsFixture(); - rawDocument = document.toObject(); + // ExtendedDocument no longer exposes toObject(); InvalidDocumentError stores + // the raw document opaquely (round-tripped as a JsValue), so a representative + // plain object is sufficient for these wrapper tests. + rawDocument = { $type: 'niceDocument', name: 'someName' }; }); it('should return errors', () => { diff --git a/packages/wasm-dpp/test/unit/identity/Identity.spec.js b/packages/wasm-dpp/test/unit/identity/Identity.spec.js index c743fdf1d33..bd8f618c4ac 100644 --- a/packages/wasm-dpp/test/unit/identity/Identity.spec.js +++ b/packages/wasm-dpp/test/unit/identity/Identity.spec.js @@ -72,7 +72,6 @@ describe('Identity', () => { purpose: KeyPurpose.AUTHENTICATION, securityLevel: KeySecurityLevel.HIGH, readOnly: false, - disabledAt: null, contractBounds: null, }; @@ -151,7 +150,6 @@ describe('Identity', () => { purpose: KeyPurpose.AUTHENTICATION, securityLevel: KeySecurityLevel.HIGH, readOnly: false, - disabledAt: null, contractBounds: null, }, ], diff --git a/packages/wasm-dpp2/src/asset_lock_proof/proof.rs b/packages/wasm-dpp2/src/asset_lock_proof/proof.rs index 451c3765b6a..2b735d48b87 100644 --- a/packages/wasm-dpp2/src/asset_lock_proof/proof.rs +++ b/packages/wasm-dpp2/src/asset_lock_proof/proof.rs @@ -20,21 +20,21 @@ const TS_TYPES: &str = r#" /** * AssetLockProof serialized as a plain object. * - * Internally-tagged discriminated union — `type` discriminates the variant and + * Internally-tagged discriminated union — `$type` discriminates the variant and * the variant's fields sit alongside it. Mirrors the rs-dpp serde shape (which - * uses `#[serde(tag = "type")]` on the enum) and the convention used by + * uses `#[serde(tag = "$type")]` on the enum) and the convention used by * `AddressWitness` / `AddressFundsFeeStrategyStep`. */ export type AssetLockProofObject = - | ({ type: "instant" } & InstantAssetLockProofObject) - | ({ type: "chain" } & ChainAssetLockProofObject); + | ({ $type: "instant" } & InstantAssetLockProofObject) + | ({ $type: "chain" } & ChainAssetLockProofObject); /** * AssetLockProof serialized as JSON. */ export type AssetLockProofJSON = - | ({ type: "instant" } & InstantAssetLockProofJSON) - | ({ type: "chain" } & ChainAssetLockProofJSON); + | ({ $type: "instant" } & InstantAssetLockProofJSON) + | ({ $type: "chain" } & ChainAssetLockProofJSON); "#; #[wasm_bindgen] @@ -166,7 +166,7 @@ impl AssetLockProofWasm { } /// Returns the lock type as a lowercase wire-shape string ("instant" or - /// "chain") — matching the `type` discriminator emitted by `toObject()` / + /// "chain") — matching the `$type` discriminator emitted by `toObject()` / /// `toJSON()`. #[wasm_bindgen(getter = "lockType")] pub fn lock_type(&self) -> String { @@ -235,9 +235,10 @@ impl AssetLockProofWasm { impl_try_from_js_value!(AssetLockProofWasm, "AssetLockProof"); impl_try_from_options!(AssetLockProofWasm); impl_wasm_type_info!(AssetLockProofWasm, AssetLockProof); -crate::impl_wasm_conversions_serde!( +crate::impl_wasm_conversions_inner!( AssetLockProofWasm, AssetLockProof, + AssetLockProof, AssetLockProofObjectJs, AssetLockProofJSONJs ); diff --git a/packages/wasm-dpp2/src/data_contract/document/model.rs b/packages/wasm-dpp2/src/data_contract/document/model.rs index 27cf5006d48..d447196c486 100644 --- a/packages/wasm-dpp2/src/data_contract/document/model.rs +++ b/packages/wasm-dpp2/src/data_contract/document/model.rs @@ -11,8 +11,13 @@ use crate::utils::{ }; use crate::version::{PlatformVersionLikeJs, PlatformVersionWasm}; use dpp::document::serialization_traits::{ - DocumentJsonMethodsV0, DocumentPlatformConversionMethodsV0, DocumentPlatformValueMethodsV0, + DocumentPlatformConversionMethodsV0, DocumentPlatformValueMethodsV0, }; +// `DocumentPlatformValueMethodsV0` is brought in for `to_map_value` +// (the only method on it the wasm wrapper still uses, after Phase D +// step 8 slice B). `DocumentPlatformConversionMethodsV0` is the binary +// serialization trait. Canonical `JsonConvertible` / `ValueConvertible` +// are imported inline at the call sites. use dpp::document::{Document, DocumentV0, DocumentV0Getters, DocumentV0Setters}; use dpp::identifier::Identifier; use dpp::platform_value::string_encoding::Encoding::{Base64, Hex}; @@ -20,7 +25,6 @@ use dpp::platform_value::string_encoding::encode; use dpp::platform_value::{Value, ValueMapHelper}; use dpp::util::entropy_generator; use dpp::util::entropy_generator::EntropyGenerator; -use dpp::version::PlatformVersion; use serde::Deserialize; use wasm_bindgen::JsValue; use wasm_bindgen::prelude::wasm_bindgen; @@ -105,7 +109,7 @@ pub struct DocumentWasm { pub(crate) document_type_name: String, #[serde( rename = "$entropy", - with = "serialization::bytes_b64::option", + with = "dpp::serialization::serde_bytes::option", skip_serializing_if = "Option::is_none", default )] @@ -485,10 +489,21 @@ impl DocumentWasm { } /// Convert to a JS object with binary fields as Uint8Array. + /// + /// Wire shape (Phase D step 8 slice B): + /// - `$formatVersion: "0"` — canonical Document version tag + /// - `$dataContractId`, `$type`, `$entropy` — wasm-side metadata + /// - V0 Document fields (`$id`, `$ownerId`, `$revision`, …) flat + /// alongside user-defined properties #[wasm_bindgen(js_name = "toObject")] pub fn to_object(&self) -> WasmDppResult { let mut map = self.document.to_map_value()?; - // Add metadata fields not in core Document + // Canonical Document version tag — matches `IdentityWasm.toObject` + // and every other rs-dpp type's canonical wire shape. Symmetric + // with `fromObject` which now uses canonical + // `Document::from_object` (requires the tag). + map.insert("$formatVersion".to_string(), Value::Text("0".to_string())); + // wasm-side metadata not in core Document let data_contract_id: Identifier = self.data_contract_id.into(); map.insert( "$dataContractId".to_string(), @@ -507,20 +522,20 @@ impl DocumentWasm { Ok(js_value.into()) } - /// Create a Document from a JS object. + /// Create a Document from a JS object (canonical-tagged shape). #[wasm_bindgen(js_name = "fromObject")] pub fn from_object( value: DocumentObjectJs, - platform_version: PlatformVersionLikeJs, + _platform_version: PlatformVersionLikeJs, ) -> WasmDppResult { - let platform_version: PlatformVersion = platform_version.try_into()?; let platform_value = serialization::js_value_to_platform_value(&value.into())?; let Value::Map(mut map) = platform_value else { return Err(WasmDppError::invalid_argument("Expected an object")); }; - // Extract metadata fields using ValueMapHelper trait methods + // Extract wasm-side metadata fields before passing to canonical + // `Document::from_object`. let data_contract_id = map .remove_optional_key("$dataContractId") .ok_or_else(|| WasmDppError::invalid_argument("Missing $dataContractId"))? @@ -545,8 +560,12 @@ impl DocumentWasm { }) }); - // Create Document from remaining fields - let document = Document::from_platform_value(Value::Map(map), &platform_version)?; + // Canonical `ValueConvertible::from_object` after Phase D step 8 + // slice B. The legacy `from_platform_value` accepted un-tagged + // shapes; with `toObject` now emitting `$formatVersion: "0"`, + // canonical handles round-trip cleanly. + use dpp::serialization::ValueConvertible; + let document = Document::from_object(Value::Map(map))?; Ok(DocumentWasm::new( document, @@ -560,11 +579,14 @@ impl DocumentWasm { #[wasm_bindgen(js_name = "toJSON")] pub fn to_json( &self, - platform_version: PlatformVersionLikeJs, + _platform_version: PlatformVersionLikeJs, ) -> WasmDppResult { - let platform_version: PlatformVersion = platform_version.try_into()?; - // Get document fields as JSON - let mut json_value = self.document.to_json(&platform_version)?; + // Canonical `JsonConvertible::to_json` after Phase D step 8 slice A. + // The legacy `to_json(&self, &PlatformVersion)` was a 1:1 canonical + // equivalent. `platform_version` stays in the JS API for SDK + // consistency. + use dpp::serialization::JsonConvertible; + let mut json_value = self.document.to_json()?; // Serialize wrapper fields using serde and merge into document JSON let wrapper_json = @@ -584,29 +606,32 @@ impl DocumentWasm { Ok(js_value.into()) } - /// Create a Document from a JSON object. + /// Create a Document from a JSON object (canonical-tagged shape). /// JSON format has identifiers as base58 strings. #[wasm_bindgen(js_name = "fromJSON")] pub fn from_json( value: DocumentJSONJs, - platform_version: PlatformVersionLikeJs, + _platform_version: PlatformVersionLikeJs, ) -> WasmDppResult { - let platform_version: PlatformVersion = platform_version.try_into()?; let mut json_value = serialization::js_value_to_json(&value.into())?; // Deserialize wrapper fields using serde let mut wrapper: DocumentWasm = serde_json::from_value(json_value.clone()) .map_err(|e| WasmDppError::serialization(e.to_string()))?; - // Remove wrapper fields from JSON before passing to Document::from_json_value + // Remove wrapper fields from JSON before passing to Document::from_json if let serde_json::Value::Object(ref mut obj) = json_value { obj.remove("$dataContractId"); obj.remove("$type"); obj.remove("$entropy"); } - // Create Document from remaining fields - wrapper.document = Document::from_json_value::(json_value, &platform_version)?; + // Canonical `JsonConvertible::from_json` after Phase D step 8 + // slice B. The wasm-dpp2 `toJSON` already emits canonical-tagged + // JSON (it routes through `Document::to_json` which carries + // `$formatVersion`), so the round-trip works through canonical. + use dpp::serialization::JsonConvertible; + wrapper.document = Document::from_json(json_value)?; Ok(wrapper) } diff --git a/packages/wasm-dpp2/src/data_contract/model.rs b/packages/wasm-dpp2/src/data_contract/model.rs index bc92665e7c8..8249d7d29f7 100644 --- a/packages/wasm-dpp2/src/data_contract/model.rs +++ b/packages/wasm-dpp2/src/data_contract/model.rs @@ -20,6 +20,7 @@ use dpp::data_contract::document_type::DocumentTypeRef; use dpp::data_contract::errors::DataContractError; use dpp::data_contract::group::Group; use dpp::data_contract::schema::DataContractSchemaMethodsV0; +use dpp::data_contract::serialized_version::DataContractInSerializationFormat; use dpp::data_contract::{ DataContract, GroupContractPosition, TokenConfiguration, TokenContractPosition, }; @@ -31,7 +32,7 @@ use dpp::serialization::{ PlatformDeserializableWithPotentialValidationFromVersionedStructure, PlatformSerializableWithPlatformVersion, }; -use dpp::version::PlatformVersion; +use dpp::version::{PlatformVersion, TryIntoPlatformVersioned}; use js_sys::{Object, Reflect}; use serde::Deserialize; use std::collections::BTreeMap; @@ -366,9 +367,12 @@ impl DataContractWasm { &self, #[wasm_bindgen(js_name = "platformVersion")] platform_version: PlatformVersionLikeJs, ) -> WasmDppResult { - let platform_version = PlatformVersionWasm::try_from(platform_version)?; - - let value = self.0.clone().to_value(&platform_version.into())?; + let pv: PlatformVersion = platform_version.try_into()?; + // Honor the caller-supplied platform version (see `to_json`). + let format: DataContractInSerializationFormat = + self.0.clone().try_into_platform_versioned(&pv)?; + let value = + dpp::platform_value::to_value(&format).map_err(dpp::ProtocolError::ValueError)?; let js_value = serialization::platform_value_to_object(&value)?; Ok(js_value.into()) } @@ -396,7 +400,16 @@ impl DataContractWasm { #[wasm_bindgen(getter = "config")] pub fn config(&self) -> WasmDppResult { - let js_value = serialization::to_object(self.0.config())?; + // DataContractConfig is a versioned enum with the canonical + // ValueConvertible trait — route through it so any future custom + // impl propagates here automatically. + use dpp::serialization::ValueConvertible; + let value = self + .0 + .config() + .to_object() + .map_err(|e| WasmDppError::serialization(format!("config: {}", e)))?; + let js_value = serialization::platform_value_to_object(&value)?; Ok(js_value.into()) } @@ -575,9 +588,16 @@ impl DataContractWasm { &self, platform_version: PlatformVersionLikeJs, ) -> WasmDppResult { - let platform_version = PlatformVersionWasm::try_from(platform_version)?; - - let json = self.0.to_json(&platform_version.into())?; + let pv: PlatformVersion = platform_version.try_into()?; + // Honor the caller-supplied platform version: route through the + // versioned serialization format so the wire shape (V0 vs V1) is + // controlled by the caller, not by global thread-local state used by + // the canonical `serde_json::to_value(&DataContract)` path. See + // Critical-4 note in `data_contract/conversion/serde/mod.rs`. + let format: DataContractInSerializationFormat = + self.0.clone().try_into_platform_versioned(&pv)?; + let json = serde_json::to_value(&format) + .map_err(|e| dpp::ProtocolError::EncodingError(e.to_string()))?; let js_value = serialization::json_value_to_js(&json)?; Ok(js_value.into()) } diff --git a/packages/wasm-dpp2/src/group/action.rs b/packages/wasm-dpp2/src/group/action.rs index 6909626c620..d72f22baf33 100644 --- a/packages/wasm-dpp2/src/group/action.rs +++ b/packages/wasm-dpp2/src/group/action.rs @@ -10,12 +10,15 @@ use wasm_bindgen::prelude::wasm_bindgen; const TS_TYPES: &str = r#" /** * GroupAction serialized as a plain object. + * + * Versioned enum tagged with `$formatVersion`. V0 fields use snake_case + * (rs-dpp's GroupActionV0 has no `rename_all` attribute). */ export interface GroupActionObject { - $formatVersion: string; - contractId: Uint8Array; - proposerId: Uint8Array; - tokenContractPosition: number; + $formatVersion: "0"; + contract_id: Uint8Array; + proposer_id: Uint8Array; + token_contract_position: number; event: GroupActionEventObject; } @@ -23,10 +26,10 @@ export interface GroupActionObject { * GroupAction serialized as JSON. */ export interface GroupActionJSON { - $formatVersion: string; - contractId: string; - proposerId: string; - tokenContractPosition: number; + $formatVersion: "0"; + contract_id: string; + proposer_id: string; + token_contract_position: number; event: GroupActionEventJSON; } "#; diff --git a/packages/wasm-dpp2/src/group/action_event.rs b/packages/wasm-dpp2/src/group/action_event.rs index dd40654e74b..dbebc8c5d60 100644 --- a/packages/wasm-dpp2/src/group/action_event.rs +++ b/packages/wasm-dpp2/src/group/action_event.rs @@ -8,19 +8,17 @@ use wasm_bindgen::prelude::wasm_bindgen; const TS_TYPES: &str = r#" /** * GroupActionEvent serialized as a plain object. + * + * Internally tagged with `$kind` (chosen over `$type` to avoid colliding with + * the inner TokenEvent's own `$type` discriminator). The inner TokenEvent + * fields flatten at the same level — both keys coexist. */ -export interface GroupActionEventObject { - type: "tokenEvent"; - data: TokenEventObject; -} +export type GroupActionEventObject = { $kind: "tokenEvent" } & TokenEventObject; /** * GroupActionEvent serialized as JSON. */ -export interface GroupActionEventJSON { - type: "tokenEvent"; - data: TokenEventJSON; -} +export type GroupActionEventJSON = { $kind: "tokenEvent" } & TokenEventJSON; "#; #[wasm_bindgen] diff --git a/packages/wasm-dpp2/src/group/token_event.rs b/packages/wasm-dpp2/src/group/token_event.rs index 1131f98c84a..a080db06bd4 100644 --- a/packages/wasm-dpp2/src/group/token_event.rs +++ b/packages/wasm-dpp2/src/group/token_event.rs @@ -7,18 +7,41 @@ use wasm_bindgen::prelude::wasm_bindgen; const TS_TYPES: &str = r#" /** * TokenEvent serialized as a plain object. + * + * Custom Serialize emits an internally-tagged flat shape: `$type:` is the + * variant discriminator, positional tuple fields are mapped to named JSON + * keys per variant. No `data` wrapper. + * + * Common per-variant payloads: + * - Mint: { $type: "mint", amount, recipient, publicNote } + * - Burn: { $type: "burn", amount, burnFromIdentifier, publicNote } + * - Freeze: { $type: "freeze", frozenIdentifier, publicNote } + * - Unfreeze:{ $type: "unfreeze",frozenIdentifier, publicNote } + * - DestroyFrozenFunds: { $type, frozenIdentifier, amount, publicNote } + * - Transfer:{ $type, recipient, publicNote, sharedEncryptedNote, + * personalEncryptedNote, amount } + * - Claim: { $type, distributionType, amount, publicNote } + * - EmergencyAction: { $type, emergencyAction, publicNote } + * - ConfigUpdate: { $type, configChange, publicNote } + * - ChangePriceForDirectPurchase: { $type, pricingSchedule, publicNote } + * - DirectPurchase: { $type, amount, credits } + * + * `amount`/`credits` are routed through json_safe_u64 — small numbers, JS + * BigInt-safe stringification above 2^53. Identifier fields use base58 in + * JSON, Uint8Array in toObject(). */ export interface TokenEventObject { - type: string; - data?: unknown; + $type: string; + [field: string]: unknown; } /** - * TokenEvent serialized as JSON. + * TokenEvent serialized as JSON. Same shape as TokenEventObject with + * Identifier fields rendered as base58 strings. */ export interface TokenEventJSON { - type: string; - data?: unknown; + $type: string; + [field: string]: unknown; } "#; diff --git a/packages/wasm-dpp2/src/identity/model.rs b/packages/wasm-dpp2/src/identity/model.rs index f97e28f0a04..fb9af6f6bf5 100644 --- a/packages/wasm-dpp2/src/identity/model.rs +++ b/packages/wasm-dpp2/src/identity/model.rs @@ -12,7 +12,9 @@ use dpp::identity::{Identity, KeyID}; use dpp::platform_value::string_encoding::Encoding::{Base64, Hex}; use dpp::platform_value::string_encoding::{decode, encode}; use dpp::prelude::Identifier; -use dpp::serialization::{PlatformDeserializable, PlatformSerializable, ValueConvertible}; +use dpp::serialization::{ + JsonConvertible, PlatformDeserializable, PlatformSerializable, ValueConvertible, +}; use dpp::version::{PlatformVersion, TryFromPlatformVersioned}; use wasm_bindgen::prelude::wasm_bindgen; @@ -175,24 +177,37 @@ impl IdentityWasm { #[wasm_bindgen(js_name = "toObject")] pub fn to_object(&self) -> WasmDppResult { - // Use platform_value conversion which handles BigInt for balance/revision - // and outputs id as Uint8Array, publicKeys as plain objects - let value = self.0.to_object()?; + let value = self + .0 + .to_object() + .map_err(|e| WasmDppError::serialization(format!("toObject: {}", e)))?; let js_value = serialization::platform_value_to_object(&value)?; Ok(js_value.into()) } #[wasm_bindgen(js_name = "toJSON")] pub fn to_json(&self) -> WasmDppResult { - let js_value = serialization::to_json(&self.0)?; + let json = self + .0 + .to_json() + .map_err(|e| WasmDppError::serialization(format!("toJSON: {}", e)))?; + let js_value = serialization::json_to_js_value(&json)?; Ok(js_value.into()) } #[wasm_bindgen(js_name = "fromJSON")] pub fn from_json(value: IdentityJSONJs) -> WasmDppResult { - serialization::from_json(value.into()).map(IdentityWasm) + let json = serialization::js_value_to_json(&value.into())?; + let identity = Identity::from_json(json) + .map_err(|e| WasmDppError::serialization(format!("fromJSON: {}", e)))?; + Ok(IdentityWasm(identity)) } + /// `fromObject` keeps the manual path because the wasm API dispatches on + /// the `platform_version` arg (via `try_from_platform_versioned`) rather + /// than the value's embedded `$formatVersion` tag — explicit version + /// coupling is the wasm SDK convention. The canonical + /// `ValueConvertible::from_object` would dispatch on the tag. #[wasm_bindgen(js_name = "fromObject")] pub fn from_object( value: IdentityObjectJs, diff --git a/packages/wasm-dpp2/src/identity/partial_identity.rs b/packages/wasm-dpp2/src/identity/partial_identity.rs index 8756afe41be..e889a240d47 100644 --- a/packages/wasm-dpp2/src/identity/partial_identity.rs +++ b/packages/wasm-dpp2/src/identity/partial_identity.rs @@ -9,11 +9,9 @@ use crate::utils::{ }; use crate::version::PlatformVersionLikeJs; use dpp::fee::Credits; -use dpp::identity::identity_public_key::conversion::json::IdentityPublicKeyJsonConversionMethodsV0; -use dpp::identity::identity_public_key::conversion::platform_value::IdentityPublicKeyPlatformValueConversionMethodsV0; use dpp::identity::{IdentityPublicKey, KeyID, PartialIdentity}; -use dpp::platform_value; use dpp::prelude::Revision; +use dpp::serialization::ValueConvertible; use dpp::version::PlatformVersion; use js_sys::{Array, Object, Reflect}; use std::collections::{BTreeMap, BTreeSet}; @@ -192,24 +190,37 @@ impl PartialIdentityWasm { #[wasm_bindgen(js_name = "toJSON")] pub fn to_json(&self) -> WasmDppResult { - // Convert to platform_value, which handles integer map keys by converting to strings - let value = platform_value::to_value(&self.0) + // Route through the canonical ValueConvertible to preserve typed + // map keys / bytes through the platform_value tree, then convert + // to a JS-friendly JSON value (Identifier -> base58, bytes -> base64). + let value = self + .0 + .to_object() .map_err(|e| WasmDppError::serialization(format!("toJSON: {}", e)))?; - // platform_value_to_json converts Identifier to base58, bytes to base64 let js_value = serialization::platform_value_to_json(&value)?; Ok(js_value.into()) } #[wasm_bindgen(js_name = "toObject")] pub fn to_object(&self) -> WasmDppResult { - // Convert to platform_value, which handles integer map keys by converting to strings - let value = platform_value::to_value(&self.0) + // Route through the canonical ValueConvertible. Bytes stay as + // Uint8Array, u64 as BigInt at the JS boundary. + let value = self + .0 + .to_object() .map_err(|e| WasmDppError::serialization(format!("toObject: {}", e)))?; - // platform_value_to_object keeps bytes as Uint8Array, u64 as BigInt let js_value = serialization::platform_value_to_object(&value)?; Ok(js_value.into()) } + // KEEP-AS-MANUAL: the `from*` pair reconstructs field-by-field instead of + // delegating to canonical `ValueConvertible::from_object` because it + // accepts lenient JS input shapes (`id` as Uint8Array | base58 string | + // Identifier instance; `balance`/`revision` as BigInt | number | string; + // optional fields omitted rather than null). The only structured leaf + // (`IdentityPublicKey`) DOES go through the canonical traits below. The + // `platform_version` arg is API-consistency only (see IdentityPublicKey's + // own docs) — not load-bearing for dispatch. #[wasm_bindgen(js_name = "fromObject")] pub fn from_object( obj: PartialIdentityObjectJs, @@ -347,7 +358,7 @@ pub fn option_array_to_not_found( /// Parse loaded public keys from an object (where values are plain objects from toObject) pub fn value_to_loaded_public_keys_from_object( loaded_public_keys: &JsValue, - platform_version: &PlatformVersion, + _platform_version: &PlatformVersion, ) -> WasmDppResult> { let mut map = BTreeMap::new(); let pub_keys_object = try_to_object(loaded_public_keys.clone(), "loadedPublicKeys")?; @@ -379,8 +390,15 @@ pub fn value_to_loaded_public_keys_from_object( })?; let platform_value = serialization::platform_value_from_object(&js_key)?; - let pub_key = IdentityPublicKey::from_object(platform_value, platform_version) - .map_err(WasmDppError::from)?; + // Canonical `ValueConvertible::from_object` dispatches on the + // value's `$formatVersion` tag (the outer `IdentityPublicKey` + // enum is internally tagged). The legacy version-aware form + // produced identical output for V0 (the only structure version + // currently defined). + let pub_key = ::from_object( + platform_value, + ) + .map_err(WasmDppError::from)?; map.insert(key_id, pub_key); } @@ -390,7 +408,7 @@ pub fn value_to_loaded_public_keys_from_object( /// Parse loaded public keys from JSON (where values are JSON objects from toJSON) pub fn value_to_loaded_public_keys_from_json( loaded_public_keys: &JsValue, - platform_version: &PlatformVersion, + _platform_version: &PlatformVersion, ) -> WasmDppResult> { let mut map = BTreeMap::new(); let pub_keys_object = try_to_object(loaded_public_keys.clone(), "loadedPublicKeys")?; @@ -425,8 +443,9 @@ pub fn value_to_loaded_public_keys_from_json( serde_wasm_bindgen::from_value(js_key).map_err(|e| { WasmDppError::serialization(format!("IdentityPublicKey fromJSON: {}", e)) })?; - let pub_key = IdentityPublicKey::from_json_object(json_value, platform_version) - .map_err(WasmDppError::from)?; + // Canonical JsonConvertible — base64 strings for binary fields. + use dpp::serialization::JsonConvertible; + let pub_key = IdentityPublicKey::from_json(json_value).map_err(WasmDppError::from)?; map.insert(key_id, pub_key); } diff --git a/packages/wasm-dpp2/src/identity/public_key.rs b/packages/wasm-dpp2/src/identity/public_key.rs index 0486d26d569..264b1bdc73a 100644 --- a/packages/wasm-dpp2/src/identity/public_key.rs +++ b/packages/wasm-dpp2/src/identity/public_key.rs @@ -20,15 +20,12 @@ use dpp::identity::hash::IdentityPublicKeyHashMethodsV0; use dpp::identity::identity_public_key::accessors::v0::{ IdentityPublicKeyGettersV0, IdentityPublicKeySettersV0, }; -use dpp::identity::identity_public_key::conversion::json::IdentityPublicKeyJsonConversionMethodsV0; -use dpp::identity::identity_public_key::conversion::platform_value::IdentityPublicKeyPlatformValueConversionMethodsV0; use dpp::identity::identity_public_key::v0::IdentityPublicKeyV0; use dpp::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel, TimestampMillis}; use dpp::platform_value::BinaryData; use dpp::platform_value::string_encoding::Encoding::{Base64, Hex}; use dpp::platform_value::string_encoding::{decode, encode}; use dpp::serialization::{PlatformDeserializable, PlatformSerializable}; -use dpp::version::PlatformVersion; use hex; use serde::Deserialize; use serde_json::Value as JsonValue; @@ -391,11 +388,17 @@ impl IdentityPublicKeyWasm { /// Serialize to JS object (non-human-readable). /// - /// Uses platform_value conversion which properly handles the tagged enum - /// and removes None fields like disabledAt. + /// Uses platform_value conversion which properly handles the tagged enum. + /// `disabledAt: null` is stripped automatically by the + /// `skip_serializing_if` attribute on the rs-dpp side. #[wasm_bindgen(js_name = "toObject")] pub fn to_object(&self) -> WasmDppResult { - let value = self.0.to_cleaned_object().map_err(WasmDppError::from)?; + // Disambiguate: both canonical `ValueConvertible::to_object` and the + // legacy `IdentityPublicKeyPlatformValueConversionMethodsV0::to_object` + // are in scope. The canonical one produces the same shape — explicit + // call so we route through it. + use dpp::serialization::ValueConvertible; + let value = ValueConvertible::to_object(&self.0).map_err(WasmDppError::from)?; let js_value = serialization::platform_value_to_object(&value)?; Ok(js_value.into()) } @@ -403,40 +406,50 @@ impl IdentityPublicKeyWasm { /// Deserialize from JS object (non-human-readable). /// /// Uses platform_value conversion which properly handles the tagged enum. + /// `platform_version` is accepted for SDK API consistency but not + /// load-bearing today — canonical `ValueConvertible::from_object` + /// dispatches on the value's `$formatVersion` tag, which produces + /// identical output for the only currently-defined V0. #[wasm_bindgen(js_name = "fromObject")] pub fn from_object( value: IdentityPublicKeyObjectJs, - platform_version: PlatformVersionLikeJs, + _platform_version: PlatformVersionLikeJs, ) -> WasmDppResult { - let platform_version: PlatformVersion = platform_version.try_into()?; let value: JsValue = value.into(); let platform_value = serialization::platform_value_from_object(&value)?; - let key = IdentityPublicKey::from_object(platform_value, &platform_version) - .map_err(WasmDppError::from)?; + let key = ::from_object( + platform_value, + ) + .map_err(WasmDppError::from)?; Ok(IdentityPublicKeyWasm(key)) } - /// Serialize to JSON-compatible JS object (human-readable). + /// Serialize to JSON-compatible JS object (canonical wire shape). /// - /// Uses serde_json conversion which properly handles the tagged enum - /// and serializes binary data as base64 strings. + /// Binary fields render as base64 strings. Identifier fields render + /// as base58 strings. This matches the canonical `JsonConvertible` + /// path used by every other rs-dpp type's JSON conversion in this + /// SDK — including `IdentityWasm.toJSON`'s embedded public keys. #[wasm_bindgen(js_name = "toJSON")] pub fn to_json(&self) -> WasmDppResult { - let json_value = self.0.to_json_object().map_err(WasmDppError::from)?; - let js_value = serialization::json_value_to_js(&json_value)?; + use dpp::serialization::JsonConvertible; + let json_value = self.0.to_json().map_err(WasmDppError::from)?; + let js_value = serialization::json_to_js_value(&json_value)?; Ok(js_value.into()) } - /// Deserialize from JSON-compatible JS object (human-readable). + /// Deserialize from JSON-compatible JS object (canonical wire shape). /// - /// Uses serde_json conversion which properly handles the tagged enum - /// and deserializes base64 strings to binary data. + /// Expects base64 strings for binary fields, base58 strings for + /// identifiers — the canonical shape produced by `toJSON`. + /// `platform_version` is accepted for SDK API consistency but not + /// load-bearing today (canonical tag-driven dispatch handles V0). #[wasm_bindgen(js_name = "fromJSON")] pub fn from_json( value: IdentityPublicKeyJSONJs, - platform_version: PlatformVersionLikeJs, + _platform_version: PlatformVersionLikeJs, ) -> WasmDppResult { - let platform_version: PlatformVersion = platform_version.try_into()?; + use dpp::serialization::JsonConvertible; let json_value: JsonValue = serde_from_value(value.into()).map_err(|err| { WasmDppError::serialization(format!( "IdentityPublicKey.fromJSON: unable to parse JSON: {}", @@ -444,8 +457,7 @@ impl IdentityPublicKeyWasm { )) })?; - let key = IdentityPublicKey::from_json_object(json_value, &platform_version) - .map_err(WasmDppError::from)?; + let key = IdentityPublicKey::from_json(json_value).map_err(WasmDppError::from)?; Ok(IdentityPublicKeyWasm(key)) } diff --git a/packages/wasm-dpp2/src/identity/transitions/pooling.rs b/packages/wasm-dpp2/src/identity/transitions/pooling.rs index 3e5923c6bf5..e676d0c9ead 100644 --- a/packages/wasm-dpp2/src/identity/transitions/pooling.rs +++ b/packages/wasm-dpp2/src/identity/transitions/pooling.rs @@ -50,37 +50,11 @@ impl<'de> Deserialize<'de> for PoolingWasm { where D: Deserializer<'de>, { - let value: serde_json::Value = Deserialize::deserialize(deserializer)?; - - // Try as string first - if let Some(s) = value.as_str() { - return match s.to_lowercase().as_str() { - "never" => Ok(PoolingWasm::Never), - "ifavailable" => Ok(PoolingWasm::IfAvailable), - "standard" => Ok(PoolingWasm::Standard), - _ => Err(serde::de::Error::custom(format!( - "unsupported pooling value ({})", - s - ))), - }; - } - - // Try as number - if let Some(n) = value.as_u64() { - return match n { - 0 => Ok(PoolingWasm::Never), - 1 => Ok(PoolingWasm::IfAvailable), - 2 => Ok(PoolingWasm::Standard), - _ => Err(serde::de::Error::custom(format!( - "unsupported pooling value ({})", - n - ))), - }; - } - - Err(serde::de::Error::custom( - "pooling must be a string or number", - )) + // Delegate to the canonical helper in dpp — already accepts both + // string variants ("never" / "ifAvailable" / "standard", with + // various capitalizations) and the numeric discriminant (0 / 1 / 2). + let pooling = dpp::withdrawal::pooling_serde::deserialize(deserializer)?; + Ok(pooling.into()) } } diff --git a/packages/wasm-dpp2/src/platform_address/address.rs b/packages/wasm-dpp2/src/platform_address/address.rs index 55dc3ee3beb..be7d63dab3b 100644 --- a/packages/wasm-dpp2/src/platform_address/address.rs +++ b/packages/wasm-dpp2/src/platform_address/address.rs @@ -203,6 +203,16 @@ impl<'de> Visitor<'de> for PlatformAddressWasmVisitor { where E: de::Error, { + // Same 21-byte guard as every public entry point (see the Uint8Array + // branch in `TryFrom` for the consensus rationale): bincode + // decode tolerates trailing bytes, so over-length input would be + // silently truncated without this. + if value.len() != 21 { + return Err(E::custom(format!( + "PlatformAddress must be exactly 21 bytes, got {}", + value.len() + ))); + } PlatformAddress::from_bytes(value) .map(PlatformAddressWasm) .map_err(|e| E::custom(e.to_string())) @@ -216,6 +226,13 @@ impl<'de> Visitor<'de> for PlatformAddressWasmVisitor { while let Some(byte) = seq.next_element::()? { bytes.push(byte); } + // Same 21-byte guard as `visit_bytes` above. + if bytes.len() != 21 { + return Err(A::Error::custom(format!( + "PlatformAddress must be exactly 21 bytes, got {}", + bytes.len() + ))); + } PlatformAddress::from_bytes(&bytes) .map(PlatformAddressWasm) .map_err(|e| A::Error::custom(e.to_string())) diff --git a/packages/wasm-dpp2/src/platform_address/fee_strategy.rs b/packages/wasm-dpp2/src/platform_address/fee_strategy.rs index 938f1e2e6cb..23b3d96fc8d 100644 --- a/packages/wasm-dpp2/src/platform_address/fee_strategy.rs +++ b/packages/wasm-dpp2/src/platform_address/fee_strategy.rs @@ -15,8 +15,8 @@ const FEE_STRATEGY_STEP_TS_TYPES: &str = r#" * `index` selects the input/output position. */ export type FeeStrategyStepObject = - | { type: "deductFromInput"; index: number } - | { type: "reduceOutput"; index: number }; + | { $type: "deductFromInput"; index: number } + | { $type: "reduceOutput"; index: number }; /** * Fee strategy step in JSON form (output of a transition's `toJSON()`). @@ -26,8 +26,8 @@ export type FeeStrategyStepObject = * human-readable formats. */ export type FeeStrategyStepJSON = - | { type: "deductFromInput"; index: number } - | { type: "reduceOutput"; index: number }; + | { $type: "deductFromInput"; index: number } + | { $type: "reduceOutput"; index: number }; "#; /// Defines how fees are paid in address-based state transitions. @@ -36,7 +36,7 @@ export type FeeStrategyStepJSON = /// should be reduced to cover the transaction fee. /// /// `#[serde(transparent)]` delegates to the inner `AddressFundsFeeStrategyStep`'s -/// custom serde, which produces the `{ type, index }` adjacent shape used by +/// custom serde, which produces the `{ $type, index }` adjacent shape used by /// every wasm-sdk consumer that round-trips a `Vec`. #[wasm_bindgen(js_name = "FeeStrategyStep")] #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/packages/wasm-dpp2/src/platform_address/transitions/address_credit_withdrawal_transition.rs b/packages/wasm-dpp2/src/platform_address/transitions/address_credit_withdrawal_transition.rs index a59a8bdaf25..a43b86f8bdd 100644 --- a/packages/wasm-dpp2/src/platform_address/transitions/address_credit_withdrawal_transition.rs +++ b/packages/wasm-dpp2/src/platform_address/transitions/address_credit_withdrawal_transition.rs @@ -1,7 +1,7 @@ use crate::core::core_script::CoreScriptWasm; use crate::error::{WasmDppError, WasmDppResult}; use crate::identity::transitions::pooling::{PoolingLikeJs, PoolingWasm}; -use crate::impl_wasm_conversions_serde; +use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; use crate::platform_address::{ PlatformAddressInputWasm, PlatformAddressOutputWasm, fee_strategy_from_js_options, @@ -292,9 +292,10 @@ impl AddressCreditWithdrawalTransitionWasm { } } -impl_wasm_conversions_serde!( +impl_wasm_conversions_inner!( AddressCreditWithdrawalTransitionWasm, AddressCreditWithdrawalTransition, + AddressCreditWithdrawalTransition, AddressCreditWithdrawalTransitionObjectJs, AddressCreditWithdrawalTransitionJSONJs ); diff --git a/packages/wasm-dpp2/src/platform_address/transitions/address_funding_from_asset_lock_transition.rs b/packages/wasm-dpp2/src/platform_address/transitions/address_funding_from_asset_lock_transition.rs index 4236aa278bf..aa4388276da 100644 --- a/packages/wasm-dpp2/src/platform_address/transitions/address_funding_from_asset_lock_transition.rs +++ b/packages/wasm-dpp2/src/platform_address/transitions/address_funding_from_asset_lock_transition.rs @@ -1,6 +1,6 @@ use crate::asset_lock_proof::AssetLockProofWasm; use crate::error::{WasmDppError, WasmDppResult}; -use crate::impl_wasm_conversions_serde; +use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; use crate::platform_address::{ PlatformAddressInputWasm, PlatformAddressOutputWasm, fee_strategy_from_js_options, @@ -230,9 +230,10 @@ impl AddressFundingFromAssetLockTransitionWasm { } } -impl_wasm_conversions_serde!( +impl_wasm_conversions_inner!( AddressFundingFromAssetLockTransitionWasm, AddressFundingFromAssetLockTransition, + AddressFundingFromAssetLockTransition, AddressFundingFromAssetLockTransitionObjectJs, AddressFundingFromAssetLockTransitionJSONJs ); diff --git a/packages/wasm-dpp2/src/platform_address/transitions/address_funds_transfer_transition.rs b/packages/wasm-dpp2/src/platform_address/transitions/address_funds_transfer_transition.rs index 879df4477f1..e46d65078ff 100644 --- a/packages/wasm-dpp2/src/platform_address/transitions/address_funds_transfer_transition.rs +++ b/packages/wasm-dpp2/src/platform_address/transitions/address_funds_transfer_transition.rs @@ -1,5 +1,5 @@ use crate::error::{WasmDppError, WasmDppResult}; -use crate::impl_wasm_conversions_serde; +use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; use crate::platform_address::{ PlatformAddressInputWasm, PlatformAddressOutputWasm, fee_strategy_from_js_options, @@ -217,9 +217,10 @@ impl AddressFundsTransferTransitionWasm { } } -impl_wasm_conversions_serde!( +impl_wasm_conversions_inner!( AddressFundsTransferTransitionWasm, AddressFundsTransferTransition, + AddressFundsTransferTransition, AddressFundsTransferTransitionObjectJs, AddressFundsTransferTransitionJSONJs ); diff --git a/packages/wasm-dpp2/src/platform_address/transitions/identity_create_from_addresses_transition.rs b/packages/wasm-dpp2/src/platform_address/transitions/identity_create_from_addresses_transition.rs index dcbd403468d..42b47e95a80 100644 --- a/packages/wasm-dpp2/src/platform_address/transitions/identity_create_from_addresses_transition.rs +++ b/packages/wasm-dpp2/src/platform_address/transitions/identity_create_from_addresses_transition.rs @@ -1,6 +1,6 @@ use crate::error::{WasmDppError, WasmDppResult}; use crate::identity::transitions::public_key_in_creation::IdentityPublicKeyInCreationWasm; -use crate::impl_wasm_conversions_serde; +use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; use crate::platform_address::{ PlatformAddressInputWasm, PlatformAddressOutputWasm, fee_strategy_from_js_options, @@ -258,9 +258,10 @@ impl IdentityCreateFromAddressesTransitionWasm { } } -impl_wasm_conversions_serde!( +impl_wasm_conversions_inner!( IdentityCreateFromAddressesTransitionWasm, IdentityCreateFromAddressesTransition, + IdentityCreateFromAddressesTransition, IdentityCreateFromAddressesTransitionObjectJs, IdentityCreateFromAddressesTransitionJSONJs ); diff --git a/packages/wasm-dpp2/src/platform_address/transitions/identity_credit_transfer_to_addresses_transition.rs b/packages/wasm-dpp2/src/platform_address/transitions/identity_credit_transfer_to_addresses_transition.rs index 895d2efedbc..bc093c73cd7 100644 --- a/packages/wasm-dpp2/src/platform_address/transitions/identity_credit_transfer_to_addresses_transition.rs +++ b/packages/wasm-dpp2/src/platform_address/transitions/identity_credit_transfer_to_addresses_transition.rs @@ -1,6 +1,6 @@ use crate::error::{WasmDppError, WasmDppResult}; use crate::identifier::{IdentifierLikeJs, IdentifierWasm}; -use crate::impl_wasm_conversions_serde; +use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; use crate::platform_address::{ PlatformAddressOutputWasm, outputs_from_js_options, outputs_to_btree_map, @@ -262,8 +262,9 @@ impl IdentityCreditTransferToAddressesTransitionWasm { } } -impl_wasm_conversions_serde!( +impl_wasm_conversions_inner!( IdentityCreditTransferToAddressesTransitionWasm, + IdentityCreditTransferToAddressesTransition, IdentityCreditTransferToAddresses, IdentityCreditTransferToAddressesObjectJs, IdentityCreditTransferToAddressesJSONJs diff --git a/packages/wasm-dpp2/src/platform_address/transitions/identity_top_up_from_addresses_transition.rs b/packages/wasm-dpp2/src/platform_address/transitions/identity_top_up_from_addresses_transition.rs index 7ad4d701822..dc3a299a78b 100644 --- a/packages/wasm-dpp2/src/platform_address/transitions/identity_top_up_from_addresses_transition.rs +++ b/packages/wasm-dpp2/src/platform_address/transitions/identity_top_up_from_addresses_transition.rs @@ -1,6 +1,6 @@ use crate::error::{WasmDppError, WasmDppResult}; use crate::identifier::{IdentifierLikeJs, IdentifierWasm}; -use crate::impl_wasm_conversions_serde; +use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; use crate::platform_address::{ PlatformAddressInputWasm, PlatformAddressOutputWasm, fee_strategy_from_js_options, @@ -243,9 +243,10 @@ impl IdentityTopUpFromAddressesTransitionWasm { } } -impl_wasm_conversions_serde!( +impl_wasm_conversions_inner!( IdentityTopUpFromAddressesTransitionWasm, IdentityTopUpFromAddressesTransition, + IdentityTopUpFromAddressesTransition, IdentityTopUpFromAddressesTransitionObjectJs, IdentityTopUpFromAddressesTransitionJSONJs ); diff --git a/packages/wasm-dpp2/src/serialization/bytes_b64.rs b/packages/wasm-dpp2/src/serialization/bytes_b64.rs deleted file mode 100644 index b90ebe79966..00000000000 --- a/packages/wasm-dpp2/src/serialization/bytes_b64.rs +++ /dev/null @@ -1,116 +0,0 @@ -use dpp::platform_value::string_encoding::{Encoding, decode, encode}; -use serde::de::{self, Visitor}; -use serde::{Deserialize, Deserializer, Serializer}; -use std::fmt; - -pub fn serialize(bytes: &[u8], serializer: S) -> Result -where - S: Serializer, -{ - if serializer.is_human_readable() { - // JSON, YAML, etc. → Base64 string - let s = encode(bytes, Encoding::Base64); - serializer.serialize_str(&s) - } else { - // Binary / wasm / serde_wasm_bindgen → real bytes - serializer.serialize_bytes(bytes) - } -} - -pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - if deserializer.is_human_readable() { - // Expect Base64 string in JSON - let s = String::deserialize(deserializer)?; - decode(&s, Encoding::Base64).map_err(de::Error::custom) - } else { - // Expect bytes for binary formats / serde_wasm_bindgen - struct BytesVisitor; - - impl<'de> Visitor<'de> for BytesVisitor { - type Value = Vec; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("bytes") - } - - fn visit_bytes(self, v: &[u8]) -> Result - where - E: de::Error, - { - Ok(v.to_vec()) - } - } - - deserializer.deserialize_bytes(BytesVisitor) - } -} - -/// Generic serde helper for `Option<[u8; N]>` as base64 -pub mod option { - use super::*; - - pub fn serialize( - value: &Option<[u8; N]>, - serializer: S, - ) -> Result - where - S: Serializer, - { - match value { - Some(bytes) => super::serialize(bytes.as_slice(), serializer), - None => serializer.serialize_none(), - } - } - - pub fn deserialize<'de, D, const N: usize>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - struct OptionVisitor; - - impl<'de, const N: usize> Visitor<'de> for OptionVisitor { - type Value = Option<[u8; N]>; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "optional {} bytes", N) - } - - fn visit_none(self) -> Result - where - E: de::Error, - { - Ok(None) - } - - fn visit_some(self, deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let bytes = super::deserialize(deserializer)?; - if bytes.len() == N { - let mut arr = [0u8; N]; - arr.copy_from_slice(&bytes); - Ok(Some(arr)) - } else { - Err(de::Error::custom(format!( - "expected {} bytes, got {}", - N, - bytes.len() - ))) - } - } - - fn visit_unit(self) -> Result - where - E: de::Error, - { - Ok(None) - } - } - - deserializer.deserialize_option(OptionVisitor::) - } -} diff --git a/packages/wasm-dpp2/src/serialization/conversions.rs b/packages/wasm-dpp2/src/serialization/conversions.rs index b7499672066..6c1063f81d1 100644 --- a/packages/wasm-dpp2/src/serialization/conversions.rs +++ b/packages/wasm-dpp2/src/serialization/conversions.rs @@ -309,16 +309,71 @@ pub fn from_json(value: JsValue) -> WasmDppResult { /// Uses serialize_maps_as_objects(true) to ensure objects are plain JS objects. /// Uses `serialize_bytes_as_arrays(false)` so bytes become Uint8Array (expected by JS API). /// Uses `serialize_large_number_types_as_bigints(true)` for u64/i64 -> BigInt. +/// +/// Pre-walks the tree to stringify non-Text map keys. JS plain objects require +/// string keys, but rs-dpp's `MapKeySerializer` (since the typed-key fix in +/// commit ec43a2a4e2) preserves the source variant — so a `BTreeMap` +/// emits `Value::U32` keys, a `BTreeMap` emits `Value::Identifier` +/// keys, etc. Without this normalization, `serde_wasm_bindgen` fails with +/// "Map key is not a string and cannot be an object key" on round-trip +/// for types like `PartialIdentity::loaded_public_keys` (`BTreeMap`). pub fn platform_value_to_object(value: &platform_value::Value) -> WasmDppResult { + let normalized = stringify_map_keys_for_object(value); let serializer = serde_wasm_bindgen::Serializer::new() .serialize_maps_as_objects(true) .serialize_bytes_as_arrays(false) .serialize_large_number_types_as_bigints(true); - value + normalized .serialize(&serializer) .map_err(|e| WasmDppError::serialization(format!("platform_value_to_object: {}", e))) } +/// Recursively normalize a `Value` tree so that all `Map` keys are `Value::Text`. +/// Identifiers become base58 strings (matches the `Identifier` JSON convention), +/// bytes become base64, integers and bools use their `Display` form. Other +/// variants pass through (will fail downstream if they end up as a map key). +fn stringify_map_keys_for_object(value: &platform_value::Value) -> platform_value::Value { + use dpp::platform_value::Value; + match value { + Value::Map(entries) => Value::Map( + entries + .iter() + .map(|(k, v)| (stringify_key(k), stringify_map_keys_for_object(v))) + .collect(), + ), + Value::Array(items) => { + Value::Array(items.iter().map(stringify_map_keys_for_object).collect()) + } + other => other.clone(), + } +} + +fn stringify_key(key: &platform_value::Value) -> platform_value::Value { + use dpp::platform_value::Value; + use dpp::platform_value::string_encoding::{Encoding, encode}; + match key { + Value::Text(_) => key.clone(), + Value::U8(n) => Value::Text(n.to_string()), + Value::U16(n) => Value::Text(n.to_string()), + Value::U32(n) => Value::Text(n.to_string()), + Value::U64(n) => Value::Text(n.to_string()), + Value::I8(n) => Value::Text(n.to_string()), + Value::I16(n) => Value::Text(n.to_string()), + Value::I32(n) => Value::Text(n.to_string()), + Value::I64(n) => Value::Text(n.to_string()), + Value::Bool(b) => Value::Text(b.to_string()), + Value::Identifier(bytes) => Value::Text(encode(bytes, Encoding::Base58)), + Value::Bytes(bytes) => Value::Text(encode(bytes, Encoding::Base64)), + Value::Bytes20(bytes) => Value::Text(encode(bytes, Encoding::Base64)), + Value::Bytes32(bytes) => Value::Text(encode(bytes, Encoding::Base64)), + Value::Bytes36(bytes) => Value::Text(encode(bytes, Encoding::Base64)), + // Float / Null / Array / Map / Tag / EnumU8 fall through to the + // serializer, which will surface a clear error if they ever appear + // as a map key (none of the rs-dpp domain types use them this way). + other => other.clone(), + } +} + /// Serialize platform_value::Value to JsValue as JSON-compatible (human-readable). /// /// Converts Value::Identifier and Value::Bytes to base58/base64 strings for JSON compatibility. @@ -355,7 +410,14 @@ fn convert_value_for_json(value: &platform_value::Value) -> platform_value::Valu } platform_value::Value::Map(map) => platform_value::Value::Map( map.iter() - .map(|(k, v)| (convert_value_for_json(k), convert_value_for_json(v))) + .map(|(k, v)| { + // Map keys must be strings on the JSON side. Stringify + // any non-Text variants (u32 KeyID, Identifier, bytes, + // etc.) per the same convention as + // `platform_value_to_object`. Then descend into the + // value to convert nested binary types. + (stringify_key(k), convert_value_for_json(v)) + }) .collect(), ), platform_value::Value::Array(arr) => { @@ -616,13 +678,16 @@ macro_rules! impl_wasm_conversions_inner { } /// Macro to implement `toObject`, `fromObject`, `toJSON`, and `fromJSON` methods -/// for a wasm_bindgen type that implements `Serialize` and `DeserializeOwned`. +/// for a wasm-only DTO that has `#[derive(Serialize, Deserialize)]` directly. /// -/// Serializes the wasm wrapper directly via **serde** (`#[serde(transparent)]`). -/// Use this as a fallback when the inner type does NOT have -/// `JsonConvertible + ValueConvertible` trait impls. +/// Use this for wasm wrappers that decompose an rs-dpp enum variant into a +/// named-field struct (e.g., spreading `StateTransitionProofResult::Verified...` +/// tuple variants into per-variant JS classes), where there is no rs-dpp +/// domain type to delegate to. /// -/// Prefer [`impl_wasm_conversions_inner!`] when trait impls are available. +/// For wasm wrappers around an rs-dpp domain type that has +/// `JsonConvertible + ValueConvertible`, use [`impl_wasm_conversions_inner!`] +/// instead — it delegates to the canonical traits and is the preferred path. /// /// # Usage /// diff --git a/packages/wasm-dpp2/src/serialization/mod.rs b/packages/wasm-dpp2/src/serialization/mod.rs index 714803fccae..6a866c8bd38 100644 --- a/packages/wasm-dpp2/src/serialization/mod.rs +++ b/packages/wasm-dpp2/src/serialization/mod.rs @@ -1,10 +1,14 @@ //! Serialization utilities for WASM bindings. //! //! This module contains: -//! - `bytes_b64`: Serde helpers for bytes that serialize as Base64 in human-readable formats //! - `conversions`: Format-aware conversion helpers between Rust/JS/JSON representations +//! +//! For bytes serde helpers (base64 in human-readable, raw bytes in binary), +//! use the canonical helpers from rs-dpp: +//! - `dpp::serialization::serde_bytes` — for `[u8; N]` (and `Option<[u8; N]>` +//! via `dpp::serialization::serde_bytes::option`) +//! - `dpp::serialization::serde_bytes_var` — for `Vec` -pub mod bytes_b64; pub mod conversions; // Re-export commonly used items from conversions diff --git a/packages/wasm-dpp2/src/shielded/address_witness.rs b/packages/wasm-dpp2/src/shielded/address_witness.rs index b782cc04874..82271081574 100644 --- a/packages/wasm-dpp2/src/shielded/address_witness.rs +++ b/packages/wasm-dpp2/src/shielded/address_witness.rs @@ -1,5 +1,6 @@ use crate::error::{WasmDppError, WasmDppResult}; use crate::impl_try_from_js_value; +use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; use crate::utils::{IntoWasm, try_from_options_with, try_to_array}; use dpp::address_funds::AddressWitness; @@ -13,7 +14,7 @@ const ADDRESS_WITNESS_TS_TYPES: &str = r#" * Address witness for P2PKH spending in Object form. */ export interface AddressWitnessP2pkhObject { - type: "p2pkh"; + $type: "p2pkh"; signature: Uint8Array; } @@ -21,7 +22,7 @@ export interface AddressWitnessP2pkhObject { * Address witness for P2SH spending in Object form. */ export interface AddressWitnessP2shObject { - type: "p2sh"; + $type: "p2sh"; signatures: Uint8Array[]; redeemScript: Uint8Array; } @@ -35,7 +36,7 @@ export type AddressWitnessObject = AddressWitnessP2pkhObject | AddressWitnessP2s * Address witness for P2PKH spending in JSON form. */ export interface AddressWitnessP2pkhJSON { - type: "p2pkh"; + $type: "p2pkh"; signature: string; } @@ -43,7 +44,7 @@ export interface AddressWitnessP2pkhJSON { * Address witness for P2SH spending in JSON form. */ export interface AddressWitnessP2shJSON { - type: "p2sh"; + $type: "p2sh"; signatures: string[]; redeemScript: string; } @@ -54,6 +55,15 @@ export interface AddressWitnessP2shJSON { export type AddressWitnessJSON = AddressWitnessP2pkhJSON | AddressWitnessP2shJSON; "#; +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "AddressWitnessObject")] + pub type AddressWitnessObjectJs; + + #[wasm_bindgen(typescript_type = "AddressWitnessJSON")] + pub type AddressWitnessJSONJs; +} + /// The input witness data required to spend from a PlatformAddress. /// /// Captures the different spending patterns for P2PKH (recoverable signature only) @@ -169,6 +179,14 @@ impl AddressWitnessWasm { impl_try_from_js_value!(AddressWitnessWasm, "AddressWitness"); impl_wasm_type_info!(AddressWitnessWasm, AddressWitness); +impl_wasm_conversions_inner!( + AddressWitnessWasm, + AddressWitness, + AddressWitness, + AddressWitnessObjectJs, + AddressWitnessJSONJs +); + /// Extract a `Vec` from a JS options object property. /// /// Reads the named property as a JS array, then extracts each element as an diff --git a/packages/wasm-dpp2/src/shielded/identity_create_from_shielded_pool_transition.rs b/packages/wasm-dpp2/src/shielded/identity_create_from_shielded_pool_transition.rs new file mode 100644 index 00000000000..2965d9cfb54 --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/identity_create_from_shielded_pool_transition.rs @@ -0,0 +1,286 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::identifier::IdentifierWasm; +use crate::identity::transitions::public_key_in_creation::IdentityPublicKeyInCreationWasm; +use crate::platform_address::PlatformAddressWasm; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::utils::try_vec_to_fixed_bytes; +use crate::utils::{try_from_options, try_from_options_with, try_to_array}; +use crate::{impl_wasm_conversions_inner, impl_wasm_type_info}; +use dpp::serialization::{PlatformDeserializable, PlatformSerializable}; +use dpp::state_transition::identity_create_from_shielded_pool_transition::IdentityCreateFromShieldedPoolTransition; +use dpp::state_transition::identity_create_from_shielded_pool_transition::derive_identity_id_from_actions; +use dpp::state_transition::identity_create_from_shielded_pool_transition::v0::IdentityCreateFromShieldedPoolTransitionV0; +use dpp::state_transition::{StateTransition, StateTransitionLike}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &str = r#" +/** + * Options for constructing an IdentityCreateFromShieldedPoolTransition. + * Uses WASM instance types for complex fields. The new identity's id is + * derived from the action nullifiers, so it is not part of the options. + */ +export interface IdentityCreateFromShieldedPoolTransitionOptions { + publicKeys: IdentityPublicKeyInCreation[]; + denomination: bigint; + actions: SerializedOrchardAction[]; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; + sendToAddressOnCreationFailure: PlatformAddressLike; +} + +/** + * IdentityCreateFromShieldedPoolTransition serialized as a plain object. + * + * `sendToAddressOnCreationFailure` is the raw 21 bytes of a PlatformAddress + * (type byte + 20-byte hash); the JSON form (below) carries the same value as + * a hex string. + */ +export interface IdentityCreateFromShieldedPoolTransitionObject { + $formatVersion: string; + publicKeys: IdentityPublicKeyInCreationObject[]; + denomination: bigint; + actions: SerializedOrchardActionObject[]; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; + sendToAddressOnCreationFailure: Uint8Array; + identityId: Uint8Array; +} + +/** + * IdentityCreateFromShieldedPoolTransition serialized as JSON (human-readable). + */ +export interface IdentityCreateFromShieldedPoolTransitionJSON { + $formatVersion: string; + publicKeys: IdentityPublicKeyInCreationJSON[]; + denomination: number | string; + actions: SerializedOrchardActionJSON[]; + anchor: string; + proof: string; + bindingSignature: string; + sendToAddressOnCreationFailure: string; + identityId: string; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IdentityCreateFromShieldedPoolTransitionOptions")] + pub type IdentityCreateFromShieldedPoolTransitionOptionsJs; + + #[wasm_bindgen(typescript_type = "IdentityCreateFromShieldedPoolTransitionObject")] + pub type IdentityCreateFromShieldedPoolTransitionObjectJs; + + #[wasm_bindgen(typescript_type = "IdentityCreateFromShieldedPoolTransitionJSON")] + pub type IdentityCreateFromShieldedPoolTransitionJSONJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +/// +/// The complex fields (`publicKeys`, `actions`, `sendToAddressOnCreationFailure`) +/// are extracted separately as WASM class instances. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct IdentityCreateFromShieldedPoolSimpleFields { + denomination: u64, + anchor: Vec, + proof: Vec, + binding_signature: Vec, +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(transparent)] +#[wasm_bindgen(js_name = IdentityCreateFromShieldedPoolTransition)] +pub struct IdentityCreateFromShieldedPoolTransitionWasm(IdentityCreateFromShieldedPoolTransition); + +impl From + for IdentityCreateFromShieldedPoolTransitionWasm +{ + fn from(v: IdentityCreateFromShieldedPoolTransition) -> Self { + IdentityCreateFromShieldedPoolTransitionWasm(v) + } +} + +impl From + for IdentityCreateFromShieldedPoolTransition +{ + fn from(v: IdentityCreateFromShieldedPoolTransitionWasm) -> Self { + v.0 + } +} + +#[wasm_bindgen(js_class = IdentityCreateFromShieldedPoolTransition)] +impl IdentityCreateFromShieldedPoolTransitionWasm { + #[wasm_bindgen(constructor)] + pub fn new( + options: IdentityCreateFromShieldedPoolTransitionOptionsJs, + ) -> WasmDppResult { + let js_opts: &JsValue = options.as_ref(); + + // Extract WASM class instances (borrow &options) + let js_public_keys_array = + try_from_options_with(js_opts, "publicKeys", |v| try_to_array(v, "publicKeys"))?; + let public_keys: Vec = + IdentityPublicKeyInCreationWasm::vec_from_array(&js_public_keys_array)?; + let actions = actions_from_js_options(js_opts, "actions")?; + let send_to_address: PlatformAddressWasm = + try_from_options(js_opts, "sendToAddressOnCreationFailure")?; + + // Extract simple fields via serde (consumes options) + let fields: IdentityCreateFromShieldedPoolSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + + let anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.anchor, "anchor")?; + let binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.binding_signature, "bindingSignature")?; + + let actions: Vec = + actions.into_iter().map(Into::into).collect(); + // The id is fully determined by the spend nullifiers (consensus re-derives + // and checks it), so it is derived here rather than taken from options. + let identity_id = derive_identity_id_from_actions(&actions); + + Ok(IdentityCreateFromShieldedPoolTransitionWasm( + IdentityCreateFromShieldedPoolTransition::V0( + IdentityCreateFromShieldedPoolTransitionV0 { + public_keys: public_keys.into_iter().map(Into::into).collect(), + denomination: fields.denomination, + actions, + anchor, + proof: fields.proof, + binding_signature, + send_to_address_on_creation_failure: send_to_address.into(), + identity_id, + }, + ), + )) + } + + /// Returns the public keys of the new identity. + #[wasm_bindgen(getter = "publicKeys")] + pub fn public_keys(&self) -> Vec { + match &self.0 { + IdentityCreateFromShieldedPoolTransition::V0(v0) => v0 + .public_keys + .iter() + .cloned() + .map(IdentityPublicKeyInCreationWasm::from) + .collect(), + } + } + + /// Returns the fixed exit denomination (credits leaving the shielded pool). + #[wasm_bindgen(getter = "denomination")] + pub fn denomination(&self) -> u64 { + match &self.0 { + IdentityCreateFromShieldedPoolTransition::V0(v0) => v0.denomination, + } + } + + /// Returns the serialized Orchard actions. + #[wasm_bindgen(getter = "actions")] + pub fn actions(&self) -> Vec { + match &self.0 { + IdentityCreateFromShieldedPoolTransition::V0(v0) => v0 + .actions + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect(), + } + } + + /// Returns the anchor (32-byte Merkle root). + #[wasm_bindgen(getter = "anchor")] + pub fn anchor(&self) -> Vec { + match &self.0 { + IdentityCreateFromShieldedPoolTransition::V0(v0) => v0.anchor.to_vec(), + } + } + + /// Returns the Halo2 proof bytes. + #[wasm_bindgen(getter = "proof")] + pub fn proof(&self) -> Vec { + match &self.0 { + IdentityCreateFromShieldedPoolTransition::V0(v0) => v0.proof.clone(), + } + } + + /// Returns the RedPallas binding signature (64 bytes). + #[wasm_bindgen(getter = "bindingSignature")] + pub fn binding_signature(&self) -> Vec { + match &self.0 { + IdentityCreateFromShieldedPoolTransition::V0(v0) => v0.binding_signature.to_vec(), + } + } + + /// Returns the fallback platform address credited if identity creation + /// fails a stateful check. + #[wasm_bindgen(getter = "sendToAddressOnCreationFailure")] + pub fn send_to_address_on_creation_failure(&self) -> PlatformAddressWasm { + match &self.0 { + IdentityCreateFromShieldedPoolTransition::V0(v0) => { + PlatformAddressWasm::from(v0.send_to_address_on_creation_failure) + } + } + } + + /// Returns the new identity's id (derived from the spend nullifiers). + #[wasm_bindgen(getter = "identityId")] + pub fn identity_id(&self) -> IdentifierWasm { + match &self.0 { + IdentityCreateFromShieldedPoolTransition::V0(v0) => v0.identity_id.into(), + } + } + + #[wasm_bindgen(js_name = getModifiedDataIds)] + pub fn modified_data_ids(&self) -> Vec { + self.0 + .modified_data_ids() + .into_iter() + .map(IdentifierWasm::from) + .collect() + } + + #[wasm_bindgen(js_name = toBytes)] + pub fn to_bytes(&self) -> WasmDppResult> { + Ok(PlatformSerializable::serialize_to_bytes( + &StateTransition::IdentityCreateFromShieldedPool(self.0.clone()), + )?) + } + + #[wasm_bindgen(js_name = fromBytes)] + pub fn from_bytes( + bytes: Vec, + ) -> WasmDppResult { + let st = StateTransition::deserialize_from_bytes(&bytes)?; + match st { + StateTransition::IdentityCreateFromShieldedPool(inner) => Ok(inner.into()), + _ => Err(WasmDppError::invalid_argument( + "Invalid state transition type: expected IdentityCreateFromShieldedPool", + )), + } + } + + #[wasm_bindgen(js_name = toStateTransition)] + pub fn to_state_transition(&self) -> crate::state_transitions::base::StateTransitionWasm { + StateTransition::IdentityCreateFromShieldedPool(self.0.clone()).into() + } +} + +impl_wasm_conversions_inner!( + IdentityCreateFromShieldedPoolTransitionWasm, + IdentityCreateFromShieldedPoolTransition, + IdentityCreateFromShieldedPoolTransition, + IdentityCreateFromShieldedPoolTransitionObjectJs, + IdentityCreateFromShieldedPoolTransitionJSONJs +); + +impl_wasm_type_info!( + IdentityCreateFromShieldedPoolTransitionWasm, + IdentityCreateFromShieldedPoolTransition +); diff --git a/packages/wasm-dpp2/src/shielded/mod.rs b/packages/wasm-dpp2/src/shielded/mod.rs index 799756e4057..886f0734bd7 100644 --- a/packages/wasm-dpp2/src/shielded/mod.rs +++ b/packages/wasm-dpp2/src/shielded/mod.rs @@ -1,4 +1,5 @@ pub mod address_witness; +pub mod identity_create_from_shielded_pool_transition; pub mod orchard_action; pub mod shield_from_asset_lock_transition; pub mod shield_transition; @@ -7,6 +8,7 @@ pub mod shielded_withdrawal_transition; pub mod unshield_transition; pub use address_witness::{AddressWitnessWasm, input_witnesses_from_js_options}; +pub use identity_create_from_shielded_pool_transition::IdentityCreateFromShieldedPoolTransitionWasm; pub use orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; pub use shield_from_asset_lock_transition::ShieldFromAssetLockTransitionWasm; pub use shield_transition::ShieldTransitionWasm; diff --git a/packages/wasm-dpp2/src/shielded/orchard_action.rs b/packages/wasm-dpp2/src/shielded/orchard_action.rs index cd131476890..713a8a22e5d 100644 --- a/packages/wasm-dpp2/src/shielded/orchard_action.rs +++ b/packages/wasm-dpp2/src/shielded/orchard_action.rs @@ -1,6 +1,6 @@ use crate::error::{WasmDppError, WasmDppResult}; use crate::impl_try_from_js_value; -use crate::impl_wasm_conversions_serde; +use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; use crate::utils::{ IntoWasm, try_from_options_with, try_to_array, try_to_bytes, try_to_fixed_bytes, @@ -159,8 +159,9 @@ impl SerializedOrchardActionWasm { impl_try_from_js_value!(SerializedOrchardActionWasm, "SerializedOrchardAction"); impl_wasm_type_info!(SerializedOrchardActionWasm, SerializedOrchardAction); -impl_wasm_conversions_serde!( +impl_wasm_conversions_inner!( SerializedOrchardActionWasm, + SerializedAction, SerializedOrchardAction, SerializedOrchardActionObjectJs, SerializedOrchardActionJSONJs diff --git a/packages/wasm-dpp2/src/shielded/shield_from_asset_lock_transition.rs b/packages/wasm-dpp2/src/shielded/shield_from_asset_lock_transition.rs index 580e6dc170b..ac9441dca2f 100644 --- a/packages/wasm-dpp2/src/shielded/shield_from_asset_lock_transition.rs +++ b/packages/wasm-dpp2/src/shielded/shield_from_asset_lock_transition.rs @@ -4,7 +4,7 @@ use crate::identifier::IdentifierWasm; use crate::platform_address::PlatformAddressWasm; use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; use crate::utils::{try_from_options, try_from_options_optional, try_vec_to_fixed_bytes}; -use crate::{impl_wasm_conversions_serde, impl_wasm_type_info}; +use crate::{impl_wasm_conversions_inner, impl_wasm_type_info}; use dpp::platform_value::BinaryData; use dpp::serialization::{PlatformDeserializable, PlatformSerializable}; use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; @@ -256,9 +256,10 @@ impl ShieldFromAssetLockTransitionWasm { } } -impl_wasm_conversions_serde!( +impl_wasm_conversions_inner!( ShieldFromAssetLockTransitionWasm, ShieldFromAssetLockTransition, + ShieldFromAssetLockTransition, ShieldFromAssetLockTransitionObjectJs, ShieldFromAssetLockTransitionJSONJs ); diff --git a/packages/wasm-dpp2/src/shielded/shield_transition.rs b/packages/wasm-dpp2/src/shielded/shield_transition.rs index 43576407a9f..1192488977f 100644 --- a/packages/wasm-dpp2/src/shielded/shield_transition.rs +++ b/packages/wasm-dpp2/src/shielded/shield_transition.rs @@ -8,7 +8,7 @@ use crate::shielded::address_witness::{AddressWitnessWasm, input_witnesses_from_ use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; use crate::utils::try_vec_to_fixed_bytes; use crate::utils::{try_from_options_optional_with, try_to_u16}; -use crate::{impl_wasm_conversions_serde, impl_wasm_type_info}; +use crate::{impl_wasm_conversions_inner, impl_wasm_type_info}; use dpp::prelude::UserFeeIncrease; use dpp::serialization::{PlatformDeserializable, PlatformSerializable}; use dpp::state_transition::shield_transition::ShieldTransition; @@ -280,9 +280,10 @@ impl ShieldTransitionWasm { } } -impl_wasm_conversions_serde!( +impl_wasm_conversions_inner!( ShieldTransitionWasm, ShieldTransition, + ShieldTransition, ShieldTransitionObjectJs, ShieldTransitionJSONJs ); diff --git a/packages/wasm-dpp2/src/shielded/shielded_transfer_transition.rs b/packages/wasm-dpp2/src/shielded/shielded_transfer_transition.rs index 6d8e18f78bc..184df9a3631 100644 --- a/packages/wasm-dpp2/src/shielded/shielded_transfer_transition.rs +++ b/packages/wasm-dpp2/src/shielded/shielded_transfer_transition.rs @@ -2,7 +2,7 @@ use crate::error::{WasmDppError, WasmDppResult}; use crate::identifier::IdentifierWasm; use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; use crate::utils::try_vec_to_fixed_bytes; -use crate::{impl_wasm_conversions_serde, impl_wasm_type_info}; +use crate::{impl_wasm_conversions_inner, impl_wasm_type_info}; use dpp::serialization::{PlatformDeserializable, PlatformSerializable}; use dpp::state_transition::shielded_transfer_transition::ShieldedTransferTransition; use dpp::state_transition::shielded_transfer_transition::v0::ShieldedTransferTransitionV0; @@ -192,9 +192,10 @@ impl ShieldedTransferTransitionWasm { } } -impl_wasm_conversions_serde!( +impl_wasm_conversions_inner!( ShieldedTransferTransitionWasm, ShieldedTransferTransition, + ShieldedTransferTransition, ShieldedTransferTransitionObjectJs, ShieldedTransferTransitionJSONJs ); diff --git a/packages/wasm-dpp2/src/shielded/shielded_withdrawal_transition.rs b/packages/wasm-dpp2/src/shielded/shielded_withdrawal_transition.rs index 3f26faad96b..3877231c1ef 100644 --- a/packages/wasm-dpp2/src/shielded/shielded_withdrawal_transition.rs +++ b/packages/wasm-dpp2/src/shielded/shielded_withdrawal_transition.rs @@ -5,7 +5,7 @@ use crate::identity::transitions::pooling::PoolingWasm; use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; use crate::utils::try_from_options; use crate::utils::try_vec_to_fixed_bytes; -use crate::{impl_wasm_conversions_serde, impl_wasm_type_info}; +use crate::{impl_wasm_conversions_inner, impl_wasm_type_info}; use dpp::identity::core_script::CoreScript; use dpp::serialization::{PlatformDeserializable, PlatformSerializable}; use dpp::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition; @@ -242,9 +242,10 @@ impl ShieldedWithdrawalTransitionWasm { } } -impl_wasm_conversions_serde!( +impl_wasm_conversions_inner!( ShieldedWithdrawalTransitionWasm, ShieldedWithdrawalTransition, + ShieldedWithdrawalTransition, ShieldedWithdrawalTransitionObjectJs, ShieldedWithdrawalTransitionJSONJs ); diff --git a/packages/wasm-dpp2/src/shielded/unshield_transition.rs b/packages/wasm-dpp2/src/shielded/unshield_transition.rs index 5873fa6eedd..19e192d835e 100644 --- a/packages/wasm-dpp2/src/shielded/unshield_transition.rs +++ b/packages/wasm-dpp2/src/shielded/unshield_transition.rs @@ -4,7 +4,7 @@ use crate::platform_address::PlatformAddressWasm; use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; use crate::utils::try_from_options; use crate::utils::try_vec_to_fixed_bytes; -use crate::{impl_wasm_conversions_serde, impl_wasm_type_info}; +use crate::{impl_wasm_conversions_inner, impl_wasm_type_info}; use dpp::serialization::{PlatformDeserializable, PlatformSerializable}; use dpp::state_transition::unshield_transition::UnshieldTransition; use dpp::state_transition::unshield_transition::v0::UnshieldTransitionV0; @@ -209,9 +209,10 @@ impl UnshieldTransitionWasm { } } -impl_wasm_conversions_serde!( +impl_wasm_conversions_inner!( UnshieldTransitionWasm, UnshieldTransition, + UnshieldTransition, UnshieldTransitionObjectJs, UnshieldTransitionJSONJs ); diff --git a/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs b/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs index 0f360f9f99f..e22ae5254dd 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs @@ -1,7 +1,6 @@ use crate::error::{WasmDppError, WasmDppResult}; use crate::identifier::{IdentifierLikeJs, IdentifierWasm}; use crate::impl_wasm_type_info; -use crate::serialization; use crate::state_transitions::StateTransitionWasm; use crate::state_transitions::batch::batched_transition::BatchedTransitionWasm; use crate::utils::{IntoWasm, try_to_u32, try_to_u64}; @@ -215,16 +214,6 @@ impl BatchTransitionWasm { self.0.serialize_to_bytes().map_err(Into::into) } - #[wasm_bindgen(js_name = "toObject")] - pub fn to_object(&self) -> WasmDppResult { - serialization::to_object(&self.0).map(Into::into) - } - - #[wasm_bindgen(js_name = "toJSON")] - pub fn to_json(&self) -> WasmDppResult { - serialization::to_json(&self.0).map(Into::into) - } - #[wasm_bindgen(js_name = "toHex")] pub fn to_hex(&self) -> WasmDppResult { Ok(encode(self.to_bytes()?.as_slice(), Hex)) @@ -242,16 +231,6 @@ impl BatchTransitionWasm { Ok(BatchTransitionWasm::from(rs_batch)) } - #[wasm_bindgen(js_name = "fromObject")] - pub fn from_object(object: BatchTransitionObjectJs) -> WasmDppResult { - serialization::from_object(object.into()).map(BatchTransitionWasm) - } - - #[wasm_bindgen(js_name = "fromJSON")] - pub fn from_json(object: BatchTransitionJSONJs) -> WasmDppResult { - serialization::from_json(object.into()).map(BatchTransitionWasm) - } - #[wasm_bindgen(js_name = "fromBase64")] pub fn from_base64(base64: String) -> WasmDppResult { BatchTransitionWasm::from_bytes( @@ -269,4 +248,11 @@ impl BatchTransitionWasm { } } +crate::impl_wasm_conversions_inner!( + BatchTransitionWasm, + BatchTransition, + BatchTransition, + BatchTransitionObjectJs, + BatchTransitionJSONJs +); impl_wasm_type_info!(BatchTransitionWasm, BatchTransition); diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_payment_info.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_payment_info.rs index e9688a8e7fe..070c6a22c7d 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/token_payment_info.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_payment_info.rs @@ -2,7 +2,7 @@ use crate::enums::batch::gas_fees_paid_by::{GasFeesPaidByLikeJs, GasFeesPaidByWa use crate::error::{WasmDppError, WasmDppResult}; use crate::identifier::{IdentifierLikeOrUndefinedJs, IdentifierWasm}; use crate::impl_try_from_js_value; -use crate::impl_wasm_conversions_serde; +use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; use crate::utils::try_from_options_optional; use dpp::balances::credits::TokenAmount; @@ -187,9 +187,10 @@ impl TokenPaymentInfoWasm { } } -impl_wasm_conversions_serde!( +impl_wasm_conversions_inner!( TokenPaymentInfoWasm, TokenPaymentInfo, + TokenPaymentInfo, TokenPaymentInfoObjectJs, TokenPaymentInfoJSONJs ); diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_pricing_schedule.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_pricing_schedule.rs index f4a63fc8ff2..7babbb54b88 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/token_pricing_schedule.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_pricing_schedule.rs @@ -1,5 +1,6 @@ use crate::error::{WasmDppError, WasmDppResult}; use crate::impl_try_from_js_value; +use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; use crate::utils::{JsValueExt, ToSerdeJSONExt}; use dpp::balances::credits::TokenAmount; @@ -110,3 +111,9 @@ impl TokenPricingScheduleWasm { impl_try_from_js_value!(TokenPricingScheduleWasm, "TokenPricingSchedule"); impl_wasm_type_info!(TokenPricingScheduleWasm, TokenPricingSchedule); + +impl_wasm_conversions_inner!( + TokenPricingScheduleWasm, + TokenPricingSchedule, + TokenPricingSchedule +); diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/address_funds.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/address_funds.rs index 71bb7f31302..6fce2e761bb 100644 --- a/packages/wasm-dpp2/src/state_transitions/proof_result/address_funds.rs +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/address_funds.rs @@ -6,7 +6,6 @@ use crate::PartialIdentityWasm; use crate::error::{WasmDppError, WasmDppResult}; use crate::impl_wasm_type_info; use js_sys::Map; -use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; use wasm_bindgen::prelude::*; @@ -40,11 +39,8 @@ impl VerifiedAddressInfosWasm { #[wasm_bindgen(js_name = fromObject)] pub fn from_object(value: JsValue) -> WasmDppResult { - let map_val = js_sys::Reflect::get(&value, &"addressInfos".into()) - .map_err(|_| WasmDppError::generic("Missing property: addressInfos"))?; - Ok(VerifiedAddressInfosWasm { - address_infos: map_val.unchecked_into(), - }) + let address_infos = super::helpers::read_map_property(&value, "addressInfos")?; + Ok(VerifiedAddressInfosWasm { address_infos }) } #[wasm_bindgen(js_name = fromJSON)] @@ -99,11 +95,10 @@ impl VerifiedIdentityFullWithAddressInfosWasm { let identity_val = js_sys::Reflect::get(&value, &"identity".into()) .map_err(|_| WasmDppError::generic("Missing property: identity"))?; let identity: IdentityWasm = crate::serialization::conversions::from_object(identity_val)?; - let map_val = js_sys::Reflect::get(&value, &"addressInfos".into()) - .map_err(|_| WasmDppError::generic("Missing property: addressInfos"))?; + let address_infos = super::helpers::read_map_property(&value, "addressInfos")?; Ok(VerifiedIdentityFullWithAddressInfosWasm { identity, - address_infos: map_val.unchecked_into(), + address_infos, }) } @@ -112,11 +107,10 @@ impl VerifiedIdentityFullWithAddressInfosWasm { let identity_val = js_sys::Reflect::get(&value, &"identity".into()) .map_err(|_| WasmDppError::generic("Missing property: identity"))?; let identity: IdentityWasm = crate::serialization::conversions::from_json(identity_val)?; - let map_val = js_sys::Reflect::get(&value, &"addressInfos".into()) - .map_err(|_| WasmDppError::generic("Missing property: addressInfos"))?; + let address_infos = super::helpers::read_map_property(&value, "addressInfos")?; Ok(VerifiedIdentityFullWithAddressInfosWasm { identity, - address_infos: map_val.unchecked_into(), + address_infos, }) } } @@ -171,11 +165,10 @@ impl VerifiedIdentityWithAddressInfosWasm { .map_err(|_| WasmDppError::generic("Missing property: partialIdentity"))?; let partial_identity: PartialIdentityWasm = crate::serialization::conversions::from_object(pi_val)?; - let map_val = js_sys::Reflect::get(&value, &"addressInfos".into()) - .map_err(|_| WasmDppError::generic("Missing property: addressInfos"))?; + let address_infos = super::helpers::read_map_property(&value, "addressInfos")?; Ok(VerifiedIdentityWithAddressInfosWasm { partial_identity, - address_infos: map_val.unchecked_into(), + address_infos, }) } @@ -185,11 +178,10 @@ impl VerifiedIdentityWithAddressInfosWasm { .map_err(|_| WasmDppError::generic("Missing property: partialIdentity"))?; let partial_identity: PartialIdentityWasm = crate::serialization::conversions::from_json(pi_val)?; - let map_val = js_sys::Reflect::get(&value, &"addressInfos".into()) - .map_err(|_| WasmDppError::generic("Missing property: addressInfos"))?; + let address_infos = super::helpers::read_map_property(&value, "addressInfos")?; Ok(VerifiedIdentityWithAddressInfosWasm { partial_identity, - address_infos: map_val.unchecked_into(), + address_infos, }) } } diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/document.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/document.rs index 1f83ef4df9f..47a51daa570 100644 --- a/packages/wasm-dpp2/src/state_transitions/proof_result/document.rs +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/document.rs @@ -1,10 +1,9 @@ //! `VerifiedDocuments` proof-result wrapper. use super::helpers::js_obj; -use crate::error::{WasmDppError, WasmDppResult}; +use crate::error::WasmDppResult; use crate::impl_wasm_type_info; use js_sys::Map; -use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; use wasm_bindgen::prelude::*; @@ -36,11 +35,8 @@ impl VerifiedDocumentsWasm { #[wasm_bindgen(js_name = fromObject)] pub fn from_object(value: JsValue) -> WasmDppResult { - let map_val = js_sys::Reflect::get(&value, &"documents".into()) - .map_err(|_| WasmDppError::generic("Missing property: documents"))?; - Ok(VerifiedDocumentsWasm { - documents: map_val.unchecked_into(), - }) + let documents = super::helpers::read_map_property(&value, "documents")?; + Ok(VerifiedDocumentsWasm { documents }) } #[wasm_bindgen(js_name = fromJSON)] diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/helpers.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/helpers.rs index 9370946ee91..73cfda05f88 100644 --- a/packages/wasm-dpp2/src/state_transitions/proof_result/helpers.rs +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/helpers.rs @@ -5,10 +5,12 @@ use crate::DocumentWasm; use crate::PlatformAddressWasm; +use crate::error::{WasmDppError, WasmDppResult}; use crate::utils::JsMapExt; use dpp::document::Document; use dpp::platform_value::Identifier; use js_sys::{BigInt, Map}; +use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; /// Build a plain JS object from key-value pairs. @@ -20,6 +22,35 @@ pub(super) fn js_obj(entries: &[(&str, JsValue)]) -> JsValue { obj.into() } +/// Read a `Map`-shaped property from an ingested JS value. +/// +/// `toJSON` normalizes a `Map` to a plain object so it survives +/// `JSON.stringify`. A value that round-tripped through +/// `JSON.parse(JSON.stringify(...))` therefore arrives here as a plain +/// object, not a `Map`. Accept both: use a real `Map` directly, otherwise +/// rebuild one from the plain object's entries so `.size`/`.get()`/iteration +/// behave as a `Map`. +pub(super) fn read_map_property(value: &JsValue, name: &str) -> WasmDppResult { + let raw = js_sys::Reflect::get(value, &name.into()) + .map_err(|_| WasmDppError::generic(format!("Missing property: {}", name)))?; + if raw.is_instance_of::() { + Ok(raw.unchecked_into()) + } else if raw.is_object() { + let entries = js_sys::Object::entries(raw.unchecked_ref()); + let map = Map::new(); + for entry in entries.iter() { + let pair: js_sys::Array = entry.unchecked_into(); + map.set(&pair.get(0), &pair.get(1)); + } + Ok(map) + } else { + Err(WasmDppError::generic(format!( + "Property {} must be a Map or plain object", + name + ))) + } +} + /// Wrap a raw `Document` into `DocumentWasm`. /// /// `DocumentWasm` requires metadata (contract ID, type name) that a bare diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/shielded.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/shielded.rs index 62779cb5d3d..dc12325413b 100644 --- a/packages/wasm-dpp2/src/state_transitions/proof_result/shielded.rs +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/shielded.rs @@ -3,7 +3,7 @@ //! These types were extracted from `proof_result` to keep shielded-specific //! code in its own module. -use super::helpers::js_obj; +use super::helpers::{js_obj, read_map_property}; use crate::IdentityWasm; use crate::error::{WasmDppError, WasmDppResult}; use crate::impl_wasm_conversions_serde; @@ -11,35 +11,9 @@ use crate::impl_wasm_type_info; use crate::serialization::conversions::normalize_js_value_for_json; use js_sys::{BigInt, Map}; use serde::{Deserialize, Serialize}; -use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; use wasm_bindgen::prelude::*; -fn read_map_property(value: &JsValue, name: &str) -> WasmDppResult { - let raw = js_sys::Reflect::get(value, &name.into()) - .map_err(|_| WasmDppError::generic(format!("Missing property: {}", name)))?; - // `to_json` normalizes the `Map` to a plain object so it survives `JSON.stringify`. A value - // that round-tripped through `JSON.parse(JSON.stringify(...))` therefore arrives here as a - // plain object, not a `Map`. Accept both: use a real `Map` directly, otherwise rebuild one - // from the plain object's entries so `.size`/`.get()`/iteration behave as a `Map`. - if raw.is_instance_of::() { - Ok(raw.unchecked_into()) - } else if raw.is_object() { - let entries = js_sys::Object::entries(raw.unchecked_ref()); - let map = Map::new(); - for entry in entries.iter() { - let pair: js_sys::Array = entry.unchecked_into(); - map.set(&pair.get(0), &pair.get(1)); - } - Ok(map) - } else { - Err(WasmDppError::generic(format!( - "Property {} must be a Map or plain object", - name - ))) - } -} - // --- VerifiedShieldedPoolState --- #[wasm_bindgen(js_name = "VerifiedShieldedPoolState")] @@ -178,29 +152,43 @@ impl VerifiedAssetLockConsumedWithAddressInfosWasm { // A credit value may arrive as a BigInt (`toObject`), a base-10 string (`toJSON` // normalizes BigInt to a string so it survives `JSON.stringify`), or a plain number // (a hand-built object). Accept all three so `fromJSON(JSON.parse(JSON.stringify(...)))` - // round-trips instead of dropping the value to `None`. - let read_opt_u64 = |name: &str| -> Option { - js_sys::Reflect::get(&value, &name.into()) - .ok() - .and_then(|v| { - if v.is_undefined() || v.is_null() { - None - } else if let Ok(b) = u64::try_from(v.clone()) { - Some(b) - } else if let Some(s) = v.as_string() { - s.parse::().ok() - } else { - v.as_f64().and_then(|n| { - (n >= 0.0 && n.fract() == 0.0 && n <= u64::MAX as f64) - .then_some(n as u64) - }) - } - }) + // round-trips. Absent / null / undefined means "no surplus" (`None`); a PRESENT value + // that cannot be cleanly read as u64 is an error — silently mapping it to `None` + // would conflate "absent" with "garbage". + let read_opt_u64 = |name: &str| -> WasmDppResult> { + let v = js_sys::Reflect::get(&value, &name.into()).unwrap_or(JsValue::UNDEFINED); + if v.is_undefined() || v.is_null() { + return Ok(None); + } + if let Ok(b) = u64::try_from(v.clone()) { + return Ok(Some(b)); + } + if let Some(s) = v.as_string() { + return s.parse::().map(Some).map_err(|e| { + WasmDppError::invalid_argument(format!( + "{} is not a valid u64 string: {}", + name, e + )) + }); + } + if let Some(n) = v.as_f64() { + if n >= 0.0 && n.fract() == 0.0 && n <= u64::MAX as f64 { + return Ok(Some(n as u64)); + } + return Err(WasmDppError::invalid_argument(format!( + "{} must be a non-negative integer within u64 range, got {}", + name, n + ))); + } + Err(WasmDppError::invalid_argument(format!( + "{} must be a BigInt, base-10 string, or integer number", + name + ))) }; Ok(VerifiedAssetLockConsumedWithAddressInfosWasm { status, - initial_credit_value: read_opt_u64("initialCreditValue"), - remaining_credit_value: read_opt_u64("remainingCreditValue"), + initial_credit_value: read_opt_u64("initialCreditValue")?, + remaining_credit_value: read_opt_u64("remainingCreditValue")?, address_infos: read_map_property(&value, "addressInfos")?, }) } diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/token.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/token.rs index a1e820e0237..206a5e6c669 100644 --- a/packages/wasm-dpp2/src/state_transitions/proof_result/token.rs +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/token.rs @@ -5,13 +5,12 @@ use crate::DocumentWasm; use crate::IdentifierWasm; use crate::IdentityTokenInfoWasm; use crate::TokenStatusWasm; -use crate::error::{WasmDppError, WasmDppResult}; +use crate::error::WasmDppResult; use crate::impl_wasm_conversions_serde; use crate::impl_wasm_type_info; use crate::state_transitions::batch::token_pricing_schedule::TokenPricingScheduleWasm; use js_sys::{BigInt, Map}; use serde::{Deserialize, Serialize}; -use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; use wasm_bindgen::prelude::*; @@ -130,11 +129,8 @@ impl VerifiedTokenIdentitiesBalancesWasm { #[wasm_bindgen(js_name = fromObject)] pub fn from_object(value: JsValue) -> WasmDppResult { - let map_val = js_sys::Reflect::get(&value, &"balances".into()) - .map_err(|_| WasmDppError::generic("Missing property: balances"))?; - Ok(VerifiedTokenIdentitiesBalancesWasm { - balances: map_val.unchecked_into(), - }) + let balances = super::helpers::read_map_property(&value, "balances")?; + Ok(VerifiedTokenIdentitiesBalancesWasm { balances }) } #[wasm_bindgen(js_name = fromJSON)] diff --git a/packages/wasm-dpp2/src/tokens/configuration/group.rs b/packages/wasm-dpp2/src/tokens/configuration/group.rs index f250b3dfdb7..f57ca523bb1 100644 --- a/packages/wasm-dpp2/src/tokens/configuration/group.rs +++ b/packages/wasm-dpp2/src/tokens/configuration/group.rs @@ -3,7 +3,6 @@ use crate::identifier::{IdentifierLikeJs, IdentifierWasm}; use crate::impl_from_for_extern_type; use crate::impl_try_from_js_value; use crate::impl_wasm_type_info; -use crate::serialization; use crate::utils::{JsMapExt, try_to_map}; use dpp::data_contract::group::accessors::v0::{GroupV0Getters, GroupV0Setters}; use dpp::data_contract::group::v0::GroupV0; @@ -156,30 +155,8 @@ impl GroupWasm { .set_member_power(member.try_into()?, member_required_power); Ok(()) } - - #[wasm_bindgen(js_name = "toJSON")] - pub fn to_json(&self) -> WasmDppResult { - serialization::to_json(&self.0).map(Into::into) - } - - #[wasm_bindgen(js_name = "fromJSON")] - pub fn from_json(object: GroupJSONJs) -> WasmDppResult { - serialization::from_json(object.into()).map(GroupWasm) - } - - #[wasm_bindgen(js_name = "toObject")] - pub fn to_object(&self) -> WasmDppResult { - // Use toJSON for serialization because it handles BTreeMap - // correctly (Identifier becomes base58 string in human-readable mode). - // This ensures all fields are automatically included when new versions are added. - serialization::to_json(&self.0).map(Into::into) - } - - #[wasm_bindgen(js_name = "fromObject")] - pub fn from_object(value: GroupObjectJs) -> WasmDppResult { - serialization::from_object(value.into()).map(GroupWasm) - } } +crate::impl_wasm_conversions_inner!(GroupWasm, Group, Group, GroupObjectJs, GroupJSONJs); impl_try_from_js_value!(GroupWasm, "Group"); impl_wasm_type_info!(GroupWasm, Group); diff --git a/packages/wasm-dpp2/src/tokens/configuration/localization.rs b/packages/wasm-dpp2/src/tokens/configuration/localization.rs index 58f62e48385..bb246e291fe 100644 --- a/packages/wasm-dpp2/src/tokens/configuration/localization.rs +++ b/packages/wasm-dpp2/src/tokens/configuration/localization.rs @@ -1,4 +1,4 @@ -use crate::error::{WasmDppError, WasmDppResult}; +use crate::error::WasmDppError; use crate::impl_wasm_type_info; use crate::serialization; use crate::utils::IntoWasm; @@ -104,32 +104,16 @@ impl TokenConfigurationLocalizationWasm { pub fn set_singular_form(&mut self, singular_form: String) { self.0.set_singular_form(singular_form); } - - #[wasm_bindgen(js_name = "toJSON")] - pub fn to_json(&self) -> WasmDppResult { - serialization::to_json(&self.0).map(Into::into) - } - - #[wasm_bindgen(js_name = "fromJSON")] - pub fn from_json( - value: TokenConfigurationLocalizationJSONJs, - ) -> WasmDppResult { - serialization::from_json(value.into()).map(TokenConfigurationLocalizationWasm) - } - - #[wasm_bindgen(js_name = "toObject")] - pub fn to_object(&self) -> WasmDppResult { - serialization::to_object(&self.0).map(Into::into) - } - - #[wasm_bindgen(js_name = "fromObject")] - pub fn from_object( - value: TokenConfigurationLocalizationObjectJs, - ) -> WasmDppResult { - serialization::from_object(value.into()).map(TokenConfigurationLocalizationWasm) - } } +crate::impl_wasm_conversions_inner!( + TokenConfigurationLocalizationWasm, + TokenConfigurationLocalization, + TokenConfigurationLocalization, + TokenConfigurationLocalizationObjectJs, + TokenConfigurationLocalizationJSONJs +); + impl TryFrom<&JsValue> for TokenConfigurationLocalizationWasm { type Error = WasmDppError; @@ -141,8 +125,13 @@ impl TryFrom<&JsValue> for TokenConfigurationLocalizationWasm { return Ok(wasm_localization.clone()); } - // Deserialize as a versioned object (with $formatVersion) - serialization::from_object(value.clone()).map(TokenConfigurationLocalizationWasm) + // Deserialize as a versioned object (with $formatVersion) via the + // canonical ValueConvertible trait. + use dpp::serialization::ValueConvertible; + let pv = serialization::platform_value_from_object(value)?; + let inner = TokenConfigurationLocalization::from_object(pv) + .map_err(|e| WasmDppError::serialization(format!("from_object: {}", e)))?; + Ok(TokenConfigurationLocalizationWasm(inner)) } } diff --git a/packages/wasm-dpp2/src/tokens/contract_info.rs b/packages/wasm-dpp2/src/tokens/contract_info.rs index f65ced8495b..07e971c87d8 100644 --- a/packages/wasm-dpp2/src/tokens/contract_info.rs +++ b/packages/wasm-dpp2/src/tokens/contract_info.rs @@ -1,5 +1,5 @@ use crate::identifier::IdentifierWasm; -use crate::{impl_wasm_conversions_serde, impl_wasm_type_info}; +use crate::{impl_wasm_conversions_inner, impl_wasm_type_info}; use dpp::data_contract::TokenContractPosition; use dpp::tokens::contract_info::TokenContractInfo; use dpp::tokens::contract_info::v0::TokenContractInfoV0Accessors; @@ -9,8 +9,12 @@ use wasm_bindgen::prelude::wasm_bindgen; const TOKEN_CONTRACT_INFO_TYPES_TS: &str = r#" /** * TokenContractInfo serialized as a plain object. + * + * Versioned enum tagged with `$formatVersion`. V0 carries `contractId` and + * `tokenContractPosition` flat at the top level via internal tagging. */ export interface TokenContractInfoObject { + $formatVersion: "0"; contractId: Uint8Array; tokenContractPosition: number; } @@ -19,6 +23,7 @@ export interface TokenContractInfoObject { * TokenContractInfo serialized as JSON (with string identifiers). */ export interface TokenContractInfoJSON { + $formatVersion: "0"; contractId: string; tokenContractPosition: number; } @@ -67,9 +72,10 @@ impl TokenContractInfoWasm { } } -impl_wasm_conversions_serde!( +impl_wasm_conversions_inner!( TokenContractInfoWasm, TokenContractInfo, + TokenContractInfo, TokenContractInfoObjectJs, TokenContractInfoJSONJs ); diff --git a/packages/wasm-dpp2/src/voting/resource_vote_choice.rs b/packages/wasm-dpp2/src/voting/resource_vote_choice.rs index b880944badf..8344d9c866d 100644 --- a/packages/wasm-dpp2/src/voting/resource_vote_choice.rs +++ b/packages/wasm-dpp2/src/voting/resource_vote_choice.rs @@ -10,19 +10,23 @@ use wasm_bindgen::prelude::wasm_bindgen; const TS_TYPES: &str = r#" /** * ResourceVoteChoice serialized as a plain object. + * + * Custom Serialize emits a flat `{type, identity?}` shape — `identity` + * (synthesized name) carries the inner Identifier for the TowardsIdentity + * variant. */ export type ResourceVoteChoiceObject = - | { type: "towardsIdentity"; data: Uint8Array } - | { type: "abstain" } - | { type: "lock" }; + | { $type: "towardsIdentity"; identity: Uint8Array } + | { $type: "abstain" } + | { $type: "lock" }; /** * ResourceVoteChoice serialized as JSON. */ export type ResourceVoteChoiceJSON = - | { type: "towardsIdentity"; data: string } - | { type: "abstain" } - | { type: "lock" }; + | { $type: "towardsIdentity"; identity: string } + | { $type: "abstain" } + | { $type: "lock" }; "#; #[wasm_bindgen] diff --git a/packages/wasm-dpp2/src/voting/vote.rs b/packages/wasm-dpp2/src/voting/vote.rs index e5f78a58360..5b409af1444 100644 --- a/packages/wasm-dpp2/src/voting/vote.rs +++ b/packages/wasm-dpp2/src/voting/vote.rs @@ -13,26 +13,26 @@ use wasm_bindgen::prelude::wasm_bindgen; const TS_TYPES: &str = r#" /** * Vote serialized as a plain object. + * + * Internally tagged with `$type` ($-prefix because the level also carries + * the inner ResourceVote's `$formatVersion`). The single ResourceVote + * variant flattens its V0 body — no `data` wrapper. */ export interface VoteObject { - type: "resourceVote"; - data: { - $formatVersion: string; - votePoll: VotePollObject; - resourceVoteChoice: ResourceVoteChoiceObject; - }; + $type: "resourceVote"; + $formatVersion: string; + votePoll: VotePollObject; + resourceVoteChoice: ResourceVoteChoiceObject; } /** * Vote serialized as JSON. */ export interface VoteJSON { - type: "resourceVote"; - data: { - $formatVersion: string; - votePoll: VotePollJSON; - resourceVoteChoice: ResourceVoteChoiceJSON; - }; + $type: "resourceVote"; + $formatVersion: string; + votePoll: VotePollJSON; + resourceVoteChoice: ResourceVoteChoiceJSON; } "#; diff --git a/packages/wasm-dpp2/src/voting/vote_poll.rs b/packages/wasm-dpp2/src/voting/vote_poll.rs index 8738000b2ed..45a433fa059 100644 --- a/packages/wasm-dpp2/src/voting/vote_poll.rs +++ b/packages/wasm-dpp2/src/voting/vote_poll.rs @@ -30,28 +30,28 @@ export interface VotePollOptions { /** * VotePoll serialized as a plain object. + * + * Internally tagged with `type` (plain — no `$`-prefixed neighbors at + * this level). Inner ContestedDocumentResourceVotePoll fields flatten at + * the same level — no `data` wrapper. */ export interface VotePollObject { - type: "contestedDocumentResourceVotePoll"; - data: { - contractId: Uint8Array; - documentTypeName: string; - indexName: string; - indexValues: any[]; - }; + $type: "contestedDocumentResourceVotePoll"; + contractId: Uint8Array; + documentTypeName: string; + indexName: string; + indexValues: any[]; } /** * VotePoll serialized as JSON. */ export interface VotePollJSON { - type: "contestedDocumentResourceVotePoll"; - data: { - contractId: string; - documentTypeName: string; - indexName: string; - indexValues: any[]; - }; + $type: "contestedDocumentResourceVotePoll"; + contractId: string; + documentTypeName: string; + indexName: string; + indexValues: any[]; } "#; diff --git a/packages/wasm-dpp2/src/voting/winner_info.rs b/packages/wasm-dpp2/src/voting/winner_info.rs index 29ea982b141..d7be506092e 100644 --- a/packages/wasm-dpp2/src/voting/winner_info.rs +++ b/packages/wasm-dpp2/src/voting/winner_info.rs @@ -9,21 +9,23 @@ use wasm_bindgen::prelude::wasm_bindgen; const TS_TYPES: &str = r#" /** * ContestedDocumentVotePollWinnerInfo serialized as a plain object. - * Simple variants serialize as strings, tuple variant as { WonByIdentity: value }. + * + * Custom Serialize emits a flat `{type, identity?}` shape — `identity` + * (synthesized name) carries the inner Identifier for the WonByIdentity + * variant. */ export type ContestedDocumentVotePollWinnerInfoObject = - | { type: "noWinner" } - | { type: "locked" } - | { type: "wonByIdentity"; data: Uint8Array }; + | { $type: "noWinner" } + | { $type: "locked" } + | { $type: "wonByIdentity"; identity: Uint8Array }; /** * ContestedDocumentVotePollWinnerInfo serialized as JSON. - * Uses adjacently tagged format with type discriminator. */ export type ContestedDocumentVotePollWinnerInfoJSON = - | { type: "noWinner" } - | { type: "locked" } - | { type: "wonByIdentity"; data: string }; + | { $type: "noWinner" } + | { $type: "locked" } + | { $type: "wonByIdentity"; identity: string }; "#; #[wasm_bindgen] diff --git a/packages/wasm-dpp2/tests/unit/AddressCreditWithdrawalTransition.spec.ts b/packages/wasm-dpp2/tests/unit/AddressCreditWithdrawalTransition.spec.ts index 9c3272327c8..71d8cfa1419 100644 --- a/packages/wasm-dpp2/tests/unit/AddressCreditWithdrawalTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/AddressCreditWithdrawalTransition.spec.ts @@ -236,7 +236,7 @@ describe('AddressCreditWithdrawalTransition', () => { const obj = transition.toObject(); expect(obj.feeStrategy).to.be.an('array'); - expect(obj.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(obj.feeStrategy[0].$type).to.be.oneOf(['deductFromInput', 'reduceOutput']); expect(obj.feeStrategy[0].index).to.be.a('number'); }); @@ -249,7 +249,7 @@ describe('AddressCreditWithdrawalTransition', () => { expect(json.output.address).to.be.a('string').with.lengthOf(42); expect(json.output.amount).to.satisfy((v: unknown) => typeof v === 'number' || typeof v === 'string'); expect(json.outputScript).to.be.a('string'); - expect(json.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(json.feeStrategy[0].$type).to.be.oneOf(['deductFromInput', 'reduceOutput']); }); it('fromObject(toObject()) round-trips identically', () => { diff --git a/packages/wasm-dpp2/tests/unit/AddressFundingFromAssetLockTransition.spec.ts b/packages/wasm-dpp2/tests/unit/AddressFundingFromAssetLockTransition.spec.ts index 9d160146cd7..96a553f4d1d 100644 --- a/packages/wasm-dpp2/tests/unit/AddressFundingFromAssetLockTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/AddressFundingFromAssetLockTransition.spec.ts @@ -231,8 +231,8 @@ describe('AddressFundingFromAssetLockTransition', () => { expect(obj.feeStrategy).to.be.an('array'); expect(obj.feeStrategy.length).to.be.greaterThan(0); - expect(obj.feeStrategy[0]).to.have.property('type'); - expect(obj.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(obj.feeStrategy[0]).to.have.property('$type'); + expect(obj.feeStrategy[0].$type).to.be.oneOf(['deductFromInput', 'reduceOutput']); expect(obj.feeStrategy[0].index).to.be.a('number'); }); @@ -262,7 +262,7 @@ describe('AddressFundingFromAssetLockTransition', () => { const json = transition.toJSON(); expect(json.feeStrategy).to.be.an('array'); - expect(json.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(json.feeStrategy[0].$type).to.be.oneOf(['deductFromInput', 'reduceOutput']); expect(json.feeStrategy[0].index).to.be.a('number'); }); diff --git a/packages/wasm-dpp2/tests/unit/AddressFundsTransferTransition.spec.ts b/packages/wasm-dpp2/tests/unit/AddressFundsTransferTransition.spec.ts index b3913b1a4e5..e1b987f7489 100644 --- a/packages/wasm-dpp2/tests/unit/AddressFundsTransferTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/AddressFundsTransferTransition.spec.ts @@ -183,7 +183,7 @@ describe('AddressFundsTransferTransition', () => { const obj = transition.toObject(); expect(obj.feeStrategy).to.be.an('array'); - expect(obj.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(obj.feeStrategy[0].$type).to.be.oneOf(['deductFromInput', 'reduceOutput']); expect(obj.feeStrategy[0].index).to.be.a('number'); }); @@ -198,7 +198,7 @@ describe('AddressFundsTransferTransition', () => { expect(json.outputs[0].address).to.be.a('string').with.lengthOf(42); expect(json.outputs[0].amount).to.satisfy((v: unknown) => typeof v === 'number' || typeof v === 'string'); - expect(json.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(json.feeStrategy[0].$type).to.be.oneOf(['deductFromInput', 'reduceOutput']); expect(json.feeStrategy[0].index).to.be.a('number'); }); diff --git a/packages/wasm-dpp2/tests/unit/AssetLockProof.spec.ts b/packages/wasm-dpp2/tests/unit/AssetLockProof.spec.ts index ee4976b9eb4..81ccb30c6cc 100644 --- a/packages/wasm-dpp2/tests/unit/AssetLockProof.spec.ts +++ b/packages/wasm-dpp2/tests/unit/AssetLockProof.spec.ts @@ -139,7 +139,7 @@ describe('AssetLockProof', () => { expect(restoredProof.toObject()).to.deep.equal(objectRepresentation); }); - it('should export internally-tagged {type:"instant", ...fields} for Instant', () => { + it('should export internally-tagged {$type:"instant", ...fields} for Instant', () => { const instantLockProof = wasm.AssetLockProof.createInstantAssetLockProof( instantLockBytes, transactionBytes, @@ -148,14 +148,14 @@ describe('AssetLockProof', () => { const objectRepresentation = instantLockProof.toObject(); - expect(objectRepresentation.type).to.equal('instant'); + expect(objectRepresentation.$type).to.equal('instant'); expect(objectRepresentation.instantLock).to.be.instanceOf(Uint8Array); expect(objectRepresentation.transaction).to.be.instanceOf(Uint8Array); expect(objectRepresentation.instantLock).to.deep.equal(instantLockBytes); expect(objectRepresentation.transaction).to.deep.equal(transactionBytes); }); - it('should export internally-tagged {type:"chain", ...fields} for Chain', () => { + it('should export internally-tagged {$type:"chain", ...fields} for Chain', () => { const outpoint = new wasm.OutPoint( 'e8b43025641eea4fd21190f01bd870ef90f1a8b199d8fc3376c5b62c0b1a179d', 1, @@ -164,7 +164,7 @@ describe('AssetLockProof', () => { const objectRepresentation = chainLockProof.toObject(); - expect(objectRepresentation.type).to.equal('chain'); + expect(objectRepresentation.$type).to.equal('chain'); expect(objectRepresentation.coreChainLockedHeight).to.equal(1); expect(objectRepresentation.outPoint).to.be.an('object'); expect(objectRepresentation.outPoint.txid).to.exist(); @@ -188,7 +188,7 @@ describe('AssetLockProof', () => { // AssetLockProof.toObject() flattens the inner fields next to `type` const outerExpected = { - type: 'instant', + $type: 'instant', ...innerExpected, }; @@ -206,7 +206,7 @@ describe('AssetLockProof', () => { ); const jsonRepresentation = instantLockProof.toJSON(); - expect(jsonRepresentation.type).to.equal('instant'); + expect(jsonRepresentation.$type).to.equal('instant'); expect(jsonRepresentation.instantLock).to.be.a('string'); expect(jsonRepresentation.transaction).to.be.a('string'); expect(Buffer.from(jsonRepresentation.instantLock, 'base64')).to.deep.equal( @@ -229,7 +229,7 @@ describe('AssetLockProof', () => { const chainLockProof = wasm.AssetLockProof.createChainAssetLockProof(1, outpoint); const jsonRepresentation = chainLockProof.toJSON(); - expect(jsonRepresentation.type).to.equal('chain'); + expect(jsonRepresentation.$type).to.equal('chain'); const restoredProof = wasm.AssetLockProof.fromJSON(jsonRepresentation); @@ -253,7 +253,7 @@ describe('AssetLockProof', () => { const chainLockProof = wasm.AssetLockProof.createChainAssetLockProof(1, outpoint); // lockType returns the lowercase wire-shape string, matching - // `AssetLockProof.toObject().type` for round-trip consistency. + // `AssetLockProof.toObject().$type` for round-trip consistency. expect(instantAssetLockProof.lockType).to.equal('instant'); expect(chainLockProof.lockType).to.equal('chain'); }); diff --git a/packages/wasm-dpp2/tests/unit/ContestedDocumentVotePollWinnerInfo.spec.ts b/packages/wasm-dpp2/tests/unit/ContestedDocumentVotePollWinnerInfo.spec.ts index 0f365d2cb0b..2246bf90d30 100644 --- a/packages/wasm-dpp2/tests/unit/ContestedDocumentVotePollWinnerInfo.spec.ts +++ b/packages/wasm-dpp2/tests/unit/ContestedDocumentVotePollWinnerInfo.spec.ts @@ -61,7 +61,7 @@ describe('ContestedDocumentVotePollWinnerInfo', () => { const info = new wasm.ContestedDocumentVotePollWinnerInfo('NoWinner'); const json = info.toJSON(); - expect(json).to.deep.equal({ type: 'noWinner' }); + expect(json).to.deep.equal({ $type: 'noWinner' }); }); it('should serialize WonByIdentity to JSON matching fixture', () => { @@ -70,20 +70,20 @@ describe('ContestedDocumentVotePollWinnerInfo', () => { const info = new wasm.ContestedDocumentVotePollWinnerInfo('WonByIdentity', identityId); const json = info.toJSON(); - expect(json).to.deep.equal({ type: 'wonByIdentity', data: identityIdBase58 }); + expect(json).to.deep.equal({ $type: 'wonByIdentity', identity: identityIdBase58 }); }); it('should serialize Locked to JSON matching fixture', () => { const info = new wasm.ContestedDocumentVotePollWinnerInfo('Locked'); const json = info.toJSON(); - expect(json).to.deep.equal({ type: 'locked' }); + expect(json).to.deep.equal({ $type: 'locked' }); }); }); describe('fromJSON()', () => { it('should create NoWinner from JSON fixture and verify getters', () => { - const restored = wasm.ContestedDocumentVotePollWinnerInfo.fromJSON({ type: 'noWinner' }); + const restored = wasm.ContestedDocumentVotePollWinnerInfo.fromJSON({ $type: 'noWinner' }); expect(restored.kind).to.equal('NoWinner'); expect(restored.isNoWinner).to.be.true(); @@ -96,7 +96,7 @@ describe('ContestedDocumentVotePollWinnerInfo', () => { const identityId = wasm.Identifier.fromHex(identityIdHex); const identityIdBase58 = identityId.toBase58(); - const fixture = { type: 'wonByIdentity', data: identityIdBase58 }; + const fixture = { $type: 'wonByIdentity', identity: identityIdBase58 }; const restored = wasm.ContestedDocumentVotePollWinnerInfo.fromJSON(fixture); expect(restored.kind).to.equal('WonByIdentity'); @@ -108,7 +108,7 @@ describe('ContestedDocumentVotePollWinnerInfo', () => { }); it('should create Locked from JSON fixture and verify getters', () => { - const restored = wasm.ContestedDocumentVotePollWinnerInfo.fromJSON({ type: 'locked' }); + const restored = wasm.ContestedDocumentVotePollWinnerInfo.fromJSON({ $type: 'locked' }); expect(restored.kind).to.equal('Locked'); expect(restored.isLocked).to.be.true(); @@ -123,7 +123,7 @@ describe('ContestedDocumentVotePollWinnerInfo', () => { const info = new wasm.ContestedDocumentVotePollWinnerInfo('NoWinner'); const obj = info.toObject(); - expect(obj).to.deep.equal({ type: 'noWinner' }); + expect(obj).to.deep.equal({ $type: 'noWinner' }); }); it('should serialize WonByIdentity to Object with Uint8Array', () => { @@ -132,22 +132,22 @@ describe('ContestedDocumentVotePollWinnerInfo', () => { const obj = info.toObject(); expect(obj).to.be.an('object'); - expect(obj.type).to.equal('wonByIdentity'); - expect(obj.data).to.be.instanceOf(Uint8Array); - expect(Buffer.from(obj.data).toString('hex')).to.equal(identityIdHex); + expect(obj.$type).to.equal('wonByIdentity'); + expect(obj.identity).to.be.instanceOf(Uint8Array); + expect(Buffer.from(obj.identity).toString('hex')).to.equal(identityIdHex); }); it('should serialize Locked to Object matching fixture', () => { const info = new wasm.ContestedDocumentVotePollWinnerInfo('Locked'); const obj = info.toObject(); - expect(obj).to.deep.equal({ type: 'locked' }); + expect(obj).to.deep.equal({ $type: 'locked' }); }); }); describe('fromObject()', () => { it('should create NoWinner from Object fixture and verify getters', () => { - const restored = wasm.ContestedDocumentVotePollWinnerInfo.fromObject({ type: 'noWinner' }); + const restored = wasm.ContestedDocumentVotePollWinnerInfo.fromObject({ $type: 'noWinner' }); expect(restored.kind).to.equal('NoWinner'); expect(restored.isNoWinner).to.be.true(); @@ -157,7 +157,7 @@ describe('ContestedDocumentVotePollWinnerInfo', () => { it('should create WonByIdentity from Object fixture and verify getters', () => { const identityIdBytes = new Uint8Array(Buffer.from(identityIdHex, 'hex')); - const fixture = { type: 'wonByIdentity', data: identityIdBytes }; + const fixture = { $type: 'wonByIdentity', identity: identityIdBytes }; const restored = wasm.ContestedDocumentVotePollWinnerInfo.fromObject(fixture); expect(restored.kind).to.equal('WonByIdentity'); @@ -167,7 +167,7 @@ describe('ContestedDocumentVotePollWinnerInfo', () => { }); it('should create Locked from Object fixture and verify getters', () => { - const restored = wasm.ContestedDocumentVotePollWinnerInfo.fromObject({ type: 'locked' }); + const restored = wasm.ContestedDocumentVotePollWinnerInfo.fromObject({ $type: 'locked' }); expect(restored.kind).to.equal('Locked'); expect(restored.isLocked).to.be.true(); diff --git a/packages/wasm-dpp2/tests/unit/ContractBounds.spec.ts b/packages/wasm-dpp2/tests/unit/ContractBounds.spec.ts index a3808503414..8b32c81d09b 100644 --- a/packages/wasm-dpp2/tests/unit/ContractBounds.spec.ts +++ b/packages/wasm-dpp2/tests/unit/ContractBounds.spec.ts @@ -52,7 +52,7 @@ describe('ContractBounds', () => { const json = bounds.toJSON(); expect(json).to.deep.equal({ - type: 'singleContract', + $type: 'singleContract', id: contractIdBase58, }); }); @@ -62,7 +62,7 @@ describe('ContractBounds', () => { const json = bounds.toJSON(); expect(json).to.deep.equal({ - type: 'documentType', + $type: 'documentType', id: contractIdBase58, documentTypeName: 'profile', }); @@ -72,7 +72,7 @@ describe('ContractBounds', () => { describe('fromJSON()', () => { it('should create SingleContract from JSON fixture and verify getters', () => { const fixture = { - type: 'singleContract', + $type: 'singleContract', id: contractIdBase58, }; @@ -85,7 +85,7 @@ describe('ContractBounds', () => { it('should create SingleContractDocumentType from JSON fixture and verify getters', () => { const fixture = { - type: 'documentType', + $type: 'documentType', id: contractIdBase58, documentTypeName: 'profile', }; @@ -103,7 +103,7 @@ describe('ContractBounds', () => { const bounds = wasm.ContractBounds.SingleContract(contractIdBase58); const obj = bounds.toObject(); - expect(obj.type).to.equal('singleContract'); + expect(obj.$type).to.equal('singleContract'); expect(obj.id).to.be.instanceOf(Uint8Array); expect(wasm.Identifier.fromBytes(obj.id).toHex()).to.equal(contractIdHex); }); @@ -112,7 +112,7 @@ describe('ContractBounds', () => { const bounds = wasm.ContractBounds.SingleContractDocumentType(contractIdBase58, 'profile'); const obj = bounds.toObject(); - expect(obj.type).to.equal('documentType'); + expect(obj.$type).to.equal('documentType'); expect(obj.id).to.be.instanceOf(Uint8Array); expect(wasm.Identifier.fromBytes(obj.id).toHex()).to.equal(contractIdHex); expect(obj.documentTypeName).to.equal('profile'); @@ -122,7 +122,7 @@ describe('ContractBounds', () => { describe('fromObject()', () => { it('should create SingleContract from Object fixture and verify getters', () => { const obj = { - type: 'singleContract', + $type: 'singleContract', id: contractIdBase58, }; @@ -135,7 +135,7 @@ describe('ContractBounds', () => { it('should create SingleContractDocumentType from Object fixture and verify getters', () => { const obj = { - type: 'documentType', + $type: 'documentType', id: contractIdBase58, documentTypeName: 'profile', }; diff --git a/packages/wasm-dpp2/tests/unit/FeeStrategyStep.spec.ts b/packages/wasm-dpp2/tests/unit/FeeStrategyStep.spec.ts index 3976949f797..659402bfea9 100644 --- a/packages/wasm-dpp2/tests/unit/FeeStrategyStep.spec.ts +++ b/packages/wasm-dpp2/tests/unit/FeeStrategyStep.spec.ts @@ -61,19 +61,19 @@ describe('FeeStrategyStep', () => { }); } - it('emits {type: "deductFromInput", index} in toObject() output', () => { + it('emits {$type: "deductFromInput", index} in toObject() output', () => { const transition = makeTransition([wasm.FeeStrategyStep.deductFromInput(0)]); const obj = transition.toObject(); expect(obj.feeStrategy).to.be.an('array').with.lengthOf(1); - expect(obj.feeStrategy[0]).to.deep.equal({ type: 'deductFromInput', index: 0 }); + expect(obj.feeStrategy[0]).to.deep.equal({ $type: 'deductFromInput', index: 0 }); }); - it('emits {type: "reduceOutput", index} in toObject() output', () => { + it('emits {$type: "reduceOutput", index} in toObject() output', () => { const transition = makeTransition([wasm.FeeStrategyStep.reduceOutput(3)]); const obj = transition.toObject(); - expect(obj.feeStrategy).to.deep.equal([{ type: 'reduceOutput', index: 3 }]); + expect(obj.feeStrategy).to.deep.equal([{ $type: 'reduceOutput', index: 3 }]); }); it('emits {type, index} in toJSON() output (matches Object form for this enum)', () => { @@ -84,8 +84,8 @@ describe('FeeStrategyStep', () => { const json = transition.toJSON(); expect(json.feeStrategy).to.deep.equal([ - { type: 'deductFromInput', index: 1 }, - { type: 'reduceOutput', index: 2 }, + { $type: 'deductFromInput', index: 1 }, + { $type: 'reduceOutput', index: 2 }, ]); }); diff --git a/packages/wasm-dpp2/tests/unit/GroupAction.spec.ts b/packages/wasm-dpp2/tests/unit/GroupAction.spec.ts index d35f39c1376..e759022b5fd 100644 --- a/packages/wasm-dpp2/tests/unit/GroupAction.spec.ts +++ b/packages/wasm-dpp2/tests/unit/GroupAction.spec.ts @@ -11,18 +11,24 @@ describe('GroupAction', () => { // Mint(amount, recipientId, publicNote) const recipientIdBase58 = '4fJLR2GYTPFdomuTVvNy3VRrvWgvkKPzqehEBpNf2nk6'; - // JSON fixture for a GroupAction V0 containing a TokenEvent::Mint + // JSON fixture for a GroupAction V0 containing a TokenEvent::Mint. + // + // Wire shape after rs-dpp PR #3573 (json-value unification): + // - GroupAction: `tag = "$formatVersion"`, V0 → "0" (unchanged) + // - GroupActionEvent: internally tagged `$kind:` (was adjacent `type/data`) + // - TokenEvent: custom Serialize emits flat named fields + // (was adjacent `type/data`-with-positional-tuple) const jsonFixture = { $formatVersion: '0', contract_id: contractIdBase58, proposer_id: proposerIdBase58, token_contract_position: 0, event: { - type: 'tokenEvent', - data: { - type: 'mint', - data: [1000, recipientIdBase58, 'test mint note'], - }, + $kind: 'tokenEvent', + $type: 'mint', + amount: 1000, + recipient: recipientIdBase58, + publicNote: 'test mint note', }, }; @@ -88,22 +94,21 @@ describe('GroupAction', () => { describe('GroupActionEvent', () => { const recipientIdBase58 = '4fJLR2GYTPFdomuTVvNy3VRrvWgvkKPzqehEBpNf2nk6'; - // TokenEvent::Freeze(frozenIdentifier, publicNote) + // GroupActionEvent: internally tagged `$kind:` (was adjacent `type/data`). + // Inner TokenEvent now flat-named — see TokenEvent describe block below. const freezeEventFixture = { - type: 'tokenEvent', - data: { - type: 'freeze', - data: [recipientIdBase58, 'freeze note'], - }, + $kind: 'tokenEvent', + $type: 'freeze', + frozenIdentifier: recipientIdBase58, + publicNote: 'freeze note', }; - // TokenEvent::Mint(amount, recipientId, publicNote) const mintEventFixture = { - type: 'tokenEvent', - data: { - type: 'mint', - data: [500, recipientIdBase58, null], - }, + $kind: 'tokenEvent', + $type: 'mint', + amount: 500, + recipient: recipientIdBase58, + publicNote: null, }; describe('fromJSON()', () => { @@ -177,22 +182,28 @@ describe('GroupActionEvent', () => { describe('TokenEvent', () => { const recipientIdBase58 = '4fJLR2GYTPFdomuTVvNy3VRrvWgvkKPzqehEBpNf2nk6'; - // TokenEvent::Mint(amount, recipientId, publicNote) + // TokenEvent now uses a custom Serialize impl that maps positional tuple + // fields to named JSON keys (`amount` / `recipient` / `burnFromIdentifier` / + // `frozenIdentifier` / `publicNote` / etc.), internally tagged with `$type:`, + // no `data` wrapper. Old shape was `{ $type: 'mint', data: [] }`. const mintFixture = { - type: 'mint', - data: [1000, recipientIdBase58, 'mint note'], + $type: 'mint', + amount: 1000, + recipient: recipientIdBase58, + publicNote: 'mint note', }; - // TokenEvent::Burn(amount, burnFromId, publicNote) const burnFixture = { - type: 'burn', - data: [500, recipientIdBase58, null], + $type: 'burn', + amount: 500, + burnFromIdentifier: recipientIdBase58, + publicNote: null, }; - // TokenEvent::Freeze(frozenId, publicNote) const freezeFixture = { - type: 'freeze', - data: [recipientIdBase58, 'frozen'], + $type: 'freeze', + frozenIdentifier: recipientIdBase58, + publicNote: 'frozen', }; describe('fromJSON()', () => { diff --git a/packages/wasm-dpp2/tests/unit/Identity.spec.ts b/packages/wasm-dpp2/tests/unit/Identity.spec.ts index 266f58ec025..f1fb5a76e8d 100644 --- a/packages/wasm-dpp2/tests/unit/Identity.spec.ts +++ b/packages/wasm-dpp2/tests/unit/Identity.spec.ts @@ -35,7 +35,9 @@ describe('Identity', () => { return identity; } - // Expected JSON representation (toJSON output - u64 fields as strings) + // Expected JSON representation (toJSON output - u64 fields as strings). + // After Phase D step 4, `disabledAt: null` is stripped at the rs-dpp layer + // for non-disabled keys via `#[serde(skip_serializing_if = "Option::is_none")]`. const expectedJSONOutput = { $formatVersion: '0', id: identifier, @@ -49,7 +51,6 @@ describe('Identity', () => { type: 0, readOnly: false, data: 'A2o5QxLkDoHZKP3iveeIAHDk+pwdHZsWjacH6kaK+itI', - disabledAt: null, }, ], balance: 100, @@ -70,14 +71,15 @@ describe('Identity', () => { type: 0, readOnly: false, data: 'A2o5QxLkDoHZKP3iveeIAHDk+pwdHZsWjacH6kaK+itI', - disabledAt: null, }, ], balance: 100, revision: 99111, }; - // Expected Object representation + // Expected Object representation. `disabledAt` is also stripped on the + // value path now (same `skip_serializing_if` attribute applies to both + // serde_json and platform_value paths). const expectedObject = { $formatVersion: '0', id: Uint8Array.from(identifierBytes), @@ -91,7 +93,6 @@ describe('Identity', () => { type: 0, readOnly: false, data: Buffer.from(binaryDataHex, 'hex'), - disabledAt: undefined, }, ], balance: BigInt(100), diff --git a/packages/wasm-dpp2/tests/unit/IdentityCreateFromAddressesTransition.spec.ts b/packages/wasm-dpp2/tests/unit/IdentityCreateFromAddressesTransition.spec.ts index 1fcdf2cf592..98c2f393323 100644 --- a/packages/wasm-dpp2/tests/unit/IdentityCreateFromAddressesTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/IdentityCreateFromAddressesTransition.spec.ts @@ -198,7 +198,7 @@ describe('IdentityCreateFromAddressesTransition', () => { const obj = transition.toObject(); expect(obj.feeStrategy).to.be.an('array'); - expect(obj.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(obj.feeStrategy[0].$type).to.be.oneOf(['deductFromInput', 'reduceOutput']); expect(obj.feeStrategy[0].index).to.be.a('number'); }); @@ -210,7 +210,7 @@ describe('IdentityCreateFromAddressesTransition', () => { expect(json.inputs[0].nonce).to.equal(0); expect(json.output).to.be.an('object'); expect(json.output.address).to.be.a('string').with.lengthOf(42); - expect(json.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(json.feeStrategy[0].$type).to.be.oneOf(['deductFromInput', 'reduceOutput']); }); it('fromObject(toObject()) round-trips identically', () => { diff --git a/packages/wasm-dpp2/tests/unit/IdentityCreateFromShieldedPoolTransition.spec.ts b/packages/wasm-dpp2/tests/unit/IdentityCreateFromShieldedPoolTransition.spec.ts new file mode 100644 index 00000000000..3850c622332 --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/IdentityCreateFromShieldedPoolTransition.spec.ts @@ -0,0 +1,144 @@ +import { expect } from './helpers/chai.ts'; +import { initWasm, wasm } from '../../dist/dpp.compressed.js'; +import { + fakeOrchardAction, + ZERO_ANCHOR, + ZERO_BINDING_SIG, + ZERO_PROOF, +} from './helpers/shielded.ts'; + +before(async () => { + await initWasm(); +}); + +describe('IdentityCreateFromShieldedPoolTransition', () => { + const addrBytes = new Uint8Array([ + 0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + ]); + + function createPublicKey() { + return new wasm.IdentityPublicKeyInCreation({ + keyId: 0, + purpose: 'AUTHENTICATION', + securityLevel: 'master', + keyType: 'ECDSA_SECP256K1', + isReadOnly: false, + data: Buffer.from( + '0333d5cf3674001d2f64c55617b7b11a2e8fc62aab09708b49355e30c7205bdb2e', + 'hex', + ), + signature: [], + }); + } + + function createTransition(actionSeed = 1) { + const fallbackAddr = wasm.PlatformAddress.fromBytes(addrBytes); + + return new wasm.IdentityCreateFromShieldedPoolTransition({ + publicKeys: [createPublicKey()], + denomination: BigInt(10_000_000_000), + actions: [fakeOrchardAction(actionSeed)], + anchor: ZERO_ANCHOR, + proof: ZERO_PROOF, + bindingSignature: ZERO_BINDING_SIG, + sendToAddressOnCreationFailure: fallbackAddr, + }); + } + + describe('constructor()', () => { + it('should construct with publicKeys + Orchard fields + fallback address', () => { + const t = createTransition(); + expect(t).to.be.an.instanceof(wasm.IdentityCreateFromShieldedPoolTransition); + }); + + it('should reject missing sendToAddressOnCreationFailure', () => { + expect(() => new wasm.IdentityCreateFromShieldedPoolTransition({ + publicKeys: [createPublicKey()], + denomination: BigInt(10_000_000_000), + actions: [fakeOrchardAction()], + anchor: ZERO_ANCHOR, + proof: ZERO_PROOF, + bindingSignature: ZERO_BINDING_SIG, + })).to.throw(); + }); + }); + + describe('getters', () => { + it('returns typed public keys', () => { + const t = createTransition(); + expect(t.publicKeys[0]).to.be.an.instanceof(wasm.IdentityPublicKeyInCreation); + }); + + it('returns the denomination', () => { + const t = createTransition(); + expect(t.denomination).to.equal(BigInt(10_000_000_000)); + }); + + it('returns typed Orchard actions', () => { + const t = createTransition(); + expect(t.actions[0]).to.be.an.instanceof(wasm.SerializedOrchardAction); + }); + + it('returns typed PlatformAddress for sendToAddressOnCreationFailure', () => { + const t = createTransition(); + const addr = t.sendToAddressOnCreationFailure; + expect(addr).to.be.an.instanceof(wasm.PlatformAddress); + expect(addr.toBytes()).to.deep.equal(addrBytes); + }); + + // The identity id is consensus-derived from the spend nullifiers (the + // constructor must not let callers pick an arbitrary id), so the same + // actions must yield the same id and different nullifiers a different one. + it('derives identityId deterministically from the action nullifiers', () => { + const a = createTransition(1); + const b = createTransition(1); + const c = createTransition(7); + expect(a.identityId.toHex()).to.equal(b.identityId.toHex()); + expect(a.identityId.toHex()).to.not.equal(c.identityId.toHex()); + }); + }); + + describe('toBytes() / fromBytes()', () => { + it('round-trips via bytes', () => { + const t = createTransition(); + const bytes = t.toBytes(); + const restored = wasm.IdentityCreateFromShieldedPoolTransition.fromBytes(bytes); + expect(Buffer.from(restored.toBytes())).to.deep.equal(Buffer.from(bytes)); + }); + }); + + describe('toObject() / toJSON()', () => { + it('toObject() emits the canonical wire shape', () => { + const t = createTransition(); + const obj = t.toObject(); + expect(obj.$formatVersion).to.equal('0'); + expect(obj.denomination).to.equal(BigInt(10_000_000_000)); + expect(obj.sendToAddressOnCreationFailure).to.be.instanceOf(Uint8Array); + expect(obj.sendToAddressOnCreationFailure.length).to.equal(21); + expect(obj.identityId).to.be.instanceOf(Uint8Array); + expect(obj.publicKeys).to.have.lengthOf(1); + }); + + it('toJSON() emits hex address + base58 identityId', () => { + const t = createTransition(); + const json = t.toJSON(); + expect(json.$formatVersion).to.equal('0'); + expect(json.sendToAddressOnCreationFailure).to.be.a('string').with.lengthOf(42); // 21 bytes hex + expect(json.identityId).to.be.a('string'); + expect(json.anchor).to.be.a('string'); + }); + + it('fromJSON(toJSON()) round-trips', () => { + const t = createTransition(); + const restored = wasm.IdentityCreateFromShieldedPoolTransition.fromJSON(t.toJSON()); + expect(Buffer.from(restored.toBytes())).to.deep.equal(Buffer.from(t.toBytes())); + }); + }); + + describe('toStateTransition()', () => { + it('should convert to StateTransition wrapper', () => { + const t = createTransition(); + expect(t.toStateTransition()).to.exist(); + }); + }); +}); diff --git a/packages/wasm-dpp2/tests/unit/IdentityCreateTransition.spec.ts b/packages/wasm-dpp2/tests/unit/IdentityCreateTransition.spec.ts index 0d7953aeb67..762bc4b57d7 100644 --- a/packages/wasm-dpp2/tests/unit/IdentityCreateTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/IdentityCreateTransition.spec.ts @@ -57,7 +57,7 @@ describe('IdentityCreateTransition', () => { expect(json.publicKeys.length).to.equal(0); // AssetLockProof emits internally-tagged shape: { type, ...inner fields } expect(json.assetLockProof).to.be.an('object'); - expect(json.assetLockProof.type).to.equal('instant'); + expect(json.assetLockProof.$type).to.equal('instant'); expect(json.assetLockProof.outputIndex).to.equal(0); expect(json.userFeeIncrease).to.equal(0); expect(json.signature).to.equal(''); diff --git a/packages/wasm-dpp2/tests/unit/IdentityPublicKey.spec.ts b/packages/wasm-dpp2/tests/unit/IdentityPublicKey.spec.ts index 062270d573e..b23e3d35247 100644 --- a/packages/wasm-dpp2/tests/unit/IdentityPublicKey.spec.ts +++ b/packages/wasm-dpp2/tests/unit/IdentityPublicKey.spec.ts @@ -296,13 +296,18 @@ describe('IdentityPublicKey', () => { const json = pubKey.toJSON(); + // Canonical JSON wire shape: binary fields render as base64 + // strings (matching `IdentityWasm.toJSON`'s embedded keys and + // every other rs-dpp type's canonical JSON output). The + // validating-JSON byte-array shape was dropped along with the + // legacy `to_json_object` trait method. expect(json.id).to.equal(keyId); expect(json.purpose).to.equal(0); // AUTHENTICATION expect(json.securityLevel).to.equal(1); // CRITICAL expect(json.contractBounds).to.be.null(); expect(json.type).to.equal(0); // ECDSA_SECP256K1 expect(json.readOnly).to.equal(false); - expect(Array.from(json.data)).to.deep.equal(Array.from(binaryData)); + expect(json.data).to.equal('A2o5QxLkDoHZKP3iveeIAHDk+pwdHZsWjacH6kaK+itI'); }); }); diff --git a/packages/wasm-dpp2/tests/unit/IdentityTopUpFromAddressesTransition.spec.ts b/packages/wasm-dpp2/tests/unit/IdentityTopUpFromAddressesTransition.spec.ts index 1a925ef8af2..e71c66a2da4 100644 --- a/packages/wasm-dpp2/tests/unit/IdentityTopUpFromAddressesTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/IdentityTopUpFromAddressesTransition.spec.ts @@ -191,7 +191,7 @@ describe('IdentityTopUpFromAddressesTransition', () => { const obj = transition.toObject(); expect(obj.feeStrategy).to.be.an('array'); - expect(obj.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(obj.feeStrategy[0].$type).to.be.oneOf(['deductFromInput', 'reduceOutput']); expect(obj.feeStrategy[0].index).to.be.a('number'); }); @@ -202,7 +202,7 @@ describe('IdentityTopUpFromAddressesTransition', () => { expect(json.inputs[0].address).to.be.a('string').with.lengthOf(42); expect(json.output).to.exist(); expect(json.output.address).to.be.a('string').with.lengthOf(42); - expect(json.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(json.feeStrategy[0].$type).to.be.oneOf(['deductFromInput', 'reduceOutput']); }); it('fromObject(toObject()) round-trips identically', () => { diff --git a/packages/wasm-dpp2/tests/unit/IdentityTopUpTransition.spec.ts b/packages/wasm-dpp2/tests/unit/IdentityTopUpTransition.spec.ts index 818d6bdec12..5efb63e85a1 100644 --- a/packages/wasm-dpp2/tests/unit/IdentityTopUpTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/IdentityTopUpTransition.spec.ts @@ -164,7 +164,7 @@ describe('IdentityTopUpTransition', () => { expect(json.identityId).to.equal(testIdentityId); // AssetLockProof emits internally-tagged shape: { type, ...inner fields } expect(json.assetLockProof).to.be.an('object'); - expect(json.assetLockProof.type).to.equal('instant'); + expect(json.assetLockProof.$type).to.equal('instant'); expect(json.assetLockProof).to.have.property('instantLock'); expect(json.assetLockProof).to.have.property('transaction'); expect(json.assetLockProof.outputIndex).to.equal(0); diff --git a/packages/wasm-dpp2/tests/unit/ProofResult.spec.ts b/packages/wasm-dpp2/tests/unit/ProofResult.spec.ts index bcd33cceda6..5aa80782f5c 100644 --- a/packages/wasm-dpp2/tests/unit/ProofResult.spec.ts +++ b/packages/wasm-dpp2/tests/unit/ProofResult.spec.ts @@ -302,26 +302,24 @@ describe('StateTransitionProofResult types', () => { describe('VerifiedMasternodeVote', () => { it('should construct from object with Abstain vote', () => { - // Vote: tag = "type", content = "data", rename_all = "camelCase" - // ResourceVote: tag = "$formatVersion", V0 renamed to "0" - // VotePoll: tag = "type", content = "data", rename_all = "camelCase" - // ResourceVoteChoice: tag = "type", content = "data", rename_all = "camelCase" + // Vote: internal tag `$type` ($-prefix because the level + // also carries the inner `$formatVersion`) + // ResourceVote: single V0 variant flattens its body, contributing + // `$formatVersion: "0"` at top level + // VotePoll: internal tag `type` (plain — no `$`-fields here) + // ResourceVoteChoice: custom serde, flat `{type, identity?}` const data = { vote: { - type: 'resourceVote', - data: { - $formatVersion: '0', - votePoll: { - type: 'contestedDocumentResourceVotePoll', - data: { - contractId: identifier, - documentTypeName: 'domain', - indexName: 'parentNameAndLabel', - indexValues: ['dash', 'test'], - }, - }, - resourceVoteChoice: { type: 'abstain' }, + $type: 'resourceVote', + $formatVersion: '0', + votePoll: { + $type: 'contestedDocumentResourceVotePoll', + contractId: identifier, + documentTypeName: 'domain', + indexName: 'parentNameAndLabel', + indexValues: ['dash', 'test'], }, + resourceVoteChoice: { $type: 'abstain' }, }, }; const result = wasm.VerifiedMasternodeVote.fromObject(data); @@ -337,20 +335,16 @@ describe('StateTransitionProofResult types', () => { it('should construct from object with Abstain vote', () => { const data = { vote: { - type: 'resourceVote', - data: { - $formatVersion: '0', - votePoll: { - type: 'contestedDocumentResourceVotePoll', - data: { - contractId: identifier, - documentTypeName: 'domain', - indexName: 'parentNameAndLabel', - indexValues: ['dash', 'test'], - }, - }, - resourceVoteChoice: { type: 'abstain' }, + $type: 'resourceVote', + $formatVersion: '0', + votePoll: { + $type: 'contestedDocumentResourceVotePoll', + contractId: identifier, + documentTypeName: 'domain', + indexName: 'parentNameAndLabel', + indexValues: ['dash', 'test'], }, + resourceVoteChoice: { $type: 'abstain' }, }, }; const result = wasm.VerifiedNextDistribution.fromObject(data); diff --git a/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts b/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts index 10d25f78dc8..f1aeef73113 100644 --- a/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts +++ b/packages/wasm-dpp2/tests/unit/ResourceVote.spec.ts @@ -26,10 +26,12 @@ describe('ResourceVote', () => { const json = vote.toJSON(); + // VotePoll inside is internally tagged — fields flat at votePoll + // level, no `data` wrapper. expect(json.$formatVersion).to.equal('0'); expect(json.votePoll).to.exist(); - expect(json.votePoll.type).to.equal('contestedDocumentResourceVotePoll'); - expect(json.votePoll.data.contractId).to.equal(testContractId); + expect(json.votePoll.$type).to.equal('contestedDocumentResourceVotePoll'); + expect(json.votePoll.contractId).to.equal(testContractId); expect(json.resourceVoteChoice).to.exist(); vote.free(); @@ -42,7 +44,7 @@ describe('ResourceVote', () => { const json = vote.toJSON(); - expect(json.resourceVoteChoice).to.deep.equal({ type: 'abstain' }); + expect(json.resourceVoteChoice).to.deep.equal({ $type: 'abstain' }); vote.free(); }); @@ -75,9 +77,9 @@ describe('ResourceVote', () => { expect(obj.$formatVersion).to.equal('0'); expect(obj.votePoll).to.exist(); - expect(obj.votePoll.type).to.equal('contestedDocumentResourceVotePoll'); - expect(obj.votePoll.data.contractId).to.be.instanceOf(Uint8Array); - expect(obj.resourceVoteChoice).to.deep.equal({ type: 'lock' }); + expect(obj.votePoll.$type).to.equal('contestedDocumentResourceVotePoll'); + expect(obj.votePoll.contractId).to.be.instanceOf(Uint8Array); + expect(obj.resourceVoteChoice).to.deep.equal({ $type: 'lock' }); vote.free(); }); diff --git a/packages/wasm-dpp2/tests/unit/ResourceVoteChoice.spec.ts b/packages/wasm-dpp2/tests/unit/ResourceVoteChoice.spec.ts index d5bf22579a7..f56a32fa4d8 100644 --- a/packages/wasm-dpp2/tests/unit/ResourceVoteChoice.spec.ts +++ b/packages/wasm-dpp2/tests/unit/ResourceVoteChoice.spec.ts @@ -51,21 +51,21 @@ describe('ResourceVoteChoice', () => { const choice = wasm.ResourceVoteChoice.TowardsIdentity(identityId); const json = choice.toJSON(); - expect(json).to.deep.equal({ type: 'towardsIdentity', data: identityIdBase58 }); + expect(json).to.deep.equal({ $type: 'towardsIdentity', identity: identityIdBase58 }); }); it('should serialize Abstain to JSON', () => { const choice = wasm.ResourceVoteChoice.Abstain(); const json = choice.toJSON(); - expect(json).to.deep.equal({ type: 'abstain' }); + expect(json).to.deep.equal({ $type: 'abstain' }); }); it('should serialize Lock to JSON', () => { const choice = wasm.ResourceVoteChoice.Lock(); const json = choice.toJSON(); - expect(json).to.deep.equal({ type: 'lock' }); + expect(json).to.deep.equal({ $type: 'lock' }); }); }); @@ -74,7 +74,7 @@ describe('ResourceVoteChoice', () => { const identityId = wasm.Identifier.fromHex(identityIdHex); const identityIdBase58 = identityId.toBase58(); - const fixture = { type: 'towardsIdentity', data: identityIdBase58 }; + const fixture = { $type: 'towardsIdentity', identity: identityIdBase58 }; const restored = wasm.ResourceVoteChoice.fromJSON(fixture); expect(restored.voteType).to.equal('TowardsIdentity'); @@ -83,13 +83,13 @@ describe('ResourceVoteChoice', () => { }); it('should create Abstain from JSON fixture', () => { - const restored = wasm.ResourceVoteChoice.fromJSON({ type: 'abstain' }); + const restored = wasm.ResourceVoteChoice.fromJSON({ $type: 'abstain' }); expect(restored.voteType).to.equal('Abstain'); expect(restored.value).to.be.undefined(); }); it('should create Lock from JSON fixture', () => { - const restored = wasm.ResourceVoteChoice.fromJSON({ type: 'lock' }); + const restored = wasm.ResourceVoteChoice.fromJSON({ $type: 'lock' }); expect(restored.voteType).to.equal('Lock'); expect(restored.value).to.be.undefined(); }); @@ -102,23 +102,23 @@ describe('ResourceVoteChoice', () => { const obj = choice.toObject(); expect(obj).to.be.an('object'); - expect(obj.type).to.equal('towardsIdentity'); - expect(obj.data).to.be.instanceOf(Uint8Array); - expect(Buffer.from(obj.data).toString('hex')).to.equal(identityIdHex); + expect(obj.$type).to.equal('towardsIdentity'); + expect(obj.identity).to.be.instanceOf(Uint8Array); + expect(Buffer.from(obj.identity).toString('hex')).to.equal(identityIdHex); }); it('should serialize Abstain to object', () => { const choice = wasm.ResourceVoteChoice.Abstain(); const obj = choice.toObject(); - expect(obj).to.deep.equal({ type: 'abstain' }); + expect(obj).to.deep.equal({ $type: 'abstain' }); }); it('should serialize Lock to object', () => { const choice = wasm.ResourceVoteChoice.Lock(); const obj = choice.toObject(); - expect(obj).to.deep.equal({ type: 'lock' }); + expect(obj).to.deep.equal({ $type: 'lock' }); }); }); @@ -126,7 +126,7 @@ describe('ResourceVoteChoice', () => { it('should create TowardsIdentity from object fixture and verify getters', () => { const identityIdBytes = new Uint8Array(Buffer.from(identityIdHex, 'hex')); - const fixture = { type: 'towardsIdentity', data: identityIdBytes }; + const fixture = { $type: 'towardsIdentity', identity: identityIdBytes }; const restored = wasm.ResourceVoteChoice.fromObject(fixture); expect(restored.voteType).to.equal('TowardsIdentity'); @@ -135,13 +135,13 @@ describe('ResourceVoteChoice', () => { }); it('should create Abstain from object fixture', () => { - const restored = wasm.ResourceVoteChoice.fromObject({ type: 'abstain' }); + const restored = wasm.ResourceVoteChoice.fromObject({ $type: 'abstain' }); expect(restored.voteType).to.equal('Abstain'); expect(restored.value).to.be.undefined(); }); it('should create Lock from object fixture', () => { - const restored = wasm.ResourceVoteChoice.fromObject({ type: 'lock' }); + const restored = wasm.ResourceVoteChoice.fromObject({ $type: 'lock' }); expect(restored.voteType).to.equal('Lock'); expect(restored.value).to.be.undefined(); }); diff --git a/packages/wasm-dpp2/tests/unit/ShieldFromAssetLockTransition.spec.ts b/packages/wasm-dpp2/tests/unit/ShieldFromAssetLockTransition.spec.ts index f1ed5196b5a..5bc84d4d8df 100644 --- a/packages/wasm-dpp2/tests/unit/ShieldFromAssetLockTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/ShieldFromAssetLockTransition.spec.ts @@ -74,9 +74,9 @@ describe('ShieldFromAssetLockTransition', () => { it('toObject() emits AssetLockProof in internally-tagged Object form', () => { const t = createTransition(); const obj = t.toObject(); - // Internally-tagged: { type: "instant" | "chain", ...flattened inner fields } + // Internally-tagged: { $type: "instant" | "chain", ...flattened inner fields } expect(obj.assetLockProof).to.be.an('object'); - expect(obj.assetLockProof.type).to.be.oneOf(['instant', 'chain']); + expect(obj.assetLockProof.$type).to.be.oneOf(['instant', 'chain']); expect(obj.actions).to.be.an('array').with.lengthOf(1); expect(obj.anchor).to.be.instanceOf(Uint8Array).with.lengthOf(32); expect(obj.bindingSignature).to.be.instanceOf(Uint8Array).with.lengthOf(64); @@ -86,7 +86,7 @@ describe('ShieldFromAssetLockTransition', () => { const t = createTransition(); const json = t.toJSON(); expect(json.assetLockProof).to.be.an('object'); - expect(json.assetLockProof.type).to.be.oneOf(['instant', 'chain']); + expect(json.assetLockProof.$type).to.be.oneOf(['instant', 'chain']); expect(json.actions).to.be.an('array').with.lengthOf(1); expect(json.anchor).to.be.a('string').with.lengthOf(44); // 32 bytes base64 expect(json.bindingSignature).to.be.a('string').with.lengthOf(88); // 64 bytes base64 diff --git a/packages/wasm-dpp2/tests/unit/ShieldTransition.spec.ts b/packages/wasm-dpp2/tests/unit/ShieldTransition.spec.ts index 33a3e228f3e..00784f26ebb 100644 --- a/packages/wasm-dpp2/tests/unit/ShieldTransition.spec.ts +++ b/packages/wasm-dpp2/tests/unit/ShieldTransition.spec.ts @@ -89,7 +89,7 @@ describe('ShieldTransition', () => { const obj = t.toObject(); expect(obj.feeStrategy).to.be.an('array'); if (obj.feeStrategy.length > 0) { - expect(obj.feeStrategy[0].type).to.be.oneOf(['deductFromInput', 'reduceOutput']); + expect(obj.feeStrategy[0].$type).to.be.oneOf(['deductFromInput', 'reduceOutput']); expect(obj.feeStrategy[0].index).to.be.a('number'); } }); diff --git a/packages/wasm-dpp2/tests/unit/TokenContractInfo.spec.ts b/packages/wasm-dpp2/tests/unit/TokenContractInfo.spec.ts index 9de2c60bf61..11734c3a94f 100644 --- a/packages/wasm-dpp2/tests/unit/TokenContractInfo.spec.ts +++ b/packages/wasm-dpp2/tests/unit/TokenContractInfo.spec.ts @@ -9,9 +9,13 @@ before(async () => { describe('TokenContractInfo', () => { const contractIdHex = '1111111111111111111111111111111111111111111111111111111111111111'; + // TokenContractInfo is a versioned enum tagged with `$formatVersion`. + // V0 -> "0". Inner V0 fields (contractId, tokenContractPosition) flatten + // at the top level via internal tagging. function createJsonFixture() { const contractId = wasm.Identifier.fromHex(contractIdHex); return { + $formatVersion: '0', contractId: contractId.toBase58(), tokenContractPosition: 3, }; @@ -19,6 +23,7 @@ describe('TokenContractInfo', () => { function createObjectFixture() { return { + $formatVersion: '0', contractId: new Uint8Array(Buffer.from(contractIdHex, 'hex')), tokenContractPosition: 3, }; diff --git a/packages/wasm-dpp2/tests/unit/VerifiedAssetLockConsumedWithAddressInfos.spec.ts b/packages/wasm-dpp2/tests/unit/VerifiedAssetLockConsumedWithAddressInfos.spec.ts new file mode 100644 index 00000000000..2f0cba86a15 --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/VerifiedAssetLockConsumedWithAddressInfos.spec.ts @@ -0,0 +1,52 @@ +import { expect } from './helpers/chai.ts'; +import { initWasm, wasm } from '../../dist/dpp.compressed.js'; + +before(async () => { + await initWasm(); +}); + +describe('VerifiedAssetLockConsumedWithAddressInfos', () => { + function fromObject(overrides = {}) { + return wasm.VerifiedAssetLockConsumedWithAddressInfos.fromObject({ + status: 'consumed', + initialCreditValue: BigInt(1000), + remainingCreditValue: BigInt(400), + addressInfos: {}, + ...overrides, + }); + } + + describe('fromObject()', () => { + it('accepts BigInt, string and number credit values', () => { + expect(fromObject().initialCreditValue).to.equal(BigInt(1000)); + expect(fromObject({ initialCreditValue: '1000' }).initialCreditValue) + .to.equal(BigInt(1000)); + expect(fromObject({ initialCreditValue: 1000 }).initialCreditValue) + .to.equal(BigInt(1000)); + }); + + it('treats absent / null credit values as absent', () => { + const t = fromObject({ initialCreditValue: undefined, remainingCreditValue: null }); + expect(t.initialCreditValue).to.be.undefined(); + expect(t.remainingCreditValue).to.be.undefined(); + }); + + // Present-but-garbage must error rather than silently becoming absent — + // otherwise a malformed proof-result object is indistinguishable from + // "no surplus" and bad input is hidden at the WASM boundary. + it('rejects present-but-malformed credit values', () => { + expect(() => fromObject({ initialCreditValue: 'not_a_number' })).to.throw(); + expect(() => fromObject({ remainingCreditValue: -5 })).to.throw(); + expect(() => fromObject({ initialCreditValue: 1.5 })).to.throw(); + }); + + it('round-trips via toJSON / JSON.stringify', () => { + const t = fromObject(); + const restored = wasm.VerifiedAssetLockConsumedWithAddressInfos.fromJSON( + JSON.parse(JSON.stringify(t.toJSON())), + ); + expect(restored.initialCreditValue).to.equal(BigInt(1000)); + expect(restored.remainingCreditValue).to.equal(BigInt(400)); + }); + }); +}); diff --git a/packages/wasm-dpp2/tests/unit/Vote.spec.ts b/packages/wasm-dpp2/tests/unit/Vote.spec.ts index 6405bbe9465..ed5708195f8 100644 --- a/packages/wasm-dpp2/tests/unit/Vote.spec.ts +++ b/packages/wasm-dpp2/tests/unit/Vote.spec.ts @@ -19,19 +19,22 @@ describe('Vote', () => { } describe('toJSON()', () => { - it('should serialize with resourceVote type tag', () => { + it('should serialize with $type tag and flat ResourceVote fields', () => { + // Vote is internally tagged with `$type` (`$`-prefix because the level + // also carries the inner ResourceVote's `$formatVersion`). The single + // ResourceVote variant flattens its V0 body at the same level — no + // `data` wrapper. VotePoll inside is also flat-tagged. const poll = createPoll(); const choice = wasm.ResourceVoteChoice.TowardsIdentity(testIdentityId); const vote = new wasm.Vote(poll, choice); const json = vote.toJSON(); - expect(json.type).to.equal('resourceVote'); - expect(json.data).to.exist(); - expect(json.data.$formatVersion).to.equal('0'); - expect(json.data.votePoll.type).to.equal('contestedDocumentResourceVotePoll'); - expect(json.data.votePoll.data.contractId).to.equal(testContractId); - expect(json.data.resourceVoteChoice).to.exist(); + expect(json.$type).to.equal('resourceVote'); + expect(json.$formatVersion).to.equal('0'); + expect(json.votePoll.$type).to.equal('contestedDocumentResourceVotePoll'); + expect(json.votePoll.contractId).to.equal(testContractId); + expect(json.resourceVoteChoice).to.exist(); vote.free(); }); @@ -62,11 +65,10 @@ describe('Vote', () => { const obj = vote.toObject(); - expect(obj.type).to.equal('resourceVote'); - expect(obj.data).to.exist(); - expect(obj.data.$formatVersion).to.equal('0'); - expect(obj.data.votePoll.type).to.equal('contestedDocumentResourceVotePoll'); - expect(obj.data.votePoll.data.contractId).to.be.instanceOf(Uint8Array); + expect(obj.$type).to.equal('resourceVote'); + expect(obj.$formatVersion).to.equal('0'); + expect(obj.votePoll.$type).to.equal('contestedDocumentResourceVotePoll'); + expect(obj.votePoll.contractId).to.be.instanceOf(Uint8Array); vote.free(); }); diff --git a/packages/wasm-dpp2/tests/unit/VotePoll.spec.ts b/packages/wasm-dpp2/tests/unit/VotePoll.spec.ts index 75d767e07cf..a5dd2e4f755 100644 --- a/packages/wasm-dpp2/tests/unit/VotePoll.spec.ts +++ b/packages/wasm-dpp2/tests/unit/VotePoll.spec.ts @@ -16,16 +16,17 @@ describe('VotePoll', () => { }; describe('toJSON()', () => { - it('should serialize with type tag and data', () => { + it('should serialize with type tag and flat fields', () => { + // VotePoll is internally tagged (`tag = "$type"`) — no `data` wrapper. + // Plain `type` because the level has no other `$`-prefixed fields. const poll = new wasm.VotePoll(votePollOptions); const json = poll.toJSON(); - expect(json.type).to.equal('contestedDocumentResourceVotePoll'); - expect(json.data).to.exist(); - expect(json.data.contractId).to.equal(testContractId); - expect(json.data.documentTypeName).to.equal('domain'); - expect(json.data.indexName).to.equal('parentNameAndLabel'); - expect(json.data.indexValues).to.deep.equal(['dash', 'alice']); + expect(json.$type).to.equal('contestedDocumentResourceVotePoll'); + expect(json.contractId).to.equal(testContractId); + expect(json.documentTypeName).to.equal('domain'); + expect(json.indexName).to.equal('parentNameAndLabel'); + expect(json.indexValues).to.deep.equal(['dash', 'alice']); poll.free(); }); @@ -34,13 +35,11 @@ describe('VotePoll', () => { describe('fromJSON()', () => { it('should deserialize from JSON fixture', () => { const fixture = { - type: 'contestedDocumentResourceVotePoll', - data: { - contractId: testContractId, - documentTypeName: 'domain', - indexName: 'parentNameAndLabel', - indexValues: ['dash', 'alice'], - }, + $type: 'contestedDocumentResourceVotePoll', + contractId: testContractId, + documentTypeName: 'domain', + indexName: 'parentNameAndLabel', + indexValues: ['dash', 'alice'], }; const poll = wasm.VotePoll.fromJSON(fixture); @@ -67,15 +66,14 @@ describe('VotePoll', () => { }); describe('toObject()', () => { - it('should serialize with type tag and Uint8Array contractId in data', () => { + it('should serialize with type tag and Uint8Array contractId at top level', () => { const poll = new wasm.VotePoll(votePollOptions); const obj = poll.toObject(); - expect(obj.type).to.equal('contestedDocumentResourceVotePoll'); - expect(obj.data).to.exist(); - expect(obj.data.contractId).to.be.instanceOf(Uint8Array); - expect(obj.data.documentTypeName).to.equal('domain'); - expect(obj.data.indexName).to.equal('parentNameAndLabel'); + expect(obj.$type).to.equal('contestedDocumentResourceVotePoll'); + expect(obj.contractId).to.be.instanceOf(Uint8Array); + expect(obj.documentTypeName).to.equal('domain'); + expect(obj.indexName).to.equal('parentNameAndLabel'); poll.free(); }); diff --git a/packages/wasm-drive-verify/src/group/verify_active_action_infos.rs b/packages/wasm-drive-verify/src/group/verify_active_action_infos.rs index 9a328ca8e19..050cdaef27c 100644 --- a/packages/wasm-drive-verify/src/group/verify_active_action_infos.rs +++ b/packages/wasm-drive-verify/src/group/verify_active_action_infos.rs @@ -1,13 +1,8 @@ use crate::utils::getters::VecU8ToUint8Array; -use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionTypeWithResolvedRecipient; -use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionResolvedRecipient; use dpp::group::action_event::GroupActionEvent; use dpp::group::group_action::GroupAction; use dpp::group::group_action_status::GroupActionStatus; use dpp::identifier::Identifier; -use dpp::tokens::emergency_action::TokenEmergencyAction; -use dpp::tokens::token_event::TokenEvent; -use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; use dpp::version::PlatformVersion; use drive::drive::Drive; use drive::verify::RootHash; @@ -245,459 +240,154 @@ pub fn verify_action_infos_in_contract_map( // Helper function to convert GroupActionEvent to JS object fn group_action_event_to_js(event: &GroupActionEvent) -> Result { - match event { - GroupActionEvent::TokenEvent(token_event) => { - let event_obj = Object::new(); - - let token_event_js = token_event_to_js(token_event)?; - Reflect::set( - &event_obj, - &JsValue::from_str("TokenEvent"), - &token_event_js, - ) - .map_err(|_| JsValue::from_str("Failed to set TokenEvent"))?; + // Serialize via DPP's canonical serde, matching wasm-dpp2's `to_object`: + // internally tagged (`$kind`/`$type`), identifier and byte fields become + // `Uint8Array`, and u64 amounts become `BigInt` (JS-safe above 2^53). This + // replaces a hand-rolled construction that emitted a divergent bare-`type`, + // PascalCase shape. + use serde::Serialize; + let value = dpp::platform_value::to_value(event) + .map_err(|e| JsValue::from_str(&format!("Failed to convert group action event: {e}")))?; + // rs-dpp preserves typed map keys (e.g. `TokenPricingSchedule::SetPrices` + // emits `Value::U64` keys); JS plain objects require string keys, so + // stringify them before handing the tree to serde_wasm_bindgen. + let normalized = stringify_map_keys_for_object(&value); + let serializer = serde_wasm_bindgen::Serializer::new() + .serialize_maps_as_objects(true) + .serialize_bytes_as_arrays(false) + .serialize_large_number_types_as_bigints(true); + normalized + .serialize(&serializer) + .map_err(|e| JsValue::from_str(&format!("Failed to serialize group action event: {e}"))) +} - Ok(event_obj.into()) +/// Recursively normalize a `Value` tree so that all `Map` keys are `Value::Text`. +/// JS plain objects require string keys, but rs-dpp preserves typed map keys +/// (e.g. `BTreeMap` emits `Value::U64` keys). Mirrors +/// wasm-dpp2's `platform_value_to_object` normalization. +fn stringify_map_keys_for_object(value: &dpp::platform_value::Value) -> dpp::platform_value::Value { + use dpp::platform_value::Value; + match value { + Value::Map(entries) => Value::Map( + entries + .iter() + .map(|(k, v)| (stringify_key(k), stringify_map_keys_for_object(v))) + .collect(), + ), + Value::Array(items) => { + Value::Array(items.iter().map(stringify_map_keys_for_object).collect()) } + other => other.clone(), } } -// Helper function to convert TokenEvent to JS object -fn token_event_to_js(event: &TokenEvent) -> Result { - let obj = Object::new(); - - match event { - TokenEvent::Mint(amount, recipient, note) => { - Reflect::set(&obj, &JsValue::from_str("type"), &JsValue::from_str("Mint")) - .map_err(|_| JsValue::from_str("Failed to set type"))?; - Reflect::set( - &obj, - &JsValue::from_str("amount"), - &JsValue::from_str(&amount.to_string()), - ) - .map_err(|_| JsValue::from_str("Failed to set amount"))?; - - let recipient_array = Uint8Array::from(recipient.as_slice()); - Reflect::set(&obj, &JsValue::from_str("recipient"), &recipient_array) - .map_err(|_| JsValue::from_str("Failed to set recipient"))?; - - match note { - Some(n) => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::from_str(n)), - None => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::NULL), - } - .map_err(|_| JsValue::from_str("Failed to set note"))?; - } - TokenEvent::Burn(amount, burn_from, note) => { - Reflect::set(&obj, &JsValue::from_str("type"), &JsValue::from_str("Burn")) - .map_err(|_| JsValue::from_str("Failed to set type"))?; - Reflect::set( - &obj, - &JsValue::from_str("amount"), - &JsValue::from_str(&amount.to_string()), - ) - .map_err(|_| JsValue::from_str("Failed to set amount"))?; - - let burn_from_array = Uint8Array::from(burn_from.as_slice()); - Reflect::set(&obj, &JsValue::from_str("burnFrom"), &burn_from_array) - .map_err(|_| JsValue::from_str("Failed to set burnFrom"))?; - - match note { - Some(n) => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::from_str(n)), - None => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::NULL), - } - .map_err(|_| JsValue::from_str("Failed to set note"))?; - } - TokenEvent::Freeze(frozen_identity, note) => { - Reflect::set( - &obj, - &JsValue::from_str("type"), - &JsValue::from_str("Freeze"), - ) - .map_err(|_| JsValue::from_str("Failed to set type"))?; - - let frozen_array = Uint8Array::from(frozen_identity.as_slice()); - Reflect::set(&obj, &JsValue::from_str("frozenIdentity"), &frozen_array) - .map_err(|_| JsValue::from_str("Failed to set frozenIdentity"))?; - - match note { - Some(n) => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::from_str(n)), - None => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::NULL), - } - .map_err(|_| JsValue::from_str("Failed to set note"))?; - } - TokenEvent::Unfreeze(frozen_identity, note) => { - Reflect::set( - &obj, - &JsValue::from_str("type"), - &JsValue::from_str("Unfreeze"), - ) - .map_err(|_| JsValue::from_str("Failed to set type"))?; - - let frozen_array = Uint8Array::from(frozen_identity.as_slice()); - Reflect::set(&obj, &JsValue::from_str("frozenIdentity"), &frozen_array) - .map_err(|_| JsValue::from_str("Failed to set frozenIdentity"))?; - - match note { - Some(n) => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::from_str(n)), - None => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::NULL), - } - .map_err(|_| JsValue::from_str("Failed to set note"))?; - } - TokenEvent::DestroyFrozenFunds(frozen_identity, amount, note) => { - Reflect::set( - &obj, - &JsValue::from_str("type"), - &JsValue::from_str("DestroyFrozenFunds"), - ) - .map_err(|_| JsValue::from_str("Failed to set type"))?; - - let frozen_array = Uint8Array::from(frozen_identity.as_slice()); - Reflect::set(&obj, &JsValue::from_str("frozenIdentity"), &frozen_array) - .map_err(|_| JsValue::from_str("Failed to set frozenIdentity"))?; - - Reflect::set( - &obj, - &JsValue::from_str("amount"), - &JsValue::from_str(&amount.to_string()), - ) - .map_err(|_| JsValue::from_str("Failed to set amount"))?; - - match note { - Some(n) => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::from_str(n)), - None => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::NULL), - } - .map_err(|_| JsValue::from_str("Failed to set note"))?; - } - TokenEvent::Transfer( - recipient, - public_note, - shared_encrypted_note, - personal_encrypted_note, - amount, - ) => { - Reflect::set( - &obj, - &JsValue::from_str("type"), - &JsValue::from_str("Transfer"), - ) - .map_err(|_| JsValue::from_str("Failed to set type"))?; - - let recipient_array = Uint8Array::from(recipient.as_slice()); - Reflect::set(&obj, &JsValue::from_str("recipient"), &recipient_array) - .map_err(|_| JsValue::from_str("Failed to set recipient"))?; - - match public_note { - Some(n) => Reflect::set( - &obj, - &JsValue::from_str("publicNote"), - &JsValue::from_str(n), - ), - None => Reflect::set(&obj, &JsValue::from_str("publicNote"), &JsValue::NULL), - } - .map_err(|_| JsValue::from_str("Failed to set publicNote"))?; - - // Serialize shared encrypted note (optional) - match shared_encrypted_note { - Some((sender_key_index, recipient_key_index, encrypted_data)) => { - let shared_note_obj = Object::new(); - Reflect::set( - &shared_note_obj, - &JsValue::from_str("senderKeyIndex"), - &JsValue::from(*sender_key_index), - ) - .map_err(|_| JsValue::from_str("Failed to set senderKeyIndex"))?; - Reflect::set( - &shared_note_obj, - &JsValue::from_str("recipientKeyIndex"), - &JsValue::from(*recipient_key_index), - ) - .map_err(|_| JsValue::from_str("Failed to set recipientKeyIndex"))?; - let encrypted_array = Uint8Array::from(encrypted_data.as_slice()); - Reflect::set( - &shared_note_obj, - &JsValue::from_str("encryptedData"), - &encrypted_array, - ) - .map_err(|_| JsValue::from_str("Failed to set encryptedData"))?; - Reflect::set( - &obj, - &JsValue::from_str("sharedEncryptedNote"), - &shared_note_obj, - ) - .map_err(|_| JsValue::from_str("Failed to set sharedEncryptedNote"))?; - } - None => { - Reflect::set( - &obj, - &JsValue::from_str("sharedEncryptedNote"), - &JsValue::NULL, - ) - .map_err(|_| JsValue::from_str("Failed to set sharedEncryptedNote"))?; - } - } - - // Serialize personal encrypted note (optional) - match personal_encrypted_note { - Some((root_key_index, derivation_key_index, encrypted_data)) => { - let personal_note_obj = Object::new(); - Reflect::set( - &personal_note_obj, - &JsValue::from_str("rootKeyIndex"), - &JsValue::from(*root_key_index), - ) - .map_err(|_| JsValue::from_str("Failed to set rootKeyIndex"))?; - Reflect::set( - &personal_note_obj, - &JsValue::from_str("derivationKeyIndex"), - &JsValue::from(*derivation_key_index), - ) - .map_err(|_| JsValue::from_str("Failed to set derivationKeyIndex"))?; - let encrypted_array = Uint8Array::from(encrypted_data.as_slice()); - Reflect::set( - &personal_note_obj, - &JsValue::from_str("encryptedData"), - &encrypted_array, - ) - .map_err(|_| JsValue::from_str("Failed to set encryptedData"))?; - Reflect::set( - &obj, - &JsValue::from_str("personalEncryptedNote"), - &personal_note_obj, - ) - .map_err(|_| JsValue::from_str("Failed to set personalEncryptedNote"))?; - } - None => { - Reflect::set( - &obj, - &JsValue::from_str("personalEncryptedNote"), - &JsValue::NULL, - ) - .map_err(|_| JsValue::from_str("Failed to set personalEncryptedNote"))?; - } - } - - Reflect::set( - &obj, - &JsValue::from_str("amount"), - &JsValue::from_str(&amount.to_string()), - ) - .map_err(|_| JsValue::from_str("Failed to set amount"))?; - } - TokenEvent::Claim(distribution_type, amount, note) => { - Reflect::set( - &obj, - &JsValue::from_str("type"), - &JsValue::from_str("Claim"), - ) - .map_err(|_| JsValue::from_str("Failed to set type"))?; - - // Serialize distribution type - let dist_obj = Object::new(); - match distribution_type { - TokenDistributionTypeWithResolvedRecipient::PreProgrammed(id) => { - Reflect::set( - &dist_obj, - &JsValue::from_str("type"), - &JsValue::from_str("PreProgrammed"), - ) - .map_err(|_| JsValue::from_str("Failed to set distribution type"))?; - let id_array = Uint8Array::from(id.as_slice()); - Reflect::set(&dist_obj, &JsValue::from_str("id"), &id_array) - .map_err(|_| JsValue::from_str("Failed to set distribution id"))?; - } - TokenDistributionTypeWithResolvedRecipient::Perpetual(recipient) => { - Reflect::set( - &dist_obj, - &JsValue::from_str("type"), - &JsValue::from_str("Perpetual"), - ) - .map_err(|_| JsValue::from_str("Failed to set distribution type"))?; - - // Serialize resolved recipient - match recipient { - TokenDistributionResolvedRecipient::ContractOwnerIdentity(id) => { - Reflect::set( - &dist_obj, - &JsValue::from_str("recipientType"), - &JsValue::from_str("ContractOwnerIdentity"), - ) - .map_err(|_| JsValue::from_str("Failed to set recipientType"))?; - let id_array = Uint8Array::from(id.as_slice()); - Reflect::set(&dist_obj, &JsValue::from_str("recipientId"), &id_array) - .map_err(|_| JsValue::from_str("Failed to set recipientId"))?; - } - TokenDistributionResolvedRecipient::Identity(id) => { - Reflect::set( - &dist_obj, - &JsValue::from_str("recipientType"), - &JsValue::from_str("Identity"), - ) - .map_err(|_| JsValue::from_str("Failed to set recipientType"))?; - let id_array = Uint8Array::from(id.as_slice()); - Reflect::set(&dist_obj, &JsValue::from_str("recipientId"), &id_array) - .map_err(|_| JsValue::from_str("Failed to set recipientId"))?; - } - TokenDistributionResolvedRecipient::Evonode(id) => { - Reflect::set( - &dist_obj, - &JsValue::from_str("recipientType"), - &JsValue::from_str("Evonode"), - ) - .map_err(|_| JsValue::from_str("Failed to set recipientType"))?; - let id_array = Uint8Array::from(id.as_slice()); - Reflect::set(&dist_obj, &JsValue::from_str("recipientId"), &id_array) - .map_err(|_| JsValue::from_str("Failed to set recipientId"))?; - } - } - } - } - Reflect::set(&obj, &JsValue::from_str("distributionType"), &dist_obj) - .map_err(|_| JsValue::from_str("Failed to set distributionType"))?; - - Reflect::set( - &obj, - &JsValue::from_str("amount"), - &JsValue::from_str(&amount.to_string()), - ) - .map_err(|_| JsValue::from_str("Failed to set amount"))?; - - match note { - Some(n) => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::from_str(n)), - None => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::NULL), - } - .map_err(|_| JsValue::from_str("Failed to set note"))?; - } - TokenEvent::EmergencyAction(action, note) => { - Reflect::set( - &obj, - &JsValue::from_str("type"), - &JsValue::from_str("EmergencyAction"), - ) - .map_err(|_| JsValue::from_str("Failed to set type"))?; +fn stringify_key(key: &dpp::platform_value::Value) -> dpp::platform_value::Value { + use dpp::platform_value::string_encoding::{encode, Encoding}; + use dpp::platform_value::Value; + match key { + Value::Text(_) => key.clone(), + Value::U8(n) => Value::Text(n.to_string()), + Value::U16(n) => Value::Text(n.to_string()), + Value::U32(n) => Value::Text(n.to_string()), + Value::U64(n) => Value::Text(n.to_string()), + Value::I8(n) => Value::Text(n.to_string()), + Value::I16(n) => Value::Text(n.to_string()), + Value::I32(n) => Value::Text(n.to_string()), + Value::I64(n) => Value::Text(n.to_string()), + Value::Bool(b) => Value::Text(b.to_string()), + Value::Identifier(bytes) => Value::Text(encode(bytes, Encoding::Base58)), + Value::Bytes(bytes) => Value::Text(encode(bytes, Encoding::Base64)), + Value::Bytes20(bytes) => Value::Text(encode(bytes, Encoding::Base64)), + Value::Bytes32(bytes) => Value::Text(encode(bytes, Encoding::Base64)), + Value::Bytes36(bytes) => Value::Text(encode(bytes, Encoding::Base64)), + // Float / Null / Array / Map / Tag / EnumU8 fall through; the serializer + // surfaces a clear error if any ever appears as a map key (no rs-dpp + // domain type in this tree uses them that way). + other => other.clone(), + } +} - let action_str = match action { - TokenEmergencyAction::Pause => "Pause", - TokenEmergencyAction::Resume => "Resume", - }; - Reflect::set( - &obj, - &JsValue::from_str("action"), - &JsValue::from_str(action_str), - ) - .map_err(|_| JsValue::from_str("Failed to set action"))?; +// Pure-Rust coverage of the `Value`-tree the group-action serializer feeds to +// `serde_wasm_bindgen`. These assert the canonical `$kind`/`$type` shape and the +// typed-map-key normalization; the final JsValue mapping (typed `U64` -> BigInt, +// `Identifier` -> Uint8Array) is the wasm-dpp2-proven serializer config and is not +// re-tested here (it needs a wasm runtime). +#[cfg(test)] +mod tests { + use super::*; + use dpp::platform_value::Value; + use dpp::tokens::token_event::TokenEvent; + use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; + use std::collections::BTreeMap; + + fn event_value(event: &GroupActionEvent) -> Value { + let value = dpp::platform_value::to_value(event).expect("to_value"); + stringify_map_keys_for_object(&value) + } - match note { - Some(n) => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::from_str(n)), - None => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::NULL), - } - .map_err(|_| JsValue::from_str("Failed to set note"))?; - } - TokenEvent::ConfigUpdate(config_item, note) => { - Reflect::set( - &obj, - &JsValue::from_str("type"), - &JsValue::from_str("ConfigUpdate"), - ) - .map_err(|_| JsValue::from_str("Failed to set type"))?; + fn field<'a>(map: &'a Value, key: &str) -> Option<&'a Value> { + map.as_map()? + .iter() + .find(|(k, _)| k.as_text() == Some(key)) + .map(|(_, v)| v) + } - // For now, we'll just serialize the config item as a string representation - // In a real implementation, you might want to handle each variant separately - Reflect::set( - &obj, - &JsValue::from_str("configItem"), - &JsValue::from_str(&format!("{:?}", config_item)), - ) - .map_err(|_| JsValue::from_str("Failed to set configItem"))?; + #[test] + fn mint_event_has_canonical_kind_type_shape() { + let event = GroupActionEvent::TokenEvent(TokenEvent::Mint( + 1234, + Identifier::new([0x11; 32]), + Some("note".to_string()), + )); + let v = event_value(&event); + // Internally tagged: `$kind` (GroupActionEvent) + `$type` (TokenEvent), + // flattened into one object. Amount stays a typed `U64` (-> BigInt), + // recipient a typed `Identifier` (-> Uint8Array) at serialize time. + assert_eq!( + field(&v, "$kind").and_then(Value::as_text), + Some("tokenEvent") + ); + assert_eq!(field(&v, "$type").and_then(Value::as_text), Some("mint")); + assert_eq!(field(&v, "amount"), Some(&Value::U64(1234))); + assert!(matches!(field(&v, "recipient"), Some(Value::Identifier(_)))); + } - match note { - Some(n) => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::from_str(n)), - None => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::NULL), - } - .map_err(|_| JsValue::from_str("Failed to set note"))?; - } - TokenEvent::ChangePriceForDirectPurchase(pricing_schedule, note) => { - Reflect::set( - &obj, - &JsValue::from_str("type"), - &JsValue::from_str("ChangePriceForDirectPurchase"), - ) - .map_err(|_| JsValue::from_str("Failed to set type"))?; - - match pricing_schedule { - Some(schedule) => { - let schedule_obj = Object::new(); - match schedule { - TokenPricingSchedule::SinglePrice(price) => { - Reflect::set( - &schedule_obj, - &JsValue::from_str("type"), - &JsValue::from_str("SinglePrice"), - ) - .map_err(|_| JsValue::from_str("Failed to set schedule type"))?; - Reflect::set( - &schedule_obj, - &JsValue::from_str("price"), - &JsValue::from_str(&price.to_string()), - ) - .map_err(|_| JsValue::from_str("Failed to set price"))?; - } - TokenPricingSchedule::SetPrices(prices) => { - Reflect::set( - &schedule_obj, - &JsValue::from_str("type"), - &JsValue::from_str("SetPrices"), - ) - .map_err(|_| JsValue::from_str("Failed to set schedule type"))?; - - let prices_obj = Object::new(); - for (amount, price) in prices { - Reflect::set( - &prices_obj, - &JsValue::from_str(&amount.to_string()), - &JsValue::from_str(&price.to_string()), - ) - .map_err(|_| JsValue::from_str("Failed to set price entry"))?; - } - Reflect::set(&schedule_obj, &JsValue::from_str("prices"), &prices_obj) - .map_err(|_| JsValue::from_str("Failed to set prices"))?; + #[test] + fn set_prices_map_keys_are_stringified() { + // `TokenPricingSchedule::SetPrices` emits `BTreeMap` -> `U64` + // map keys; JS plain objects require string keys, so normalization must + // convert them (else serde_wasm_bindgen errors "Map key is not a string"). + let mut prices = BTreeMap::new(); + prices.insert(5u64, 50u64); + prices.insert(9_007_199_254_740_993u64, 1u64); // > 2^53 + let event = GroupActionEvent::TokenEvent(TokenEvent::ChangePriceForDirectPurchase( + Some(TokenPricingSchedule::SetPrices(prices)), + None, + )); + let v = event_value(&event); + assert_eq!( + field(&v, "$type").and_then(Value::as_text), + Some("changePriceForDirectPurchase") + ); + // Walk to the pricing-schedule map and assert every key is now `Text`. + let mut found_map = false; + fn assert_all_keys_text(v: &Value, found: &mut bool) { + match v { + Value::Map(entries) => { + for (k, val) in entries { + if matches!(val, Value::U64(_)) && matches!(k, Value::U64(_)) { + panic!("u64 map key survived normalization: {k:?}"); } + assert!(matches!(k, Value::Text(_)), "non-string map key: {k:?}"); + *found = true; + assert_all_keys_text(val, found); } - Reflect::set(&obj, &JsValue::from_str("pricingSchedule"), &schedule_obj) - .map_err(|_| JsValue::from_str("Failed to set pricingSchedule"))?; - } - None => { - Reflect::set(&obj, &JsValue::from_str("pricingSchedule"), &JsValue::NULL) - .map_err(|_| JsValue::from_str("Failed to set pricingSchedule"))?; } + Value::Array(items) => items.iter().for_each(|i| assert_all_keys_text(i, found)), + _ => {} } - - match note { - Some(n) => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::from_str(n)), - None => Reflect::set(&obj, &JsValue::from_str("note"), &JsValue::NULL), - } - .map_err(|_| JsValue::from_str("Failed to set note"))?; - } - TokenEvent::DirectPurchase(amount, credits) => { - Reflect::set( - &obj, - &JsValue::from_str("type"), - &JsValue::from_str("DirectPurchase"), - ) - .map_err(|_| JsValue::from_str("Failed to set type"))?; - Reflect::set( - &obj, - &JsValue::from_str("amount"), - &JsValue::from_str(&amount.to_string()), - ) - .map_err(|_| JsValue::from_str("Failed to set amount"))?; - Reflect::set( - &obj, - &JsValue::from_str("credits"), - &JsValue::from_str(&credits.to_string()), - ) - .map_err(|_| JsValue::from_str("Failed to set credits"))?; } + assert_all_keys_text(&v, &mut found_map); + assert!(found_map, "expected at least one map in the tree"); } - - Ok(obj.into()) } diff --git a/packages/wasm-sdk/src/queries/mod.rs b/packages/wasm-sdk/src/queries/mod.rs index 8554dd2dac5..daa10d61bd9 100644 --- a/packages/wasm-sdk/src/queries/mod.rs +++ b/packages/wasm-sdk/src/queries/mod.rs @@ -17,12 +17,12 @@ pub use group::*; use crate::impl_wasm_serde_conversions; use crate::WasmSdkError; +use dash_sdk::dpp::serialization::serde_bytes_var as bytes_b64; use js_sys::Uint8Array; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; use wasm_bindgen::prelude::*; use wasm_bindgen::JsValue; -use wasm_dpp2::serialization::bytes_b64; use wasm_dpp2::serialization::conversions as serialization; #[dpp_json_convertible_derive::json_safe_fields(crate = "dash_sdk::dpp")] diff --git a/packages/wasm-sdk/tests/unit/conversion-vote.spec.ts b/packages/wasm-sdk/tests/unit/conversion-vote.spec.ts index 4b695c56ffd..f3313db79ef 100644 --- a/packages/wasm-sdk/tests/unit/conversion-vote.spec.ts +++ b/packages/wasm-sdk/tests/unit/conversion-vote.spec.ts @@ -11,22 +11,23 @@ describe('Vote Conversions', () => { describe('ResourceVoteChoice', () => { describe('TowardsIdentity', () => { - it('should serialize to JSON with type tag and Base58 data', () => { + it('should serialize to JSON with type tag and Base58 identity', () => { const choice = sdk.ResourceVoteChoice.TowardsIdentity(testIdentityId); const json = choice.toJSON(); - // Serde adjacently tagged enum format - expect(json).to.deep.equal({ type: 'towardsIdentity', data: testIdentityId }); + // Custom serde: flat `{$type, identity}` shape (no `data` wrapper — + // see ResourceVoteChoice Serialize impl in rs-dpp). + expect(json).to.deep.equal({ $type: 'towardsIdentity', identity: testIdentityId }); choice.free(); }); - it('should serialize to Object with type tag and Uint8Array data', () => { + it('should serialize to Object with type tag and Uint8Array identity', () => { const choice = sdk.ResourceVoteChoice.TowardsIdentity(testIdentityId); const obj = choice.toObject(); - expect(obj.type).to.equal('towardsIdentity'); - expect(obj.data).to.be.instanceOf(Uint8Array); + expect(obj.$type).to.equal('towardsIdentity'); + expect(obj.identity).to.be.instanceOf(Uint8Array); choice.free(); }); @@ -61,7 +62,7 @@ describe('Vote Conversions', () => { const choice = sdk.ResourceVoteChoice.Abstain(); const json = choice.toJSON(); - expect(json).to.deep.equal({ type: 'abstain' }); + expect(json).to.deep.equal({ $type: 'abstain' }); choice.free(); }); @@ -81,7 +82,7 @@ describe('Vote Conversions', () => { const choice = sdk.ResourceVoteChoice.Lock(); const json = choice.toJSON(); - expect(json).to.deep.equal({ type: 'lock' }); + expect(json).to.deep.equal({ $type: 'lock' }); choice.free(); }); @@ -106,16 +107,16 @@ describe('Vote Conversions', () => { }; describe('toJSON()', () => { - it('should serialize with type tag and data', () => { + it('should serialize with internal type tag and flattened fields', () => { const poll = new sdk.VotePoll(votePollOptions); const json = poll.toJSON(); - expect(json.type).to.equal('contestedDocumentResourceVotePoll'); - expect(json.data).to.exist(); - expect(json.data.contractId).to.equal(testContractId); - expect(json.data.documentTypeName).to.equal('domain'); - expect(json.data.indexName).to.equal('parentNameAndLabel'); - expect(json.data.indexValues).to.deep.equal(['dash', 'alice']); + // Internal serde tagging: `{$type, ...fields}` with no `data` wrapper. + expect(json.$type).to.equal('contestedDocumentResourceVotePoll'); + expect(json.contractId).to.equal(testContractId); + expect(json.documentTypeName).to.equal('domain'); + expect(json.indexName).to.equal('parentNameAndLabel'); + expect(json.indexValues).to.deep.equal(['dash', 'alice']); poll.free(); }); @@ -124,13 +125,11 @@ describe('Vote Conversions', () => { describe('fromJSON()', () => { it('should deserialize from JSON fixture', () => { const fixture = { - type: 'contestedDocumentResourceVotePoll', - data: { - contractId: testContractId, - documentTypeName: 'domain', - indexName: 'parentNameAndLabel', - indexValues: ['dash', 'alice'], - }, + $type: 'contestedDocumentResourceVotePoll', + contractId: testContractId, + documentTypeName: 'domain', + indexName: 'parentNameAndLabel', + indexValues: ['dash', 'alice'], }; const poll = sdk.VotePoll.fromJSON(fixture); @@ -157,15 +156,14 @@ describe('Vote Conversions', () => { }); describe('toObject()', () => { - it('should serialize with type tag and Uint8Array contractId in data', () => { + it('should serialize with internal type tag and Uint8Array contractId', () => { const poll = new sdk.VotePoll(votePollOptions); const obj = poll.toObject(); - expect(obj.type).to.equal('contestedDocumentResourceVotePoll'); - expect(obj.data).to.exist(); - expect(obj.data.contractId).to.be.instanceOf(Uint8Array); - expect(obj.data.documentTypeName).to.equal('domain'); - expect(obj.data.indexName).to.equal('parentNameAndLabel'); + expect(obj.$type).to.equal('contestedDocumentResourceVotePoll'); + expect(obj.contractId).to.be.instanceOf(Uint8Array); + expect(obj.documentTypeName).to.equal('domain'); + expect(obj.indexName).to.equal('parentNameAndLabel'); poll.free(); }); @@ -203,8 +201,8 @@ describe('Vote Conversions', () => { expect(json.$formatVersion).to.equal('0'); expect(json.votePoll).to.exist(); - expect(json.votePoll.type).to.equal('contestedDocumentResourceVotePoll'); - expect(json.votePoll.data.contractId).to.equal(testContractId); + expect(json.votePoll.$type).to.equal('contestedDocumentResourceVotePoll'); + expect(json.votePoll.contractId).to.equal(testContractId); expect(json.resourceVoteChoice).to.exist(); vote.free(); @@ -252,7 +250,7 @@ describe('Vote Conversions', () => { }); describe('Vote', () => { - it('should serialize to JSON with resourceVote type tag', () => { + it('should serialize to JSON with resourceVote $type tag', () => { const poll = new sdk.VotePoll({ contractId: testContractId, documentTypeName: 'domain', @@ -264,12 +262,12 @@ describe('Vote Conversions', () => { const json = vote.toJSON(); - expect(json.type).to.equal('resourceVote'); - expect(json.data).to.exist(); - expect(json.data.$formatVersion).to.equal('0'); - expect(json.data.votePoll.type).to.equal('contestedDocumentResourceVotePoll'); - expect(json.data.votePoll.data.contractId).to.equal(testContractId); - expect(json.data.resourceVoteChoice).to.exist(); + // Vote uses internal tagging on `$type` (system field convention). + expect(json.$type).to.equal('resourceVote'); + expect(json.$formatVersion).to.equal('0'); + expect(json.votePoll.$type).to.equal('contestedDocumentResourceVotePoll'); + expect(json.votePoll.contractId).to.equal(testContractId); + expect(json.resourceVoteChoice).to.exist(); vote.free(); }); diff --git a/packages/wasm-sdk/tests/unit/fixtures/data-contract-v1-with-docs-tokens-groups.ts b/packages/wasm-sdk/tests/unit/fixtures/data-contract-v1-with-docs-tokens-groups.ts index 8dd676d50ae..b6a21c122fc 100644 --- a/packages/wasm-sdk/tests/unit/fixtures/data-contract-v1-with-docs-tokens-groups.ts +++ b/packages/wasm-sdk/tests/unit/fixtures/data-contract-v1-with-docs-tokens-groups.ts @@ -117,8 +117,8 @@ const contract = { }, conventionsChangeRules: { $formatVersion: '0', - authorizedToMakeChange: 'ContractOwner', - adminActionTakers: 'ContractOwner', + authorizedToMakeChange: { $type: 'contractOwner' }, + adminActionTakers: { $type: 'contractOwner' }, changingAuthorizedActionTakersToNoOneAllowed: true, changingAdminActionTakersToNoOneAllowed: true, selfChangingAdminActionTakersAllowed: true, @@ -138,8 +138,8 @@ const contract = { allowTransferToFrozenBalance: true, maxSupplyChangeRules: { $formatVersion: '0', - authorizedToMakeChange: 'ContractOwner', - adminActionTakers: 'ContractOwner', + authorizedToMakeChange: { $type: 'contractOwner' }, + adminActionTakers: { $type: 'contractOwner' }, changingAuthorizedActionTakersToNoOneAllowed: true, changingAdminActionTakersToNoOneAllowed: true, selfChangingAdminActionTakersAllowed: true, @@ -149,21 +149,19 @@ const contract = { perpetualDistribution: { $formatVersion: '0', distributionType: { - BlockBasedDistribution: { - interval: 100, - function: { - FixedAmount: { - amount: 1, - }, - }, + $type: 'blockBasedDistribution', + interval: 100, + function: { + $type: 'fixedAmount', + amount: 1, }, }, - distributionRecipient: 'ContractOwner', + distributionRecipient: { $type: 'contractOwner' }, }, perpetualDistributionRules: { $formatVersion: '0', - authorizedToMakeChange: 'ContractOwner', - adminActionTakers: 'ContractOwner', + authorizedToMakeChange: { $type: 'contractOwner' }, + adminActionTakers: { $type: 'contractOwner' }, changingAuthorizedActionTakersToNoOneAllowed: true, changingAdminActionTakersToNoOneAllowed: true, selfChangingAdminActionTakersAllowed: true, @@ -172,8 +170,8 @@ const contract = { newTokensDestinationIdentity: '7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC', newTokensDestinationIdentityRules: { $formatVersion: '0', - authorizedToMakeChange: 'ContractOwner', - adminActionTakers: 'ContractOwner', + authorizedToMakeChange: { $type: 'contractOwner' }, + adminActionTakers: { $type: 'contractOwner' }, changingAuthorizedActionTakersToNoOneAllowed: true, changingAdminActionTakersToNoOneAllowed: true, selfChangingAdminActionTakersAllowed: true, @@ -181,16 +179,16 @@ const contract = { mintingAllowChoosingDestination: false, mintingAllowChoosingDestinationRules: { $formatVersion: '0', - authorizedToMakeChange: 'ContractOwner', - adminActionTakers: 'ContractOwner', + authorizedToMakeChange: { $type: 'contractOwner' }, + adminActionTakers: { $type: 'contractOwner' }, changingAuthorizedActionTakersToNoOneAllowed: true, changingAdminActionTakersToNoOneAllowed: true, selfChangingAdminActionTakersAllowed: true, }, changeDirectPurchasePricingRules: { $formatVersion: '0', - authorizedToMakeChange: 'ContractOwner', - adminActionTakers: 'ContractOwner', + authorizedToMakeChange: { $type: 'contractOwner' }, + adminActionTakers: { $type: 'contractOwner' }, changingAuthorizedActionTakersToNoOneAllowed: true, changingAdminActionTakersToNoOneAllowed: true, selfChangingAdminActionTakersAllowed: true, @@ -201,8 +199,8 @@ const contract = { tradeMode: 'NotTradeable', tradeModeChangeRules: { $formatVersion: '0', - authorizedToMakeChange: 'ContractOwner', - adminActionTakers: 'ContractOwner', + authorizedToMakeChange: { $type: 'contractOwner' }, + adminActionTakers: { $type: 'contractOwner' }, changingAuthorizedActionTakersToNoOneAllowed: true, changingAdminActionTakersToNoOneAllowed: true, selfChangingAdminActionTakersAllowed: true, @@ -210,54 +208,54 @@ const contract = { }, manualMintingRules: { $formatVersion: '0', - authorizedToMakeChange: 'ContractOwner', - adminActionTakers: 'ContractOwner', + authorizedToMakeChange: { $type: 'contractOwner' }, + adminActionTakers: { $type: 'contractOwner' }, changingAuthorizedActionTakersToNoOneAllowed: true, changingAdminActionTakersToNoOneAllowed: true, selfChangingAdminActionTakersAllowed: true, }, manualBurningRules: { $formatVersion: '0', - authorizedToMakeChange: 'ContractOwner', - adminActionTakers: 'ContractOwner', + authorizedToMakeChange: { $type: 'contractOwner' }, + adminActionTakers: { $type: 'contractOwner' }, changingAuthorizedActionTakersToNoOneAllowed: true, changingAdminActionTakersToNoOneAllowed: true, selfChangingAdminActionTakersAllowed: true, }, freezeRules: { $formatVersion: '0', - authorizedToMakeChange: 'ContractOwner', - adminActionTakers: 'ContractOwner', + authorizedToMakeChange: { $type: 'contractOwner' }, + adminActionTakers: { $type: 'contractOwner' }, changingAuthorizedActionTakersToNoOneAllowed: true, changingAdminActionTakersToNoOneAllowed: true, selfChangingAdminActionTakersAllowed: true, }, unfreezeRules: { $formatVersion: '0', - authorizedToMakeChange: 'ContractOwner', - adminActionTakers: 'ContractOwner', + authorizedToMakeChange: { $type: 'contractOwner' }, + adminActionTakers: { $type: 'contractOwner' }, changingAuthorizedActionTakersToNoOneAllowed: true, changingAdminActionTakersToNoOneAllowed: true, selfChangingAdminActionTakersAllowed: true, }, destroyFrozenFundsRules: { $formatVersion: '0', - authorizedToMakeChange: 'ContractOwner', - adminActionTakers: 'ContractOwner', + authorizedToMakeChange: { $type: 'contractOwner' }, + adminActionTakers: { $type: 'contractOwner' }, changingAuthorizedActionTakersToNoOneAllowed: true, changingAdminActionTakersToNoOneAllowed: true, selfChangingAdminActionTakersAllowed: true, }, emergencyActionRules: { $formatVersion: '0', - authorizedToMakeChange: 'ContractOwner', - adminActionTakers: 'ContractOwner', + authorizedToMakeChange: { $type: 'contractOwner' }, + adminActionTakers: { $type: 'contractOwner' }, changingAuthorizedActionTakersToNoOneAllowed: true, changingAdminActionTakersToNoOneAllowed: true, selfChangingAdminActionTakersAllowed: true, }, mainControlGroup: 0, - mainControlGroupCanBeModified: 'ContractOwner', + mainControlGroupCanBeModified: { $type: 'contractOwner' }, description: null, }, }, diff --git a/packages/wasm-sdk/tests/unit/token-results/TokenBurnResult.spec.ts b/packages/wasm-sdk/tests/unit/token-results/TokenBurnResult.spec.ts index c8c3e131d15..8a3bebaeaae 100644 --- a/packages/wasm-sdk/tests/unit/token-results/TokenBurnResult.spec.ts +++ b/packages/wasm-sdk/tests/unit/token-results/TokenBurnResult.spec.ts @@ -25,6 +25,7 @@ describe('TokenBurnResult', () => { }; const documentJSON = { + $formatVersion: "0", $id: '9tSsCqKHTZ8ro16MydChSxgHBukFW36eMLJKKRtebJEn', $ownerId: 'CXH2kZCATjvDTnQAPVg28EgPg9WySUvwvnR5ZkmNqY5i', $dataContractId: 'GnXgMaiqAwTxh44ccQe8AoCgFvcseHK5CncH3sUorW4X', diff --git a/packages/wasm-sdk/tests/unit/token-results/TokenClaimResult.spec.ts b/packages/wasm-sdk/tests/unit/token-results/TokenClaimResult.spec.ts index 97bb15c307c..8aa1420f6cf 100644 --- a/packages/wasm-sdk/tests/unit/token-results/TokenClaimResult.spec.ts +++ b/packages/wasm-sdk/tests/unit/token-results/TokenClaimResult.spec.ts @@ -17,6 +17,7 @@ describe('TokenClaimResult', () => { }; const documentJSON = { + $formatVersion: "0", $id: '9tSsCqKHTZ8ro16MydChSxgHBukFW36eMLJKKRtebJEn', $ownerId: 'CXH2kZCATjvDTnQAPVg28EgPg9WySUvwvnR5ZkmNqY5i', $dataContractId: 'GnXgMaiqAwTxh44ccQe8AoCgFvcseHK5CncH3sUorW4X', diff --git a/packages/wasm-sdk/tests/unit/token-results/TokenConfigUpdateResult.spec.ts b/packages/wasm-sdk/tests/unit/token-results/TokenConfigUpdateResult.spec.ts index 0f17df4b34b..842fc8da0ea 100644 --- a/packages/wasm-sdk/tests/unit/token-results/TokenConfigUpdateResult.spec.ts +++ b/packages/wasm-sdk/tests/unit/token-results/TokenConfigUpdateResult.spec.ts @@ -17,6 +17,7 @@ describe('TokenConfigUpdateResult', () => { }; const documentJSON = { + $formatVersion: "0", $id: '9tSsCqKHTZ8ro16MydChSxgHBukFW36eMLJKKRtebJEn', $ownerId: 'CXH2kZCATjvDTnQAPVg28EgPg9WySUvwvnR5ZkmNqY5i', $dataContractId: 'GnXgMaiqAwTxh44ccQe8AoCgFvcseHK5CncH3sUorW4X', diff --git a/packages/wasm-sdk/tests/unit/token-results/TokenDestroyFrozenResult.spec.ts b/packages/wasm-sdk/tests/unit/token-results/TokenDestroyFrozenResult.spec.ts index 132873111a9..d8ad89dfd61 100644 --- a/packages/wasm-sdk/tests/unit/token-results/TokenDestroyFrozenResult.spec.ts +++ b/packages/wasm-sdk/tests/unit/token-results/TokenDestroyFrozenResult.spec.ts @@ -17,6 +17,7 @@ describe('TokenDestroyFrozenResult', () => { }; const documentJSON = { + $formatVersion: "0", $id: '9tSsCqKHTZ8ro16MydChSxgHBukFW36eMLJKKRtebJEn', $ownerId: 'CXH2kZCATjvDTnQAPVg28EgPg9WySUvwvnR5ZkmNqY5i', $dataContractId: 'GnXgMaiqAwTxh44ccQe8AoCgFvcseHK5CncH3sUorW4X', diff --git a/packages/wasm-sdk/tests/unit/token-results/TokenDirectPurchaseResult.spec.ts b/packages/wasm-sdk/tests/unit/token-results/TokenDirectPurchaseResult.spec.ts index 49baf5317d5..78284fd401c 100644 --- a/packages/wasm-sdk/tests/unit/token-results/TokenDirectPurchaseResult.spec.ts +++ b/packages/wasm-sdk/tests/unit/token-results/TokenDirectPurchaseResult.spec.ts @@ -23,6 +23,7 @@ describe('TokenDirectPurchaseResult', () => { }; const documentJSON = { + $formatVersion: "0", $id: '9tSsCqKHTZ8ro16MydChSxgHBukFW36eMLJKKRtebJEn', $ownerId: 'CXH2kZCATjvDTnQAPVg28EgPg9WySUvwvnR5ZkmNqY5i', $dataContractId: 'GnXgMaiqAwTxh44ccQe8AoCgFvcseHK5CncH3sUorW4X', diff --git a/packages/wasm-sdk/tests/unit/token-results/TokenEmergencyActionResult.spec.ts b/packages/wasm-sdk/tests/unit/token-results/TokenEmergencyActionResult.spec.ts index 4259de14f62..4ce468edd0d 100644 --- a/packages/wasm-sdk/tests/unit/token-results/TokenEmergencyActionResult.spec.ts +++ b/packages/wasm-sdk/tests/unit/token-results/TokenEmergencyActionResult.spec.ts @@ -17,6 +17,7 @@ describe('TokenEmergencyActionResult', () => { }; const documentJSON = { + $formatVersion: "0", $id: '9tSsCqKHTZ8ro16MydChSxgHBukFW36eMLJKKRtebJEn', $ownerId: 'CXH2kZCATjvDTnQAPVg28EgPg9WySUvwvnR5ZkmNqY5i', $dataContractId: 'GnXgMaiqAwTxh44ccQe8AoCgFvcseHK5CncH3sUorW4X', diff --git a/packages/wasm-sdk/tests/unit/token-results/TokenFreezeResult.spec.ts b/packages/wasm-sdk/tests/unit/token-results/TokenFreezeResult.spec.ts index 30e854a0eb2..c6605f9b6e8 100644 --- a/packages/wasm-sdk/tests/unit/token-results/TokenFreezeResult.spec.ts +++ b/packages/wasm-sdk/tests/unit/token-results/TokenFreezeResult.spec.ts @@ -21,6 +21,7 @@ describe('TokenFreezeResult', () => { }; const documentJSON = { + $formatVersion: "0", $id: '9tSsCqKHTZ8ro16MydChSxgHBukFW36eMLJKKRtebJEn', $ownerId: 'CXH2kZCATjvDTnQAPVg28EgPg9WySUvwvnR5ZkmNqY5i', $dataContractId: 'GnXgMaiqAwTxh44ccQe8AoCgFvcseHK5CncH3sUorW4X', diff --git a/packages/wasm-sdk/tests/unit/token-results/TokenMintResult.spec.ts b/packages/wasm-sdk/tests/unit/token-results/TokenMintResult.spec.ts index 32daed49570..b9e9896efcd 100644 --- a/packages/wasm-sdk/tests/unit/token-results/TokenMintResult.spec.ts +++ b/packages/wasm-sdk/tests/unit/token-results/TokenMintResult.spec.ts @@ -5,6 +5,7 @@ describe('TokenMintResult', () => { const testIdentifier = 'H2pb35GtKpjLinncBYeMsXkdDYXCbsFzzVmssce6pSJ1'; const documentJSON = { + $formatVersion: "0", $id: '9tSsCqKHTZ8ro16MydChSxgHBukFW36eMLJKKRtebJEn', $ownerId: 'CXH2kZCATjvDTnQAPVg28EgPg9WySUvwvnR5ZkmNqY5i', $dataContractId: 'GnXgMaiqAwTxh44ccQe8AoCgFvcseHK5CncH3sUorW4X', diff --git a/packages/wasm-sdk/tests/unit/token-results/TokenSetPriceResult.spec.ts b/packages/wasm-sdk/tests/unit/token-results/TokenSetPriceResult.spec.ts index da477e9b476..c4289d99123 100644 --- a/packages/wasm-sdk/tests/unit/token-results/TokenSetPriceResult.spec.ts +++ b/packages/wasm-sdk/tests/unit/token-results/TokenSetPriceResult.spec.ts @@ -23,6 +23,7 @@ describe('TokenSetPriceResult', () => { }; const documentJSON = { + $formatVersion: "0", $id: '9tSsCqKHTZ8ro16MydChSxgHBukFW36eMLJKKRtebJEn', $ownerId: 'CXH2kZCATjvDTnQAPVg28EgPg9WySUvwvnR5ZkmNqY5i', $dataContractId: 'GnXgMaiqAwTxh44ccQe8AoCgFvcseHK5CncH3sUorW4X', @@ -140,7 +141,9 @@ describe('TokenSetPriceResult', () => { it('should include pricingSchedule in toJSON when present', () => { const data = { ownerId: testIdentifier, - pricingSchedule: { SinglePrice: 5000 }, + // TokenPricingSchedule is internally `$type`-tagged (camelCase variant, + // JS-safe `price`); externally-tagged `{ SinglePrice: N }` is no longer valid. + pricingSchedule: { $type: 'singlePrice', price: 5000 }, groupPower: 70, }; @@ -149,13 +152,14 @@ describe('TokenSetPriceResult', () => { const json = result.toJSON(); expect(json.pricingSchedule).to.exist(); - expect(json.pricingSchedule.SinglePrice).to.equal(5000); + expect(json.pricingSchedule.$type).to.equal('singlePrice'); + expect(json.pricingSchedule.price).to.equal(5000); }); it('should round-trip pricingSchedule through toJSON/fromJSON', () => { const data = { ownerId: testIdentifier, - pricingSchedule: { SinglePrice: 5000 }, + pricingSchedule: { $type: 'singlePrice', price: 5000 }, groupPower: 70, }; @@ -170,7 +174,7 @@ describe('TokenSetPriceResult', () => { it('should round-trip pricingSchedule through toObject/fromObject', () => { const data = { ownerId: testIdentifier, - pricingSchedule: { SinglePrice: 5000n }, + pricingSchedule: { $type: 'singlePrice', price: 5000n }, groupPower: 70, }; diff --git a/packages/wasm-sdk/tests/unit/token-results/TokenTransferResult.spec.ts b/packages/wasm-sdk/tests/unit/token-results/TokenTransferResult.spec.ts index bbf1c53bb0a..607d8f9859e 100644 --- a/packages/wasm-sdk/tests/unit/token-results/TokenTransferResult.spec.ts +++ b/packages/wasm-sdk/tests/unit/token-results/TokenTransferResult.spec.ts @@ -21,6 +21,7 @@ describe('TokenTransferResult', () => { }; const documentJSON = { + $formatVersion: "0", $id: '9tSsCqKHTZ8ro16MydChSxgHBukFW36eMLJKKRtebJEn', $ownerId: 'CXH2kZCATjvDTnQAPVg28EgPg9WySUvwvnR5ZkmNqY5i', $dataContractId: 'GnXgMaiqAwTxh44ccQe8AoCgFvcseHK5CncH3sUorW4X', diff --git a/packages/wasm-sdk/tests/unit/token-results/TokenUnfreezeResult.spec.ts b/packages/wasm-sdk/tests/unit/token-results/TokenUnfreezeResult.spec.ts index beb9a1ce724..1dd0776b8a8 100644 --- a/packages/wasm-sdk/tests/unit/token-results/TokenUnfreezeResult.spec.ts +++ b/packages/wasm-sdk/tests/unit/token-results/TokenUnfreezeResult.spec.ts @@ -21,6 +21,7 @@ describe('TokenUnfreezeResult', () => { }; const documentJSON = { + $formatVersion: "0", $id: '9tSsCqKHTZ8ro16MydChSxgHBukFW36eMLJKKRtebJEn', $ownerId: 'CXH2kZCATjvDTnQAPVg28EgPg9WySUvwvnR5ZkmNqY5i', $dataContractId: 'GnXgMaiqAwTxh44ccQe8AoCgFvcseHK5CncH3sUorW4X',