Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
291ded9
feat(wasm-sdk): add prepare_* APIs for idempotent document state tran…
thepastaclaw Feb 17, 2026
b3e62d6
style: use map_or for revision check instead of is_some+unwrap
thepastaclaw Feb 17, 2026
466c825
fix(wasm-sdk): reject create-eligible documents in prepare_document_r…
thepastaclaw Feb 17, 2026
24fbe7d
fix(wasm-sdk): use is_some_and instead of map_or to satisfy clippy
thepastaclaw Feb 20, 2026
3d84f1b
fix(wasm-sdk): validate prepared document transitions
thepastaclaw Apr 22, 2026
ed7dc8c
fix(wasm-sdk): tighten prepared document transition retries
thepastaclaw Apr 24, 2026
b6d0637
fix(wasm-sdk): refresh delete nonce cache on prepare failure
thepastaclaw Apr 24, 2026
3919715
Merge upstream/v3.1-dev into feat/sdk-prepare-document-apis
thepastaclaw May 8, 2026
bec5fb9
fix(wasm-sdk): support token payments in prepare document APIs
thepastaclaw May 8, 2026
b828e8c
test(wasm-sdk): cover prepared token-paid documents
thepastaclaw May 8, 2026
d3d3300
test(wasm-sdk): cover prepared document rebroadcasts
thepastaclaw May 9, 2026
b24f121
fix(wasm-sdk): reuse document transition builder
thepastaclaw May 10, 2026
fa0706e
fix(wasm-sdk): address document prepare review follow-ups
thepastaclaw May 10, 2026
97953ab
fix(rs-sdk): roll back document prepare nonces
thepastaclaw May 10, 2026
70c884b
fix(rs-sdk): tighten document transition nonce handling
thepastaclaw May 10, 2026
f4416d8
fix(rs-sdk): address document prepare review findings
thepastaclaw May 10, 2026
8c607ed
fix(rs-sdk): share document delete prepare helper
thepastaclaw May 12, 2026
3ba4b98
fix(rs-sdk): address document prepare review findings
thepastaclaw May 12, 2026
ff2285b
fix(rs-sdk): roll back document builder nonces
thepastaclaw May 12, 2026
214dde3
fix(rs-sdk): tighten document create validation
thepastaclaw May 13, 2026
730f6b3
fix(sdk): address document prepare review follow-up
thepastaclaw May 13, 2026
18a7df1
fix(sdk): address document prepare review findings
thepastaclaw May 13, 2026
a523a1e
fix(sdk): address document prepare review follow-up
thepastaclaw May 13, 2026
0daa6c5
fix(sdk): address document prepare review follow-up
thepastaclaw May 14, 2026
0d84e9b
fix(sdk): harden document transition builder inputs
thepastaclaw May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,10 @@ pub struct DocumentMethodVersions {
pub get_raw_for_contract: FeatureVersion,
pub get_raw_for_document_type: FeatureVersion,
pub try_into_asset_unlock_base_transaction_info: FeatureVersion,
/// Version of the document-id derivation used by client-side helpers
/// (e.g. `rs-sdk` put-document strict create path, wasm-sdk
/// `prepareDocumentCreate` fast-path id check) when computing
/// `(contract_id, owner_id, document_type_name, entropy)` →
/// document id. `0` selects `Document::generate_document_id_v0`.
pub derive_document_id: FeatureVersion,
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ pub const DOCUMENT_VERSIONS_V1: DPPDocumentVersions = DPPDocumentVersions {
get_raw_for_contract: 0,
get_raw_for_document_type: 0,
try_into_asset_unlock_base_transaction_info: 0,
derive_document_id: 0,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ pub const DOCUMENT_VERSIONS_V2: DPPDocumentVersions = DPPDocumentVersions {
get_raw_for_contract: 0,
get_raw_for_document_type: 0,
try_into_asset_unlock_base_transaction_info: 0,
derive_document_id: 0,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ pub const DOCUMENT_VERSIONS_V3: DPPDocumentVersions = DPPDocumentVersions {
get_raw_for_contract: 0,
get_raw_for_document_type: 0,
try_into_asset_unlock_base_transaction_info: 0,
derive_document_id: 0,
},
};
48 changes: 32 additions & 16 deletions packages/rs-sdk-ffi/src/document/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0;
use dash_sdk::dpp::platform_value::string_encoding::Encoding;
use dash_sdk::dpp::prelude::{Identifier, UserFeeIncrease};
use dash_sdk::platform::documents::transitions::DocumentDeleteTransitionBuilder;
use dash_sdk::platform::documents::transitions::{
build_signed_document_delete_transition, DocumentDeleteTransitionBuilder,
};
use dash_sdk::platform::IdentityPublicKey;
use drive_proof_verifier::ContextProvider;
use std::ffi::CStr;
use std::os::raw::c_char;
use tracing::{debug, error, info};

use crate::document::helpers::{
convert_state_transition_creation_options, convert_token_payment_info,
convert_state_transition_creation_options, convert_token_payment_info, map_document_sdk_error,
};
use crate::sdk::SDKWrapper;
use crate::types::{
Expand Down Expand Up @@ -153,19 +155,33 @@ pub unsafe extern "C" fn dash_sdk_document_delete(
builder = builder.with_state_transition_creation_options(options);
}

let state_transition = builder
.sign(
&wrapper.sdk,
identity_public_key,
signer,
wrapper.sdk.version(),
)
.await
.map_err(|e| {
FFIError::InternalError(format!("Failed to create delete transition: {}", e))
})?;

// Serialize the state transition with bincode
// Delegate the nonce-allocate / sign / structure-validate / rollback
// sequence to rs-sdk's shared helper. Any pre-broadcast failure
// inside the helper (sign or local structure validation) rolls the
// bumped identity-contract nonce back internally, so the local
// nonce cache cannot advance past a nonce the network never
// observed.
let state_transition = build_signed_document_delete_transition(
&wrapper.sdk,
&builder,
identity_public_key,
signer,
)
.await
.map_err(|e| map_document_sdk_error(e, "Failed to create delete transition"))?;

// Serialize the state transition with bincode.
//
// Note on nonce rollback: at this point the shared helper has
// already allocated and embedded the identity-contract nonce in
// `state_transition` and consumed its rollback handle. `bincode`
// encoding into a `Vec<u8>` writes to an in-memory buffer with no
// IO, and `Encode` for `StateTransition`'s nested data types has
// no failure mode in practice, so this step is effectively
// infallible for any transition the helper just returned. A
// failure here would leave the local nonce cache advanced by one
// until the caller's next refresh, but cannot leave the network
// in an inconsistent state because nothing has been broadcast.
let config = bincode::config::standard();
let serialized = bincode::encode_to_vec(&state_transition, config).map_err(|e| {
FFIError::InternalError(format!("Failed to serialize state transition: {}", e))
Expand Down Expand Up @@ -353,7 +369,7 @@ pub unsafe extern "C" fn dash_sdk_document_delete_and_wait(
.await
.map_err(|e| {
error!(error = %e, key_id = identity_public_key.id(), "[DOCUMENT DELETE] SDK call failed");
FFIError::InternalError(format!("Failed to delete document and wait: {}", e))
map_document_sdk_error(e, "Failed to delete document and wait")
})?;

info!("[DOCUMENT DELETE] SDK call completed successfully");
Expand Down
80 changes: 80 additions & 0 deletions packages/rs-sdk-ffi/src/document/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@ use crate::types::{
};
use crate::FFIError;

/// Map a `dash_sdk::Error` produced by a document state-transition
/// builder (build / sign / SDK broadcast helper) into an `FFIError`,
/// preserving caller-supplied context for non-`InvalidArgument` variants
/// while letting `InvalidArgument` flow through the typed
/// `FFIError::SDKError` → `DashSDKErrorCode::InvalidParameter` branch
/// in `error.rs`.
///
/// Without this routing, a typed `Error::InvalidArgument` from the new
/// strict create/replace guards in rs-sdk would be wrapped as
/// `FFIError::InternalError(format!("{context}: {}", e))` and surface as
/// `DashSDKErrorCode::InternalError`, hiding the precise classification
/// from FFI callers.
pub(crate) fn map_document_sdk_error(e: dash_sdk::Error, context: &str) -> FFIError {
if matches!(e, dash_sdk::Error::InvalidArgument(_)) {
FFIError::SDKError(e)
} else {
FFIError::InternalError(format!("{}: {}", context, e))
}
}

/// Convert FFI GasFeesPaidBy to Rust enum
///
/// # Safety
Expand Down Expand Up @@ -105,3 +125,63 @@ pub unsafe fn convert_state_transition_creation_options(
},
})
}

#[cfg(test)]
mod tests {
use super::*;
use crate::DashSDKErrorCode;

/// A typed `dash_sdk::Error::InvalidArgument` from the document
/// builder/sign paths must flow through `FFIError::SDKError` (not
/// `InternalError`) so the `From<FFIError> for DashSDKError` typed
/// dispatch in `error.rs` can map it to
/// `DashSDKErrorCode::InvalidParameter`. Without this routing, the
/// new strict create/replace guards in rs-sdk would surface as
/// `InternalError` to FFI callers.
#[test]
fn map_document_sdk_error_routes_invalid_argument_to_invalid_parameter() {
let sdk_err = dash_sdk::Error::InvalidArgument("entropy mismatch".to_string());
let ffi_err = map_document_sdk_error(sdk_err, "Failed to create document transition");
assert!(
matches!(
ffi_err,
FFIError::SDKError(dash_sdk::Error::InvalidArgument(_))
),
"InvalidArgument must pass through as FFIError::SDKError, got: {ffi_err:?}"
);

// End-to-end through the public `From<FFIError> for DashSDKError`
// conversion: the user-facing error code must be InvalidParameter,
// not InternalError.
let api_err: crate::DashSDKError = ffi_err.into();
assert_eq!(api_err.code, DashSDKErrorCode::InvalidParameter);
}

/// Non-`InvalidArgument` `dash_sdk::Error` variants must keep the
/// caller-supplied context prefix (e.g. "Failed to create document
/// transition") so existing FFI error messages are not regressed by
/// the typed pass-through.
#[test]
fn map_document_sdk_error_preserves_context_for_non_invalid_argument() {
// Use a `Protocol` variant — anything that is not
// `InvalidArgument`. The exact variant does not matter; what
// matters is that the context prefix is retained.
let sdk_err = dash_sdk::Error::Generic("boom".to_string());
let ffi_err = map_document_sdk_error(sdk_err, "Failed to create document transition");
match ffi_err {
FFIError::InternalError(msg) => {
assert!(
msg.starts_with("Failed to create document transition:"),
"expected context prefix, got: {msg}"
);
assert!(
msg.contains("boom"),
"expected underlying error in msg, got: {msg}"
);
}
other => {
panic!("expected FFIError::InternalError for non-InvalidArgument, got: {other:?}")
}
}
}
}
10 changes: 3 additions & 7 deletions packages/rs-sdk-ffi/src/document/price.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Document price update operations

use crate::document::helpers::{
convert_state_transition_creation_options, convert_token_payment_info,
convert_state_transition_creation_options, convert_token_payment_info, map_document_sdk_error,
};
use crate::sdk::SDKWrapper;
use crate::types::{
Expand Down Expand Up @@ -147,9 +147,7 @@ pub unsafe extern "C" fn dash_sdk_document_update_price_of_document(
wrapper.sdk.version(),
)
.await
.map_err(|e| {
FFIError::InternalError(format!("Failed to create set price transition: {}", e))
})?;
.map_err(|e| map_document_sdk_error(e, "Failed to create set price transition"))?;

// Serialize the state transition with bincode
let config = bincode::config::standard();
Expand Down Expand Up @@ -285,9 +283,7 @@ pub unsafe extern "C" fn dash_sdk_document_update_price_of_document_and_wait(
.sdk
.document_set_price(builder, identity_public_key, signer)
.await
.map_err(|e| {
FFIError::InternalError(format!("Failed to update document price and wait: {}", e))
})?;
.map_err(|e| map_document_sdk_error(e, "Failed to update document price and wait"))?;

let dash_sdk::platform::documents::transitions::DocumentSetPriceResult::Document(
updated_document,
Expand Down
10 changes: 3 additions & 7 deletions packages/rs-sdk-ffi/src/document/purchase.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Document purchasing operations

use crate::document::helpers::{
convert_state_transition_creation_options, convert_token_payment_info,
convert_state_transition_creation_options, convert_token_payment_info, map_document_sdk_error,
};
use crate::sdk::SDKWrapper;
use crate::types::{
Expand Down Expand Up @@ -164,9 +164,7 @@ pub unsafe extern "C" fn dash_sdk_document_purchase(
wrapper.sdk.version(),
)
.await
.map_err(|e| {
FFIError::InternalError(format!("Failed to create purchase transition: {}", e))
})?;
.map_err(|e| map_document_sdk_error(e, "Failed to create purchase transition"))?;

// Serialize the state transition with bincode
let config = bincode::config::standard();
Expand Down Expand Up @@ -332,9 +330,7 @@ pub unsafe extern "C" fn dash_sdk_document_purchase_and_wait(
.sdk
.document_purchase(builder, identity_public_key, signer)
.await
.map_err(|e| {
FFIError::InternalError(format!("Failed to purchase document and wait: {}", e))
})?;
.map_err(|e| map_document_sdk_error(e, "Failed to purchase document and wait"))?;

let dash_sdk::platform::documents::transitions::DocumentPurchaseResult::Document(
purchased_document,
Expand Down
61 changes: 47 additions & 14 deletions packages/rs-sdk-ffi/src/document/put.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,27 @@ use std::ffi::CStr;
use std::os::raw::c_char;

use crate::document::helpers::{
convert_state_transition_creation_options, convert_token_payment_info,
convert_state_transition_creation_options, convert_token_payment_info, map_document_sdk_error,
};

/// Whether the FFI document put paths should route to the create builder
/// for a given document revision.
///
/// Returns `true` for `None` (legacy create intent, supported by the
/// strict rs-sdk create helper) and `Some(1)` (the
/// [`dash_sdk::dpp::document::INITIAL_REVISION`] sentinel for a freshly
/// minted document). Any other value routes to the replace builder.
///
/// Both the no-wait (`dash_sdk_document_put_to_platform`) and wait
/// (`dash_sdk_document_put_to_platform_and_wait`) entry points share this
/// helper so they cannot drift on revision routing — historically the
/// no-wait path used `unwrap_or(0) == 1` and the wait path used
/// `unwrap_or(1) == 1`, which disagreed on the `None` case and silently
/// routed `None`-revision documents into the replace builder on the
/// no-wait path only.
fn is_document_create_revision(revision: Option<u64>) -> bool {
matches!(revision, None | Some(1))
}
use crate::sdk::SDKWrapper;
use crate::types::{
DashSDKPutSettings, DashSDKResultDataType, DashSDKStateTransitionCreationOptions,
Expand Down Expand Up @@ -112,8 +131,11 @@ pub unsafe extern "C" fn dash_sdk_document_put_to_platform(
(*put_settings).user_fee_increase
};

// Use the new DocumentCreateTransitionBuilder or DocumentReplaceTransitionBuilder
let state_transition = if document.revision().unwrap_or(0) == 1 {
// Route via the shared `is_document_create_revision` helper so the
// no-wait and wait paths cannot drift on revision routing. The
// helper treats `revision = None` as create — the legacy create
// intent, which the strict rs-sdk create helper also accepts.
let state_transition = if is_document_create_revision(document.revision()) {
// Create transition for new documents
let mut builder = DocumentCreateTransitionBuilder::new(
data_contract.clone(),
Expand Down Expand Up @@ -179,9 +201,7 @@ pub unsafe extern "C" fn dash_sdk_document_put_to_platform(
)
.await
}
.map_err(|e| {
FFIError::InternalError(format!("Failed to create document transition: {}", e))
})?;
.map_err(|e| map_document_sdk_error(e, "Failed to create document transition"))?;

// Serialize the state transition with bincode
let config = bincode::config::standard();
Expand Down Expand Up @@ -284,8 +304,10 @@ pub unsafe extern "C" fn dash_sdk_document_put_to_platform_and_wait(
(*put_settings).user_fee_increase
};

// Use the new builder pattern and SDK methods
let confirmed_document = if document.revision().unwrap_or(1) == 1 {
// Use the new builder pattern and SDK methods. Routes through the
// shared `is_document_create_revision` helper so this wait path
// agrees with the no-wait path above on revision = None routing.
let confirmed_document = if is_document_create_revision(document.revision()) {
// Create transition for new documents
let mut builder = DocumentCreateTransitionBuilder::new(
data_contract.clone(),
Expand Down Expand Up @@ -314,9 +336,7 @@ pub unsafe extern "C" fn dash_sdk_document_put_to_platform_and_wait(
.sdk
.document_create(builder, identity_public_key, signer)
.await
.map_err(|e| {
FFIError::InternalError(format!("Failed to create document and wait: {}", e))
})?;
.map_err(|e| map_document_sdk_error(e, "Failed to create document and wait"))?;

match result {
dash_sdk::platform::documents::transitions::DocumentCreateResult::Document(doc) => {
Expand Down Expand Up @@ -351,9 +371,7 @@ pub unsafe extern "C" fn dash_sdk_document_put_to_platform_and_wait(
.sdk
.document_replace(builder, identity_public_key, signer)
.await
.map_err(|e| {
FFIError::InternalError(format!("Failed to replace document and wait: {}", e))
})?;
.map_err(|e| map_document_sdk_error(e, "Failed to replace document and wait"))?;

match result {
dash_sdk::platform::documents::transitions::DocumentReplaceResult::Document(
Expand Down Expand Up @@ -424,6 +442,21 @@ mod tests {
[42u8; 32]
}

/// `is_document_create_revision` must agree with the routing rules
/// the FFI put paths used historically: `None` and `Some(1)` are the
/// create branch; any other value (including `Some(0)`) is the
/// replace branch. Both the no-wait and wait paths route through
/// this helper, so this test pins the shared decision so the
/// previous `unwrap_or(0)` / `unwrap_or(1)` drift cannot reappear.
#[test]
fn is_document_create_revision_matches_initial_revision_and_none() {
assert!(super::is_document_create_revision(None));
assert!(super::is_document_create_revision(Some(1)));
assert!(!super::is_document_create_revision(Some(0)));
assert!(!super::is_document_create_revision(Some(2)));
assert!(!super::is_document_create_revision(Some(u64::MAX)));
}

#[test]
fn test_put_with_null_sdk_handle() {
let document = create_mock_document_with_revision(1);
Expand Down
6 changes: 3 additions & 3 deletions packages/rs-sdk-ffi/src/document/replace.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Document replacement operations

use crate::document::helpers::{
convert_state_transition_creation_options, convert_token_payment_info,
convert_state_transition_creation_options, convert_token_payment_info, map_document_sdk_error,
};
use crate::sdk::SDKWrapper;
use crate::types::{
Expand Down Expand Up @@ -149,7 +149,7 @@ pub unsafe extern "C" fn dash_sdk_document_replace_on_platform(
.await
.map_err(|e| {
error!(error = %e, key_id = identity_public_key.id(), "[DOCUMENT REPLACE] failed to sign transition");
FFIError::InternalError(format!("Failed to create replace transition: {}", e))
map_document_sdk_error(e, "Failed to create replace transition")
})?;

debug!("[DOCUMENT REPLACE] state transition created, serializing");
Expand Down Expand Up @@ -357,7 +357,7 @@ pub unsafe extern "C" fn dash_sdk_document_replace_on_platform_and_wait(
"❌ [DOCUMENT REPLACE] Failed with key ID: {}",
identity_public_key.id()
);
FFIError::InternalError(format!("Failed to replace document and wait: {}", e))
map_document_sdk_error(e, "Failed to replace document and wait")
})?;

eprintln!("✅ [DOCUMENT REPLACE] SDK call completed successfully");
Expand Down
Loading
Loading