Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sdk): implement document transfers in WASM DPP #2406

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
3 changes: 3 additions & 0 deletions packages/js-dash-sdk/src/SDK/Client/Platform/Platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import createAssetLockTransaction from './createAssetLockTransaction';

import broadcastDocument from './methods/documents/broadcast';
import createDocument from './methods/documents/create';
import transferDocument from './methods/documents/transfer';
import getDocument from './methods/documents/get';

import publishContract from './methods/contracts/publish';
Expand Down Expand Up @@ -58,6 +59,7 @@ export interface PlatformOpts {
interface Records {
broadcast: Function,
create: Function,
transfer: Function,
get: Function,
}

Expand Down Expand Up @@ -165,6 +167,7 @@ export class Platform {
this.documents = {
broadcast: broadcastDocument.bind(this),
create: createDocument.bind(this),
transfer: transferDocument.bind(this),
get: getDocument.bind(this),
};
this.contracts = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@ import { signStateTransition } from '../../signStateTransition';
/**
* Broadcast document onto the platform
*
* @param {Platform} this - bound instance class
* @param {Object} documents
* @param {ExtendedDocument[]} [documents.create]
* @param {ExtendedDocument[]} [documents.replace]
* @param {ExtendedDocument[]} [documents.delete]
* @param identity - identity
* @param keyIndex - identity key index
*/
export default async function broadcast(
this: Platform,
documents: {
create?: ExtendedDocument[],
replace?: ExtendedDocument[],
delete?: ExtendedDocument[]
delete?: ExtendedDocument[],
},
identity: any,
keyIndex : number,
): Promise<any> {
this.logger.debug('[Document#broadcast] Broadcast documents', {
create: documents.create?.length || 0,
Expand Down Expand Up @@ -53,7 +54,7 @@ export default async function broadcast(

this.logger.silly('[Document#broadcast] Created documents batch transition');

await signStateTransition(this, documentsBatchTransition, identity, 1);
await signStateTransition(this, documentsBatchTransition, identity, keyIndex ?? 1);

// Broadcast state transition also wait for the result to be obtained
await broadcastStateTransition(this, documentsBatchTransition);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Identity, ExtendedDocument } from '@dashevo/wasm-dpp';
import { Platform } from '../../Platform';
import broadcastStateTransition from '../../broadcastStateTransition';
import { signStateTransition } from '../../signStateTransition';
/**
* Transfer document in the platform
*
* @param {Platform} this - bound instance class
* @param {string} typeLocator - type locator
* @param identity - identity
* @param {Object} [data] - options
* @returns {StateTransition}
*/
export async function transfer(
this: Platform,
document: ExtendedDocument,
receiver: Identity,
sender: Identity,
): Promise<any> {
this.logger.debug('[Document#transfer] Transfer document');
await this.initialize();

const identityContractNonce = await this.nonceManager
.bumpIdentityContractNonce(sender.getId(), document.getDataContractId());

const documentsBatchTransition = document
.createTransferTransition(receiver.getId(), BigInt(identityContractNonce));

await signStateTransition(this, documentsBatchTransition, sender, 1);

await broadcastStateTransition(this, documentsBatchTransition);
}

export default transfer;
108 changes: 106 additions & 2 deletions packages/rs-dpp/src/document/document_factory/v0/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use crate::state_transition::documents_batch_transition::{
DocumentsBatchTransition, DocumentsBatchTransitionV0,
};
use itertools::Itertools;
use crate::state_transition::documents_batch_transition::document_transition::DocumentTransferTransition;

const PROPERTY_FEATURE_VERSION: &str = "$version";
const PROPERTY_ENTROPY: &str = "$entropy";
Expand Down Expand Up @@ -262,9 +263,25 @@ impl DocumentFactoryV0 {
nonce_counter,
platform_version,
),
_ => Err(ProtocolError::InvalidStateTransitionType(
"action type not accounted for".to_string(),
DocumentTransitionActionType::Transfer => Self::document_transfer_transitions(
documents
.into_iter()
.map(|(document, document_type, _)| (document, document_type))
.collect(),
nonce_counter,
platform_version,
),
DocumentTransitionActionType::Purchase => {
Err(ProtocolError::InvalidStateTransitionType(
"action type not accounted for Transfer".to_string(),
))
}
DocumentTransitionActionType::UpdatePrice => Err(ProtocolError::InvalidStateTransitionType(
"action type not accounted for UpdatePrice".to_string(),
)),
DocumentTransitionActionType::IgnoreWhileBumpingRevision => Err(ProtocolError::InvalidStateTransitionType(
"action type not accounted for IgnoreWhileBumpingRevision".to_string(),
))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Mismatch in error message for Purchase action
When DocumentTransitionActionType::Purchase is encountered, the error message incorrectly states "action type not accounted for Transfer" instead of referring to "Purchase." This might confuse developers or users.

Consider applying the following fix:

-                DocumentTransitionActionType::Purchase => {
-                    Err(ProtocolError::InvalidStateTransitionType(
-                        "action type not accounted for Transfer".to_string(),
-                    ))
-                }
+                DocumentTransitionActionType::Purchase => {
+                    Err(ProtocolError::InvalidStateTransitionType(
+                        "action type not accounted for Purchase".to_string(),
+                    ))
+                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DocumentTransitionActionType::Transfer => Self::document_transfer_transitions(
documents
.into_iter()
.map(|(document, document_type, _)| (document, document_type))
.collect(),
nonce_counter,
platform_version,
),
DocumentTransitionActionType::Purchase => {
Err(ProtocolError::InvalidStateTransitionType(
"action type not accounted for Transfer".to_string(),
))
}
DocumentTransitionActionType::UpdatePrice => Err(ProtocolError::InvalidStateTransitionType(
"action type not accounted for UpdatePrice".to_string(),
)),
DocumentTransitionActionType::IgnoreWhileBumpingRevision => Err(ProtocolError::InvalidStateTransitionType(
"action type not accounted for IgnoreWhileBumpingRevision".to_string(),
))
DocumentTransitionActionType::Transfer => Self::document_transfer_transitions(
documents
.into_iter()
.map(|(document, document_type, _)| (document, document_type))
.collect(),
nonce_counter,
platform_version,
),
DocumentTransitionActionType::Purchase => {
Err(ProtocolError::InvalidStateTransitionType(
"action type not accounted for Purchase".to_string(),
))
}
DocumentTransitionActionType::UpdatePrice => Err(ProtocolError::InvalidStateTransitionType(
"action type not accounted for UpdatePrice".to_string(),
)),
DocumentTransitionActionType::IgnoreWhileBumpingRevision => Err(ProtocolError::InvalidStateTransitionType(
"action type not accounted for IgnoreWhileBumpingRevision".to_string(),
))

})
.collect::<Result<Vec<_>, ProtocolError>>()?
.into_iter()
Expand Down Expand Up @@ -507,6 +524,93 @@ impl DocumentFactoryV0 {
// Ok(raw_transitions)
}

#[cfg(feature = "state-transitions")]
fn document_transfer_transitions(
documents: Vec<(Document, DocumentTypeRef)>,
nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, //IdentityID/ContractID -> nonce
platform_version: &PlatformVersion,
) -> Result<Vec<DocumentTransition>, ProtocolError> {
documents
.into_iter()
.map(|(mut document, document_type)| {
if !document_type.documents_mutable() {
return Err(DocumentError::TryingToReplaceImmutableDocument {
document: Box::new(document),
}
.into());
}
if document.revision().is_none() {
return Err(DocumentError::RevisionAbsentError {
document: Box::new(document),
}
.into());
};

document.increment_revision()?;
document.set_updated_at(Some(Utc::now().timestamp_millis() as TimestampMillis));

let recipient_owner_id = document.owner_id();

let nonce = nonce_counter
.entry((document.owner_id(), document_type.data_contract_id()))
.or_default();

let transition = DocumentTransferTransition::from_document(
document,
document_type,
*nonce,
recipient_owner_id,
platform_version,
None,
None,
)?;

*nonce += 1;

Ok(transition.into())
})
.collect()
// let mut raw_transitions = vec![];
// for (document, document_type) in documents {
// if !document_type.documents_mutable() {
// return Err(DocumentError::TryingToReplaceImmutableDocument {
// document: Box::new(document),
// }
// .into());
// }
// let Some(document_revision) = document.revision() else {
// return Err(DocumentError::RevisionAbsentError {
// document: Box::new(document),
// }.into());
// };
// let mut map = document.to_map_value()?;
//
// map.retain(|key, _| {
// !key.starts_with('$') || DOCUMENT_REPLACE_KEYS_TO_STAY.contains(&key.as_str())
// });
// map.insert(
// PROPERTY_ACTION.to_string(),
// Value::U8(DocumentTransitionActionType::Replace as u8),
// );
// let new_revision = document_revision + 1;
// map.insert(PROPERTY_REVISION.to_string(), Value::U64(new_revision));
//
// // If document have an originally set `updatedAt`
// // we should update it then
// let contains_updated_at = document_type
// .required_fields()
// .contains(PROPERTY_UPDATED_AT);
//
// if contains_updated_at {
// let now = Utc::now().timestamp_millis() as TimestampMillis;
// map.insert(PROPERTY_UPDATED_AT.to_string(), Value::U64(now));
// }
//
// raw_transitions.push(map.into());
// }
// Ok(raw_transitions)
}

#[cfg(feature = "state-transitions")]
fn document_delete_transitions(
documents: Vec<(Document, DocumentTypeRef)>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,22 @@ impl SpecializedDocumentFactoryV0 {
nonce_counter,
platform_version,
),
_ => Err(ProtocolError::InvalidStateTransitionType(
"action type not accounted for".to_string(),
DocumentTransitionActionType::Transfer => {
Err(ProtocolError::InvalidStateTransitionType(
"action type not accounted for Transfer".to_string(),
))
},
DocumentTransitionActionType::Purchase => {
Err(ProtocolError::InvalidStateTransitionType(
"action type not accounted for Purchase".to_string(),
))
}
DocumentTransitionActionType::UpdatePrice => Err(ProtocolError::InvalidStateTransitionType(
"action type not accounted for UpdatePrice".to_string(),
)),
DocumentTransitionActionType::IgnoreWhileBumpingRevision => Err(ProtocolError::InvalidStateTransitionType(
"action type not accounted for IgnoreWhileBumpingRevision".to_string(),
))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Missing Transfer transition support.
Although transfer transitions are defined elsewhere, here the code returns “action type not accounted for Transfer” as an error. This creates an inconsistency with other segments that do implement transfer logic. Consider implementing the logic or removing the partial references.

})
.collect::<Result<Vec<_>, ProtocolError>>()?
.into_iter()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ impl DocumentsBatchTransitionInternalTransformerV0 for DocumentsBatchTransition
StateError::InvalidDocumentRevisionError(InvalidDocumentRevisionError::new(
document_id,
Some(previous_revision),
transition_revision,
expected_revision,
)),
))
}
Expand Down
36 changes: 32 additions & 4 deletions packages/wasm-dpp/src/document/extended_document.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
use dpp::document::{
DocumentV0Getters, DocumentV0Setters, ExtendedDocument, EXTENDED_DOCUMENT_IDENTIFIER_FIELDS,
};
use dpp::document::{DocumentV0Getters, DocumentV0Setters, ExtendedDocument, EXTENDED_DOCUMENT_IDENTIFIER_FIELDS};
use serde_json::Value as JsonValue;

use dpp::platform_value::{Bytes32, Value};
use dpp::prelude::{Identifier, Revision, TimestampMillis};
use dpp::prelude::{Identifier, IdentityNonce, Revision, TimestampMillis, UserFeeIncrease};

Check warning on line 5 in packages/wasm-dpp/src/document/extended_document.rs

View workflow job for this annotation

GitHub Actions / Rust packages (wasm-dpp) / Linting

unused import: `UserFeeIncrease`

warning: unused import: `UserFeeIncrease` --> packages/wasm-dpp/src/document/extended_document.rs:5:74 | 5 | use dpp::prelude::{Identifier, IdentityNonce, Revision, TimestampMillis, UserFeeIncrease}; | ^^^^^^^^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default

use dpp::util::json_value::JsonValueExt;

Expand All @@ -16,12 +14,15 @@
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use wasm_bindgen::prelude::*;
use dpp::state_transition::documents_batch_transition::document_transition::DocumentTransferTransition;
use dpp::state_transition::documents_batch_transition::{DocumentsBatchTransition, DocumentsBatchTransitionV0};

use crate::buffer::Buffer;
use crate::data_contract::DataContractWasm;
#[allow(deprecated)] // BinaryType is unsed in unused code below
use crate::document::BinaryType;
use crate::document::{ConversionOptions, DocumentWasm};
use crate::document_batch_transition::DocumentsBatchTransitionWasm;
use crate::errors::RustConversionError;
use crate::identifier::{identifier_from_js_value, IdentifierWrapper};
use crate::lodash::lodash_set;
Expand Down Expand Up @@ -235,6 +236,33 @@
.set_created_at(ts.map(|t| t.get_time() as TimestampMillis));
}

#[wasm_bindgen(js_name=createTransferStateTransition)]
pub fn create_transfer_state_transition(&mut self, recipient: IdentifierWrapper, identity_contract_nonce: IdentityNonce) -> DocumentsBatchTransitionWasm {
let mut cloned_document = self.0.document().clone();

cloned_document.set_revision(Some(cloned_document.revision().unwrap() + 1));

let transfer_transition = DocumentTransferTransition::from_document(
cloned_document,
self.0.document_type().unwrap(),
identity_contract_nonce,
recipient.into(),
PlatformVersion::latest(),
None,
None,
).unwrap();

let documents_batch_transition: DocumentsBatchTransition = DocumentsBatchTransitionV0 {
owner_id: self.0.owner_id(),
transitions: vec![transfer_transition.into()],
user_fee_increase: Default::default(),
signature_public_key_id: Default::default(),
signature: Default::default(),
}.into();

documents_batch_transition.into()
}
Comment on lines +239 to +264
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Check for None revision before incrementing.
The method does not verify cloned_document.revision() is Some before computing unwrap() + 1. If a document is missing a revision, this will panic. To prevent runtime errors, handle the case of None:

     let mut cloned_document = self.0.document().clone();

-    cloned_document.set_revision(Some(cloned_document.revision().unwrap() + 1));
+    if let Some(current_revision) = cloned_document.revision() {
+        cloned_document.set_revision(Some(current_revision + 1));
+    } else {
+        // Decide how to handle the absence of a revision, e.g., return an error or set a default revision
+        return Err(JsValue::from_str("Document is missing a revision"));
+    }

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


#[wasm_bindgen(js_name=setUpdatedAt)]
pub fn set_updated_at(&mut self, ts: Option<js_sys::Date>) {
self.0
Expand Down
8 changes: 8 additions & 0 deletions packages/wasm-dpp/src/document/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub struct DocumentTransitions {
create: Vec<ExtendedDocumentWasm>,
replace: Vec<ExtendedDocumentWasm>,
delete: Vec<ExtendedDocumentWasm>,
transfer: Vec<ExtendedDocumentWasm>,
}

#[wasm_bindgen(js_class=DocumentTransitions)]
Expand All @@ -55,6 +56,11 @@ impl DocumentTransitions {
pub fn add_transition_delete(&mut self, transition: ExtendedDocumentWasm) {
self.delete.push(transition)
}

#[wasm_bindgen(js_name = "addTransitionTransfer")]
pub fn add_transition_transfer(&mut self, transition: ExtendedDocumentWasm) {
self.transfer.push(transition)
}
}

#[wasm_bindgen(js_name = DocumentFactory)]
Expand Down Expand Up @@ -278,10 +284,12 @@ fn extract_documents_by_action(
let documents_create = extract_documents_of_action(documents, "create").with_js_error()?;
let documents_replace = extract_documents_of_action(documents, "replace").with_js_error()?;
let documents_delete = extract_documents_of_action(documents, "delete").with_js_error()?;
let documents_transfer = extract_documents_of_action(documents, "transfer").with_js_error()?;

documents_by_action.insert(DocumentTransitionActionType::Create, documents_create);
documents_by_action.insert(DocumentTransitionActionType::Replace, documents_replace);
documents_by_action.insert(DocumentTransitionActionType::Delete, documents_delete);
documents_by_action.insert(DocumentTransitionActionType::Transfer, documents_transfer);

Ok(documents_by_action)
}
Expand Down
Loading
Loading