diff --git a/Cargo.lock b/Cargo.lock index 06da0bb182..a343a58750 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1318,6 +1318,7 @@ dependencies = [ "futures", "hex", "http", + "js-sys", "lru", "rs-dapi-client", "rustls-pemfile", @@ -5030,8 +5031,9 @@ version = "2.0.0" dependencies = [ "base64 0.22.1", "bincode", - "dashcore-rpc", + "dashcore", "dpp", + "hex", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9bf5ad30ba..fa9cfc53df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,3 +42,4 @@ exclude = ["packages/wasm-sdk"] # This one is experimental and not ready for use [workspace.package] rust-version = "1.85" + diff --git a/packages/dash-platform-balance-checker/src/main_simple.rs b/packages/dash-platform-balance-checker/src/main_simple.rs index 011bed7f7b..46af65431c 100644 --- a/packages/dash-platform-balance-checker/src/main_simple.rs +++ b/packages/dash-platform-balance-checker/src/main_simple.rs @@ -1,10 +1,10 @@ use anyhow::Result; use dapi_grpc::platform::v0::{ - GetIdentityBalanceRequest, GetIdentityBalanceResponse, GetIdentityRequest, GetIdentityResponse, + get_identity_balance_request, get_identity_balance_response, get_identity_request, + get_identity_response, GetIdentityBalanceRequest, GetIdentityRequest, }; -use dpp::dashcore::Network; use dpp::prelude::Identifier; -use rs_dapi_client::{Address, AddressList, DapiClient, RequestSettings}; +use rs_dapi_client::{Address, AddressList, DapiClient, DapiRequestExecutor, RequestSettings}; use std::str::FromStr; #[tokio::main] @@ -37,48 +37,80 @@ async fn main() -> Result<()> { // Create request for identity (proved) let request = GetIdentityRequest { - version: None, - id: identity_id.to_vec(), - prove: true, + version: Some(get_identity_request::Version::V0( + get_identity_request::GetIdentityRequestV0 { + id: identity_id.to_vec(), + prove: true, + }, + )), }; println!("Fetching identity (proved)..."); - match dapi.platform.get_identity(request).await { + match dapi.execute(request, RequestSettings::default()).await { Ok(response) => { - if let Some(identity_bytes) = response.identity { - println!( - "✓ Identity found! Raw response length: {} bytes", - identity_bytes.len() - ); + // Check if we have a response with version + if let Some(version) = response.inner.version { + match version { + get_identity_response::Version::V0(v0) => { + if let Some(result) = v0.result { + match result { + get_identity_response::get_identity_response_v0::Result::Identity(identity_bytes) => { + println!( + "✓ Identity found! Raw response length: {} bytes", + identity_bytes.len() + ); - // Now get balance - let balance_request = GetIdentityBalanceRequest { - version: None, - id: identity_id.to_vec(), - prove: true, - }; + // Now get balance + let balance_request = GetIdentityBalanceRequest { + version: Some(get_identity_balance_request::Version::V0( + get_identity_balance_request::GetIdentityBalanceRequestV0 { + id: identity_id.to_vec(), + prove: true, + } + )), + }; - println!("\nFetching balance..."); - match dapi.platform.get_identity_balance(balance_request).await { - Ok(balance_response) => { - if let Some(balance) = balance_response.balance { - let balance_value = balance.balance; - println!("\n✓ Balance found!"); - println!(" Balance: {} credits", balance_value); - println!( - " Balance in DASH: {} DASH", - balance_value as f64 / 100_000_000.0 - ); + println!("\nFetching balance..."); + match dapi.execute(balance_request, RequestSettings::default()).await { + Ok(balance_response) => { + if let Some(version) = balance_response.inner.version { + match version { + get_identity_balance_response::Version::V0(v0) => { + if let Some(result) = v0.result { + match result { + get_identity_balance_response::get_identity_balance_response_v0::Result::Balance(balance) => { + println!("\n✓ Balance found!"); + println!(" Balance: {} credits", balance); + println!( + " Balance in DASH: {} DASH", + balance as f64 / 100_000_000.0 + ); + } + _ => println!("✗ Unexpected response type") + } + } else { + println!("✗ No balance data in response"); + } + } + } + } else { + println!("✗ No version in balance response"); + } + } + Err(e) => { + println!("✗ Error fetching balance: {}", e); + } + } + } + _ => println!("✗ Identity not found or proof returned") + } } else { - println!("✗ No balance data in response"); + println!("✗ No result in response"); } } - Err(e) => { - println!("✗ Error fetching balance: {}", e); - } } } else { - println!("✗ Identity not found"); + println!("✗ No version in response"); } } Err(e) => { diff --git a/packages/rs-drive-abci/Cargo.toml b/packages/rs-drive-abci/Cargo.toml index b56f85de2e..b4aea2733a 100644 --- a/packages/rs-drive-abci/Cargo.toml +++ b/packages/rs-drive-abci/Cargo.toml @@ -30,7 +30,7 @@ hex = "0.4.3" indexmap = { version = "2.2.6", features = ["serde"] } dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", tag = "v0.39.6" } dpp = { path = "../rs-dpp", default-features = false, features = ["abci"] } -simple-signer = { path = "../simple-signer" } +simple-signer = { path = "../simple-signer", features = ["state-transitions"] } rust_decimal = "1.2.5" rust_decimal_macros = "1.25.0" mockall = { version = "0.13", optional = true } diff --git a/packages/rs-drive-abci/src/query/identity_based_queries/identity_by_non_unique_public_key_hash/v0/mod.rs b/packages/rs-drive-abci/src/query/identity_based_queries/identity_by_non_unique_public_key_hash/v0/mod.rs index f31423a2ae..62d17debc0 100644 --- a/packages/rs-drive-abci/src/query/identity_based_queries/identity_by_non_unique_public_key_hash/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/identity_based_queries/identity_by_non_unique_public_key_hash/v0/mod.rs @@ -179,7 +179,7 @@ mod tests { core_chain_locked_height: 0, epoch: 0, time_ms: 0, - protocol_version: 9, + protocol_version: 10, chain_id: "chain_id".to_string() }), result: Some( diff --git a/packages/rs-drive/QUERY_QUICK_REFERENCE.md b/packages/rs-drive/QUERY_QUICK_REFERENCE.md new file mode 100644 index 0000000000..256dacc202 --- /dev/null +++ b/packages/rs-drive/QUERY_QUICK_REFERENCE.md @@ -0,0 +1,73 @@ +# Dash Platform Query Quick Reference + +## Can we do multiple ranges on one index? + +**YES, with limitations:** +- ✅ **2 range clauses on SAME field** → Combined into Between operator +- ❌ **Range clauses on DIFFERENT fields** → Not supported +- ❌ **More than 2 range clauses** → Not supported + +### Valid Example: +```json +{ + "where": [ + ["age", ">", 18], + ["age", "<=", 65] + ] +} +``` +→ Internally becomes: `age BetweenExcludeLeft [18, 65]` + +### Invalid Example: +```json +{ + "where": [ + ["age", ">", 18], + ["salary", "<", 100000] + ] +} +``` +→ Error: Range clauses must be on same field + +## Compound Index Queries + +For index [A, B, C], ranges can be on ANY property BUT: +- Range on A: No requirements +- Range on B: MUST have `A == value` +- Range on C: MUST have `A == value` AND `B == value` + +### Examples: +```json +// ✅ Valid: Range on B with equality on A +{ + "where": [ + ["A", "==", 5], + ["B", ">", 10] + ] +} + +// ❌ Invalid: Range on B without equality on A +{ + "where": [ + ["B", ">", 10] // Error: Missing A + ] +} +``` + +## Key Query Rules + +1. **Fields MUST be indexed** (no queries on non-indexed fields) +2. **One IN clause per query** (max 100 values) +3. **One range clause per query** (can be combined from 2 on same field) +4. **Limits**: Default 100, Max 100 (configurable) +5. **Special queryable fields**: `$id`, `$ownerId`, `$createdAt`, `$updatedAt`, `$revision` + +## ProveOptions Warning + +When using proof generation with GroveDB: +```rust +ProveOptions { + decrease_limit_on_empty_sub_query_result: true, // ALWAYS use true in production +} +``` +Setting to `false` can cause memory exhaustion if query matches many empty subtrees! \ No newline at end of file diff --git a/packages/rs-drive/SECONDARY_INDEX_QUERIES.md b/packages/rs-drive/SECONDARY_INDEX_QUERIES.md new file mode 100644 index 0000000000..65356b4564 --- /dev/null +++ b/packages/rs-drive/SECONDARY_INDEX_QUERIES.md @@ -0,0 +1,340 @@ +# Dash Platform Secondary Index Document Queries - Comprehensive Guide for AI Agents + +## Overview + +Dash Platform uses a hierarchical authenticated data structure based on GroveDB (an AVL merkle forest) to store and query documents. This guide explains the query system capabilities, limitations, and implementation details for secondary index queries. + +## Key Concepts + +### 1. Query Structure + +All document queries in Dash Platform are represented by the `DriveDocumentQuery` struct, which contains: + +```rust +pub struct DriveDocumentQuery<'a> { + pub contract: &'a DataContract, + pub document_type: DocumentTypeRef<'a>, + pub internal_clauses: InternalClauses, + pub offset: Option, + pub limit: Option, + pub order_by: IndexMap, + pub start_at: Option<[u8; 32]>, + pub start_at_included: bool, + pub block_time_ms: Option, +} +``` + +### 2. Internal Clauses + +The `InternalClauses` struct organizes WHERE clauses into categories: + +```rust +pub struct InternalClauses { + pub primary_key_in_clause: Option, // IN clause on $id + pub primary_key_equal_clause: Option, // == clause on $id + pub in_clause: Option, // Single IN clause on indexed field + pub range_clause: Option, // Single range clause (may be combined from 2) + pub equal_clauses: BTreeMap, // Multiple == clauses +} +``` + +## Query Capabilities + +### 1. Supported Operators + +- **Equality**: `==` (Equal) +- **Comparison**: `>` (GreaterThan), `>=` (GreaterThanOrEquals), `<` (LessThan), `<=` (LessThanOrEquals) +- **Range**: `Between`, `BetweenExcludeBounds`, `BetweenExcludeLeft`, `BetweenExcludeRight` +- **Membership**: `in` (In) +- **String**: `StartsWith` + +### 2. Multiple Range Queries on Single Field + +**YES, but with strict limitations:** + +- **Exactly 2 range clauses** can be combined on the **same field** +- They must form valid bounds (one upper, one lower) +- Valid combinations are automatically converted to Between operators: + - `field >= X && field <= Y` → `field Between [X, Y]` + - `field >= X && field < Y` → `field BetweenExcludeRight [X, Y]` + - `field > X && field <= Y` → `field BetweenExcludeLeft [X, Y]` + - `field > X && field < Y` → `field BetweenExcludeBounds [X, Y]` + +**Example:** +```json +{ + "where": [ + ["age", ">", 18], + ["age", "<", 65] + ] +} +``` +This is valid and will be internally converted to: `age BetweenExcludeBounds [18, 65]` + +### 3. Query Types + +#### Simple Equality Query +```json +{ + "where": [ + ["firstName", "==", "Alice"] + ] +} +``` + +#### Range Query +```json +{ + "where": [ + ["age", ">=", 18] + ] +} +``` + +#### IN Query +```json +{ + "where": [ + ["status", "in", ["active", "pending", "approved"]] + ] +} +``` + +#### Combined Query +```json +{ + "where": [ + ["category", "==", "electronics"], + ["price", ">=", 100], + ["price", "<=", 1000] + ], + "orderBy": [ + ["price", "asc"] + ], + "limit": 50 +} +``` + +### 4. Special Fields + +These fields can be queried without being explicitly defined in the document type: +- `$id` - Document identifier +- `$ownerId` - Owner identifier +- `$createdAt` - Creation timestamp +- `$updatedAt` - Update timestamp +- `$revision` - Document revision number + +## Compound Index Queries + +### How Compound Indexes Work + +When an index has multiple properties [A, B, C], the query system creates a hierarchical tree structure: +- Level 1: Property A values +- Level 2: Property B values (under each A value) +- Level 3: Property C values (under each A,B combination) +- Level 4: Document IDs + +### Range Queries on Compound Indexes + +**Range queries CAN be performed on any property in a compound index**, but you MUST provide equality constraints for ALL properties that come before it in the index. + +#### For index [A, B, C]: + +✅ **Valid - Range on A (first property):** +```json +{ + "where": [ + ["A", ">", 10], + ["A", "<", 20] + ] +} +``` + +✅ **Valid - Range on B with equality on A:** +```json +{ + "where": [ + ["A", "==", 5], // Required: equality on A + ["B", ">", 100], + ["B", "<=", 200] + ] +} +``` + +✅ **Valid - Range on C with equalities on A and B:** +```json +{ + "where": [ + ["A", "==", 5], // Required: equality on A + ["B", "==", 10], // Required: equality on B + ["C", ">=", 1000] + ] +} +``` + +❌ **Invalid - Range on B without equality on A:** +```json +{ + "where": [ + ["B", ">", 100] // Error: Cannot query B without specifying A + ] +} +``` + +❌ **Invalid - Range on C without equality on B:** +```json +{ + "where": [ + ["A", "==", 5], + ["C", ">", 1000] // Error: Cannot skip B in the index + ] +} +``` + +### Why This Design? + +The hierarchical structure means: +1. To query at any level, you must provide the complete path to that level +2. This is like a file system where you can't list `/home/*/documents/` without specifying which home directory +3. It ensures efficient traversal of the authenticated merkle tree structure + +### Practical Example + +Given an index on [category, brand, price]: +```json +// Query electronics from Apple with price range +{ + "where": [ + ["category", "==", "electronics"], // Required + ["brand", "==", "Apple"], // Required + ["price", ">=", 500], + ["price", "<=", 2000] + ] +} +``` + +This creates the path: `/category=electronics/brand=Apple/price=[500,2000]/` + +## Restrictions and Validation Rules + +### 1. Index Requirements + +- **Queries MUST match an index** defined in the document type +- Non-indexed fields cannot be queried (error: `WhereClauseOnNonIndexedProperty`) +- Query must be within `MAX_INDEX_DIFFERENCE` (2) of an existing index + +### 2. Limit Restrictions + +- **Default limit**: 100 documents +- **Maximum limit**: 100 documents +- **Absolute maximum**: 65,535 (u16::MAX) +- If limit is 0, defaults to system default (100) + +### 3. Clause Restrictions + +- **Primary key queries**: Only ONE equal or IN clause allowed on `$id` +- **IN clause limits**: + - Minimum: 1 value + - Maximum: 100 values + - No duplicate values +- **Range clauses**: Maximum 2 on the same field (combined into Between) +- **One IN clause per query**: Cannot have multiple IN clauses +- **Field overlap**: Same field cannot appear in different clause types + +### 4. Invalid Query Examples + +#### Multiple IN clauses (INVALID) +```json +{ + "where": [ + ["category", "in", ["A", "B"]], + ["status", "in", ["active", "pending"]] + ] +} +``` + +#### Range on multiple fields (INVALID) +```json +{ + "where": [ + ["age", ">", 18], + ["salary", "<", 100000] + ] +} +``` + +#### Conflicting clauses (INVALID) +```json +{ + "where": [ + ["age", "==", 25], + ["age", ">", 30] + ] +} +``` + +## ProveOptions and GroveDB Integration + +### ProveOptions Structure + +The `ProveOptions` struct (from grovedb v3.0.0) configures proof generation: + +```rust +pub struct ProveOptions { + /// Decrease the available limit by 1 for empty subtrees + /// WARNING: Set to false only if you know there are few empty subtrees + /// Otherwise can cause memory exhaustion + pub decrease_limit_on_empty_sub_query_result: bool, +} +``` + +### Key Points: + +1. **Default behavior**: When `None` is passed (common in codebase), default options are used +2. **Memory safety**: The `decrease_limit_on_empty_sub_query_result` flag prevents memory exhaustion when traversing many empty branches in the merkle tree +3. **Usage**: Pass to `grovedb.get_proved_path_query()` as the second parameter + +## Query Execution Flow + +1. **Parsing**: JSON/CBOR query → `DocumentQuery` struct +2. **Validation**: + - Check field existence + - Validate operators + - Ensure index match + - Check limits +3. **Internal Clause Extraction**: Group clauses into `InternalClauses` +4. **Index Selection**: Find best matching index +5. **Path Query Generation**: Convert to GroveDB `PathQuery` +6. **Execution**: + - Without proof: Direct query execution + - With proof: Use `get_proved_path_query()` with optional `ProveOptions` +7. **Result**: Documents with optional cryptographic proof + +## Best Practices for AI Agents + +1. **Always check if fields are indexed** before querying +2. **Use compound indexes** when querying multiple fields +3. **Prefer equality queries** over range queries for performance +4. **Limit IN clause values** to reasonable amounts (<50 recommended) +5. **Use pagination** (start_at/start_after) for large result sets +6. **Combine range clauses** on same field for efficiency +7. **Avoid queries far from indexes** (will be rejected) + +## Error Handling + +Common query errors: +- `QuerySyntaxError::WhereClauseOnNonIndexedProperty` - Field not indexed +- `QuerySyntaxError::InvalidInClauseValue` - IN clause validation failed +- `QuerySyntaxError::MultipleRangeClauses` - Invalid range combination +- `QuerySyntaxError::QueryTooFarFromIndex` - No suitable index found +- `QuerySyntaxError::InvalidLimit` - Limit exceeds maximum + +## Summary + +Dash Platform's query system is designed for: +- **Efficiency**: Requires indexed fields +- **Safety**: Limits prevent resource exhaustion +- **Flexibility**: Supports various operators and combinations +- **Verifiability**: Optional cryptographic proofs via GroveDB + +The system intelligently combines multiple range clauses on the same field but restricts complex multi-field range queries to ensure performance and predictability in the distributed environment. \ No newline at end of file diff --git a/packages/rs-sdk-trusted-context-provider/examples/with_fallback.rs b/packages/rs-sdk-trusted-context-provider/examples/with_fallback.rs new file mode 100644 index 0000000000..c07a04c6ac --- /dev/null +++ b/packages/rs-sdk-trusted-context-provider/examples/with_fallback.rs @@ -0,0 +1,69 @@ +//! Example showing how to use TrustedHttpContextProvider with a custom fallback provider + +use dash_context_provider::{ContextProvider, ContextProviderError}; +use dpp::dashcore::Network; +use dpp::data_contract::TokenConfiguration; +use dpp::prelude::{CoreBlockHeight, DataContract, Identifier}; +use dpp::version::PlatformVersion; +use rs_sdk_trusted_context_provider::TrustedHttpContextProvider; +use std::num::NonZeroUsize; +use std::sync::Arc; + +/// Example fallback provider that could fetch data contracts from another source +struct MyFallbackProvider; + +impl ContextProvider for MyFallbackProvider { + fn get_quorum_public_key( + &self, + _quorum_type: u32, + _quorum_hash: [u8; 32], + _core_chain_locked_height: u32, + ) -> Result<[u8; 48], ContextProviderError> { + // This would not be called as the trusted provider handles quorum keys + Err(ContextProviderError::Generic("Not implemented".to_string())) + } + + fn get_data_contract( + &self, + _id: &Identifier, + _platform_version: &PlatformVersion, + ) -> Result>, ContextProviderError> { + // In a real implementation, this would fetch from Core RPC or another source + println!("Fallback provider: get_data_contract called"); + Ok(None) + } + + fn get_token_configuration( + &self, + _token_id: &Identifier, + ) -> Result, ContextProviderError> { + // In a real implementation, this would fetch from Core RPC or another source + println!("Fallback provider: get_token_configuration called"); + Ok(None) + } + + fn get_platform_activation_height(&self) -> Result { + // This would not be called as the trusted provider handles this + Ok(1320) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create the trusted HTTP provider with custom fallback + let _trusted_provider = + TrustedHttpContextProvider::new(Network::Testnet, None, NonZeroUsize::new(100).unwrap())? + .with_fallback_provider(MyFallbackProvider); + + println!("Created trusted HTTP provider with custom fallback!"); + + // The provider can now: + // 1. Fetch quorum public keys from HTTP endpoints (trusted provider) + // 2. Delegate data contract requests to MyFallbackProvider + // 3. Delegate token configuration requests to MyFallbackProvider + + // In a real application, you would use this provider with the SDK: + // sdk.set_context_provider(trusted_provider); + + Ok(()) +} diff --git a/packages/rs-sdk-trusted-context-provider/src/provider.rs b/packages/rs-sdk-trusted-context-provider/src/provider.rs index 6f82686ed7..b5caddb251 100644 --- a/packages/rs-sdk-trusted-context-provider/src/provider.rs +++ b/packages/rs-sdk-trusted-context-provider/src/provider.rs @@ -48,7 +48,7 @@ pub struct TrustedHttpContextProvider { fallback_provider: Option>, /// Known contracts cache - contracts that are pre-loaded and can be served immediately - known_contracts: HashMap>, + known_contracts: Arc>>>, /// Whether to refetch quorums if not found in cache refetch_if_not_found: bool, @@ -127,7 +127,7 @@ impl TrustedHttpContextProvider { last_current_quorums: Arc::new(ArcSwap::new(Arc::new(None))), last_previous_quorums: Arc::new(ArcSwap::new(Arc::new(None))), fallback_provider: None, - known_contracts: HashMap::new(), + known_contracts: Arc::new(Mutex::new(HashMap::new())), refetch_if_not_found: true, }) } @@ -140,10 +140,12 @@ impl TrustedHttpContextProvider { /// Set known contracts that will be served immediately without fallback pub fn with_known_contracts(mut self, contracts: Vec) -> Self { + let mut known = self.known_contracts.lock().unwrap(); for contract in contracts { let id = contract.id(); - self.known_contracts.insert(id, Arc::new(contract)); + known.insert(id, Arc::new(contract)); } + drop(known); self } @@ -153,6 +155,13 @@ impl TrustedHttpContextProvider { self } + /// Add a data contract to the known contracts cache + pub fn add_known_contract(&self, contract: DataContract) { + let id = contract.id(); + let mut known = self.known_contracts.lock().unwrap(); + known.insert(id, Arc::new(contract)); + } + /// Update the quorum caches by fetching current and previous quorums pub async fn update_quorum_caches(&self) -> Result<(), TrustedContextProviderError> { // Fetch current quorums @@ -471,9 +480,11 @@ impl ContextProvider for TrustedHttpContextProvider { platform_version: &PlatformVersion, ) -> Result>, ContextProviderError> { // First check known contracts cache - if let Some(contract) = self.known_contracts.get(id) { + let known = self.known_contracts.lock().unwrap(); + if let Some(contract) = known.get(id) { return Ok(Some(contract.clone())); } + drop(known); // If not found in known contracts, delegate to fallback provider if available if let Some(ref provider) = self.fallback_provider { diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index 0d48f639af..1e2a8a18d4 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -47,6 +47,9 @@ zeroize = { version = "1.8", features = ["derive"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.40", features = ["macros", "time", "rt-multi-thread"] } +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = "0.3" + [dev-dependencies] rs-dapi-client = { path = "../rs-dapi-client" } drive-proof-verifier = { path = "../rs-drive-proof-verifier" } diff --git a/packages/rs-sdk/src/sdk.rs b/packages/rs-sdk/src/sdk.rs index f3d0398d82..d98c8dc60c 100644 --- a/packages/rs-sdk/src/sdk.rs +++ b/packages/rs-sdk/src/sdk.rs @@ -40,6 +40,7 @@ use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use std::sync::atomic::Ordering; use std::sync::{atomic, Arc}; +#[cfg(not(target_arch = "wasm32"))] use std::time::{SystemTime, UNIX_EPOCH}; #[cfg(feature = "mocks")] use tokio::sync::{Mutex, MutexGuard}; @@ -205,6 +206,24 @@ enum SdkInstance { }, } +/// Helper function to get current timestamp in seconds +/// Works in both native and WASM environments +fn get_current_time_seconds() -> u64 { + #[cfg(not(target_arch = "wasm32"))] + { + match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(n) => n.as_secs(), + Err(_) => panic!("SystemTime before UNIX EPOCH!"), + } + } + #[cfg(target_arch = "wasm32")] + { + // In WASM, we use JavaScript's Date.now() which returns milliseconds + // We need to convert to seconds + (js_sys::Date::now() / 1000.0) as u64 + } +} + impl Sdk { /// Initialize Dash Platform SDK in mock mode. /// @@ -360,10 +379,7 @@ impl Sdk { settings: Option, ) -> Result { let settings = settings.unwrap_or_default(); - let current_time_s = match SystemTime::now().duration_since(UNIX_EPOCH) { - Ok(n) => n.as_secs(), - Err(_) => panic!("SystemTime before UNIX EPOCH!"), - }; + let current_time_s = get_current_time_seconds(); // we start by only using a read lock, as this speeds up the system let mut identity_nonce_counter = self.internal_cache.identity_nonce_counter.lock().await; @@ -449,10 +465,7 @@ impl Sdk { settings: Option, ) -> Result { let settings = settings.unwrap_or_default(); - let current_time_s = match SystemTime::now().duration_since(UNIX_EPOCH) { - Ok(n) => n.as_secs(), - Err(_) => panic!("SystemTime before UNIX EPOCH!"), - }; + let current_time_s = get_current_time_seconds(); // we start by only using a read lock, as this speeds up the system let mut identity_contract_nonce_counter = self diff --git a/packages/simple-signer/Cargo.toml b/packages/simple-signer/Cargo.toml index 83ab513f48..b7b3832680 100644 --- a/packages/simple-signer/Cargo.toml +++ b/packages/simple-signer/Cargo.toml @@ -6,8 +6,14 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = [] +# Enable state transitions support (requires additional dpp features) +state-transitions = ["dpp/state-transitions", "dpp/bls-signatures", "dpp/state-transition-signing"] + [dependencies] bincode = { version = "=2.0.0-rc.3", features = ["serde"] } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", tag = "v0.39.6" } -dpp = { path = "../rs-dpp", default-features = false, features = ["abci"] } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", tag = "v0.39.6", features = ["signer"] } +dpp = { path = "../rs-dpp", default-features = false, features = ["ed25519-dalek"] } base64 = { version = "0.22.1" } +hex = { version = "0.4.3" } diff --git a/packages/simple-signer/src/lib.rs b/packages/simple-signer/src/lib.rs index eef58b8f7b..a694fe8956 100644 --- a/packages/simple-signer/src/lib.rs +++ b/packages/simple-signer/src/lib.rs @@ -1 +1,6 @@ +#[cfg(feature = "state-transitions")] pub mod signer; + +pub mod single_key_signer; + +pub use single_key_signer::SingleKeySigner; diff --git a/packages/simple-signer/src/signer.rs b/packages/simple-signer/src/signer.rs index 125e9bcdb8..65d284ad0f 100644 --- a/packages/simple-signer/src/signer.rs +++ b/packages/simple-signer/src/signer.rs @@ -1,6 +1,6 @@ use base64::prelude::BASE64_STANDARD; use base64::Engine; -use dashcore_rpc::dashcore::signer; +use dashcore::signer; use dpp::bincode::{Decode, Encode}; use dpp::bls_signatures::{Bls12381G2Impl, SignatureSchemes}; use dpp::ed25519_dalek::Signer as BlsSigner; diff --git a/packages/simple-signer/src/single_key_signer.rs b/packages/simple-signer/src/single_key_signer.rs new file mode 100644 index 0000000000..f016ca37cd --- /dev/null +++ b/packages/simple-signer/src/single_key_signer.rs @@ -0,0 +1,172 @@ +use dashcore::signer; +use dashcore::PrivateKey; +use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dpp::identity::signer::Signer; +use dpp::identity::{IdentityPublicKey, KeyType}; +use dpp::platform_value::BinaryData; +use dpp::ProtocolError; + +/// A simple signer that uses a single private key +/// This is designed for WASM and other single-key use cases +#[derive(Debug, Clone)] +pub struct SingleKeySigner { + private_key: PrivateKey, +} + +impl SingleKeySigner { + /// Create a new SingleKeySigner from a WIF-encoded private key + pub fn new(private_key_wif: &str) -> Result { + let private_key = PrivateKey::from_wif(private_key_wif) + .map_err(|e| format!("Invalid WIF private key: {}", e))?; + Ok(Self { private_key }) + } + + /// Create a new SingleKeySigner from a hex-encoded private key + pub fn from_hex(private_key_hex: &str, network: dashcore::Network) -> Result { + if private_key_hex.len() != 64 { + return Err("Private key hex must be exactly 64 characters".to_string()); + } + + let key_bytes = + hex::decode(private_key_hex).map_err(|e| format!("Invalid hex private key: {}", e))?; + + if key_bytes.len() != 32 { + return Err("Private key must be 32 bytes".to_string()); + } + + let private_key = PrivateKey::from_slice(&key_bytes, network) + .map_err(|e| format!("Invalid private key bytes: {}", e))?; + + Ok(Self { private_key }) + } + + /// Create a new SingleKeySigner from a private key + pub fn from_private_key(private_key: PrivateKey) -> Self { + Self { private_key } + } + + /// Create from a hex or WIF string (auto-detect format) + pub fn from_string(private_key_str: &str, network: dashcore::Network) -> Result { + // Try hex first if it looks like hex, then WIF + if private_key_str.len() == 64 && private_key_str.chars().all(|c| c.is_ascii_hexdigit()) { + Self::from_hex(private_key_str, network) + } else { + Self::new(private_key_str) + } + } + + /// Get the private key + pub fn private_key(&self) -> &PrivateKey { + &self.private_key + } +} + +impl Signer for SingleKeySigner { + fn sign( + &self, + identity_public_key: &IdentityPublicKey, + data: &[u8], + ) -> Result { + // Only support ECDSA keys for now + match identity_public_key.key_type() { + KeyType::ECDSA_SECP256K1 | KeyType::ECDSA_HASH160 => { + let signature = signer::sign(data, &self.private_key.inner.secret_bytes())?; + Ok(signature.to_vec().into()) + } + _ => Err(ProtocolError::Generic(format!( + "SingleKeySigner only supports ECDSA keys, got {:?}", + identity_public_key.key_type() + ))), + } + } + + fn can_sign_with(&self, identity_public_key: &IdentityPublicKey) -> bool { + // Check if the public key matches our private key + match identity_public_key.key_type() { + KeyType::ECDSA_SECP256K1 => { + // Compare full public key + let secp = dashcore::secp256k1::Secp256k1::new(); + let secret_key = match dashcore::secp256k1::SecretKey::from_slice( + &self.private_key.inner.secret_bytes(), + ) { + Ok(sk) => sk, + Err(_) => return false, + }; + let public_key = + dashcore::secp256k1::PublicKey::from_secret_key(&secp, &secret_key); + let public_key_bytes = public_key.serialize(); + + identity_public_key.data().as_slice() == public_key_bytes + } + KeyType::ECDSA_HASH160 => { + // Compare hash160 of public key + use dashcore::hashes::{hash160, Hash}; + + let secp = dashcore::secp256k1::Secp256k1::new(); + let secret_key = match dashcore::secp256k1::SecretKey::from_slice( + &self.private_key.inner.secret_bytes(), + ) { + Ok(sk) => sk, + Err(_) => return false, + }; + let public_key = + dashcore::secp256k1::PublicKey::from_secret_key(&secp, &secret_key); + let public_key_bytes = public_key.serialize(); + let public_key_hash160 = hash160::Hash::hash(&public_key_bytes) + .to_byte_array() + .to_vec(); + + identity_public_key.data().as_slice() == public_key_hash160.as_slice() + } + _ => false, // We only support ECDSA keys + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use dashcore::Network; + + #[test] + fn test_single_key_signer_from_wif() { + // Create a valid testnet WIF + let private_key = PrivateKey::from_slice( + &[0x01; 32], // Valid 32-byte private key + Network::Testnet, + ) + .unwrap(); + let wif = private_key.to_wif(); + + let signer = SingleKeySigner::new(&wif).unwrap(); + assert!(signer.private_key().to_wif().starts_with('c')); // Testnet WIF + assert_eq!(signer.private_key().to_wif(), wif); + } + + #[test] + fn test_single_key_signer_from_hex() { + let hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + let signer = SingleKeySigner::from_hex(hex, Network::Testnet).unwrap(); + assert_eq!(signer.private_key().inner.secret_bytes().len(), 32); + } + + #[test] + fn test_single_key_signer_auto_detect() { + // Test hex detection + let hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + let signer = SingleKeySigner::from_string(hex, Network::Testnet).unwrap(); + assert_eq!(signer.private_key().inner.secret_bytes().len(), 32); + + // Test WIF detection + let private_key = PrivateKey::from_slice( + &[0x02; 32], // Valid 32-byte private key + Network::Testnet, + ) + .unwrap(); + let wif = private_key.to_wif(); + + let signer = SingleKeySigner::from_string(&wif, Network::Testnet).unwrap(); + assert!(signer.private_key().to_wif().starts_with('c')); + assert_eq!(signer.private_key().to_wif(), wif); + } +} diff --git a/packages/strategy-tests/Cargo.toml b/packages/strategy-tests/Cargo.toml index 5cecfae0f8..e8953185a5 100644 --- a/packages/strategy-tests/Cargo.toml +++ b/packages/strategy-tests/Cargo.toml @@ -35,7 +35,7 @@ dpp = { path = "../rs-dpp", default-features = false, features = [ "data-contract-json-conversion", "data-contract-cbor-conversion", ] } -simple-signer = { path = "../simple-signer" } +simple-signer = { path = "../simple-signer", features = ["state-transitions"] } platform-version = { path = "../rs-platform-version" } platform-serialization = { path = "../rs-platform-serialization" } platform-serialization-derive = { path = "../rs-platform-serialization-derive" } diff --git a/packages/wasm-sdk/CLAUDE.md b/packages/wasm-sdk/CLAUDE.md new file mode 100644 index 0000000000..8191e363c2 --- /dev/null +++ b/packages/wasm-sdk/CLAUDE.md @@ -0,0 +1,82 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Important Notes + +### Network Connectivity +**THERE ARE NO CORS OR SSL ISSUES WITH THE DASH PLATFORM ENDPOINTS IN WASM-SDK** +- The Dash Platform HTTPS endpoints (e.g., https://52.12.176.90:1443) work perfectly fine from browsers +- These endpoints have proper CORS headers configured +- SSL certificates are valid and accepted by browsers +- If you see connection errors, check: + - SDK initialization and configuration + - Parameter validation (identity IDs, contract IDs, etc.) + - Whether the SDK is in the correct network mode (testnet vs mainnet) + - The actual error message details (not just assuming it's CORS/SSL) + +## Architecture + +The WASM SDK is a WebAssembly build of the Dash SDK that runs in browsers. It provides: + +1. **Queries** - Read operations that fetch data from Dash Platform +2. **State Transitions** - Write operations that modify state on Dash Platform + +### Key Components + +- `src/sdk.rs` - Main SDK wrapper with WasmSdk and WasmSdkBuilder +- `src/queries/` - All query implementations (identity, documents, tokens, etc.) +- `src/state_transitions/` - State transition implementations +- `src/context_provider/` - Context providers for trusted/untrusted modes +- `index.html` - Example web interface for testing SDK functionality + +### Building + +Run `./build.sh` to build the WASM module. Output goes to `pkg/` directory. + +### Testing + +1. Start web server: `python3 -m http.server 8888` +2. Open http://localhost:8888 +3. Select network (testnet/mainnet) +4. Choose operation type (queries/state transitions) +5. Fill in parameters and execute + +## Common Issues + +1. **"time not implemented on this platform"** - Fixed by using `js_sys::Date::now()` in WASM builds +2. **Import errors** - Token functions are methods on WasmSdk, not standalone functions +3. **Network timeouts** - Usually means invalid parameters or identities, NOT network issues + +## Query Support + +The WASM SDK now fully supports where and orderBy clauses for document queries: + +### Where Clauses +- Format: JSON array of clause arrays `[[field, operator, value], ...]` +- Supported operators: + - `==` or `=` - Equal + - `>` - Greater than + - `>=` - Greater than or equals + - `<` - Less than + - `<=` - Less than or equals + - `in` or `In` - In array + - `startsWith` or `StartsWith` - String prefix match + - `Between`, `BetweenExcludeBounds`, `BetweenExcludeLeft`, `BetweenExcludeRight` - Range operators + +### Order By Clauses +- Format: JSON array of clause arrays `[[field, direction], ...]` +- Direction: `"asc"` or `"desc"` + +### Example +```javascript +const whereClause = JSON.stringify([ + ["$ownerId", ">", "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk"], + ["age", ">=", 18] +]); + +const orderBy = JSON.stringify([ + ["$createdAt", "desc"], + ["name", "asc"] +]); +``` \ No newline at end of file diff --git a/packages/wasm-sdk/Cargo.toml b/packages/wasm-sdk/Cargo.toml index f111a2eee6..7a7f5cd7b5 100644 --- a/packages/wasm-sdk/Cargo.toml +++ b/packages/wasm-sdk/Cargo.toml @@ -1,6 +1,3 @@ -[workspace] -members = ["."] - [package] name = "wasm-sdk" edition = "2021" @@ -17,6 +14,8 @@ token_reward_explanations = ["dash-sdk/token_reward_explanations"] [dependencies] dash-sdk = { path = "../rs-sdk", default-features = false } +simple-signer = { path = "../simple-signer" } +drive = { path = "../rs-drive", default-features = false, features = ["verify"] } console_error_panic_hook = { version = "0.1.6" } thiserror = { version = "2.0.12" } web-sys = { version = "0.3.4", features = [ @@ -26,6 +25,7 @@ web-sys = { version = "0.3.4", features = [ 'HtmlElement', 'Node', 'Window', + 'Crypto', ] } wasm-bindgen = { version = "=0.2.100" } wasm-bindgen-futures = { version = "0.4.49" } @@ -34,16 +34,24 @@ tracing = { version = "0.1" } tracing-wasm = { version = "0.2.1" } wee_alloc = "0.4" platform-value = { path = "../rs-platform-value", features = ["json"] } +serde = { version = "1.0", features = ["derive"] } serde-wasm-bindgen = { version = "0.6.5" } +serde_json = "1.0" +hex = "0.4" +base64 = "0.22" getrandom = { version = "0.2", features = ["js"] } rs-sdk-trusted-context-provider = { path = "../rs-sdk-trusted-context-provider" } once_cell = "1.19" +js-sys = "0.3" +dapi-grpc = { path = "../dapi-grpc" } +rs-dapi-client = { path = "../rs-dapi-client" } [profile.release] opt-level = "z" panic = "abort" debug = false +lto = "fat" + +[package.metadata.wasm-pack] +wasm-opt = false -#[package.metadata.wasm-pack.profile.release] -#wasm-opt = ['-g', '-O'] # -g for profiling -# -Oz -Oz -g \ No newline at end of file diff --git a/packages/wasm-sdk/GROUP_QUERIES_DOCUMENTATION.md b/packages/wasm-sdk/GROUP_QUERIES_DOCUMENTATION.md new file mode 100644 index 0000000000..c33d7410c5 --- /dev/null +++ b/packages/wasm-sdk/GROUP_QUERIES_DOCUMENTATION.md @@ -0,0 +1,168 @@ +# Group Queries Implementation + +This document describes the implementation of group queries in the WASM SDK. + +## Overview + +The group queries allow you to interact with group functionality in Dash Platform data contracts. Groups are used to manage collective ownership and permissions. + +## Implemented Queries + +### 1. `get_group_info` + +Fetches information about a specific group. + +**Parameters:** +- `sdk`: The WASM SDK instance +- `data_contract_id`: The data contract ID (Base58 encoded string) +- `group_contract_position`: The position of the group in the contract (u32) + +**Returns:** +```javascript +{ + "members": { + "identityId1": 100, // member ID -> voting power + "identityId2": 50 + }, + "requiredPower": 100 // minimum power needed for decisions +} +``` +Returns `null` if the group doesn't exist. + +### 2. `get_group_members` + +Gets members of a specific group with optional filtering and pagination. + +**Parameters:** +- `sdk`: The WASM SDK instance +- `data_contract_id`: The data contract ID (Base58 encoded string) +- `group_contract_position`: The position of the group in the contract (u32) +- `member_ids`: Optional array of specific member IDs to fetch +- `start_at`: Optional member ID to start pagination from +- `limit`: Optional limit on number of results + +**Returns:** +```javascript +[ + { + "memberId": "identityId1", + "power": 100 + }, + { + "memberId": "identityId2", + "power": 50 + } +] +``` + +### 3. `get_identity_groups` + +Retrieves all groups associated with a specific identity. + +**Parameters:** +- `sdk`: The WASM SDK instance +- `identity_id`: The identity ID to search for (Base58 encoded string) +- `member_data_contracts`: Optional array of contract IDs to search for member roles +- `owner_data_contracts`: Optional array of contract IDs to search for owner roles (not yet implemented) +- `moderator_data_contracts`: Optional array of contract IDs to search for moderator roles (not yet implemented) + +**Returns:** +```javascript +[ + { + "dataContractId": "contractId1", + "groupContractPosition": 0, + "role": "member", + "power": 100 // only for member role + } +] +``` + +**Note:** Currently only member role queries are implemented. Owner and moderator roles require additional contract queries not yet available in the SDK. + +### 4. `get_groups_data_contracts` + +Fetches all groups for multiple data contracts. + +**Parameters:** +- `sdk`: The WASM SDK instance +- `data_contract_ids`: Array of data contract IDs to fetch groups from + +**Returns:** +```javascript +[ + { + "dataContractId": "contractId1", + "groups": [ + { + "position": 0, + "group": { + "members": { + "identityId1": 100 + }, + "requiredPower": 100 + } + } + ] + } +] +``` + +## Implementation Details + +The implementation uses: +- Dash SDK's `Fetch` and `FetchMany` traits for querying +- `GroupQuery` and `GroupInfosQuery` types from the SDK +- `serde_wasm_bindgen` with `json_compatible` serializer for proper JavaScript object conversion +- Base58 encoding for all identifiers passed to/from JavaScript + +## Error Handling + +All functions return proper error messages when: +- Invalid identifiers are provided +- Network errors occur +- Groups or contracts don't exist + +## Example Usage + +```javascript +import init, { + WasmSdkBuilder, + get_group_info, + get_group_members, + get_identity_groups, + get_groups_data_contracts +} from './pkg/wasm_sdk.js'; + +// Initialize SDK +await init(); +const builder = new WasmSdkBuilder(); +builder.with_core("127.0.0.1", 20002, "regtest", ""); +const sdk = await builder.build(); + +// Get group info +const groupInfo = await get_group_info( + sdk, + 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec', + 0 +); + +// Get group members with pagination +const members = await get_group_members( + sdk, + 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec', + 0, + null, // all members + null, // start from beginning + 10 // limit to 10 results +); + +// Get groups for an identity +const identityGroups = await get_identity_groups( + sdk, + '4EfA9Jrvv3nnCFdSf7fad59851iiTRZ6Wcu6YVJ4iSeF', + ['GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec'], // check member role in this contract + null, + null +); +``` \ No newline at end of file diff --git a/packages/wasm-sdk/index.html b/packages/wasm-sdk/index.html index f0b3c6908b..2bacaa7cef 100644 --- a/packages/wasm-sdk/index.html +++ b/packages/wasm-sdk/index.html @@ -17,11 +17,45 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - background: rgba(0, 0, 0, 0.8); + background: rgba(0, 0, 0, 0.9); color: white; - padding: 20px; - border-radius: 5px; + padding: 30px; + border-radius: 8px; z-index: 1000; + min-width: 300px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + } + + .preloader-content { + text-align: center; + } + + .preloader-text { + font-size: 16px; + margin-bottom: 15px; + } + + .progress-bar { + width: 100%; + height: 20px; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + overflow: hidden; + margin-bottom: 10px; + } + + .progress-fill { + height: 100%; + background: linear-gradient(90deg, #4CAF50, #45a049); + width: 0%; + transition: width 0.3s ease; + border-radius: 10px; + } + + .progress-percent { + font-size: 14px; + font-weight: bold; + color: #4CAF50; } .app-container { @@ -31,7 +65,7 @@ } .sidebar { - width: 400px; + width: 450px; background-color: white; border-right: 1px solid #e0e0e0; display: flex; @@ -70,115 +104,175 @@ color: white; } - .actions-container { + .query-container { padding: 20px; flex: 1; overflow-y: auto; } - .action-group { - margin-bottom: 30px; - padding: 15px; + .query-selector { + margin-bottom: 20px; + } + + .query-selector select { + width: 100%; + padding: 10px; + font-size: 14px; + border: 1px solid #ddd; + border-radius: 4px; + background-color: white; + margin-bottom: 10px; + } + + .query-inputs { background-color: #f8f9fa; + padding: 15px; border-radius: 8px; + margin-bottom: 15px; + } + + .input-group { + margin-bottom: 15px; + } + + .input-group label { + display: block; + margin-bottom: 5px; + font-weight: 500; + color: #333; + } + + .input-group input[type="text"], + .input-group input[type="password"] { + width: 100%; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; } - .action-group h3 { + .query-inputs h4 { margin: 0 0 15px 0; color: #333; - font-size: 1.1em; + font-size: 1em; } .input-group { - display: flex; - gap: 10px; - margin-bottom: 10px; + margin-bottom: 15px; } - .input-group input { - flex: 1; - padding: 10px; + .input-group label { + display: block; + margin-bottom: 5px; + color: #555; + font-size: 0.9em; + } + + .input-group input, + .input-group select { + width: 100%; + padding: 8px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; + box-sizing: border-box; } - .input-group button { - padding: 10px 20px; - background-color: #2196F3; + .input-group input[type="checkbox"] { + width: auto; + margin-right: 5px; + } + + .input-group input[type="number"] { + -moz-appearance: textfield; + } + + .input-group input[type="number"]::-webkit-inner-spin-button, + .input-group input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + + .optional-label { + color: #888; + font-size: 0.85em; + font-style: italic; + } + + .execute-button { + width: 100%; + padding: 12px; + background-color: #1976d2; color: white; border: none; border-radius: 4px; - cursor: pointer; + font-size: 16px; font-weight: 500; - transition: background-color 0.2s; + cursor: pointer; + transition: background-color 0.3s; } - .input-group button:hover { - background-color: #1976D2; + .execute-button:hover { + background-color: #1565c0; } - .input-group button:disabled { + .execute-button:disabled { background-color: #ccc; cursor: not-allowed; } .result-container { flex: 1; - background-color: #fafafa; - padding: 20px; - overflow-y: auto; + background-color: white; + display: flex; + flex-direction: column; + overflow: hidden; } .result-header { + padding: 20px; + background-color: #f8f9fa; + border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; - margin-bottom: 20px; } .result-header h2 { margin: 0; color: #333; - } - - .result-actions { - display: flex; - gap: 10px; + font-size: 1.3em; } .result-actions button { - padding: 6px 12px; - background-color: #f0f0f0; + margin-left: 10px; + padding: 8px 16px; border: 1px solid #ddd; + background-color: white; border-radius: 4px; cursor: pointer; - font-size: 12px; + font-size: 14px; } .result-actions button:hover { - background-color: #e0e0e0; + background-color: #f5f5f5; } .result-content { - background-color: white; - border: 1px solid #e0e0e0; - border-radius: 8px; + flex: 1; padding: 20px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + overflow-y: auto; + font-family: 'Monaco', 'Consolas', 'Courier New', monospace; font-size: 13px; - line-height: 1.5; + line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; - color: #333; - min-height: 400px; } .result-content.empty { - color: #999; - text-align: center; - padding: 100px 20px; + color: #888; font-style: italic; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; } .status-banner { @@ -186,11 +280,12 @@ bottom: 0; left: 0; right: 0; - padding: 15px; + padding: 10px 20px; text-align: center; - font-weight: bold; + font-weight: 500; + font-size: 14px; + z-index: 999; transition: all 0.3s ease; - z-index: 100; } .status-banner.success { @@ -215,301 +310,4166 @@ border-radius: 4px; padding: 10px; } - - - - -
Loading...
- -
- -
-
-

Results

-
- - -
-
-
No data fetched yet. Use the actions on the left to fetch identity or data contract information.
-
-
+ .credits-value { + color: #1976d2; + cursor: help; + text-decoration: underline; + text-decoration-style: dotted; + text-underline-offset: 3px; + position: relative; + } + + .credits-value:hover { + background-color: #e3f2fd; + border-radius: 3px; + padding: 0 2px; + } + + .credits-value::after { + content: attr(title); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background-color: #333; + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; + margin-bottom: 5px; + z-index: 1000; + } + + .credits-value:hover::after { + opacity: 1; + } + + .json-key { + color: #d73a49; + font-weight: 500; + } + + .result-content pre { + margin: 0; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 13px; + line-height: 1.5; + } -
Initializing WASM SDK...
+ .mode-selector { + margin-bottom: 15px; + display: none; + } - + + diff --git a/packages/wasm-sdk/service-worker-simple.js b/packages/wasm-sdk/service-worker-simple.js new file mode 100644 index 0000000000..00c4bdaf13 --- /dev/null +++ b/packages/wasm-sdk/service-worker-simple.js @@ -0,0 +1,106 @@ +// Simple cache-first service worker +const CACHE_NAME = 'wasm-sdk-cache-v7'; // Increment to force cache update + +// Files to cache +const urlsToCache = [ + '/pkg/wasm_sdk.js', + '/pkg/wasm_sdk_bg.wasm', + '/pkg/wasm_sdk.d.ts' +]; + +// Install event - pre-cache resources +self.addEventListener('install', event => { + console.log('[SW] Installing and caching files...'); + event.waitUntil( + caches.open(CACHE_NAME) + .then(cache => cache.addAll(urlsToCache)) + .then(() => { + console.log('[SW] All files cached'); + return self.skipWaiting(); + }) + ); +}); + +// Activate event - clean up old caches +self.addEventListener('activate', event => { + console.log('[SW] Activating...'); + event.waitUntil( + caches.keys().then(cacheNames => { + return Promise.all( + cacheNames.map(cacheName => { + if (cacheName !== CACHE_NAME && cacheName.startsWith('wasm-sdk-cache-')) { + console.log('[SW] Deleting old cache:', cacheName); + return caches.delete(cacheName); + } + }) + ); + }).then(() => { + console.log('[SW] Claiming all clients'); + return self.clients.claim(); + }) + ); +}); + +// Fetch event - cache first, with background update +self.addEventListener('fetch', event => { + const url = new URL(event.request.url); + + // Only handle our cached files + if (!urlsToCache.some(path => url.pathname === path)) { + return; + } + + event.respondWith( + caches.match(event.request).then(cachedResponse => { + // Always return from cache if available + if (cachedResponse) { + console.log('[SW] Cache hit:', url.pathname); + + // Update cache in background + event.waitUntil( + fetch(event.request).then(response => { + if (response && response.status === 200) { + const responseToCache = response.clone(); + caches.open(CACHE_NAME).then(cache => { + cache.put(event.request, responseToCache); + console.log('[SW] Cache updated in background:', url.pathname); + }); + } + }).catch(() => { + console.log('[SW] Background update failed, keeping cached version'); + }) + ); + + return cachedResponse; + } + + // Cache miss - fetch and cache + console.log('[SW] Cache miss, fetching:', url.pathname); + return fetch(event.request).then(response => { + if (!response || response.status !== 200) { + return response; + } + + const responseToCache = response.clone(); + caches.open(CACHE_NAME).then(cache => { + cache.put(event.request, responseToCache); + console.log('[SW] Cached:', url.pathname); + }); + + return response; + }); + }) + ); +}); + +// Handle cache clear message +self.addEventListener('message', event => { + if (event.data.action === 'clearCache') { + caches.delete(CACHE_NAME).then(() => { + console.log('[SW] Cache cleared'); + if (event.ports[0]) { + event.ports[0].postMessage({ success: true }); + } + }); + } +}); \ No newline at end of file diff --git a/packages/wasm-sdk/src/context_provider.rs b/packages/wasm-sdk/src/context_provider.rs index 4ad7df3143..ca2e383b47 100644 --- a/packages/wasm-sdk/src/context_provider.rs +++ b/packages/wasm-sdk/src/context_provider.rs @@ -125,4 +125,9 @@ impl WasmTrustedContext { ContextProviderError::Generic(format!("Failed to prefetch quorums: {}", e)) }) } + + /// Add a data contract to the known contracts cache + pub fn add_known_contract(&self, contract: DataContract) { + self.inner.add_known_contract(contract); + } } diff --git a/packages/wasm-sdk/src/lib.rs b/packages/wasm-sdk/src/lib.rs index ccbc036845..ab24b75562 100644 --- a/packages/wasm-sdk/src/lib.rs +++ b/packages/wasm-sdk/src/lib.rs @@ -6,6 +6,12 @@ pub mod error; pub mod sdk; pub mod state_transitions; pub mod verify; +pub mod queries; + +// Re-export commonly used items +pub use sdk::{WasmSdk, WasmSdkBuilder}; +pub use queries::*; +pub use state_transitions::*; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; diff --git a/packages/wasm-sdk/src/queries/data_contract.rs b/packages/wasm-sdk/src/queries/data_contract.rs new file mode 100644 index 0000000000..8e79467866 --- /dev/null +++ b/packages/wasm-sdk/src/queries/data_contract.rs @@ -0,0 +1,119 @@ +use crate::dpp::DataContractWasm; +use crate::sdk::WasmSdk; +use dash_sdk::platform::{DataContract, Fetch, FetchMany, Identifier}; +use dash_sdk::platform::query::LimitQuery; +use drive_proof_verifier::types::{DataContractHistory, DataContracts}; +use dash_sdk::dpp::data_contract::conversion::json::DataContractJsonConversionMethodsV0; +use serde::{Serialize, Deserialize}; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; +use std::collections::BTreeMap; + +#[wasm_bindgen] +pub async fn data_contract_fetch( + sdk: &WasmSdk, + base58_id: &str, +) -> Result { + let id = Identifier::from_string( + base58_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + DataContract::fetch_by_identifier(sdk, id) + .await? + .ok_or_else(|| JsError::new("Data contract not found")) + .map(Into::into) +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct DataContractHistoryResponse { + versions: BTreeMap, +} + +#[wasm_bindgen] +pub async fn get_data_contract_history( + sdk: &WasmSdk, + id: &str, + limit: Option, + _offset: Option, + start_at_ms: Option, +) -> Result { + // Parse contract ID + let contract_id = Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Create query with start timestamp + let query = LimitQuery { + query: (contract_id, start_at_ms.unwrap_or(0)), + start_info: None, + limit, + }; + + // Fetch contract history + let history_result = DataContractHistory::fetch(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch data contract history: {}", e)))?; + + // Convert to response format + let mut versions = BTreeMap::new(); + let platform_version = sdk.as_ref().version(); + + if let Some(history) = history_result { + for (revision, contract) in history { + versions.insert(revision, contract.to_json(platform_version)?); + } + } + + let response = DataContractHistoryResponse { versions }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct DataContractsResponse { + data_contracts: BTreeMap>, +} + +#[wasm_bindgen] +pub async fn get_data_contracts(sdk: &WasmSdk, ids: Vec) -> Result { + // Parse all contract IDs + let identifiers: Result, _> = ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect(); + let identifiers = identifiers?; + + // Fetch all contracts + let contracts_result: DataContracts = DataContract::fetch_many(sdk.as_ref(), identifiers) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch data contracts: {}", e)))?; + + // Convert to response format + let mut data_contracts = BTreeMap::new(); + let platform_version = sdk.as_ref().version(); + for (id, contract_opt) in contracts_result { + let id_str = id.to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58); + let contract_json = match contract_opt { + Some(contract) => Some(contract.to_json(platform_version)?), + None => None, + }; + data_contracts.insert(id_str, contract_json); + } + + let response = DataContractsResponse { data_contracts }; + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/document.rs b/packages/wasm-sdk/src/queries/document.rs new file mode 100644 index 0000000000..aa6260c917 --- /dev/null +++ b/packages/wasm-sdk/src/queries/document.rs @@ -0,0 +1,452 @@ +use crate::sdk::WasmSdk; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; +use serde::{Serialize, Deserialize}; +use dash_sdk::platform::Fetch; +use dash_sdk::dpp::prelude::Identifier; +use dash_sdk::dpp::document::Document; +use dash_sdk::dpp::document::DocumentV0Getters; +use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use serde_json::Value as JsonValue; +use drive::query::{WhereClause, WhereOperator, OrderClause}; +use dash_sdk::dpp::platform_value::{Value, platform_value}; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct DocumentResponse { + id: String, + owner_id: String, + revision: u64, + #[serde(skip_serializing_if = "Option::is_none")] + created_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + updated_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + transferred_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + created_at_block_height: Option, + #[serde(skip_serializing_if = "Option::is_none")] + updated_at_block_height: Option, + #[serde(skip_serializing_if = "Option::is_none")] + transferred_at_block_height: Option, + #[serde(skip_serializing_if = "Option::is_none")] + created_at_core_block_height: Option, + #[serde(skip_serializing_if = "Option::is_none")] + updated_at_core_block_height: Option, + #[serde(skip_serializing_if = "Option::is_none")] + transferred_at_core_block_height: Option, + data: serde_json::Map, +} + +impl DocumentResponse { + fn from_document( + doc: &Document, + _data_contract: &dash_sdk::platform::DataContract, + _document_type: dash_sdk::dpp::data_contract::document_type::DocumentTypeRef + ) -> Result { + use dash_sdk::dpp::document::DocumentV0Getters; + + // For now, we'll continue with the existing approach + // In the future, we could use the document type to better interpret the data + + // Get document properties and convert each to JSON + let mut data = serde_json::Map::new(); + let properties = doc.properties(); + + for (key, value) in properties { + // Convert platform Value to JSON + let json_value: JsonValue = value.clone().try_into() + .map_err(|e| JsError::new(&format!("Failed to convert value to JSON: {:?}", e)))?; + + data.insert(key.clone(), json_value); + } + + let response = Self { + id: doc.id().to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + owner_id: doc.owner_id().to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + revision: doc.revision().unwrap_or(0), + created_at: doc.created_at(), + updated_at: doc.updated_at(), + transferred_at: doc.transferred_at(), + created_at_block_height: doc.created_at_block_height(), + updated_at_block_height: doc.updated_at_block_height(), + transferred_at_block_height: doc.transferred_at_block_height(), + created_at_core_block_height: doc.created_at_core_block_height(), + updated_at_core_block_height: doc.updated_at_core_block_height(), + transferred_at_core_block_height: doc.transferred_at_core_block_height(), + data, + }; + + Ok(response) + } +} + +/// Parse JSON where clause into WhereClause +fn parse_where_clause(json_clause: &JsonValue) -> Result { + let clause_array = json_clause.as_array() + .ok_or_else(|| JsError::new("where clause must be an array"))?; + + if clause_array.len() != 3 { + return Err(JsError::new("where clause must have exactly 3 elements: [field, operator, value]")); + } + + let field = clause_array[0].as_str() + .ok_or_else(|| JsError::new("where clause field must be a string"))? + .to_string(); + + let operator_str = clause_array[1].as_str() + .ok_or_else(|| JsError::new("where clause operator must be a string"))?; + + let operator = match operator_str { + "==" | "=" => WhereOperator::Equal, + ">" => WhereOperator::GreaterThan, + ">=" => WhereOperator::GreaterThanOrEquals, + "<" => WhereOperator::LessThan, + "<=" => WhereOperator::LessThanOrEquals, + "Between" | "between" => WhereOperator::Between, + "BetweenExcludeBounds" => WhereOperator::BetweenExcludeBounds, + "BetweenExcludeLeft" => WhereOperator::BetweenExcludeLeft, + "BetweenExcludeRight" => WhereOperator::BetweenExcludeRight, + "in" | "In" => WhereOperator::In, + "startsWith" | "StartsWith" => WhereOperator::StartsWith, + _ => return Err(JsError::new(&format!("Unknown operator: {}", operator_str))), + }; + + // Convert JSON value to platform Value + let value = json_to_platform_value(&clause_array[2])?; + + Ok(WhereClause { + field, + operator, + value, + }) +} + +/// Parse JSON order by clause into OrderClause +fn parse_order_clause(json_clause: &JsonValue) -> Result { + let clause_array = json_clause.as_array() + .ok_or_else(|| JsError::new("order by clause must be an array"))?; + + if clause_array.len() != 2 { + return Err(JsError::new("order by clause must have exactly 2 elements: [field, direction]")); + } + + let field = clause_array[0].as_str() + .ok_or_else(|| JsError::new("order by field must be a string"))? + .to_string(); + + let direction = clause_array[1].as_str() + .ok_or_else(|| JsError::new("order by direction must be a string"))?; + + let ascending = match direction { + "asc" => true, + "desc" => false, + _ => return Err(JsError::new("order by direction must be 'asc' or 'desc'")), + }; + + Ok(OrderClause { + field, + ascending, + }) +} + +/// Convert JSON value to platform Value +fn json_to_platform_value(json_val: &JsonValue) -> Result { + match json_val { + JsonValue::Null => Ok(Value::Null), + JsonValue::Bool(b) => Ok(Value::Bool(*b)), + JsonValue::Number(n) => { + if let Some(i) = n.as_i64() { + Ok(Value::I64(i)) + } else if let Some(u) = n.as_u64() { + Ok(Value::U64(u)) + } else if let Some(f) = n.as_f64() { + Ok(Value::Float(f)) + } else { + Err(JsError::new("Unsupported number type")) + } + }, + JsonValue::String(s) => { + // Check if it's an identifier (base58 encoded) + if s.len() == 44 && s.chars().all(|c| c.is_alphanumeric()) { + // Try to parse as identifier + match Identifier::from_string(s, dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58) { + Ok(id) => Ok(platform_value!(id)), + Err(_) => Ok(Value::Text(s.clone())), + } + } else { + Ok(Value::Text(s.clone())) + } + }, + JsonValue::Array(arr) => { + let values: Result, JsError> = arr.iter() + .map(json_to_platform_value) + .collect(); + Ok(Value::Array(values?)) + }, + JsonValue::Object(obj) => { + let mut map = Vec::new(); + for (key, val) in obj { + map.push((Value::Text(key.clone()), json_to_platform_value(val)?)); + } + Ok(Value::Map(map)) + }, + } +} + + +#[wasm_bindgen] +pub async fn get_documents( + sdk: &WasmSdk, + data_contract_id: &str, + document_type: &str, + where_clause: Option, + order_by: Option, + limit: Option, + start_after: Option, + start_at: Option, +) -> Result { + use dash_sdk::platform::documents::document_query::DocumentQuery; + use dash_sdk::platform::FetchMany; + use drive_proof_verifier::types::Documents; + + // Parse data contract ID + let contract_id = Identifier::from_string( + data_contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Create base document query + let mut query = DocumentQuery::new_with_data_contract_id( + sdk.as_ref(), + contract_id, + document_type, + ) + .await + .map_err(|e| JsError::new(&format!("Failed to create document query: {}", e)))?; + + // Set limit if provided + if let Some(limit_val) = limit { + query.limit = limit_val; + } else { + query.limit = 100; // Default limit + } + + // Handle start parameters + if let Some(start_after_id) = start_after { + let doc_id = Identifier::from_string( + &start_after_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + query.start = Some(dash_sdk::dapi_grpc::platform::v0::get_documents_request::get_documents_request_v0::Start::StartAfter( + doc_id.to_vec() + )); + } else if let Some(start_at_id) = start_at { + let doc_id = Identifier::from_string( + &start_at_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + query.start = Some(dash_sdk::dapi_grpc::platform::v0::get_documents_request::get_documents_request_v0::Start::StartAt( + doc_id.to_vec() + )); + } + + // Parse and apply where clauses + if let Some(where_json) = where_clause { + let json_value: JsonValue = serde_json::from_str(&where_json) + .map_err(|e| JsError::new(&format!("Failed to parse where clause JSON: {}", e)))?; + + // Expect an array of where clauses + let where_array = json_value.as_array() + .ok_or_else(|| JsError::new("where clause must be an array of clauses"))?; + + for clause_json in where_array { + let where_clause = parse_where_clause(clause_json)?; + query = query.with_where(where_clause); + } + } + + // Parse and apply order by clauses + if let Some(order_json) = order_by { + let json_value: JsonValue = serde_json::from_str(&order_json) + .map_err(|e| JsError::new(&format!("Failed to parse order by JSON: {}", e)))?; + + // Expect an array of order clauses + let order_array = json_value.as_array() + .ok_or_else(|| JsError::new("order by must be an array of clauses"))?; + + for clause_json in order_array { + let order_clause = parse_order_clause(clause_json)?; + query = query.with_order_by(order_clause); + } + } + + // Execute query + let documents_result: Documents = Document::fetch_many(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch documents: {}", e)))?; + + // Fetch the data contract to get the document type + let data_contract = dash_sdk::platform::DataContract::fetch(sdk.as_ref(), contract_id) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch data contract: {}", e)))? + .ok_or_else(|| JsError::new("Data contract not found"))?; + + // Get the document type + let document_type_ref = data_contract + .document_type_for_name(document_type) + .map_err(|e| JsError::new(&format!("Document type not found: {}", e)))?; + + // Convert documents to response format + let mut responses: Vec = Vec::new(); + for (_, doc_opt) in documents_result { + if let Some(doc) = doc_opt { + responses.push(DocumentResponse::from_document(&doc, &data_contract, document_type_ref)?); + } + } + + // Use json_compatible serializer to convert maps to objects + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + responses.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_document( + sdk: &WasmSdk, + data_contract_id: &str, + document_type: &str, + document_id: &str, +) -> Result { + use dash_sdk::platform::documents::document_query::DocumentQuery; + + // Parse IDs + let contract_id = Identifier::from_string( + data_contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let doc_id = Identifier::from_string( + document_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Create document query + let query = DocumentQuery::new_with_data_contract_id( + sdk.as_ref(), + contract_id, + document_type, + ) + .await + .map_err(|e| JsError::new(&format!("Failed to create document query: {}", e)))? + .with_document_id(&doc_id); + + // Fetch the data contract to get the document type + let data_contract = dash_sdk::platform::DataContract::fetch(sdk.as_ref(), contract_id) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch data contract: {}", e)))? + .ok_or_else(|| JsError::new("Data contract not found"))?; + + // Get the document type + let document_type = data_contract + .document_type_for_name(document_type) + .map_err(|e| JsError::new(&format!("Document type not found: {}", e)))?; + + // Execute query + let document_result: Option = Document::fetch(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch document: {}", e)))?; + + match document_result { + Some(doc) => { + let response = DocumentResponse::from_document(&doc, &data_contract, document_type)?; + + // Use json_compatible serializer to convert maps to objects + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + }, + None => Ok(JsValue::NULL), + } +} + +#[wasm_bindgen] +pub async fn get_dpns_username( + sdk: &WasmSdk, + identity_id: &str, +) -> Result { + use dash_sdk::platform::documents::document_query::DocumentQuery; + use dash_sdk::platform::FetchMany; + use drive_proof_verifier::types::Documents; + + // DPNS contract ID on testnet + const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; + const DPNS_DOCUMENT_TYPE: &str = "domain"; + + // Parse identity ID + let identity_id_parsed = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse DPNS contract ID + let contract_id = Identifier::from_string( + DPNS_CONTRACT_ID, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Create document query for DPNS domains owned by this identity + let mut query = DocumentQuery::new_with_data_contract_id( + sdk.as_ref(), + contract_id, + DPNS_DOCUMENT_TYPE, + ) + .await + .map_err(|e| JsError::new(&format!("Failed to create document query: {}", e)))?; + + // Query by records.identity using the identityId index + let where_clause = WhereClause { + field: "records.identity".to_string(), + operator: WhereOperator::Equal, + value: Value::Identifier(identity_id_parsed.to_buffer()), + }; + + query = query.with_where(where_clause); + query.limit = 1; // We only need the first result + + // Execute query + let documents_result: Documents = Document::fetch_many(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch DPNS documents: {}", e)))?; + + // Process the result + for (_, doc_opt) in documents_result { + if let Some(doc) = doc_opt { + // Extract the username from the document + let properties = doc.properties(); + + let label = properties.get("label") + .and_then(|v| match v { + Value::Text(s) => Some(s.clone()), + _ => None, + }) + .ok_or_else(|| JsError::new("DPNS document missing label field"))?; + + let parent_domain = properties.get("normalizedParentDomainName") + .and_then(|v| match v { + Value::Text(s) => Some(s.clone()), + _ => None, + }) + .ok_or_else(|| JsError::new("DPNS document missing normalizedParentDomainName field"))?; + + // Construct the full username + let username = format!("{}.{}", label, parent_domain); + + // Return the username as a JSON string + return Ok(JsValue::from_str(&username)); + } + } + + // No DPNS name found for this identity + Ok(JsValue::NULL) +} \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/epoch.rs b/packages/wasm-sdk/src/queries/epoch.rs new file mode 100644 index 0000000000..0ddf1d455a --- /dev/null +++ b/packages/wasm-sdk/src/queries/epoch.rs @@ -0,0 +1,297 @@ +use crate::sdk::WasmSdk; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; +use serde::{Serialize, Deserialize}; +use dash_sdk::platform::{FetchMany, LimitQuery}; +use dash_sdk::platform::fetch_current_no_parameters::FetchCurrent; +use dash_sdk::dpp::block::extended_epoch_info::ExtendedEpochInfo; +use dash_sdk::dpp::block::extended_epoch_info::v0::ExtendedEpochInfoV0Getters; +use dash_sdk::dpp::dashcore::hashes::Hash; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct EpochInfo { + index: u16, + first_core_block_height: u32, + first_block_height: u64, + start_time: u64, + fee_multiplier: f64, + protocol_version: u32, +} + +impl From for EpochInfo { + fn from(epoch: ExtendedEpochInfo) -> Self { + Self { + index: epoch.index(), + first_core_block_height: epoch.first_core_block_height(), + first_block_height: epoch.first_block_height(), + start_time: epoch.first_block_time(), + fee_multiplier: epoch.fee_multiplier_permille() as f64 / 1000.0, + protocol_version: epoch.protocol_version(), + } + } +} + +#[wasm_bindgen] +pub async fn get_epochs_info( + sdk: &WasmSdk, + start_epoch: Option, + count: Option, + ascending: Option, +) -> Result { + use dash_sdk::platform::types::epoch::EpochQuery; + + let query = LimitQuery { + query: EpochQuery { + start: start_epoch, + ascending: ascending.unwrap_or(true), + }, + limit: count, + start_info: None, + }; + + let epochs_result: drive_proof_verifier::types::ExtendedEpochInfos = ExtendedEpochInfo::fetch_many(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch epochs info: {}", e)))?; + + // Convert to our response format + let epochs: Vec = epochs_result + .into_iter() + .filter_map(|(_, epoch_opt)| epoch_opt.map(Into::into)) + .collect(); + + serde_wasm_bindgen::to_value(&epochs) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_finalized_epoch_infos( + sdk: &WasmSdk, + start_epoch: Option, + count: Option, + ascending: Option, +) -> Result { + use dash_sdk::platform::types::finalized_epoch::FinalizedEpochQuery; + + if start_epoch.is_none() { + return Err(JsError::new("start_epoch is required for finalized epoch queries")); + } + + let start = start_epoch.unwrap(); + let is_ascending = ascending.unwrap_or(true); + let limit = count.unwrap_or(100); + + // Ensure limit is at least 1 to avoid underflow + let limit = limit.max(1); + + // Calculate end epoch based on direction and limit + let end_epoch = if is_ascending { + start.saturating_add((limit - 1) as u16) + } else { + start.saturating_sub((limit - 1) as u16) + }; + + let query = if is_ascending { + FinalizedEpochQuery { + start_epoch_index: start, + start_epoch_index_included: true, + end_epoch_index: end_epoch, + end_epoch_index_included: true, + } + } else { + FinalizedEpochQuery { + start_epoch_index: end_epoch, + start_epoch_index_included: true, + end_epoch_index: start, + end_epoch_index_included: true, + } + }; + + let epochs_result: drive_proof_verifier::types::FinalizedEpochInfos = dash_sdk::dpp::block::finalized_epoch_info::FinalizedEpochInfo::fetch_many(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch finalized epochs info: {}", e)))?; + + // Convert to our response format and sort by epoch index + let mut epochs: Vec = epochs_result + .into_iter() + .filter_map(|(epoch_index, epoch_opt)| { + epoch_opt.map(|epoch| { + use dash_sdk::dpp::block::finalized_epoch_info::v0::getters::FinalizedEpochInfoGettersV0; + EpochInfo { + index: epoch_index as u16, + first_core_block_height: epoch.first_core_block_height(), + first_block_height: epoch.first_block_height(), + start_time: epoch.first_block_time(), + fee_multiplier: epoch.fee_multiplier_permille() as f64 / 1000.0, + protocol_version: epoch.protocol_version(), + } + }) + }) + .collect(); + + // Sort based on ascending flag + epochs.sort_by(|a, b| { + if is_ascending { + a.index.cmp(&b.index) + } else { + b.index.cmp(&a.index) + } + }); + + serde_wasm_bindgen::to_value(&epochs) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct ProposerBlockCount { + proposer_pro_tx_hash: String, + count: u64, +} + +#[wasm_bindgen] +pub async fn get_evonodes_proposed_epoch_blocks_by_ids( + sdk: &WasmSdk, + epoch: u32, + ids: Vec, +) -> Result { + use dash_sdk::dpp::dashcore::ProTxHash; + use std::str::FromStr; + use dash_sdk::platform::FetchMany; + use drive_proof_verifier::types::ProposerBlockCountById; + + // Parse all ProTxHashes + let pro_tx_hashes: Result, _> = ids + .iter() + .map(|id| ProTxHash::from_str(id)) + .collect(); + let pro_tx_hashes = pro_tx_hashes + .map_err(|e| JsError::new(&format!("Invalid ProTxHash: {}", e)))?; + + // Check if epoch fits in u16 before casting + if epoch > u16::MAX as u32 { + return Err(JsError::new(&format!( + "Epoch value {} is invalid: must be less than or equal to {}", + epoch, + u16::MAX + ))); + } + + // TODO: Use the SDK's FetchMany trait to get proposer block counts + // This would automatically handle proof verification when sdk.prove() is true + // Currently commented out due to query format issues - needs investigation + /* + let proposer_block_counts = ProposerBlockCountById::fetch_many( + sdk.as_ref(), + (Some(epoch as u16), pro_tx_hashes), + ) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch evonode proposed blocks by ids: {}", e)))?; + + // Convert the response to our format + let all_counts: Vec = proposer_block_counts.0 + .into_iter() + .map(|(identifier, count)| { + // Convert Identifier back to ProTxHash + let bytes = identifier.to_buffer(); + let hash = dash_sdk::dpp::dashcore::hashes::sha256d::Hash::from_slice(&bytes).unwrap(); + let pro_tx_hash = ProTxHash::from_raw_hash(hash); + ProposerBlockCount { + proposer_pro_tx_hash: pro_tx_hash.to_string(), + count, + } + }) + .collect(); + */ + + // For now, return empty results until the proper SDK query format is determined + let all_counts: Vec = vec![]; + + serde_wasm_bindgen::to_value(&all_counts) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_evonodes_proposed_epoch_blocks_by_range( + sdk: &WasmSdk, + epoch: u32, + limit: Option, + start_after: Option, + order_ascending: Option, +) -> Result { + use dash_sdk::platform::types::proposed_blocks::ProposedBlockCountEx; + use drive_proof_verifier::types::ProposerBlockCounts; + use dash_sdk::dpp::dashcore::ProTxHash; + use std::str::FromStr; + use dash_sdk::platform::QueryStartInfo; + + // Parse start_after if provided + let start_info = if let Some(start) = start_after { + let pro_tx_hash = ProTxHash::from_str(&start) + .map_err(|e| JsError::new(&format!("Invalid start_after ProTxHash: {}", e)))?; + Some(QueryStartInfo { + start_key: pro_tx_hash.to_byte_array().to_vec(), + start_included: false, + }) + } else { + None + }; + + // Check if epoch fits in u16 before casting + if epoch > u16::MAX as u32 { + return Err(JsError::new(&format!( + "Epoch value {} is invalid: must be less than or equal to {}", + epoch, + u16::MAX + ))); + } + + let counts_result = ProposerBlockCounts::fetch_proposed_blocks_by_range( + sdk.as_ref(), + Some(epoch as u16), + limit, + start_info, + ) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch evonode proposed blocks by range: {}", e)))?; + + // Convert to response format + let mut responses: Vec = counts_result.0 + .into_iter() + .map(|(identifier, count)| { + // Convert Identifier back to ProTxHash + let bytes = identifier.to_buffer(); + let hash = dash_sdk::dpp::dashcore::hashes::sha256d::Hash::from_slice(&bytes).unwrap(); + let pro_tx_hash = ProTxHash::from_raw_hash(hash); + ProposerBlockCount { + proposer_pro_tx_hash: pro_tx_hash.to_string(), + count, + } + }) + .collect(); + + // Sort based on order_ascending (default is true) + let ascending = order_ascending.unwrap_or(true); + responses.sort_by(|a, b| { + if ascending { + a.proposer_pro_tx_hash.cmp(&b.proposer_pro_tx_hash) + } else { + b.proposer_pro_tx_hash.cmp(&a.proposer_pro_tx_hash) + } + }); + + serde_wasm_bindgen::to_value(&responses) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_current_epoch(sdk: &WasmSdk) -> Result { + let epoch = ExtendedEpochInfo::fetch_current(sdk.as_ref()) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch current epoch: {}", e)))?; + + let epoch_info = EpochInfo::from(epoch); + + serde_wasm_bindgen::to_value(&epoch_info) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/group.rs b/packages/wasm-sdk/src/queries/group.rs new file mode 100644 index 0000000000..f0be03a47a --- /dev/null +++ b/packages/wasm-sdk/src/queries/group.rs @@ -0,0 +1,517 @@ +use crate::sdk::WasmSdk; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; +use serde::{Serialize, Deserialize}; +use dash_sdk::platform::{Fetch, FetchMany, Identifier}; +use dash_sdk::dpp::data_contract::group::Group; +use dash_sdk::dpp::data_contract::GroupContractPosition; +use dash_sdk::dpp::data_contract::group::accessors::v0::GroupV0Getters; +use dash_sdk::platform::group_actions::{GroupQuery, GroupInfosQuery, GroupActionsQuery, GroupActionSignersQuery}; +use dash_sdk::dpp::group::group_action::GroupAction; +use dash_sdk::dpp::group::group_action_status::GroupActionStatus; +use dash_sdk::dpp::data_contract::group::GroupMemberPower; +use std::collections::BTreeMap; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct GroupInfoResponse { + members: BTreeMap, + required_power: u32, +} + +impl GroupInfoResponse { + fn from_group(group: &Group) -> Self { + let members = group.members() + .iter() + .map(|(id, power)| { + (id.to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), *power) + }) + .collect(); + + Self { + members, + required_power: group.required_power(), + } + } +} + +#[wasm_bindgen] +pub async fn get_group_info( + sdk: &WasmSdk, + data_contract_id: &str, + group_contract_position: u32, +) -> Result { + // Parse data contract ID + let contract_id = Identifier::from_string( + data_contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Create group query + let query = GroupQuery { + contract_id, + group_contract_position: group_contract_position as GroupContractPosition, + }; + + // Fetch the group + let group_result: Option = Group::fetch(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch group: {}", e)))?; + + match group_result { + Some(group) => { + let response = GroupInfoResponse::from_group(&group); + + // Use json_compatible serializer to convert maps to objects + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + }, + None => Ok(JsValue::NULL), + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct GroupMember { + member_id: String, + power: u32, +} + +#[wasm_bindgen] +pub async fn get_group_members( + sdk: &WasmSdk, + data_contract_id: &str, + group_contract_position: u32, + member_ids: Option>, + start_at: Option, + limit: Option, +) -> Result { + // Parse data contract ID + let contract_id = Identifier::from_string( + data_contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Create group query + let query = GroupQuery { + contract_id, + group_contract_position: group_contract_position as GroupContractPosition, + }; + + // Fetch the group + let group_result: Option = Group::fetch(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch group: {}", e)))?; + + match group_result { + Some(group) => { + let mut members: Vec = Vec::new(); + + // If specific member IDs are requested, filter by them + if let Some(requested_ids) = member_ids { + let requested_identifiers: Result, _> = requested_ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect(); + let requested_identifiers = requested_identifiers?; + + for id in requested_identifiers { + if let Ok(power) = group.member_power(id) { + members.push(GroupMember { + member_id: id.to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + power, + }); + } + } + } else { + // Return all members with pagination + let all_members = group.members(); + let mut sorted_members: Vec<_> = all_members.iter().collect(); + sorted_members.sort_by_key(|(id, _)| *id); + + // Apply start_at if provided + let start_index = if let Some(start_id) = start_at { + let start_identifier = Identifier::from_string( + &start_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + sorted_members.iter().position(|(id, _)| **id > start_identifier).unwrap_or(sorted_members.len()) + } else { + 0 + }; + + // Apply limit + let end_index = if let Some(lim) = limit { + (start_index + lim as usize).min(sorted_members.len()) + } else { + sorted_members.len() + }; + + for (id, power) in &sorted_members[start_index..end_index] { + members.push(GroupMember { + member_id: (*id).to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + power: **power, + }); + } + } + + // Use json_compatible serializer to convert response + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + members.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + }, + None => Ok(JsValue::NULL), + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct IdentityGroupInfo { + data_contract_id: String, + group_contract_position: u32, + role: String, // "member", "owner", or "moderator" + power: Option, // Only for members +} + +#[wasm_bindgen] +pub async fn get_identity_groups( + sdk: &WasmSdk, + identity_id: &str, + member_data_contracts: Option>, + owner_data_contracts: Option>, + moderator_data_contracts: Option>, +) -> Result { + // Parse identity ID + let id = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let mut groups: Vec = Vec::new(); + + // Check member data contracts + if let Some(contracts) = member_data_contracts { + for contract_id_str in contracts { + let contract_id = Identifier::from_string( + &contract_id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Fetch all groups for this contract + let query = GroupInfosQuery { + contract_id, + start_group_contract_position: None, + limit: None, + }; + + let groups_result = Group::fetch_many(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch groups: {}", e)))?; + + // Check each group for the identity + for (position, group_opt) in groups_result { + if let Some(group) = group_opt { + if let Ok(power) = group.member_power(id) { + groups.push(IdentityGroupInfo { + data_contract_id: contract_id_str.clone(), + group_contract_position: position as u32, + role: "member".to_string(), + power: Some(power), + }); + } + } + } + } + } + + // Note: Owner and moderator roles would require additional contract queries + // which are not yet implemented in the SDK. For now, return a warning. + if owner_data_contracts.is_some() || moderator_data_contracts.is_some() { + web_sys::console::warn_1(&JsValue::from_str( + "Warning: Owner and moderator role queries are not yet implemented" + )); + } + + // Use json_compatible serializer to convert response + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + groups.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct GroupsDataContractInfo { + data_contract_id: String, + groups: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct GroupContractPositionInfo { + position: u32, + group: GroupInfoResponse, +} + +#[wasm_bindgen] +pub async fn get_group_infos( + sdk: &WasmSdk, + contract_id: &str, + start_at_info: JsValue, + count: Option, +) -> Result { + // Parse contract ID + let contract_id = Identifier::from_string( + contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse start at info if provided + let start_group_contract_position = if !start_at_info.is_null() && !start_at_info.is_undefined() { + let info = serde_wasm_bindgen::from_value::(start_at_info); + match info { + Ok(json) => { + let position = json["position"].as_u64().ok_or_else(|| JsError::new("Invalid start position"))? as u32; + let included = json["included"].as_bool().unwrap_or(false); + Some((position as GroupContractPosition, included)) + } + Err(_) => None + } + } else { + None + }; + + // Create query + let query = GroupInfosQuery { + contract_id, + start_group_contract_position, + limit: count.map(|c| c as u16), + }; + + // Fetch groups + let groups_result = Group::fetch_many(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch groups: {}", e)))?; + + // Convert result to response format + let mut group_infos = Vec::new(); + for (position, group_opt) in groups_result { + if let Some(group) = group_opt { + let members: Vec = group.members() + .iter() + .map(|(id, power)| { + serde_json::json!({ + "memberId": id.to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + "power": *power + }) + }) + .collect(); + + group_infos.push(serde_json::json!({ + "groupContractPosition": position, + "members": members, + "groupRequiredPower": group.required_power() + })); + } + } + + let response = serde_json::json!({ + "groupInfos": group_infos + }); + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_group_actions( + sdk: &WasmSdk, + contract_id: &str, + group_contract_position: u32, + status: &str, + start_at_info: JsValue, + count: Option, +) -> Result { + // Parse contract ID + let contract_id = Identifier::from_string( + contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse status + let status = match status { + "ACTIVE" => GroupActionStatus::ActionActive, + "CLOSED" => GroupActionStatus::ActionClosed, + _ => return Err(JsError::new(&format!("Invalid status: {}. Must be ACTIVE or CLOSED", status))), + }; + + // Parse start action ID if provided + let start_at_action_id = if !start_at_info.is_null() && !start_at_info.is_undefined() { + let info = serde_wasm_bindgen::from_value::(start_at_info); + match info { + Ok(json) => { + let action_id = json["actionId"].as_str().ok_or_else(|| JsError::new("Invalid action ID"))?; + let included = json["included"].as_bool().unwrap_or(false); + Some(( + Identifier::from_string( + action_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?, + included + )) + } + Err(_) => None + } + } else { + None + }; + + // Create query + let query = GroupActionsQuery { + contract_id, + group_contract_position: group_contract_position as GroupContractPosition, + status, + start_at_action_id, + limit: count.map(|c| c as u16), + }; + + // Fetch actions + let actions_result = GroupAction::fetch_many(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch group actions: {}", e)))?; + + // Convert result to response format + let mut group_actions = Vec::new(); + for (action_id, action_opt) in actions_result { + if let Some(_action) = action_opt { + // For now, just return the action ID + // The full action structure requires custom serialization + group_actions.push(serde_json::json!({ + "actionId": action_id.to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + // TODO: Serialize the full action event structure + })); + } + } + + let response = serde_json::json!({ + "groupActions": group_actions + }); + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_group_action_signers( + sdk: &WasmSdk, + contract_id: &str, + group_contract_position: u32, + status: &str, + action_id: &str, +) -> Result { + // Parse contract ID + let contract_id = Identifier::from_string( + contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse action ID + let action_id = Identifier::from_string( + action_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse status + let status = match status { + "ACTIVE" => GroupActionStatus::ActionActive, + "CLOSED" => GroupActionStatus::ActionClosed, + _ => return Err(JsError::new(&format!("Invalid status: {}. Must be ACTIVE or CLOSED", status))), + }; + + // Create query + let query = GroupActionSignersQuery { + contract_id, + group_contract_position: group_contract_position as GroupContractPosition, + status, + action_id, + }; + + // Fetch signers + let signers_result = GroupMemberPower::fetch_many(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch group action signers: {}", e)))?; + + // Convert result to response format + let mut signers = Vec::new(); + for (signer_id, power_opt) in signers_result { + if let Some(power) = power_opt { + signers.push(serde_json::json!({ + "signerId": signer_id.to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + "power": power + })); + } + } + + let response = serde_json::json!({ + "signers": signers + }); + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + response.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_groups_data_contracts( + sdk: &WasmSdk, + data_contract_ids: Vec, +) -> Result { + let mut results: Vec = Vec::new(); + + for contract_id_str in data_contract_ids { + let contract_id = Identifier::from_string( + &contract_id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Fetch all groups for this contract + let query = GroupInfosQuery { + contract_id, + start_group_contract_position: None, + limit: None, + }; + + let groups_result = Group::fetch_many(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch groups for contract {}: {}", contract_id_str, e)))?; + + let mut groups: Vec = Vec::new(); + + for (position, group_opt) in groups_result { + if let Some(group) = group_opt { + groups.push(GroupContractPositionInfo { + position: position as u32, + group: GroupInfoResponse::from_group(&group), + }); + } + } + + results.push(GroupsDataContractInfo { + data_contract_id: contract_id_str, + groups, + }); + } + + // Use json_compatible serializer to convert response + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + results.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/identity.rs b/packages/wasm-sdk/src/queries/identity.rs new file mode 100644 index 0000000000..3ca70fc923 --- /dev/null +++ b/packages/wasm-sdk/src/queries/identity.rs @@ -0,0 +1,537 @@ +use crate::dpp::IdentityWasm; +use crate::sdk::WasmSdk; +use dash_sdk::platform::{Fetch, FetchMany, Identifier, Identity}; +use dash_sdk::dpp::identity::identity_public_key::IdentityPublicKey; +use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; +use serde::{Serialize, Deserialize}; +use js_sys::Array; + +#[wasm_bindgen] +pub async fn identity_fetch(sdk: &WasmSdk, base58_id: &str) -> Result { + let id = Identifier::from_string( + base58_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + Identity::fetch_by_identifier(sdk, id) + .await? + .ok_or_else(|| JsError::new("Identity not found")) + .map(Into::into) +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct IdentityKeyResponse { + key_id: u32, + key_type: String, + public_key_data: String, + purpose: String, + security_level: String, + read_only: bool, + disabled: bool, +} + +#[wasm_bindgen] +pub async fn get_identity_keys( + sdk: &WasmSdk, + identity_id: &str, + key_request_type: &str, + specific_key_ids: Option>, + limit: Option, + offset: Option, +) -> Result { + + // DapiRequestExecutor not needed anymore + + let id = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Fetch all keys for now - TODO: implement specific key request once available in SDK + if key_request_type != "all" { + return Err(JsError::new("Currently only 'all' key request type is supported")); + } + + // Use FetchMany to get identity keys + let keys_result = IdentityPublicKey::fetch_many(sdk.as_ref(), id) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity keys: {}", e)))?; + + // Convert keys to response format + let mut keys: Vec = Vec::new(); + + // Apply offset and limit if provided + let start = offset.unwrap_or(0) as usize; + let end = if let Some(lim) = limit { + start + lim as usize + } else { + usize::MAX + }; + + for (idx, (key_id, key_opt)) in keys_result.into_iter().enumerate() { + if idx < start { + continue; + } + if idx >= end { + break; + } + + if let Some(key) = key_opt { + keys.push(IdentityKeyResponse { + key_id: key_id, + key_type: format!("{:?}", key.key_type()), + public_key_data: hex::encode(key.data().as_slice()), + purpose: format!("{:?}", key.purpose()), + security_level: format!("{:?}", key.security_level()), + read_only: key.read_only(), + disabled: key.disabled_at().is_some(), + }); + } + } + + serde_wasm_bindgen::to_value(&keys) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_identity_nonce(sdk: &WasmSdk, identity_id: &str) -> Result { + use dash_sdk::dpp::prelude::IdentityNonce; + use dash_sdk::platform::Fetch; + + let id = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let nonce_result = IdentityNonce::fetch(sdk.as_ref(), id) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity nonce: {}", e)))?; + + let nonce = nonce_result.ok_or_else(|| JsError::new("Identity nonce not found"))?; + + // Return as a JSON object with nonce as string to avoid BigInt serialization issues + #[derive(Serialize)] + struct NonceResponse { + nonce: String, + } + + let response = NonceResponse { + nonce: nonce.to_string(), + }; + + serde_wasm_bindgen::to_value(&response) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_identity_contract_nonce( + sdk: &WasmSdk, + identity_id: &str, + contract_id: &str, +) -> Result { + use drive_proof_verifier::types::IdentityContractNonceFetcher; + use dash_sdk::platform::Fetch; + + let identity_id = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let contract_id = Identifier::from_string( + contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let nonce_result = IdentityContractNonceFetcher::fetch(sdk.as_ref(), (identity_id, contract_id)) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity contract nonce: {}", e)))?; + + let nonce = nonce_result + .map(|fetcher| fetcher.0) + .ok_or_else(|| JsError::new("Identity contract nonce not found"))?; + + // Return as a JSON object with nonce as string to avoid BigInt serialization issues + #[derive(Serialize)] + struct NonceResponse { + nonce: String, + } + + let response = NonceResponse { + nonce: nonce.to_string(), + }; + + serde_wasm_bindgen::to_value(&response) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_identity_balance(sdk: &WasmSdk, id: &str) -> Result { + use drive_proof_verifier::types::IdentityBalance; + use dash_sdk::platform::Fetch; + + let identity_id = Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let balance_result = IdentityBalance::fetch(sdk.as_ref(), identity_id) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity balance: {}", e)))?; + + if let Some(balance) = balance_result { + // Return as object with balance as string to handle large numbers + #[derive(Serialize)] + struct BalanceResponse { + balance: String, + } + + let response = BalanceResponse { + balance: balance.to_string(), + }; + + serde_wasm_bindgen::to_value(&response) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } else { + Err(JsError::new("Identity balance not found")) + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct IdentityBalanceResponse { + identity_id: String, + balance: String, // String to handle large numbers +} + +#[wasm_bindgen] +pub async fn get_identities_balances(sdk: &WasmSdk, identity_ids: Vec) -> Result { + use drive_proof_verifier::types::IdentityBalance; + + + // Convert string IDs to Identifiers + let identifiers: Vec = identity_ids + .into_iter() + .map(|id| Identifier::from_string( + &id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect::, _>>()?; + + let balances_result: drive_proof_verifier::types::IdentityBalances = IdentityBalance::fetch_many(sdk.as_ref(), identifiers.clone()) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identities balances: {}", e)))?; + + // Convert to response format + let responses: Vec = identifiers + .into_iter() + .filter_map(|id| { + balances_result.get(&id).and_then(|balance_opt| { + balance_opt.map(|balance| { + IdentityBalanceResponse { + identity_id: id.to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + balance: balance.to_string(), + } + }) + }) + }) + .collect(); + + serde_wasm_bindgen::to_value(&responses) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct IdentityBalanceAndRevisionResponse { + balance: String, // String to handle large numbers + revision: u64, +} + +#[wasm_bindgen] +pub async fn get_identity_balance_and_revision(sdk: &WasmSdk, identity_id: &str) -> Result { + use drive_proof_verifier::types::IdentityBalanceAndRevision; + use dash_sdk::platform::Fetch; + + let id = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let result = IdentityBalanceAndRevision::fetch(sdk.as_ref(), id) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity balance and revision: {}", e)))?; + + if let Some(balance_and_revision) = result { + let response = IdentityBalanceAndRevisionResponse { + balance: balance_and_revision.0.to_string(), + revision: balance_and_revision.1, + }; + + serde_wasm_bindgen::to_value(&response) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } else { + Err(JsError::new("Identity balance and revision not found")) + } +} + +#[wasm_bindgen] +pub async fn get_identity_by_public_key_hash(sdk: &WasmSdk, public_key_hash: &str) -> Result { + use dash_sdk::platform::types::identity::PublicKeyHash; + + // Parse the hex-encoded public key hash + let hash_bytes = hex::decode(public_key_hash) + .map_err(|e| JsError::new(&format!("Invalid public key hash hex: {}", e)))?; + + if hash_bytes.len() != 20 { + return Err(JsError::new("Public key hash must be 20 bytes (40 hex characters)")); + } + + let mut hash_array = [0u8; 20]; + hash_array.copy_from_slice(&hash_bytes); + + let result = Identity::fetch(sdk.as_ref(), PublicKeyHash(hash_array)) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity by public key hash: {}", e)))?; + + result + .ok_or_else(|| JsError::new("Identity not found for public key hash")) + .map(Into::into) +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct IdentityContractKeyResponse { + identity_id: String, + purpose: u32, + key_id: u32, + key_type: String, + public_key_data: String, + security_level: String, + read_only: bool, + disabled: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct IdentityContractKeysResponse { + identity_id: String, + keys: Vec, +} + +#[wasm_bindgen] +pub async fn get_identities_contract_keys( + sdk: &WasmSdk, + identities_ids: Vec, + contract_id: &str, + document_type_name: Option, + purposes: Option>, +) -> Result { + use dash_sdk::dpp::identity::Purpose; + + // Convert string IDs to Identifiers + let identity_ids: Vec = identities_ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect::, _>>()?; + + // Contract ID is not used in the individual key queries, but we validate it + let _contract_identifier = Identifier::from_string( + contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Convert purposes if provided + let purposes_opt = purposes.map(|p| { + p.into_iter() + .filter_map(|purpose_int| match purpose_int { + 0 => Some(Purpose::AUTHENTICATION as u32), + 1 => Some(Purpose::ENCRYPTION as u32), + 2 => Some(Purpose::DECRYPTION as u32), + 3 => Some(Purpose::TRANSFER as u32), + 4 => Some(Purpose::SYSTEM as u32), + 5 => Some(Purpose::VOTING as u32), + _ => None, + }) + .collect::>() + }); + + // For now, we'll implement this by fetching keys for each identity individually + // The SDK doesn't fully expose the batch query yet + let mut responses: Vec = Vec::new(); + + for identity_id_str in identities_ids { + let identity_id = Identifier::from_string( + &identity_id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Get keys for this identity using the regular identity keys query + let keys_result = IdentityPublicKey::fetch_many(sdk.as_ref(), identity_id) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch keys for identity {}: {}", identity_id_str, e)))?; + + let mut identity_keys = Vec::new(); + + // Filter keys by purpose if specified + for (key_id, key_opt) in keys_result { + if let Some(key) = key_opt { + // Check if this key matches the requested purposes + if let Some(ref purposes) = purposes_opt { + if !purposes.contains(&(key.purpose() as u32)) { + continue; + } + } + + let key_response = IdentityKeyResponse { + key_id: key_id, + key_type: format!("{:?}", key.key_type()), + public_key_data: hex::encode(key.data().as_slice()), + purpose: format!("{:?}", key.purpose()), + security_level: format!("{:?}", key.security_level()), + read_only: key.read_only(), + disabled: key.disabled_at().is_some(), + }; + identity_keys.push(key_response); + } + } + + if !identity_keys.is_empty() { + responses.push(IdentityContractKeysResponse { + identity_id: identity_id_str, + keys: identity_keys, + }); + } + } + + serde_wasm_bindgen::to_value(&responses) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_identity_by_non_unique_public_key_hash( + sdk: &WasmSdk, + public_key_hash: &str, + start_after: Option, +) -> Result { + + + // Parse the hex-encoded public key hash + let hash_bytes = hex::decode(public_key_hash) + .map_err(|e| JsError::new(&format!("Invalid public key hash hex: {}", e)))?; + + if hash_bytes.len() != 20 { + return Err(JsError::new("Public key hash must be 20 bytes (40 hex characters)")); + } + + let mut hash_array = [0u8; 20]; + hash_array.copy_from_slice(&hash_bytes); + + // Convert start_after if provided + let start_id = if let Some(start) = start_after { + Some(Identifier::from_string( + &start, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?) + } else { + None + }; + + use dash_sdk::platform::types::identity::NonUniquePublicKeyHashQuery; + + let query = NonUniquePublicKeyHashQuery { + key_hash: hash_array, + after: start_id.map(|id| *id.as_bytes()), + }; + + // Fetch identity by non-unique public key hash + let identity = Identity::fetch(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identities by non-unique public key hash: {}", e)))?; + + // Return array with single identity if found + let results = if let Some(id) = identity { + vec![id] + } else { + vec![] + }; + + // Convert results to IdentityWasm + let identities: Vec = results + .into_iter() + .map(Into::into) + .collect(); + + // Create JS array directly + let js_array = Array::new(); + for identity in identities { + let json = identity.to_json().map_err(|e| JsError::new(&format!("Failed to convert identity to JSON: {:?}", e)))?; + js_array.push(&json); + } + Ok(js_array.into()) +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct TokenBalanceResponse { + token_id: String, + balance: String, // String to handle large numbers +} + +#[wasm_bindgen] +pub async fn get_identity_token_balances( + sdk: &WasmSdk, + identity_id: &str, + token_ids: Vec, +) -> Result { + use dash_sdk::platform::tokens::identity_token_balances::IdentityTokenBalancesQuery; + use dash_sdk::dpp::balances::credits::TokenAmount; + + let identity_id = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Convert token IDs to Identifiers + let token_identifiers: Vec = token_ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect::, _>>()?; + + let query = IdentityTokenBalancesQuery { + identity_id, + token_ids: token_identifiers.clone(), + }; + + + + // Use FetchMany trait to fetch token balances + let balances: drive_proof_verifier::types::identity_token_balance::IdentityTokenBalances = TokenAmount::fetch_many(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity token balances: {}", e)))?; + + // Convert to response format + let responses: Vec = token_identifiers + .into_iter() + .zip(token_ids.into_iter()) + .filter_map(|(token_id, token_id_str)| { + balances.get(&token_id).and_then(|balance_opt| { + balance_opt.map(|balance| TokenBalanceResponse { + token_id: token_id_str, + balance: balance.to_string(), + }) + }) + }) + .collect(); + + serde_wasm_bindgen::to_value(&responses) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/mod.rs b/packages/wasm-sdk/src/queries/mod.rs new file mode 100644 index 0000000000..512666d2c3 --- /dev/null +++ b/packages/wasm-sdk/src/queries/mod.rs @@ -0,0 +1,20 @@ +pub mod identity; +pub mod data_contract; +pub mod document; +pub mod protocol; +pub mod epoch; +pub mod token; +pub mod voting; +pub mod group; +pub mod system; + +// Re-export all query functions for easy access +pub use identity::*; +pub use data_contract::*; +pub use document::*; +pub use protocol::*; +pub use epoch::*; +pub use token::*; +pub use voting::*; +pub use group::*; +pub use system::*; \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/protocol.rs b/packages/wasm-sdk/src/queries/protocol.rs new file mode 100644 index 0000000000..e25690463a --- /dev/null +++ b/packages/wasm-sdk/src/queries/protocol.rs @@ -0,0 +1,102 @@ +use crate::sdk::WasmSdk; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct ProtocolVersionUpgradeState { + current_protocol_version: u32, + next_protocol_version: Option, + activation_height: Option, + vote_count: Option, + threshold_reached: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct ProtocolVersionUpgradeVoteStatus { + pro_tx_hash: String, + version: u32, +} + +#[wasm_bindgen] +pub async fn get_protocol_version_upgrade_state(sdk: &WasmSdk) -> Result { + use dash_sdk::platform::FetchMany; + use drive_proof_verifier::types::ProtocolVersionVoteCount; + + let upgrade_result: drive_proof_verifier::types::ProtocolVersionUpgrades = ProtocolVersionVoteCount::fetch_many(sdk.as_ref(), ()) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch protocol version upgrade state: {}", e)))?; + + // Get the current protocol version from the SDK + let current_version = sdk.version(); + + // Find the next version with votes + let mut next_version = None; + let mut activation_height = None; + let mut vote_count = None; + let mut threshold_reached = false; + + // The result is an IndexMap> where u32 is version and Option is activation height + for (version, height_opt) in upgrade_result.iter() { + if *version > current_version { + next_version = Some(*version); + activation_height = *height_opt; + // TODO: Get actual vote count and threshold from platform + vote_count = None; + threshold_reached = height_opt.is_some(); + break; + } + } + + let state = ProtocolVersionUpgradeState { + current_protocol_version: current_version, + next_protocol_version: next_version, + activation_height, + vote_count, + threshold_reached, + }; + + serde_wasm_bindgen::to_value(&state) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_protocol_version_upgrade_vote_status( + sdk: &WasmSdk, + start_pro_tx_hash: &str, + count: u32, +) -> Result { + use dash_sdk::platform::types::version_votes::MasternodeProtocolVoteEx; + use drive_proof_verifier::types::MasternodeProtocolVote; + use dash_sdk::dpp::dashcore::ProTxHash; + use std::str::FromStr; + + // Parse the ProTxHash + let start_hash = if start_pro_tx_hash.is_empty() { + None + } else { + Some(ProTxHash::from_str(start_pro_tx_hash) + .map_err(|e| JsError::new(&format!("Invalid ProTxHash: {}", e)))?) + }; + + let votes_result = MasternodeProtocolVote::fetch_votes(sdk.as_ref(), start_hash, Some(count)) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch protocol version votes: {}", e)))?; + + // Convert to our response format + let votes: Vec = votes_result + .into_iter() + .filter_map(|(pro_tx_hash, vote_opt)| { + // vote_opt is Option + vote_opt.map(|vote| ProtocolVersionUpgradeVoteStatus { + pro_tx_hash: pro_tx_hash.to_string(), + version: vote.voted_version, + }) + }) + .collect(); + + serde_wasm_bindgen::to_value(&votes) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/system.rs b/packages/wasm-sdk/src/queries/system.rs new file mode 100644 index 0000000000..41a18a654e --- /dev/null +++ b/packages/wasm-sdk/src/queries/system.rs @@ -0,0 +1,357 @@ +use crate::sdk::WasmSdk; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; +use serde::{Serialize, Deserialize}; +use dash_sdk::dpp::core_types::validator_set::v0::ValidatorSetV0Getters; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct PlatformStatus { + version: u32, + network: String, + block_height: Option, + core_height: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct QuorumInfo { + quorum_hash: String, + quorum_type: String, + member_count: u32, + threshold: u32, + is_verified: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct CurrentQuorumsInfo { + quorums: Vec, + height: u64, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct TotalCreditsResponse { + total_credits_in_platform: String, // Use String to handle large numbers +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct StateTransitionResult { + state_transition_hash: String, + status: String, + error: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct PrefundedSpecializedBalance { + identity_id: String, + balance: u64, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct PathElement { + path: Vec, + value: Option, +} + +#[wasm_bindgen] +pub async fn get_status(sdk: &WasmSdk) -> Result { + use dash_sdk::platform::fetch_current_no_parameters::FetchCurrent; + use dash_sdk::dpp::block::extended_epoch_info::ExtendedEpochInfo; + use dash_sdk::dpp::block::extended_epoch_info::v0::ExtendedEpochInfoV0Getters; + + // Get the network from SDK + let network_str = match sdk.network { + dash_sdk::dpp::dashcore::Network::Dash => "mainnet", + dash_sdk::dpp::dashcore::Network::Testnet => "testnet", + dash_sdk::dpp::dashcore::Network::Devnet => "devnet", + dash_sdk::dpp::dashcore::Network::Regtest => "regtest", + _ => "unknown", + }.to_string(); + + // Try to fetch current epoch info to get block heights + let (block_height, core_height) = match ExtendedEpochInfo::fetch_current(sdk.as_ref()).await { + Ok(epoch_info) => { + // Extract heights from epoch info + let platform_height = Some(epoch_info.first_block_height()); + let core_height = Some(epoch_info.first_core_block_height() as u64); + (platform_height, core_height) + } + Err(_) => { + // If we can't fetch epoch info, heights remain None + (None, None) + } + }; + + let status = PlatformStatus { + version: sdk.version(), + network: network_str, + block_height, + core_height, + }; + + serde_wasm_bindgen::to_value(&status) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_current_quorums_info(sdk: &WasmSdk) -> Result { + use dash_sdk::platform::FetchUnproved; + use drive_proof_verifier::types::{NoParamQuery, CurrentQuorumsInfo as SdkCurrentQuorumsInfo}; + + let quorums_result = SdkCurrentQuorumsInfo::fetch_unproved(sdk.as_ref(), NoParamQuery {}) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch quorums info: {}", e)))?; + + // The result is Option + if let Some(quorum_info) = quorums_result { + // Convert the SDK response to our structure + // Match quorum hashes with validator sets to get detailed information + let quorums: Vec = quorum_info.quorum_hashes + .into_iter() + .map(|quorum_hash| { + // Try to find the corresponding validator set + let validator_set = quorum_info.validator_sets + .iter() + .find(|vs| { + // Compare the quorum hash bytes directly + + let vs_hash_bytes: &[u8] = vs.quorum_hash().as_ref(); + vs_hash_bytes == &quorum_hash[..] + }); + + if let Some(vs) = validator_set { + let member_count = vs.members().len() as u32; + + // Determine quorum type based on member count and quorum index + // This is an approximation based on common quorum sizes + // TODO: Get actual quorum type from the platform when available + let (quorum_type, threshold) = match member_count { + 50..=70 => ("LLMQ_60_75".to_string(), (member_count * 75 / 100).max(1)), + 90..=110 => ("LLMQ_100_67".to_string(), (member_count * 67 / 100).max(1)), + 350..=450 => ("LLMQ_400_60".to_string(), (member_count * 60 / 100).max(1)), + _ => ("LLMQ_TYPE_UNKNOWN".to_string(), (member_count * 2 / 3).max(1)), + }; + + QuorumInfo { + quorum_hash: hex::encode(&quorum_hash), + quorum_type, + member_count, + threshold, + is_verified: true, // We have the validator set, so it's verified + } + } else { + // No validator set found for this quorum hash + // TODO: This should not happen in normal circumstances. When the SDK + // provides complete quorum information, this fallback can be removed. + QuorumInfo { + quorum_hash: hex::encode(&quorum_hash), + quorum_type: "LLMQ_TYPE_UNKNOWN".to_string(), + member_count: 0, + threshold: 0, + is_verified: false, + } + } + }) + .collect(); + + let info = CurrentQuorumsInfo { + quorums, + height: quorum_info.last_platform_block_height, + }; + + serde_wasm_bindgen::to_value(&info) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } else { + // No quorum info available + let info = CurrentQuorumsInfo { + quorums: vec![], + height: 0, + }; + + serde_wasm_bindgen::to_value(&info) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } +} + +#[wasm_bindgen] +pub async fn get_total_credits_in_platform(sdk: &WasmSdk) -> Result { + use dash_sdk::platform::Fetch; + use drive_proof_verifier::types::{TotalCreditsInPlatform as TotalCreditsQuery, NoParamQuery}; + + let total_credits_result = TotalCreditsQuery::fetch(sdk.as_ref(), NoParamQuery {}) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch total credits: {}", e)))?; + + // TotalCreditsInPlatform is likely a newtype wrapper around u64 + let credits_value = if let Some(credits) = total_credits_result { + // Extract the inner value - assuming it has a field or can be dereferenced + // We'll try to access it as a tuple struct + credits.0 + } else { + 0 + }; + + let response = TotalCreditsResponse { + total_credits_in_platform: credits_value.to_string(), + }; + + serde_wasm_bindgen::to_value(&response) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_prefunded_specialized_balance( + sdk: &WasmSdk, + identity_id: &str, +) -> Result { + use dash_sdk::platform::{Identifier, Fetch}; + use drive_proof_verifier::types::PrefundedSpecializedBalance as PrefundedBalance; + + // Parse identity ID + let identity_identifier = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Fetch prefunded specialized balance + let balance_result = PrefundedBalance::fetch(sdk.as_ref(), identity_identifier) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch prefunded specialized balance: {}", e)))?; + + if let Some(balance) = balance_result { + let response = PrefundedSpecializedBalance { + identity_id: identity_id.to_string(), + balance: balance.0, // PrefundedSpecializedBalance is a newtype wrapper around u64 + }; + + serde_wasm_bindgen::to_value(&response) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } else { + // Return zero balance if not found + let response = PrefundedSpecializedBalance { + identity_id: identity_id.to_string(), + balance: 0, + }; + + serde_wasm_bindgen::to_value(&response) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } +} + +#[wasm_bindgen] +pub async fn wait_for_state_transition_result( + sdk: &WasmSdk, + state_transition_hash: &str, +) -> Result { + use dapi_grpc::platform::v0::wait_for_state_transition_result_request::{ + Version, WaitForStateTransitionResultRequestV0, + }; + use dapi_grpc::platform::v0::WaitForStateTransitionResultRequest; + + use dash_sdk::RequestSettings; + use rs_dapi_client::DapiRequestExecutor; + + // Parse the hash from hex string to bytes + let hash_bytes = hex::decode(state_transition_hash) + .map_err(|e| JsError::new(&format!("Invalid state transition hash: {}", e)))?; + + // Create the gRPC request + let request = WaitForStateTransitionResultRequest { + version: Some(Version::V0(WaitForStateTransitionResultRequestV0 { + state_transition_hash: hash_bytes, + prove: sdk.prove(), + })), + }; + + // Execute the request + let response = sdk + .as_ref() + .execute(request, RequestSettings::default()) + .await + .map_err(|e| JsError::new(&format!("Failed to wait for state transition result: {}", e)))?; + + // Parse the response + use dapi_grpc::platform::v0::wait_for_state_transition_result_response::{ + wait_for_state_transition_result_response_v0::Result as V0Result, + Version as ResponseVersion, + }; + + let (status, error) = match response.inner.version { + Some(ResponseVersion::V0(v0)) => match v0.result { + Some(V0Result::Error(e)) => { + let error_message = format!("Code: {}, Message: {}", e.code, e.message); + ("ERROR".to_string(), Some(error_message)) + }, + Some(V0Result::Proof(_)) => { + // State transition was successful + ("SUCCESS".to_string(), None) + }, + None => ("UNKNOWN".to_string(), Some("No result returned".to_string())), + }, + None => ("UNKNOWN".to_string(), Some("No version in response".to_string())), + }; + + let result = StateTransitionResult { + state_transition_hash: state_transition_hash.to_string(), + status, + error, + }; + + serde_wasm_bindgen::to_value(&result) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[wasm_bindgen] +pub async fn get_path_elements( + sdk: &WasmSdk, + keys: Vec, +) -> Result { + use dash_sdk::platform::FetchMany; + use drive_proof_verifier::types::{KeysInPath, Elements}; + use dash_sdk::drive::grovedb::Element; + + // Convert string keys to byte vectors + let key_bytes: Vec> = keys.iter() + .map(|k| k.as_bytes().to_vec()) + .collect(); + + // Create the query + let query = KeysInPath { + path: vec![], // Root path - can be adjusted if needed + keys: key_bytes, + }; + + // Fetch path elements + let path_elements_result: Elements = Element::fetch_many(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch path elements: {}", e)))?; + + // Convert the result to our response format + let elements: Vec = keys.into_iter() + .map(|key| { + // Check if this key exists in the result + let value = path_elements_result.get(key.as_bytes()) + .and_then(|element_opt| element_opt.as_ref()) + .and_then(|element| { + // Element can contain different types, we'll serialize it as base64 + element.as_item_bytes().ok().map(|bytes| { + use base64::Engine; + base64::engine::general_purpose::STANDARD.encode(bytes) + }) + }); + + PathElement { + path: vec![key], + value, + } + }) + .collect(); + + serde_wasm_bindgen::to_value(&elements) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/token.rs b/packages/wasm-sdk/src/queries/token.rs new file mode 100644 index 0000000000..16def2d6f5 --- /dev/null +++ b/packages/wasm-sdk/src/queries/token.rs @@ -0,0 +1,495 @@ +use crate::sdk::WasmSdk; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; +use serde::{Serialize, Deserialize}; +use dash_sdk::platform::{Identifier, FetchMany}; +use dash_sdk::dpp::balances::credits::TokenAmount; +use dash_sdk::dpp::tokens::status::TokenStatus; +use dash_sdk::dpp::tokens::status::v0::TokenStatusV0Accessors; +use dash_sdk::dpp::tokens::info::IdentityTokenInfo; +use dash_sdk::dpp::tokens::token_pricing_schedule::TokenPricingSchedule; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct IdentityTokenBalanceResponse { + identity_id: String, + balance: String, // String to handle large numbers +} + +#[wasm_bindgen] +pub async fn get_identities_token_balances( + sdk: &WasmSdk, + identity_ids: Vec, + token_id: &str, +) -> Result { + use dash_sdk::platform::tokens::identity_token_balances::IdentitiesTokenBalancesQuery; + use drive_proof_verifier::types::identity_token_balance::IdentitiesTokenBalances; + + // Parse token ID + let token_identifier = Identifier::from_string( + token_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse identity IDs + let identities: Result, _> = identity_ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect(); + let identities = identities?; + + // Create query + let query = IdentitiesTokenBalancesQuery { + identity_ids: identities.clone(), + token_id: token_identifier, + }; + + // Fetch balances + let balances_result: IdentitiesTokenBalances = TokenAmount::fetch_many(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identities token balances: {}", e)))?; + + // Convert to response format + let responses: Vec = identity_ids + .into_iter() + .filter_map(|id_str| { + let id = Identifier::from_string( + &id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + ).ok()?; + + balances_result.get(&id).and_then(|balance_opt| { + balance_opt.map(|balance| { + IdentityTokenBalanceResponse { + identity_id: id_str, + balance: balance.to_string(), + } + }) + }) + }) + .collect(); + + serde_wasm_bindgen::to_value(&responses) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct TokenInfoResponse { + token_id: String, + is_frozen: bool, +} + +#[wasm_bindgen] +pub async fn get_identity_token_infos( + sdk: &WasmSdk, + identity_id: &str, + token_ids: Option>, + _limit: Option, + _offset: Option, +) -> Result { + use dash_sdk::platform::tokens::token_info::IdentityTokenInfosQuery; + use drive_proof_verifier::types::token_info::IdentityTokenInfos; + + // Parse identity ID + let identity_identifier = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // If no token IDs specified, we can't query (SDK requires specific token IDs) + let token_id_strings = token_ids.ok_or_else(|| JsError::new("token_ids are required for this query"))?; + + // Parse token IDs + let tokens: Result, _> = token_id_strings + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect(); + let tokens = tokens?; + + // Create query + let query = IdentityTokenInfosQuery { + identity_id: identity_identifier, + token_ids: tokens.clone(), + }; + + // Fetch token infos + let infos_result: IdentityTokenInfos = IdentityTokenInfo::fetch_many(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identity token infos: {}", e)))?; + + // Convert to response format + let responses: Vec = token_id_strings + .into_iter() + .filter_map(|id_str| { + let id = Identifier::from_string( + &id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + ).ok()?; + + infos_result.get(&id).and_then(|info_opt| { + info_opt.as_ref().map(|info| { + use dash_sdk::dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; + + // IdentityTokenInfo only contains frozen status + let is_frozen = match &info { + dash_sdk::dpp::tokens::info::IdentityTokenInfo::V0(v0) => v0.frozen(), + }; + + TokenInfoResponse { + token_id: id_str, + is_frozen, + } + }) + }) + }) + .collect(); + + serde_wasm_bindgen::to_value(&responses) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct IdentityTokenInfoResponse { + identity_id: String, + is_frozen: bool, +} + +#[wasm_bindgen] +pub async fn get_identities_token_infos( + sdk: &WasmSdk, + identity_ids: Vec, + token_id: &str, +) -> Result { + use dash_sdk::platform::tokens::token_info::IdentitiesTokenInfosQuery; + use drive_proof_verifier::types::token_info::IdentitiesTokenInfos; + + // Parse token ID + let token_identifier = Identifier::from_string( + token_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse identity IDs + let identities: Result, _> = identity_ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect(); + let identities = identities?; + + // Create query + let query = IdentitiesTokenInfosQuery { + identity_ids: identities.clone(), + token_id: token_identifier, + }; + + // Fetch token infos + let infos_result: IdentitiesTokenInfos = IdentityTokenInfo::fetch_many(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch identities token infos: {}", e)))?; + + // Convert to response format + let responses: Vec = identity_ids + .into_iter() + .filter_map(|id_str| { + let id = Identifier::from_string( + &id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + ).ok()?; + + infos_result.get(&id).and_then(|info_opt| { + info_opt.as_ref().map(|info| { + use dash_sdk::dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; + + // IdentityTokenInfo only contains frozen status + let is_frozen = match &info { + dash_sdk::dpp::tokens::info::IdentityTokenInfo::V0(v0) => v0.frozen(), + }; + + IdentityTokenInfoResponse { + identity_id: id_str, + is_frozen, + } + }) + }) + }) + .collect(); + + serde_wasm_bindgen::to_value(&responses) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct TokenStatusResponse { + token_id: String, + is_paused: bool, +} + +#[wasm_bindgen] +pub async fn get_token_statuses(sdk: &WasmSdk, token_ids: Vec) -> Result { + use drive_proof_verifier::types::token_status::TokenStatuses; + + // Parse token IDs + let tokens: Result, _> = token_ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect(); + let tokens = tokens?; + + // Fetch token statuses + let statuses_result: TokenStatuses = TokenStatus::fetch_many(sdk.as_ref(), tokens.clone()) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch token statuses: {}", e)))?; + + // Convert to response format + let responses: Vec = token_ids + .into_iter() + .filter_map(|id_str| { + let id = Identifier::from_string( + &id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + ).ok()?; + + statuses_result.get(&id).and_then(|status_opt| { + status_opt.as_ref().map(|status| { + TokenStatusResponse { + token_id: id_str, + is_paused: status.paused(), + } + }) + }) + }) + .collect(); + + serde_wasm_bindgen::to_value(&responses) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct TokenPriceResponse { + token_id: String, + current_price: String, + base_price: String, +} + +#[wasm_bindgen] +pub async fn get_token_direct_purchase_prices(sdk: &WasmSdk, token_ids: Vec) -> Result { + use drive_proof_verifier::types::TokenDirectPurchasePrices; + + // Parse token IDs + let tokens: Result, _> = token_ids + .iter() + .map(|id| Identifier::from_string( + id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )) + .collect(); + let tokens = tokens?; + + // Fetch token prices - use slice reference + let prices_result: TokenDirectPurchasePrices = TokenPricingSchedule::fetch_many(sdk.as_ref(), &tokens[..]) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch token direct purchase prices: {}", e)))?; + + // Convert to response format + let responses: Vec = token_ids + .into_iter() + .filter_map(|id_str| { + let id = Identifier::from_string( + &id_str, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + ).ok()?; + + prices_result.get(&id).and_then(|price_opt| { + price_opt.as_ref().map(|schedule| { + // Get prices based on the schedule type + let (base_price, current_price) = match &schedule { + dash_sdk::dpp::tokens::token_pricing_schedule::TokenPricingSchedule::SinglePrice(price) => { + (price.to_string(), price.to_string()) + }, + dash_sdk::dpp::tokens::token_pricing_schedule::TokenPricingSchedule::SetPrices(prices) => { + // Use first price as base, last as current + let base = prices.first_key_value() + .map(|(_, p)| p.to_string()) + .unwrap_or_else(|| "0".to_string()); + let current = prices.last_key_value() + .map(|(_, p)| p.to_string()) + .unwrap_or_else(|| "0".to_string()); + (base, current) + }, + }; + + TokenPriceResponse { + token_id: id_str, + current_price, + base_price, + } + }) + }) + }) + .collect(); + + serde_wasm_bindgen::to_value(&responses) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct TokenContractInfoResponse { + contract_id: String, + token_contract_position: u16, +} + +#[wasm_bindgen] +pub async fn get_token_contract_info(sdk: &WasmSdk, data_contract_id: &str) -> Result { + use dash_sdk::dpp::tokens::contract_info::TokenContractInfo; + use dash_sdk::platform::Fetch; + + // Parse contract ID + let contract_id = Identifier::from_string( + data_contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Fetch token contract info + let info_result = TokenContractInfo::fetch(sdk.as_ref(), contract_id) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch token contract info: {}", e)))?; + + if let Some(info) = info_result { + use dash_sdk::dpp::tokens::contract_info::v0::TokenContractInfoV0Accessors; + + // Extract fields based on the enum variant + let (contract_id, position) = match &info { + dash_sdk::dpp::tokens::contract_info::TokenContractInfo::V0(v0) => { + (v0.contract_id(), v0.token_contract_position()) + }, + }; + + let response = TokenContractInfoResponse { + contract_id: contract_id.to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58), + token_contract_position: position, + }; + + serde_wasm_bindgen::to_value(&response) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } else { + Ok(JsValue::NULL) + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct LastClaimResponse { + last_claim_timestamp_ms: u64, + last_claim_block_height: u64, +} + +#[wasm_bindgen] +pub async fn get_token_perpetual_distribution_last_claim( + sdk: &WasmSdk, + identity_id: &str, + token_id: &str, +) -> Result { + use dash_sdk::platform::query::TokenLastClaimQuery; + use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; + use dash_sdk::platform::Fetch; + + // Parse IDs + let identity_identifier = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let token_identifier = Identifier::from_string( + token_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Create query + let query = TokenLastClaimQuery { + token_id: token_identifier, + identity_id: identity_identifier, + }; + + // Fetch last claim info + let claim_result = RewardDistributionMoment::fetch(sdk.as_ref(), query) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch token perpetual distribution last claim: {}", e)))?; + + if let Some(moment) = claim_result { + // Extract timestamp and block height based on the moment type + // Since we need both timestamp and block height in the response, + // we'll return the moment value and type + let (last_claim_timestamp_ms, last_claim_block_height) = match moment { + dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment::BlockBasedMoment(height) => { + (0, height) // No timestamp available for block-based + }, + dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment::TimeBasedMoment(timestamp) => { + (timestamp, 0) // No block height available for time-based + }, + dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment::EpochBasedMoment(epoch) => { + (0, epoch as u64) // Convert epoch to u64, no timestamp available + }, + }; + + let response = LastClaimResponse { + last_claim_timestamp_ms, + last_claim_block_height, + }; + + serde_wasm_bindgen::to_value(&response) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } else { + Ok(JsValue::NULL) + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct TokenTotalSupplyResponse { + total_supply: String, +} + +#[wasm_bindgen] +pub async fn get_token_total_supply(sdk: &WasmSdk, token_id: &str) -> Result { + use dash_sdk::dpp::balances::total_single_token_balance::TotalSingleTokenBalance; + use dash_sdk::platform::Fetch; + + // Parse token ID + let token_identifier = Identifier::from_string( + token_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Fetch total supply + let supply_result = TotalSingleTokenBalance::fetch(sdk.as_ref(), token_identifier) + .await + .map_err(|e| JsError::new(&format!("Failed to fetch token total supply: {}", e)))?; + + if let Some(supply) = supply_result { + let response = TokenTotalSupplyResponse { + total_supply: supply.token_supply.to_string(), + }; + + serde_wasm_bindgen::to_value(&response) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) + } else { + Ok(JsValue::NULL) + } +} \ No newline at end of file diff --git a/packages/wasm-sdk/src/queries/voting.rs b/packages/wasm-sdk/src/queries/voting.rs new file mode 100644 index 0000000000..a391b3a8a3 --- /dev/null +++ b/packages/wasm-sdk/src/queries/voting.rs @@ -0,0 +1,392 @@ +use crate::sdk::WasmSdk; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; +use serde::Serialize; +use dash_sdk::platform::Identifier; +use dapi_grpc::platform::v0::{ + GetContestedResourcesRequest, GetContestedResourceVoteStateRequest, + GetContestedResourceVotersForIdentityRequest, GetContestedResourceIdentityVotesRequest, + GetVotePollsByEndDateRequest, + get_contested_resources_request::{self, GetContestedResourcesRequestV0}, + get_contested_resource_vote_state_request::{self, GetContestedResourceVoteStateRequestV0}, + get_contested_resource_voters_for_identity_request::{self, GetContestedResourceVotersForIdentityRequestV0}, + get_contested_resource_identity_votes_request::{self, GetContestedResourceIdentityVotesRequestV0}, + get_vote_polls_by_end_date_request::{self, GetVotePollsByEndDateRequestV0}, +}; +use dapi_grpc::platform::VersionedGrpcResponse; +use dash_sdk::RequestSettings; +use rs_dapi_client::DapiRequestExecutor; + + + +#[wasm_bindgen] +pub async fn get_contested_resources( + sdk: &WasmSdk, + document_type_name: &str, + data_contract_id: &str, + index_name: &str, + result_type: &str, + _allow_include_locked_and_abstaining_vote_tally: Option, + start_at_value: Option>, + limit: Option, + _offset: Option, + order_ascending: Option, +) -> Result { + // Parse contract ID + let contract_id = Identifier::from_string( + data_contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse result_type to get start_index_values + // For now, we'll use the standard "dash" parent domain + let start_index_values = vec!["dash".as_bytes().to_vec()]; + + // Create start_at_value_info if provided + let start_at_value_info = start_at_value.map(|bytes| { + get_contested_resources_request::get_contested_resources_request_v0::StartAtValueInfo { + start_value: bytes, + start_value_included: true, + } + }); + + // Create the gRPC request directly + let request = GetContestedResourcesRequest { + version: Some(get_contested_resources_request::Version::V0( + GetContestedResourcesRequestV0 { + contract_id: contract_id.to_vec(), + document_type_name: document_type_name.to_string(), + index_name: index_name.to_string(), + start_index_values, + end_index_values: vec![], + start_at_value_info, + count: limit, + order_ascending: order_ascending.unwrap_or(true), + prove: sdk.prove(), + }, + )), + }; + + // Execute the request + let response = sdk + .as_ref() + .execute(request, RequestSettings::default()) + .await + .map_err(|e| JsError::new(&format!("Failed to get contested resources: {}", e)))?; + + // For now, return a simple response structure + // The actual response parsing would require the ContestedResource type + let result = serde_json::json!({ + "contestedResources": [], + "metadata": { + "height": response.inner.metadata().ok().map(|m| m.height), + "coreChainLockedHeight": response.inner.metadata().ok().map(|m| m.core_chain_locked_height), + "timeMs": response.inner.metadata().ok().map(|m| m.time_ms), + "protocolVersion": response.inner.metadata().ok().map(|m| m.protocol_version), + } + }); + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + result.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + + + +#[wasm_bindgen] +pub async fn get_contested_resource_vote_state( + sdk: &WasmSdk, + data_contract_id: &str, + document_type_name: &str, + index_name: &str, + result_type: &str, + allow_include_locked_and_abstaining_vote_tally: Option, + start_at_identifier_info: Option, + count: Option, + _order_ascending: Option, +) -> Result { + // Parse contract ID + let contract_id = Identifier::from_string( + data_contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse start_at_identifier_info if provided + let start_at_identifier_info = if let Some(info_str) = start_at_identifier_info { + let info: serde_json::Value = serde_json::from_str(&info_str) + .map_err(|e| JsError::new(&format!("Invalid start_at_identifier_info JSON: {}", e)))?; + + if let (Some(start_id), Some(included)) = (info.get("startIdentifier"), info.get("startIdentifierIncluded")) { + let start_identifier = start_id.as_str() + .ok_or_else(|| JsError::new("startIdentifier must be a string"))? + .as_bytes() + .to_vec(); + let start_identifier_included = included.as_bool().unwrap_or(true); + + Some(get_contested_resource_vote_state_request::get_contested_resource_vote_state_request_v0::StartAtIdentifierInfo { + start_identifier, + start_identifier_included, + }) + } else { + None + } + } else { + None + }; + + // Parse result_type to determine resource path + let index_values = match result_type { + "documentTypeName" => vec!["dash".as_bytes().to_vec()], + _ => vec!["dash".as_bytes().to_vec()], // Default to dash + }; + + // Create the gRPC request directly + let request = GetContestedResourceVoteStateRequest { + version: Some(get_contested_resource_vote_state_request::Version::V0( + GetContestedResourceVoteStateRequestV0 { + contract_id: contract_id.to_vec(), + document_type_name: document_type_name.to_string(), + index_name: index_name.to_string(), + index_values, + result_type: if allow_include_locked_and_abstaining_vote_tally.unwrap_or(false) { 0 } else { 1 }, + allow_include_locked_and_abstaining_vote_tally: allow_include_locked_and_abstaining_vote_tally.unwrap_or(false), + start_at_identifier_info, + count, + prove: sdk.prove(), + }, + )), + }; + + // Execute the request + let response = sdk + .as_ref() + .execute(request, RequestSettings::default()) + .await + .map_err(|e| JsError::new(&format!("Failed to get contested resource vote state: {}", e)))?; + + // Return a simple response structure + let result = serde_json::json!({ + "contenders": [], + "abstainVoteTally": null, + "lockVoteTally": null, + "finishedVoteInfo": null, + "metadata": { + "height": response.inner.metadata().ok().map(|m| m.height), + "coreChainLockedHeight": response.inner.metadata().ok().map(|m| m.core_chain_locked_height), + "timeMs": response.inner.metadata().ok().map(|m| m.time_ms), + "protocolVersion": response.inner.metadata().ok().map(|m| m.protocol_version), + } + }); + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + result.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + + + +#[wasm_bindgen] +pub async fn get_contested_resource_voters_for_identity( + sdk: &WasmSdk, + data_contract_id: &str, + document_type_name: &str, + index_name: &str, + contestant_id: &str, + start_at_identifier_info: Option, + count: Option, + order_ascending: Option, +) -> Result { + // Parse IDs + let contract_id = Identifier::from_string( + data_contract_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + let contestant_identifier = Identifier::from_string( + contestant_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Parse start_at_identifier_info if provided + let start_at_identifier_info = if let Some(info_str) = start_at_identifier_info { + let info: serde_json::Value = serde_json::from_str(&info_str) + .map_err(|e| JsError::new(&format!("Invalid start_at_identifier_info JSON: {}", e)))?; + + if let (Some(start_id), Some(included)) = (info.get("startIdentifier"), info.get("startIdentifierIncluded")) { + let start_identifier = start_id.as_str() + .ok_or_else(|| JsError::new("startIdentifier must be a string"))? + .as_bytes() + .to_vec(); + let start_identifier_included = included.as_bool().unwrap_or(true); + + Some(get_contested_resource_voters_for_identity_request::get_contested_resource_voters_for_identity_request_v0::StartAtIdentifierInfo { + start_identifier, + start_identifier_included, + }) + } else { + None + } + } else { + None + }; + + // Create the gRPC request directly + let request = GetContestedResourceVotersForIdentityRequest { + version: Some(get_contested_resource_voters_for_identity_request::Version::V0( + GetContestedResourceVotersForIdentityRequestV0 { + contract_id: contract_id.to_vec(), + document_type_name: document_type_name.to_string(), + index_name: index_name.to_string(), + index_values: vec!["dash".as_bytes().to_vec()], // Default to dash domain + contestant_id: contestant_identifier.to_vec(), + start_at_identifier_info, + count, + order_ascending: order_ascending.unwrap_or(true), + prove: sdk.prove(), + }, + )), + }; + + // Execute the request + let response = sdk + .as_ref() + .execute(request, RequestSettings::default()) + .await + .map_err(|e| JsError::new(&format!("Failed to get contested resource voters: {}", e)))?; + + // Return a simple response structure + let result = serde_json::json!({ + "voters": [], + "finishedResults": false, + "metadata": { + "height": response.inner.metadata().ok().map(|m| m.height), + "coreChainLockedHeight": response.inner.metadata().ok().map(|m| m.core_chain_locked_height), + "timeMs": response.inner.metadata().ok().map(|m| m.time_ms), + "protocolVersion": response.inner.metadata().ok().map(|m| m.protocol_version), + } + }); + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + result.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + + + +#[wasm_bindgen] +pub async fn get_contested_resource_identity_votes( + sdk: &WasmSdk, + identity_id: &str, + limit: Option, + offset: Option, + order_ascending: Option, +) -> Result { + // Parse identity ID + let identity_identifier = Identifier::from_string( + identity_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + )?; + + // Create the gRPC request directly + let request = GetContestedResourceIdentityVotesRequest { + version: Some(get_contested_resource_identity_votes_request::Version::V0( + GetContestedResourceIdentityVotesRequestV0 { + identity_id: identity_identifier.to_vec(), + limit, + offset, + order_ascending: order_ascending.unwrap_or(true), + start_at_vote_poll_id_info: None, + prove: sdk.prove(), + }, + )), + }; + + // Execute the request + let response = sdk + .as_ref() + .execute(request, RequestSettings::default()) + .await + .map_err(|e| JsError::new(&format!("Failed to get contested resource identity votes: {}", e)))?; + + // Return a simple response structure + let result = serde_json::json!({ + "votes": [], + "finishedResults": false, + "metadata": { + "height": response.inner.metadata().ok().map(|m| m.height), + "coreChainLockedHeight": response.inner.metadata().ok().map(|m| m.core_chain_locked_height), + "timeMs": response.inner.metadata().ok().map(|m| m.time_ms), + "protocolVersion": response.inner.metadata().ok().map(|m| m.protocol_version), + } + }); + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + result.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} + + + +#[wasm_bindgen] +pub async fn get_vote_polls_by_end_date( + sdk: &WasmSdk, + start_time_ms: Option, + end_time_ms: Option, + limit: Option, + offset: Option, + order_ascending: Option, +) -> Result { + // Note: GetVotePollsByEndDateRequestV0 doesn't have start_at_poll_info, only offset + + // Create the gRPC request directly + let request = GetVotePollsByEndDateRequest { + version: Some(get_vote_polls_by_end_date_request::Version::V0( + GetVotePollsByEndDateRequestV0 { + start_time_info: start_time_ms.map(|ms| { + get_vote_polls_by_end_date_request::get_vote_polls_by_end_date_request_v0::StartAtTimeInfo { + start_time_ms: ms, + start_time_included: true, + } + }), + end_time_info: end_time_ms.map(|ms| { + get_vote_polls_by_end_date_request::get_vote_polls_by_end_date_request_v0::EndAtTimeInfo { + end_time_ms: ms, + end_time_included: true, + } + }), + limit, + offset, + ascending: order_ascending.unwrap_or(true), + prove: sdk.prove(), + }, + )), + }; + + // Execute the request + let response = sdk + .as_ref() + .execute(request, RequestSettings::default()) + .await + .map_err(|e| JsError::new(&format!("Failed to get vote polls by end date: {}", e)))?; + + // Return a simple response structure + let result = serde_json::json!({ + "votePollsByTimestamps": {}, + "finishedResults": false, + "metadata": { + "height": response.inner.metadata().ok().map(|m| m.height), + "coreChainLockedHeight": response.inner.metadata().ok().map(|m| m.core_chain_locked_height), + "timeMs": response.inner.metadata().ok().map(|m| m.time_ms), + "protocolVersion": response.inner.metadata().ok().map(|m| m.protocol_version), + } + }); + + // Use json_compatible serializer + let serializer = serde_wasm_bindgen::Serializer::json_compatible(); + result.serialize(&serializer) + .map_err(|e| JsError::new(&format!("Failed to serialize response: {}", e))) +} \ No newline at end of file diff --git a/packages/wasm-sdk/src/sdk.rs b/packages/wasm-sdk/src/sdk.rs index 5e75166acf..469df2ea3b 100644 --- a/packages/wasm-sdk/src/sdk.rs +++ b/packages/wasm-sdk/src/sdk.rs @@ -1,5 +1,4 @@ use crate::context_provider::WasmContext; -use crate::dpp::{DataContractWasm, IdentityWasm}; use dash_sdk::dpp::block::extended_epoch_info::ExtendedEpochInfo; use dash_sdk::dpp::dashcore::{Network, PrivateKey}; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; @@ -20,8 +19,9 @@ use std::fmt::Debug; use std::ops::{Deref, DerefMut}; use std::str::FromStr; use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::JsError; +use wasm_bindgen::{JsError, JsValue}; use web_sys::{console, js_sys}; +use serde_json; #[wasm_bindgen] pub struct WasmSdk(Sdk); @@ -45,6 +45,72 @@ impl From for WasmSdk { } } +#[wasm_bindgen] +impl WasmSdk { + pub fn version(&self) -> u32 { + self.0.version().protocol_version + } + + /// Get the network this SDK is configured for + pub(crate) fn network(&self) -> dash_sdk::dpp::dashcore::Network { + self.0.network + } + + /// Test serialization of different object types + #[wasm_bindgen(js_name = testSerialization)] + pub fn test_serialization(&self, test_type: &str) -> Result { + use serde_wasm_bindgen::to_value; + + match test_type { + "simple" => { + let simple = serde_json::json!({ + "type": "simple", + "value": "test" + }); + to_value(&simple).map_err(|e| JsValue::from_str(&format!("Simple serialization failed: {}", e))) + } + "complex" => { + let complex = serde_json::json!({ + "type": "complex", + "nested": { + "id": "123", + "number": 42, + "array": [1, 2, 3], + "null_value": null, + "bool_value": true + } + }); + to_value(&complex).map_err(|e| JsValue::from_str(&format!("Complex serialization failed: {}", e))) + } + "document" => { + // Simulate the exact structure we're trying to return + let doc = serde_json::json!({ + "type": "DocumentCreated", + "documentId": "8kGVyLBpghr4jBG7nJepKzyo3gyhPLitePxNSSGtbTwj", + "document": { + "id": "8kGVyLBpghr4jBG7nJepKzyo3gyhPLitePxNSSGtbTwj", + "ownerId": "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk", + "dataContractId": "9nzpvjVSStUrhkEs3eNHw2JYpcNoLh1MjmqW45QiyjSa", + "documentType": "post", + "revision": 1, + "createdAt": 1736300191752i64, + "updatedAt": 1736300191752i64, + } + }); + to_value(&doc).map_err(|e| JsValue::from_str(&format!("Document serialization failed: {}", e))) + } + _ => Err(JsValue::from_str("Unknown test type")) + } + } +} + +impl WasmSdk { + /// Clone the inner Sdk (not exposed to WASM) + pub(crate) fn inner_clone(&self) -> Sdk { + self.0.clone() + } +} + #[wasm_bindgen] pub struct WasmSdkBuilder(SdkBuilder); @@ -594,9 +660,9 @@ impl WasmSdkBuilder { use once_cell::sync::Lazy; use std::sync::Mutex; -static MAINNET_TRUSTED_CONTEXT: Lazy>> = +pub(crate) static MAINNET_TRUSTED_CONTEXT: Lazy>> = Lazy::new(|| Mutex::new(None)); -static TESTNET_TRUSTED_CONTEXT: Lazy>> = +pub(crate) static TESTNET_TRUSTED_CONTEXT: Lazy>> = Lazy::new(|| Mutex::new(None)); #[wasm_bindgen] @@ -635,34 +701,7 @@ pub async fn prefetch_trusted_quorums_testnet() -> Result<(), JsError> { Ok(()) } -#[wasm_bindgen] -pub async fn identity_fetch(sdk: &WasmSdk, base58_id: &str) -> Result { - let id = Identifier::from_string( - base58_id, - dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, - )?; - - Identity::fetch_by_identifier(sdk, id) - .await? - .ok_or_else(|| JsError::new("Identity not found")) - .map(Into::into) -} - -#[wasm_bindgen] -pub async fn data_contract_fetch( - sdk: &WasmSdk, - base58_id: &str, -) -> Result { - let id = Identifier::from_string( - base58_id, - dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, - )?; - - DataContract::fetch_by_identifier(sdk, id) - .await? - .ok_or_else(|| JsError::new("Data contract not found")) - .map(Into::into) -} +// Query functions have been moved to src/queries/ modules #[wasm_bindgen] pub async fn identity_put(sdk: &WasmSdk) { @@ -724,7 +763,7 @@ pub async fn docs_testing(sdk: &WasmSdk) { .expect("data contract not found"); let dcs = dc - .serialize_to_bytes_with_platform_version(sdk.version()) + .serialize_to_bytes_with_platform_version(sdk.0.version()) .expect("serialize data contract"); let query = DocumentQuery::new(dc.clone(), "asd").expect("create query"); @@ -737,7 +776,7 @@ pub async fn docs_testing(sdk: &WasmSdk) { .document_type_for_name("aaa") .expect("document type for name"); let doc_serialized = doc - .serialize(document_type, &dc, sdk.version()) + .serialize(document_type, &dc, sdk.0.version()) .expect("serialize document"); let msg = js_sys::JsString::from_str(&format!("{:?} {:?} ", dcs, doc_serialized)) diff --git a/packages/wasm-sdk/src/state_transitions/contracts/mod.rs b/packages/wasm-sdk/src/state_transitions/contracts/mod.rs new file mode 100644 index 0000000000..063af435be --- /dev/null +++ b/packages/wasm-sdk/src/state_transitions/contracts/mod.rs @@ -0,0 +1,321 @@ +use crate::sdk::WasmSdk; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dash_sdk::dpp::identity::{KeyType, Purpose}; +use dash_sdk::dpp::platform_value::{Identifier, string_encoding::Encoding}; +use dash_sdk::dpp::data_contract::DataContract; +use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::data_contract::conversion::json::DataContractJsonConversionMethodsV0; +use dash_sdk::dpp::state_transition::data_contract_update_transition::methods::DataContractUpdateTransitionMethodsV0; +use dash_sdk::dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; +use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; +use dash_sdk::platform::transition::put_contract::PutContract; +use dash_sdk::platform::Fetch; +use simple_signer::SingleKeySigner; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; +use js_sys; +use std::collections::BTreeMap; + +#[wasm_bindgen] +impl WasmSdk { + /// Create a new data contract on Dash Platform. + /// + /// # Arguments + /// + /// * `owner_id` - The identity ID that will own the contract + /// * `contract_definition` - JSON string containing the contract definition + /// * `private_key_wif` - The private key in WIF format for signing + /// * `key_id` - Optional key ID to use for signing (if None, will auto-select) + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the created contract + #[wasm_bindgen(js_name = contractCreate)] + pub async fn contract_create( + &self, + owner_id: String, + contract_definition: String, + private_key_wif: String, + key_id: Option, + ) -> Result { + let sdk = self.inner_clone(); + + // Parse owner identifier + let owner_identifier = Identifier::from_string(&owner_id, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid owner ID: {}", e)))?; + + // Parse contract definition JSON + let contract_json: serde_json::Value = serde_json::from_str(&contract_definition) + .map_err(|e| JsValue::from_str(&format!("Invalid contract definition JSON: {}", e)))?; + + // Fetch owner identity + let owner_identity = dash_sdk::platform::Identity::fetch(&sdk, owner_identifier) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch owner identity: {}", e)))? + .ok_or_else(|| JsValue::from_str("Owner identity not found"))?; + + // Parse private key and find matching public key + let private_key_bytes = dash_sdk::dpp::dashcore::PrivateKey::from_wif(&private_key_wif) + .map_err(|e| JsValue::from_str(&format!("Invalid private key: {}", e)))? + .inner + .secret_bytes(); + + let secp = dash_sdk::dpp::dashcore::secp256k1::Secp256k1::new(); + let secret_key = dash_sdk::dpp::dashcore::secp256k1::SecretKey::from_slice(&private_key_bytes) + .map_err(|e| JsValue::from_str(&format!("Invalid secret key: {}", e)))?; + let public_key = dash_sdk::dpp::dashcore::secp256k1::PublicKey::from_secret_key(&secp, &secret_key); + let public_key_bytes = public_key.serialize(); + + // Create public key hash using hash160 + let public_key_hash160 = { + use dash_sdk::dpp::dashcore::hashes::{Hash, hash160}; + hash160::Hash::hash(&public_key_bytes[..]).to_byte_array().to_vec() + }; + + // Find matching key - prioritize key_id if provided, otherwise find any authentication key + let matching_key = if let Some(requested_key_id) = key_id { + // Find specific key by ID + owner_identity.public_keys() + .get(&requested_key_id) + .filter(|key| { + key.purpose() == Purpose::AUTHENTICATION && + key.key_type() == KeyType::ECDSA_HASH160 && + key.data().as_slice() == public_key_hash160.as_slice() + }) + .ok_or_else(|| JsValue::from_str(&format!("Key with ID {} not found or doesn't match private key", requested_key_id)))? + .clone() + } else { + // Find any matching authentication key + owner_identity.public_keys().iter() + .find(|(_, key)| { + key.purpose() == Purpose::AUTHENTICATION && + key.key_type() == KeyType::ECDSA_HASH160 && + key.data().as_slice() == public_key_hash160.as_slice() + }) + .map(|(_, key)| key.clone()) + .ok_or_else(|| JsValue::from_str("No matching authentication key found for the provided private key"))? + }; + + // Create the data contract from JSON definition + let data_contract = DataContract::from_json( + contract_json, + true, // validate + sdk.version(), + ) + .map_err(|e| JsValue::from_str(&format!("Failed to create data contract from JSON: {}", e)))?; + + // Create signer + let signer = SingleKeySigner::from_string(&private_key_wif, dash_sdk::dpp::dashcore::Network::Testnet) + .map_err(|e| JsValue::from_str(&e))?; + + // Create and broadcast the contract + let created_contract = data_contract + .put_to_platform_and_wait_for_response(&sdk, matching_key, &signer, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to create contract: {}", e)))?; + + // Create JavaScript result object + let result_obj = js_sys::Object::new(); + + js_sys::Reflect::set(&result_obj, &JsValue::from_str("status"), &JsValue::from_str("success")) + .map_err(|e| JsValue::from_str(&format!("Failed to set status: {:?}", e)))?; + + // Convert contract ID to base58 + let contract_id_base58 = created_contract.id().to_string(Encoding::Base58); + js_sys::Reflect::set(&result_obj, &JsValue::from_str("contractId"), &JsValue::from_str(&contract_id_base58)) + .map_err(|e| JsValue::from_str(&format!("Failed to set contractId: {:?}", e)))?; + + js_sys::Reflect::set(&result_obj, &JsValue::from_str("ownerId"), &JsValue::from_str(&owner_id)) + .map_err(|e| JsValue::from_str(&format!("Failed to set ownerId: {:?}", e)))?; + + js_sys::Reflect::set(&result_obj, &JsValue::from_str("version"), &JsValue::from_f64(created_contract.version() as f64)) + .map_err(|e| JsValue::from_str(&format!("Failed to set version: {:?}", e)))?; + + // Add document type names + let schema = created_contract.document_types(); + let doc_types_array = js_sys::Array::new(); + for (doc_type_name, _) in schema.iter() { + doc_types_array.push(&JsValue::from_str(doc_type_name)); + } + js_sys::Reflect::set(&result_obj, &JsValue::from_str("documentTypes"), &doc_types_array) + .map_err(|e| JsValue::from_str(&format!("Failed to set documentTypes: {:?}", e)))?; + + js_sys::Reflect::set(&result_obj, &JsValue::from_str("message"), &JsValue::from_str("Data contract created successfully")) + .map_err(|e| JsValue::from_str(&format!("Failed to set message: {:?}", e)))?; + + Ok(result_obj.into()) + } + + /// Update an existing data contract on Dash Platform. + /// + /// # Arguments + /// + /// * `contract_id` - The ID of the contract to update + /// * `owner_id` - The identity ID that owns the contract + /// * `contract_updates` - JSON string containing the updated contract definition + /// * `private_key_wif` - The private key in WIF format for signing + /// * `key_id` - Optional key ID to use for signing (if None, will auto-select) + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the update result + #[wasm_bindgen(js_name = contractUpdate)] + pub async fn contract_update( + &self, + contract_id: String, + owner_id: String, + contract_updates: String, + private_key_wif: String, + key_id: Option, + ) -> Result { + let sdk = self.inner_clone(); + + // Parse identifiers + let contract_identifier = Identifier::from_string(&contract_id, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid contract ID: {}", e)))?; + + let owner_identifier = Identifier::from_string(&owner_id, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid owner ID: {}", e)))?; + + // Parse contract updates JSON + let updates_json: serde_json::Value = serde_json::from_str(&contract_updates) + .map_err(|e| JsValue::from_str(&format!("Invalid contract updates JSON: {}", e)))?; + + // Fetch the existing contract + let existing_contract = DataContract::fetch(&sdk, contract_identifier) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch contract: {}", e)))? + .ok_or_else(|| JsValue::from_str("Contract not found"))?; + + // Verify ownership + if existing_contract.owner_id() != owner_identifier { + return Err(JsValue::from_str("Identity does not own this contract")); + } + + // Fetch owner identity + let owner_identity = dash_sdk::platform::Identity::fetch(&sdk, owner_identifier) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch owner identity: {}", e)))? + .ok_or_else(|| JsValue::from_str("Owner identity not found"))?; + + // Parse private key and find matching public key + let private_key_bytes = dash_sdk::dpp::dashcore::PrivateKey::from_wif(&private_key_wif) + .map_err(|e| JsValue::from_str(&format!("Invalid private key: {}", e)))? + .inner + .secret_bytes(); + + let secp = dash_sdk::dpp::dashcore::secp256k1::Secp256k1::new(); + let secret_key = dash_sdk::dpp::dashcore::secp256k1::SecretKey::from_slice(&private_key_bytes) + .map_err(|e| JsValue::from_str(&format!("Invalid secret key: {}", e)))?; + let public_key = dash_sdk::dpp::dashcore::secp256k1::PublicKey::from_secret_key(&secp, &secret_key); + let public_key_bytes = public_key.serialize(); + + // Create public key hash using hash160 + let public_key_hash160 = { + use dash_sdk::dpp::dashcore::hashes::{Hash, hash160}; + hash160::Hash::hash(&public_key_bytes[..]).to_byte_array().to_vec() + }; + + // Find matching key - prioritize key_id if provided, otherwise find any authentication key + let matching_key = if let Some(requested_key_id) = key_id { + // Find specific key by ID + owner_identity.public_keys() + .get(&requested_key_id) + .filter(|key| { + key.purpose() == Purpose::AUTHENTICATION && + key.key_type() == KeyType::ECDSA_HASH160 && + key.data().as_slice() == public_key_hash160.as_slice() + }) + .ok_or_else(|| JsValue::from_str(&format!("Key with ID {} not found or doesn't match private key", requested_key_id)))? + .clone() + } else { + // Find any matching authentication key + owner_identity.public_keys().iter() + .find(|(_, key)| { + key.purpose() == Purpose::AUTHENTICATION && + key.key_type() == KeyType::ECDSA_HASH160 && + key.data().as_slice() == public_key_hash160.as_slice() + }) + .map(|(_, key)| key.clone()) + .ok_or_else(|| JsValue::from_str("No matching authentication key found for the provided private key"))? + }; + + // Create updated contract from JSON definition + // Note: The updates should be a complete contract definition with incremented version + let updated_contract = DataContract::from_json( + updates_json, + true, // validate + sdk.version(), + ) + .map_err(|e| JsValue::from_str(&format!("Failed to create updated contract from JSON: {}", e)))?; + + // Verify the version was incremented + if updated_contract.version() <= existing_contract.version() { + return Err(JsValue::from_str(&format!( + "Contract version must be incremented. Current: {}, Provided: {}", + existing_contract.version(), + updated_contract.version() + ))); + } + + // Get identity nonce + let identity_nonce = sdk + .get_identity_nonce(owner_identifier, true, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to get identity nonce: {}", e)))?; + + // Create partial identity for signing + let partial_identity = dash_sdk::dpp::identity::PartialIdentity { + id: owner_identifier, + loaded_public_keys: BTreeMap::from([(matching_key.id(), matching_key.clone())]), + balance: None, + revision: None, + not_found_public_keys: Default::default(), + }; + + // Create signer + let signer = SingleKeySigner::from_string(&private_key_wif, dash_sdk::dpp::dashcore::Network::Testnet) + .map_err(|e| JsValue::from_str(&e))?; + + // Create the update transition + let state_transition = DataContractUpdateTransition::new_from_data_contract( + updated_contract.clone(), + &partial_identity, + matching_key.id(), + identity_nonce, + dash_sdk::dpp::prelude::UserFeeIncrease::default(), + &signer, + sdk.version(), + None, + ) + .map_err(|e| JsValue::from_str(&format!("Failed to create update transition: {}", e)))?; + + // Broadcast the transition + use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; + let result = state_transition + .broadcast_and_wait::(&sdk, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to broadcast update: {}", e)))?; + + // Extract updated contract from result + let updated_version = match result { + StateTransitionProofResult::VerifiedDataContract(contract) => contract.version(), + _ => updated_contract.version(), + }; + + // Create JavaScript result object + let result_obj = js_sys::Object::new(); + + js_sys::Reflect::set(&result_obj, &JsValue::from_str("status"), &JsValue::from_str("success")) + .map_err(|e| JsValue::from_str(&format!("Failed to set status: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("contractId"), &JsValue::from_str(&contract_id)) + .map_err(|e| JsValue::from_str(&format!("Failed to set contractId: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("version"), &JsValue::from_f64(updated_version as f64)) + .map_err(|e| JsValue::from_str(&format!("Failed to set version: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("message"), &JsValue::from_str("Data contract updated successfully")) + .map_err(|e| JsValue::from_str(&format!("Failed to set message: {:?}", e)))?; + + Ok(result_obj.into()) + } +} \ No newline at end of file diff --git a/packages/wasm-sdk/src/state_transitions/documents.rs b/packages/wasm-sdk/src/state_transitions/documents.rs deleted file mode 100644 index 06f33917d5..0000000000 --- a/packages/wasm-sdk/src/state_transitions/documents.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::error::to_js_error; -use dash_sdk::dpp::identity::KeyID; -use dash_sdk::dpp::serialization::PlatformSerializable; -use dash_sdk::dpp::state_transition::StateTransition; -use dash_sdk::dpp::state_transition::batch_transition::batched_transition::document_base_transition::v0::DocumentBaseTransitionV0; -use dash_sdk::dpp::state_transition::batch_transition::batched_transition::document_base_transition::DocumentBaseTransition; -use dash_sdk::dpp::state_transition::batch_transition::batched_transition::document_create_transition::DocumentCreateTransitionV0; -use dash_sdk::dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; -use dash_sdk::dpp::state_transition::batch_transition::{ - document_create_transition::DocumentCreateTransition, BatchTransition, BatchTransitionV0, -}; -use wasm_bindgen::prelude::*; -use web_sys::js_sys::{Number, Uint8Array}; - -#[wasm_bindgen] -pub fn create_document( - _document: JsValue, - _identity_contract_nonce: Number, - signature_public_key_id: Number, -) -> Result { - // TODO: Extract document fields from JsValue - - let _base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Default::default(), - identity_contract_nonce: 1, - document_type_name: "".to_string(), - data_contract_id: Default::default(), - }); - - let transition = DocumentCreateTransition::V0(DocumentCreateTransitionV0 { - base: Default::default(), - entropy: [0; 32], - data: Default::default(), - prefunded_voting_balance: None, - }); - - create_batch_transition( - vec![DocumentTransition::Create(transition)], - signature_public_key_id, - ) -} - -fn create_batch_transition( - transitions: Vec, - signature_public_key_id: Number, -) -> Result { - let signature_public_key_id = signature_public_key_id - .as_f64() - .ok_or_else(|| JsError::new("public_key_id must be a number"))?; - - // boundary checks - let signature_public_key_id = if signature_public_key_id.is_finite() - && signature_public_key_id >= KeyID::MIN as f64 - && signature_public_key_id <= (KeyID::MAX as f64) - { - signature_public_key_id as KeyID - } else { - return Err(JsError::new(&format!( - "signature_public_key_id {} out of valid range", - signature_public_key_id - ))); - }; - - let document_batch_transition = BatchTransition::V0(BatchTransitionV0 { - owner_id: Default::default(), - transitions, - user_fee_increase: 0, - signature_public_key_id, - signature: Default::default(), - }); - - let state_transition: StateTransition = document_batch_transition.into(); - - state_transition - .serialize_to_bytes() - .map_err(to_js_error) - .map(|bytes| Uint8Array::from(bytes.as_slice())) -} diff --git a/packages/wasm-sdk/src/state_transitions/documents/mod.rs b/packages/wasm-sdk/src/state_transitions/documents/mod.rs new file mode 100644 index 0000000000..651af2f7e5 --- /dev/null +++ b/packages/wasm-sdk/src/state_transitions/documents/mod.rs @@ -0,0 +1,1320 @@ +//! Document state transition implementations for the WASM SDK. +//! +//! This module provides WASM bindings for document operations like create, replace, delete, etc. + +use crate::sdk::{WasmSdk, MAINNET_TRUSTED_CONTEXT, TESTNET_TRUSTED_CONTEXT}; +use dash_sdk::dpp::dashcore::PrivateKey; +use dash_sdk::dpp::identity::{IdentityPublicKey, KeyType, Purpose}; +use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::platform_value::{Identifier, string_encoding::Encoding, Value as PlatformValue}; +use dash_sdk::dpp::prelude::UserFeeIncrease; +use dash_sdk::dpp::state_transition::batch_transition::BatchTransition; +use dash_sdk::dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; +use dash_sdk::dpp::fee::Credits; +use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; +use dash_sdk::dpp::state_transition::StateTransition; +use dash_sdk::dpp::document::{Document, DocumentV0Getters, DocumentV0}; +use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::data_contract::document_type::methods::DocumentTypeV0Methods; +use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; +use dash_sdk::platform::Fetch; +use dash_sdk::dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; +use simple_signer::SingleKeySigner; +use serde_json; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; +use web_sys; +use js_sys; + +// WasmSigner has been replaced with SingleKeySigner from simple-signer crate + +// Helper functions for document operations +impl WasmSdk { + /// Parse identifier strings into Identifier objects + fn parse_identifiers( + contract_id_str: &str, + owner_id_str: &str, + doc_id_str: Option<&str>, + ) -> Result<(Identifier, Identifier, Option), JsValue> { + let contract_id = Identifier::from_string(contract_id_str, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid contract ID: {}", e)))?; + + let owner_id = Identifier::from_string(owner_id_str, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid owner ID: {}", e)))?; + + let doc_id = doc_id_str + .map(|id| Identifier::from_string(id, Encoding::Base58)) + .transpose() + .map_err(|e| JsValue::from_str(&format!("Invalid document ID: {}", e)))?; + + Ok((contract_id, owner_id, doc_id)) + } + + /// Fetch and cache data contract + async fn fetch_and_cache_contract( + &self, + contract_id: Identifier, + ) -> Result { + // Fetch from network + let sdk = self.inner_clone(); + let contract = dash_sdk::platform::DataContract::fetch(&sdk, contract_id) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch data contract: {}", e)))? + .ok_or_else(|| JsValue::from_str("Data contract not found"))?; + + // Cache the contract in the trusted context + if self.network() == dash_sdk::dpp::dashcore::Network::Testnet { + if let Some(ref context) = *TESTNET_TRUSTED_CONTEXT.lock().unwrap() { + context.add_known_contract(contract.clone()); + } + } else if self.network() == dash_sdk::dpp::dashcore::Network::Dash { + if let Some(ref context) = *MAINNET_TRUSTED_CONTEXT.lock().unwrap() { + context.add_known_contract(contract.clone()); + } + } + + Ok(contract) + } + + /// Find authentication key matching the provided private key + fn find_authentication_key<'a>( + identity: &'a dash_sdk::platform::Identity, + private_key_wif: &str, + ) -> Result<(u32, &'a IdentityPublicKey), JsValue> { + // Derive public key from private key + let private_key = PrivateKey::from_wif(private_key_wif) + .map_err(|e| JsValue::from_str(&format!("Invalid private key: {}", e)))?; + + let secp = dash_sdk::dpp::dashcore::secp256k1::Secp256k1::new(); + let private_key_bytes = private_key.inner.secret_bytes(); + let secret_key = dash_sdk::dpp::dashcore::secp256k1::SecretKey::from_slice(&private_key_bytes) + .map_err(|e| JsValue::from_str(&format!("Invalid private key: {}", e)))?; + let public_key = dash_sdk::dpp::dashcore::secp256k1::PublicKey::from_secret_key(&secp, &secret_key); + let public_key_bytes = public_key.serialize().to_vec(); + + // Calculate hash160 for ECDSA_HASH160 keys + let public_key_hash160 = { + use dash_sdk::dpp::dashcore::hashes::{Hash, hash160}; + hash160::Hash::hash(&public_key_bytes).to_byte_array().to_vec() + }; + + // Log debug information + web_sys::console::log_1(&JsValue::from_str(&format!( + "Looking for authentication key with public key: {}", + hex::encode(&public_key_bytes) + ))); + web_sys::console::log_1(&JsValue::from_str(&format!( + "Public key hash160: {}", + hex::encode(&public_key_hash160) + ))); + + // Find matching authentication key + let (key_id, matching_key) = identity + .public_keys() + .iter() + .find(|(_, key)| { + if key.purpose() != Purpose::AUTHENTICATION { + return false; + } + + let matches = match key.key_type() { + KeyType::ECDSA_SECP256K1 => { + key.data().as_slice() == public_key_bytes.as_slice() + }, + KeyType::ECDSA_HASH160 => { + key.data().as_slice() == public_key_hash160.as_slice() + }, + _ => false + }; + + if matches { + web_sys::console::log_1(&JsValue::from_str(&format!( + "Found matching key: ID={}, Type={:?}", + key.id(), + key.key_type() + ))); + } + + matches + }) + .ok_or_else(|| JsValue::from_str("No matching authentication key found for the provided private key"))?; + + Ok((*key_id, matching_key)) + } + + /// Create a signer from WIF private key + fn create_signer_from_wif( + private_key_wif: &str, + network: dash_sdk::dpp::dashcore::Network, + ) -> Result { + SingleKeySigner::from_string(private_key_wif, network) + .map_err(|e| JsValue::from_str(&e)) + } + + /// Build JavaScript result object for state transition results + fn build_js_result_object( + transition_type: &str, + document_id: &str, + additional_fields: Vec<(&str, JsValue)>, + ) -> Result { + let result_obj = js_sys::Object::new(); + + // Set type + js_sys::Reflect::set( + &result_obj, + &JsValue::from_str("type"), + &JsValue::from_str(transition_type), + ).map_err(|_| JsValue::from_str("Failed to set type"))?; + + // Set document ID + js_sys::Reflect::set( + &result_obj, + &JsValue::from_str("documentId"), + &JsValue::from_str(document_id), + ).map_err(|_| JsValue::from_str("Failed to set documentId"))?; + + // Set additional fields + for (key, value) in additional_fields { + js_sys::Reflect::set( + &result_obj, + &JsValue::from_str(key), + &value, + ).map_err(|_| JsValue::from_str(&format!("Failed to set {}", key)))?; + } + + Ok(result_obj.into()) + } +} + +#[wasm_bindgen] +impl WasmSdk { + /// Create a new document on the platform. + /// + /// # Arguments + /// + /// * `data_contract_id` - The ID of the data contract + /// * `document_type` - The name of the document type + /// * `owner_id` - The identity ID of the document owner + /// * `document_data` - The document data as a JSON string + /// * `entropy` - 32 bytes of entropy for the state transition (hex string) + /// * `private_key_wif` - The private key in WIF format for signing + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the created document + #[wasm_bindgen(js_name = documentCreate)] + pub async fn document_create( + &self, + data_contract_id: String, + document_type: String, + owner_id: String, + document_data: String, + entropy: String, + private_key_wif: String, + ) -> Result { + let sdk = self.inner_clone(); + + // Parse identifiers + let (contract_id, owner_identifier, _) = Self::parse_identifiers(&data_contract_id, &owner_id, None)?; + + // Parse entropy + let entropy_bytes = hex::decode(&entropy) + .map_err(|e| JsValue::from_str(&format!("Invalid entropy hex: {}", e)))?; + + if entropy_bytes.len() != 32 { + return Err(JsValue::from_str("Entropy must be exactly 32 bytes")); + } + + let mut entropy_array = [0u8; 32]; + entropy_array.copy_from_slice(&entropy_bytes); + + // Parse document data + let document_data_value: serde_json::Value = serde_json::from_str(&document_data) + .map_err(|e| JsValue::from_str(&format!("Invalid JSON document data: {}", e)))?; + + // Fetch and cache the data contract + let data_contract = self.fetch_and_cache_contract(contract_id).await?; + + // Get document type + let document_type_result = data_contract.document_type_for_name(&document_type); + let document_type_ref = document_type_result + .map_err(|e| JsValue::from_str(&format!("Document type '{}' not found: {}", document_type, e)))?; + + // Convert JSON data to platform value + let document_data_platform_value: PlatformValue = document_data_value.into(); + + // Create the document directly using the document type's method + let platform_version = sdk.version(); + let document = document_type_ref.create_document_from_data( + document_data_platform_value, + owner_identifier, + 0, // block_time (will be set by platform) + 0, // core_block_height (will be set by platform) + entropy_array, + platform_version, + ).map_err(|e| JsValue::from_str(&format!("Failed to create document: {}", e)))?; + + // Fetch the identity to get the correct key + let identity = dash_sdk::platform::Identity::fetch(&sdk, owner_identifier) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch identity: {}", e)))? + .ok_or_else(|| JsValue::from_str("Identity not found"))?; + + // Get identity contract nonce + let identity_contract_nonce = sdk + .get_identity_contract_nonce(owner_identifier, contract_id, true, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch nonce: {}", e)))?; + + // Find matching authentication key and create signer + let (_, matching_key) = Self::find_authentication_key(&identity, &private_key_wif)?; + let signer = Self::create_signer_from_wif(&private_key_wif, self.network())?; + let public_key = matching_key.clone(); + + // Create the state transition + let state_transition = BatchTransition::new_document_creation_transition_from_document( + document.clone(), + document_type_ref, + entropy_array, + &public_key, + identity_contract_nonce, + UserFeeIncrease::default(), + None, // token_payment_info + &signer, + platform_version, + None, // state_transition_creation_options + ).map_err(|e| JsValue::from_str(&format!("Failed to create document transition: {}", e)))?; + + // Broadcast the transition + let proof_result = state_transition + .broadcast_and_wait::(&sdk, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to broadcast transition: {}", e)))?; + + // Log the result for debugging + web_sys::console::log_1(&JsValue::from_str("Processing state transition proof result")); + + // Convert result to JsValue based on the type + match proof_result { + StateTransitionProofResult::VerifiedDocuments(documents) => { + web_sys::console::log_1(&JsValue::from_str(&format!( + "Documents in result: {}", + documents.len() + ))); + + // Try to find the created document + for (doc_id, maybe_doc) in documents.iter() { + web_sys::console::log_1(&JsValue::from_str(&format!( + "Document ID: {}, Document present: {}", + doc_id.to_string(Encoding::Base58), + maybe_doc.is_some() + ))); + } + + if let Some((doc_id, maybe_doc)) = documents.into_iter().next() { + if let Some(doc) = maybe_doc { + // Create JsValue directly instead of using serde_wasm_bindgen + let js_result = js_sys::Object::new(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("type"), + &JsValue::from_str("DocumentCreated"), + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("documentId"), + &JsValue::from_str(&doc_id.to_string(Encoding::Base58)), + ).unwrap(); + + // Create document object + let js_document = js_sys::Object::new(); + + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("id"), + &JsValue::from_str(&doc.id().to_string(Encoding::Base58)), + ).unwrap(); + + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("ownerId"), + &JsValue::from_str(&doc.owner_id().to_string(Encoding::Base58)), + ).unwrap(); + + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("dataContractId"), + &JsValue::from_str(&data_contract_id), + ).unwrap(); + + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("documentType"), + &JsValue::from_str(&document_type), + ).unwrap(); + + if let Some(revision) = doc.revision() { + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("revision"), + &JsValue::from_f64(revision as f64), + ).unwrap(); + } + + if let Some(created_at) = doc.created_at() { + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("createdAt"), + &JsValue::from_f64(created_at as f64), + ).unwrap(); + } + + if let Some(updated_at) = doc.updated_at() { + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("updatedAt"), + &JsValue::from_f64(updated_at as f64), + ).unwrap(); + } + + // Add document properties in a "data" field (like DocumentResponse does) + let data_obj = js_sys::Object::new(); + let properties = doc.properties(); + + for (key, value) in properties { + // Convert platform Value to JSON value first, then to JsValue + if let Ok(json_value) = serde_json::to_value(value) { + if let Ok(js_value) = serde_wasm_bindgen::to_value(&json_value) { + js_sys::Reflect::set( + &data_obj, + &JsValue::from_str(key), + &js_value, + ).unwrap(); + } + } + } + + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("data"), + &data_obj, + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("document"), + &js_document, + ).unwrap(); + + web_sys::console::log_1(&JsValue::from_str("Document created successfully, returning JS object")); + + Ok(js_result.into()) + } else { + // Document was created but not included in response (this is normal) + let js_result = js_sys::Object::new(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("type"), + &JsValue::from_str("DocumentCreated"), + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("documentId"), + &JsValue::from_str(&doc_id.to_string(Encoding::Base58)), + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("message"), + &JsValue::from_str("Document created successfully"), + ).unwrap(); + + Ok(js_result.into()) + } + } else { + // No documents in result, but transition was successful + let js_result = js_sys::Object::new(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("type"), + &JsValue::from_str("DocumentCreated"), + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("documentId"), + &JsValue::from_str(&document.id().to_string(Encoding::Base58)), + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("message"), + &JsValue::from_str("Document created successfully"), + ).unwrap(); + + Ok(js_result.into()) + } + } + _ => { + // For other result types, just indicate success + let js_result = js_sys::Object::new(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("type"), + &JsValue::from_str("DocumentCreated"), + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("documentId"), + &JsValue::from_str(&document.id().to_string(Encoding::Base58)), + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("message"), + &JsValue::from_str("Document created successfully"), + ).unwrap(); + + Ok(js_result.into()) + } + } + } + + /// Replace an existing document on the platform. + /// + /// # Arguments + /// + /// * `data_contract_id` - The ID of the data contract + /// * `document_type` - The name of the document type + /// * `document_id` - The ID of the document to replace + /// * `owner_id` - The identity ID of the document owner + /// * `document_data` - The new document data as a JSON string + /// * `revision` - The current revision of the document + /// * `private_key_wif` - The private key in WIF format for signing + /// * `key_id` - The key ID to use for signing + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the replaced document + #[wasm_bindgen(js_name = documentReplace)] + pub async fn document_replace( + &self, + data_contract_id: String, + document_type: String, + document_id: String, + owner_id: String, + document_data: String, + revision: u64, + private_key_wif: String, + _key_id: u32, + ) -> Result { + let sdk = self.inner_clone(); + + // Parse identifiers + let (contract_id, owner_identifier, doc_id) = Self::parse_identifiers( + &data_contract_id, + &owner_id, + Some(&document_id) + )?; + let doc_id = doc_id.unwrap(); + + // Parse document data + let document_data_value: serde_json::Value = serde_json::from_str(&document_data) + .map_err(|e| JsValue::from_str(&format!("Invalid JSON document data: {}", e)))?; + + // Fetch and cache the data contract + let data_contract = self.fetch_and_cache_contract(contract_id).await?; + + // Get document type + let document_type_result = data_contract.document_type_for_name(&document_type); + let document_type_ref = document_type_result + .map_err(|e| JsValue::from_str(&format!("Document type '{}' not found: {}", document_type, e)))?; + + // Convert JSON data to platform value + let document_data_platform_value: PlatformValue = document_data_value.into(); + + // Create the document using the DocumentV0 constructor + let platform_version = sdk.version(); + let document = Document::V0(DocumentV0 { + id: doc_id, + owner_id: owner_identifier, + properties: document_data_platform_value + .into_btree_string_map() + .map_err(|e| JsValue::from_str(&format!("Failed to convert document data: {}", e)))?, + revision: Some(revision + 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, + }); + + // Fetch the identity to get the correct key + let identity = dash_sdk::platform::Identity::fetch(&sdk, owner_identifier) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch identity: {}", e)))? + .ok_or_else(|| JsValue::from_str("Identity not found"))?; + + // Get identity contract nonce + let identity_contract_nonce = sdk + .get_identity_contract_nonce(owner_identifier, contract_id, true, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch nonce: {}", e)))?; + + // Find matching authentication key and create signer + let (_, matching_key) = Self::find_authentication_key(&identity, &private_key_wif)?; + let public_key = matching_key.clone(); + let signer = Self::create_signer_from_wif(&private_key_wif, self.network())?; + + // Create the state transition + let state_transition = BatchTransition::new_document_replacement_transition_from_document( + document, + document_type_ref, + &public_key, + identity_contract_nonce, + UserFeeIncrease::default(), + None, // token_payment_info + &signer, + platform_version, + None, // state_transition_creation_options + ).map_err(|e| JsValue::from_str(&format!("Failed to create document replace transition: {}", e)))?; + + // Broadcast the transition + let proof_result = state_transition + .broadcast_and_wait::(&sdk, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to broadcast transition: {}", e)))?; + + // Convert result to JsValue based on the type + match proof_result { + StateTransitionProofResult::VerifiedDocuments(documents) => { + if let Some((doc_id, maybe_doc)) = documents.into_iter().next() { + if let Some(doc) = maybe_doc { + // Create JsValue directly instead of using serde_wasm_bindgen + let js_result = js_sys::Object::new(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("type"), + &JsValue::from_str("DocumentReplaced"), + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("documentId"), + &JsValue::from_str(&doc_id.to_string(Encoding::Base58)), + ).unwrap(); + + // Create document object + let js_document = js_sys::Object::new(); + + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("id"), + &JsValue::from_str(&doc.id().to_string(Encoding::Base58)), + ).unwrap(); + + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("ownerId"), + &JsValue::from_str(&doc.owner_id().to_string(Encoding::Base58)), + ).unwrap(); + + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("dataContractId"), + &JsValue::from_str(&data_contract_id), + ).unwrap(); + + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("documentType"), + &JsValue::from_str(&document_type), + ).unwrap(); + + if let Some(revision) = doc.revision() { + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("revision"), + &JsValue::from_f64(revision as f64), + ).unwrap(); + } + + if let Some(created_at) = doc.created_at() { + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("createdAt"), + &JsValue::from_f64(created_at as f64), + ).unwrap(); + } + + if let Some(updated_at) = doc.updated_at() { + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("updatedAt"), + &JsValue::from_f64(updated_at as f64), + ).unwrap(); + } + + // Add document properties in a "data" field (like DocumentResponse does) + let data_obj = js_sys::Object::new(); + let properties = doc.properties(); + + for (key, value) in properties { + // Convert platform Value to JSON value first, then to JsValue + if let Ok(json_value) = serde_json::to_value(value) { + if let Ok(js_value) = serde_wasm_bindgen::to_value(&json_value) { + js_sys::Reflect::set( + &data_obj, + &JsValue::from_str(key), + &js_value, + ).unwrap(); + } + } + } + + js_sys::Reflect::set( + &js_document, + &JsValue::from_str("data"), + &data_obj, + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("document"), + &js_document, + ).unwrap(); + + web_sys::console::log_1(&JsValue::from_str("Document replaced successfully")); + + Ok(js_result.into()) + } else { + // Document was replaced but not included in response + let js_result = js_sys::Object::new(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("type"), + &JsValue::from_str("DocumentReplaced"), + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("documentId"), + &JsValue::from_str(&doc_id.to_string(Encoding::Base58)), + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("message"), + &JsValue::from_str("Document replaced successfully"), + ).unwrap(); + + Ok(js_result.into()) + } + } else { + // No documents in result, but transition was successful + let js_result = js_sys::Object::new(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("type"), + &JsValue::from_str("DocumentReplaced"), + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("documentId"), + &JsValue::from_str(&document_id), + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("message"), + &JsValue::from_str("Document replaced successfully"), + ).unwrap(); + + Ok(js_result.into()) + } + } + _ => { + // For other result types, just indicate success + let js_result = js_sys::Object::new(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("type"), + &JsValue::from_str("DocumentReplaced"), + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("documentId"), + &JsValue::from_str(&document_id), + ).unwrap(); + + js_sys::Reflect::set( + &js_result, + &JsValue::from_str("message"), + &JsValue::from_str("Document replaced successfully"), + ).unwrap(); + + Ok(js_result.into()) + } + } + } + + /// Delete a document from the platform. + /// + /// # Arguments + /// + /// * `data_contract_id` - The ID of the data contract + /// * `document_type` - The name of the document type + /// * `document_id` - The ID of the document to delete + /// * `owner_id` - The identity ID of the document owner + /// * `private_key_wif` - The private key in WIF format for signing + /// * `key_id` - The key ID to use for signing + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue confirming deletion + #[wasm_bindgen(js_name = documentDelete)] + pub async fn document_delete( + &self, + data_contract_id: String, + document_type: String, + document_id: String, + owner_id: String, + private_key_wif: String, + _key_id: u32, + ) -> Result { + let sdk = self.inner_clone(); + + // Parse identifiers + let (contract_id, owner_identifier, doc_id) = Self::parse_identifiers( + &data_contract_id, + &owner_id, + Some(&document_id) + )?; + let doc_id = doc_id.unwrap(); + + // Fetch and cache the data contract + let data_contract = self.fetch_and_cache_contract(contract_id).await?; + + // Get document type + let document_type_result = data_contract.document_type_for_name(&document_type); + let document_type_ref = document_type_result + .map_err(|e| JsValue::from_str(&format!("Document type '{}' not found: {}", document_type, e)))?; + + // Fetch the document to get its current revision + use dash_sdk::platform::DocumentQuery; + + let query = DocumentQuery::new_with_data_contract_id( + &sdk, + contract_id, + &document_type, + ) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to create document query: {}", e)))? + .with_document_id(&doc_id); + + let existing_doc = dash_sdk::platform::Document::fetch(&sdk, query) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch document: {}", e)))? + .ok_or_else(|| JsValue::from_str("Document not found"))?; + + let current_revision = existing_doc.revision().unwrap_or(0); + + // Fetch the identity to get the correct key + let identity = dash_sdk::platform::Identity::fetch(&sdk, owner_identifier) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch identity: {}", e)))? + .ok_or_else(|| JsValue::from_str("Identity not found"))?; + + // Get identity contract nonce + let identity_contract_nonce = sdk + .get_identity_contract_nonce(owner_identifier, contract_id, true, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch nonce: {}", e)))?; + + // Find matching authentication key and create signer + let (_, matching_key) = Self::find_authentication_key(&identity, &private_key_wif)?; + let signer = Self::create_signer_from_wif(&private_key_wif, self.network())?; + + // Create a document for deletion with the correct revision + let document = Document::V0(DocumentV0 { + id: doc_id, + owner_id: owner_identifier, + properties: Default::default(), + revision: Some(current_revision), // Use the actual current revision + 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, + }); + + // Create a delete transition + let transition = BatchTransition::new_document_deletion_transition_from_document( + document, + document_type_ref, + matching_key, + identity_contract_nonce, + UserFeeIncrease::default(), + None, // token_payment_info + &signer, + sdk.version(), + None, // options + ) + .map_err(|e| JsValue::from_str(&format!("Failed to create transition: {}", e)))?; + + // The transition is already signed, convert to StateTransition + let state_transition: StateTransition = transition.into(); + + // Broadcast the state transition + state_transition + .broadcast(&sdk, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to broadcast: {}", e)))?; + + // Return the result with document ID + Self::build_js_result_object( + "DocumentDeleted", + &document_id, + vec![("deleted", JsValue::from_bool(true))], + ) + } + + /// Transfer document ownership to another identity. + /// + /// # Arguments + /// + /// * `data_contract_id` - The ID of the data contract + /// * `document_type` - The name of the document type + /// * `document_id` - The ID of the document to transfer + /// * `owner_id` - The current owner's identity ID + /// * `recipient_id` - The new owner's identity ID + /// * `private_key_wif` - The private key in WIF format for signing + /// * `key_id` - The key ID to use for signing + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the transfer result + #[wasm_bindgen(js_name = documentTransfer)] + pub async fn document_transfer( + &self, + data_contract_id: String, + document_type: String, + document_id: String, + owner_id: String, + recipient_id: String, + private_key_wif: String, + _key_id: u32, + ) -> Result { + let sdk = self.inner_clone(); + + // Parse identifiers + let (contract_id, owner_identifier, doc_id) = Self::parse_identifiers( + &data_contract_id, + &owner_id, + Some(&document_id), + )?; + let doc_id = doc_id.expect("Document ID was provided"); + + let recipient_identifier = Identifier::from_string(&recipient_id, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid recipient ID: {}", e)))?; + + // Fetch and cache the data contract + let data_contract = self.fetch_and_cache_contract(contract_id).await?; + + // Get document type + let document_type_result = data_contract.document_type_for_name(&document_type); + let document_type_ref = document_type_result + .map_err(|e| JsValue::from_str(&format!("Document type '{}' not found: {}", document_type, e)))?; + + // Fetch the document to get its current state + use dash_sdk::platform::DocumentQuery; + + let query = DocumentQuery::new_with_data_contract_id( + &sdk, + contract_id, + &document_type, + ) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to create document query: {}", e)))? + .with_document_id(&doc_id); + + let document = dash_sdk::platform::Document::fetch(&sdk, query) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch document: {}", e)))? + .ok_or_else(|| JsValue::from_str("Document not found"))?; + + // Fetch the identity to get the correct key + let identity = dash_sdk::platform::Identity::fetch(&sdk, owner_identifier) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch identity: {}", e)))? + .ok_or_else(|| JsValue::from_str("Identity not found"))?; + + // Get identity contract nonce + let identity_contract_nonce = sdk + .get_identity_contract_nonce(owner_identifier, contract_id, true, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch nonce: {}", e)))?; + + // Find matching authentication key and create signer + let (_, matching_key) = Self::find_authentication_key(&identity, &private_key_wif)?; + let signer = Self::create_signer_from_wif(&private_key_wif, self.network())?; + + // Create a transfer transition + let transition = BatchTransition::new_document_transfer_transition_from_document( + document, + document_type_ref, + recipient_identifier, + matching_key, + identity_contract_nonce, + UserFeeIncrease::default(), + None, // token_payment_info + &signer, + sdk.version(), + None, // options + ) + .map_err(|e| JsValue::from_str(&format!("Failed to create transition: {}", e)))?; + + // The transition is already signed, convert to StateTransition + let state_transition: StateTransition = transition.into(); + + // Broadcast the state transition + state_transition + .broadcast(&sdk, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to broadcast: {}", e)))?; + + // Return the result with document ID and new owner + Self::build_js_result_object( + "DocumentTransferred", + &document_id, + vec![ + ("newOwnerId", JsValue::from_str(&recipient_id)), + ("transferred", JsValue::from_bool(true)), + ], + ) + } + + /// Purchase a document that has a price set. + /// + /// # Arguments + /// + /// * `data_contract_id` - The ID of the data contract + /// * `document_type` - The name of the document type + /// * `document_id` - The ID of the document to purchase + /// * `buyer_id` - The buyer's identity ID + /// * `price` - The purchase price in credits + /// * `private_key_wif` - The private key in WIF format for signing + /// * `key_id` - The key ID to use for signing + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the purchase result + #[wasm_bindgen(js_name = documentPurchase)] + pub async fn document_purchase( + &self, + data_contract_id: String, + document_type: String, + document_id: String, + buyer_id: String, + price: u64, + private_key_wif: String, + key_id: u32, + ) -> Result { + let sdk = self.inner_clone(); + + // Parse identifiers + let (contract_id, buyer_identifier, doc_id) = Self::parse_identifiers( + &data_contract_id, + &buyer_id, + Some(&document_id), + )?; + let doc_id = doc_id.expect("Document ID was provided"); + + // Fetch and cache the data contract + let data_contract = self.fetch_and_cache_contract(contract_id).await?; + + // Get document type from contract + let document_type_ref = data_contract + .document_type_for_name(&document_type) + .map_err(|e| JsValue::from_str(&format!("Document type not found: {}", e)))?; + + // Fetch the document to purchase + let query = dash_sdk::platform::documents::document_query::DocumentQuery::new_with_data_contract_id( + &sdk, + contract_id, + &document_type, + ) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to create document query: {}", e)))? + .with_document_id(&doc_id); + + let document = dash_sdk::platform::Document::fetch(&sdk, query) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch document: {}", e)))? + .ok_or_else(|| JsValue::from_str("Document not found"))?; + + // Verify the document has a price and it matches + let listed_price = document + .properties() + .get_optional_integer::("$price") + .map_err(|e| JsValue::from_str(&format!("Failed to get document price: {}", e)))? + .ok_or_else(|| JsValue::from_str("Document is not for sale (no price set)"))?; + + if listed_price != price { + return Err(JsValue::from_str(&format!( + "Price mismatch: document is listed for {} but purchase attempted with {}", + listed_price, price + ))); + } + + // Fetch buyer identity + let buyer_identity = dash_sdk::platform::Identity::fetch(&sdk, buyer_identifier) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch buyer identity: {}", e)))? + .ok_or_else(|| JsValue::from_str("Buyer identity not found"))?; + + // Find matching authentication key and create signer + let (_, matching_key) = Self::find_authentication_key(&buyer_identity, &private_key_wif)?; + let signer = Self::create_signer_from_wif(&private_key_wif, self.network())?; + + // Get identity contract nonce + let identity_contract_nonce = sdk + .get_identity_contract_nonce(buyer_identifier, contract_id, true, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to get identity contract nonce: {}", e)))?; + + // Create document purchase transition + let transition = BatchTransition::new_document_purchase_transition_from_document( + document.into(), + document_type_ref, + buyer_identifier, + price as Credits, + matching_key, + identity_contract_nonce, + UserFeeIncrease::default(), + None, // No token payment info + &signer, + sdk.version(), + None, // Default options + ) + .map_err(|e| JsValue::from_str(&format!("Failed to create purchase transition: {}", e)))?; + + // Broadcast the transition + let proof_result = transition + .broadcast_and_wait::(&sdk, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to broadcast purchase: {}", e)))?; + + // Handle the proof result + match proof_result { + StateTransitionProofResult::VerifiedDocuments(documents) => { + // Document purchase was successful + let mut additional_fields = vec![ + ("status", JsValue::from_str("success")), + ("newOwnerId", JsValue::from_str(&buyer_id)), + ("pricePaid", JsValue::from_f64(price as f64)), + ("message", JsValue::from_str("Document purchased successfully")), + ]; + + // If we have the updated document in the response, include basic info + if let Some((_, maybe_doc)) = documents.into_iter().next() { + if let Some(doc) = maybe_doc { + additional_fields.push(("documentUpdated", JsValue::from_bool(true))); + additional_fields.push(("revision", JsValue::from_f64(doc.revision().unwrap_or(0) as f64))); + } + } + + Self::build_js_result_object("DocumentPurchased", &doc_id.to_string(Encoding::Base58), additional_fields) + }, + _ => { + // Purchase was processed but document not returned + Self::build_js_result_object( + "DocumentPurchased", + &doc_id.to_string(Encoding::Base58), + vec![ + ("status", JsValue::from_str("success")), + ("message", JsValue::from_str("Document purchase processed")), + ], + ) + } + } + } + + /// Set a price for a document to enable purchases. + /// + /// # Arguments + /// + /// * `data_contract_id` - The ID of the data contract + /// * `document_type` - The name of the document type + /// * `document_id` - The ID of the document + /// * `owner_id` - The owner's identity ID + /// * `price` - The price in credits (0 to remove price) + /// * `private_key_wif` - The private key in WIF format for signing + /// * `key_id` - The key ID to use for signing + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the result + #[wasm_bindgen(js_name = documentSetPrice)] + pub async fn document_set_price( + &self, + data_contract_id: String, + document_type: String, + document_id: String, + owner_id: String, + price: u64, + private_key_wif: String, + key_id: u32, + ) -> Result { + let sdk = self.inner_clone(); + + // Parse identifiers + let (contract_id, owner_identifier, doc_id) = Self::parse_identifiers( + &data_contract_id, + &owner_id, + Some(&document_id), + )?; + let doc_id = doc_id.expect("Document ID was provided"); + + // Fetch and cache the data contract + let data_contract = self.fetch_and_cache_contract(contract_id).await?; + + // Get document type from contract + let document_type_ref = data_contract + .document_type_for_name(&document_type) + .map_err(|e| JsValue::from_str(&format!("Document type not found: {}", e)))?; + + // Fetch the existing document to update its price + let query = dash_sdk::platform::documents::document_query::DocumentQuery::new_with_data_contract_id( + &sdk, + contract_id, + &document_type, + ) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to create document query: {}", e)))? + .with_document_id(&doc_id); + + let existing_doc = Document::fetch(&sdk, query) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch document: {}", e)))? + .ok_or_else(|| JsValue::from_str("Document not found"))?; + + // Verify ownership + if existing_doc.owner_id() != owner_identifier { + return Err(JsValue::from_str("Only the document owner can set its price")); + } + + // Get existing document properties and convert to mutable map + let mut properties = existing_doc.properties().clone(); + + // Update the price in the document properties + let price_value = if price > 0 { + PlatformValue::U64(price) + } else { + PlatformValue::Null + }; + + properties.insert("$price".to_string(), price_value); + + // Create updated document with new properties + let new_revision = existing_doc.revision().unwrap_or(0) + 1; + let updated_doc = Document::V0(DocumentV0 { + id: doc_id, + owner_id: owner_identifier, + properties, + revision: Some(new_revision), + created_at: existing_doc.created_at(), + updated_at: existing_doc.updated_at(), + transferred_at: existing_doc.transferred_at(), + created_at_block_height: existing_doc.created_at_block_height(), + updated_at_block_height: existing_doc.updated_at_block_height(), + transferred_at_block_height: existing_doc.transferred_at_block_height(), + created_at_core_block_height: existing_doc.created_at_core_block_height(), + updated_at_core_block_height: existing_doc.updated_at_core_block_height(), + transferred_at_core_block_height: existing_doc.transferred_at_core_block_height(), + }); + + // Fetch the identity to get the authentication key + let identity = dash_sdk::platform::Identity::fetch(&sdk, owner_identifier) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch identity: {}", e)))? + .ok_or_else(|| JsValue::from_str("Identity not found"))?; + + // Find matching authentication key and create signer + let (_, matching_key) = Self::find_authentication_key(&identity, &private_key_wif)?; + let signer = Self::create_signer_from_wif(&private_key_wif, self.network())?; + + // Get identity contract nonce + let identity_contract_nonce = sdk + .get_identity_contract_nonce(owner_identifier, contract_id, true, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch nonce: {}", e)))?; + + // Generate entropy for the state transition + let entropy_bytes = { + let mut entropy = [0u8; 32]; + if let Some(window) = web_sys::window() { + if let Ok(crypto) = window.crypto() { + let _ = crypto.get_random_values_with_u8_array(&mut entropy); + } + } + entropy + }; + + // Create the price update transition + let transition = BatchTransition::new_document_replacement_transition_from_document( + updated_doc, + document_type_ref, + matching_key, + identity_contract_nonce, + UserFeeIncrease::default(), + None, // token_payment_info + &signer, + sdk.version(), + None, // options + ) + .map_err(|e| JsValue::from_str(&format!("Failed to create transition: {}", e)))?; + + // The transition is already signed, convert to StateTransition + let state_transition: StateTransition = transition.into(); + + // Broadcast the state transition + state_transition + .broadcast(&sdk, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to broadcast: {}", e)))?; + + // Return the result with document ID and price + Self::build_js_result_object( + "DocumentPriceSet", + &document_id, + vec![ + ("price", JsValue::from_f64(price as f64)), + ("priceSet", JsValue::from_bool(true)), + ], + ) + } +} + diff --git a/packages/wasm-sdk/src/state_transitions/identity/mod.rs b/packages/wasm-sdk/src/state_transitions/identity/mod.rs new file mode 100644 index 0000000000..8b20a6af38 --- /dev/null +++ b/packages/wasm-sdk/src/state_transitions/identity/mod.rs @@ -0,0 +1,710 @@ +use crate::sdk::WasmSdk; +use dash_sdk::dpp::dashcore::PrivateKey; +use dash_sdk::dpp::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel}; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dash_sdk::dpp::platform_value::{BinaryData, Identifier, string_encoding::Encoding}; +use dash_sdk::dpp::prelude::UserFeeIncrease; +use dash_sdk::dpp::state_transition::identity_credit_transfer_transition::IdentityCreditTransferTransition; +use dash_sdk::dpp::state_transition::identity_credit_transfer_transition::methods::IdentityCreditTransferTransitionMethodsV0; +use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; +use dash_sdk::platform::Fetch; +use simple_signer::SingleKeySigner; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; +use js_sys; + +#[wasm_bindgen] +impl WasmSdk { + /// Transfer credits from one identity to another. + /// + /// # Arguments + /// + /// * `sender_id` - The identity ID of the sender + /// * `recipient_id` - The identity ID of the recipient + /// * `amount` - The amount of credits to transfer + /// * `private_key_wif` - The private key in WIF format for signing + /// * `key_id` - Optional key ID to use for signing (if None, will auto-select) + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the transfer result + #[wasm_bindgen(js_name = identityCreditTransfer)] + pub async fn identity_credit_transfer( + &self, + sender_id: String, + recipient_id: String, + amount: u64, + private_key_wif: String, + key_id: Option, + ) -> Result { + let sdk = self.inner_clone(); + + // Parse identifiers + let sender_identifier = Identifier::from_string(&sender_id, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid sender ID: {}", e)))?; + + let recipient_identifier = Identifier::from_string(&recipient_id, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid recipient ID: {}", e)))?; + + // Validate not sending to self + if sender_identifier == recipient_identifier { + return Err(JsValue::from_str("Cannot transfer credits to yourself")); + } + + // Validate amount + if amount == 0 { + return Err(JsValue::from_str("Transfer amount must be greater than 0")); + } + + // Fetch sender identity + let sender_identity = dash_sdk::platform::Identity::fetch(&sdk, sender_identifier) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch sender identity: {}", e)))? + .ok_or_else(|| JsValue::from_str("Sender identity not found"))?; + + // Parse private key and find matching public key + let private_key_bytes = dash_sdk::dpp::dashcore::PrivateKey::from_wif(&private_key_wif) + .map_err(|e| JsValue::from_str(&format!("Invalid private key: {}", e)))? + .inner + .secret_bytes(); + + let secp = dash_sdk::dpp::dashcore::secp256k1::Secp256k1::new(); + let secret_key = dash_sdk::dpp::dashcore::secp256k1::SecretKey::from_slice(&private_key_bytes) + .map_err(|e| JsValue::from_str(&format!("Invalid secret key: {}", e)))?; + let public_key = dash_sdk::dpp::dashcore::secp256k1::PublicKey::from_secret_key(&secp, &secret_key); + let public_key_bytes = public_key.serialize(); + + // Create public key hash using hash160 + let public_key_hash160 = { + use dash_sdk::dpp::dashcore::hashes::{Hash, hash160}; + hash160::Hash::hash(&public_key_bytes[..]).to_byte_array().to_vec() + }; + + // Find matching key - prioritize key_id if provided, otherwise find any matching key + let matching_key = if let Some(requested_key_id) = key_id { + // Find specific key by ID + sender_identity.public_keys() + .get(&requested_key_id) + .filter(|key| { + key.purpose() == Purpose::TRANSFER && + key.key_type() == KeyType::ECDSA_HASH160 && + key.data().as_slice() == public_key_hash160.as_slice() + }) + .ok_or_else(|| JsValue::from_str(&format!("Key with ID {} not found or doesn't match private key", requested_key_id)))? + } else { + // Find any matching transfer key + sender_identity.public_keys().iter() + .find(|(_, key)| { + key.purpose() == Purpose::TRANSFER && + key.key_type() == KeyType::ECDSA_HASH160 && + key.data().as_slice() == public_key_hash160.as_slice() + }) + .map(|(_, key)| key) + .ok_or_else(|| JsValue::from_str("No matching transfer key found for the provided private key"))? + }; + + // Get identity nonce + let identity_nonce = sdk + .get_identity_nonce(sender_identifier, true, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to get identity nonce: {}", e)))?; + + // Create signer + let signer = SingleKeySigner::from_string(&private_key_wif, dash_sdk::dpp::dashcore::Network::Testnet) + .map_err(|e| JsValue::from_str(&e))?; + + // Create the credit transfer transition + let state_transition = IdentityCreditTransferTransition::try_from_identity( + &sender_identity, + recipient_identifier, + amount, + UserFeeIncrease::default(), + signer, + Some(matching_key), + identity_nonce, + sdk.version(), + None, // No version override + ) + .map_err(|e| JsValue::from_str(&format!("Failed to create transfer transition: {}", e)))?; + + // Broadcast the transition + use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; + let _result = state_transition + .broadcast_and_wait::(&sdk, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to broadcast transfer: {}", e)))?; + + + // Create JavaScript result object + let result_obj = js_sys::Object::new(); + + js_sys::Reflect::set(&result_obj, &JsValue::from_str("status"), &JsValue::from_str("success")) + .map_err(|e| JsValue::from_str(&format!("Failed to set status: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("senderId"), &JsValue::from_str(&sender_id)) + .map_err(|e| JsValue::from_str(&format!("Failed to set senderId: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("recipientId"), &JsValue::from_str(&recipient_id)) + .map_err(|e| JsValue::from_str(&format!("Failed to set recipientId: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("amount"), &JsValue::from_f64(amount as f64)) + .map_err(|e| JsValue::from_str(&format!("Failed to set amount: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("message"), &JsValue::from_str("Credits transferred successfully")) + .map_err(|e| JsValue::from_str(&format!("Failed to set message: {:?}", e)))?; + + Ok(result_obj.into()) + } + + /// Withdraw credits from an identity to a Dash address. + /// + /// # Arguments + /// + /// * `identity_id` - The identity ID to withdraw from + /// * `to_address` - The Dash address to send the withdrawn credits to + /// * `amount` - The amount of credits to withdraw + /// * `core_fee_per_byte` - Optional core fee per byte (defaults to 1) + /// * `private_key_wif` - The private key in WIF format for signing + /// * `key_id` - Optional key ID to use for signing (if None, will auto-select) + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the withdrawal result + #[wasm_bindgen(js_name = identityCreditWithdrawal)] + pub async fn identity_credit_withdrawal( + &self, + identity_id: String, + to_address: String, + amount: u64, + core_fee_per_byte: Option, + private_key_wif: String, + key_id: Option, + ) -> Result { + let sdk = self.inner_clone(); + + // Parse identity identifier + let identifier = Identifier::from_string(&identity_id, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid identity ID: {}", e)))?; + + // Parse the Dash address + use dash_sdk::dpp::dashcore::Address; + use std::str::FromStr; + let address = Address::from_str(&to_address) + .map_err(|e| JsValue::from_str(&format!("Invalid Dash address: {}", e)))? + .assume_checked(); + + // Validate amount + if amount == 0 { + return Err(JsValue::from_str("Withdrawal amount must be greater than 0")); + } + + // Fetch the identity + let identity = dash_sdk::platform::Identity::fetch(&sdk, identifier) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch identity: {}", e)))? + .ok_or_else(|| JsValue::from_str("Identity not found"))?; + + // Parse private key and find matching public key + let private_key_bytes = dash_sdk::dpp::dashcore::PrivateKey::from_wif(&private_key_wif) + .map_err(|e| JsValue::from_str(&format!("Invalid private key: {}", e)))? + .inner + .secret_bytes(); + + let secp = dash_sdk::dpp::dashcore::secp256k1::Secp256k1::new(); + let secret_key = dash_sdk::dpp::dashcore::secp256k1::SecretKey::from_slice(&private_key_bytes) + .map_err(|e| JsValue::from_str(&format!("Invalid secret key: {}", e)))?; + let public_key = dash_sdk::dpp::dashcore::secp256k1::PublicKey::from_secret_key(&secp, &secret_key); + let public_key_bytes = public_key.serialize(); + + // Create public key hash using hash160 + let public_key_hash160 = { + use dash_sdk::dpp::dashcore::hashes::{Hash, hash160}; + hash160::Hash::hash(&public_key_bytes[..]).to_byte_array().to_vec() + }; + + // Find matching key - prioritize key_id if provided, otherwise find any matching key + // For withdrawals, we can use either TRANSFER or OWNER keys + let matching_key = if let Some(requested_key_id) = key_id { + // Find specific key by ID + identity.public_keys() + .get(&requested_key_id) + .filter(|key| { + (key.purpose() == Purpose::TRANSFER || key.purpose() == Purpose::OWNER) && + key.key_type() == KeyType::ECDSA_HASH160 && + key.data().as_slice() == public_key_hash160.as_slice() + }) + .ok_or_else(|| JsValue::from_str(&format!("Key with ID {} not found or doesn't match private key", requested_key_id)))? + } else { + // Find any matching withdrawal-capable key (prefer TRANSFER keys) + identity.public_keys().iter() + .find(|(_, key)| { + key.purpose() == Purpose::TRANSFER && + key.key_type() == KeyType::ECDSA_HASH160 && + key.data().as_slice() == public_key_hash160.as_slice() + }) + .or_else(|| { + identity.public_keys().iter() + .find(|(_, key)| { + key.purpose() == Purpose::OWNER && + key.key_type() == KeyType::ECDSA_HASH160 && + key.data().as_slice() == public_key_hash160.as_slice() + }) + }) + .map(|(_, key)| key) + .ok_or_else(|| JsValue::from_str("No matching withdrawal key found for the provided private key"))? + }; + + // Create signer + let signer = SingleKeySigner::from_string(&private_key_wif, dash_sdk::dpp::dashcore::Network::Testnet) + .map_err(|e| JsValue::from_str(&e))?; + + // Import the withdraw trait + use dash_sdk::platform::transition::withdraw_from_identity::WithdrawFromIdentity; + + // Perform the withdrawal + let remaining_balance = identity + .withdraw( + &sdk, + Some(address), + amount, + core_fee_per_byte, + Some(matching_key), + signer, + None, // No special settings + ) + .await + .map_err(|e| JsValue::from_str(&format!("Withdrawal failed: {}", e)))?; + + // Create JavaScript result object + let result_obj = js_sys::Object::new(); + + js_sys::Reflect::set(&result_obj, &JsValue::from_str("status"), &JsValue::from_str("success")) + .map_err(|e| JsValue::from_str(&format!("Failed to set status: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("identityId"), &JsValue::from_str(&identity_id)) + .map_err(|e| JsValue::from_str(&format!("Failed to set identityId: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("toAddress"), &JsValue::from_str(&to_address)) + .map_err(|e| JsValue::from_str(&format!("Failed to set toAddress: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("amount"), &JsValue::from_f64(amount as f64)) + .map_err(|e| JsValue::from_str(&format!("Failed to set amount: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("remainingBalance"), &JsValue::from_f64(remaining_balance as f64)) + .map_err(|e| JsValue::from_str(&format!("Failed to set remainingBalance: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("message"), &JsValue::from_str("Credits withdrawn successfully")) + .map_err(|e| JsValue::from_str(&format!("Failed to set message: {:?}", e)))?; + + Ok(result_obj.into()) + } + + /// Update an identity by adding or disabling public keys. + /// + /// # Arguments + /// + /// * `identity_id` - The identity ID to update + /// * `add_public_keys` - JSON array of public keys to add + /// * `disable_public_keys` - Array of key IDs to disable + /// * `private_key_wif` - The private key in WIF format for signing (must be a master key) + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the update result + #[wasm_bindgen(js_name = identityUpdate)] + pub async fn identity_update( + &self, + identity_id: String, + add_public_keys: Option, + disable_public_keys: Option>, + private_key_wif: String, + ) -> Result { + let sdk = self.inner_clone(); + + // Parse identity identifier + let identifier = Identifier::from_string(&identity_id, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid identity ID: {}", e)))?; + + // Fetch the identity + let identity = dash_sdk::platform::Identity::fetch(&sdk, identifier) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch identity: {}", e)))? + .ok_or_else(|| JsValue::from_str("Identity not found"))?; + + // Get current revision + let current_revision = identity.revision(); + + // Parse private key and verify it's a master key + let private_key = PrivateKey::from_wif(&private_key_wif) + .map_err(|e| JsValue::from_str(&format!("Invalid private key: {}", e)))?; + + // Create public key hash to find matching master key + let secp = dash_sdk::dpp::dashcore::secp256k1::Secp256k1::new(); + let secret_key = dash_sdk::dpp::dashcore::secp256k1::SecretKey::from_slice(&private_key.inner.secret_bytes()) + .map_err(|e| JsValue::from_str(&format!("Invalid secret key: {}", e)))?; + let public_key = dash_sdk::dpp::dashcore::secp256k1::PublicKey::from_secret_key(&secp, &secret_key); + let public_key_bytes = public_key.serialize(); + + // Create public key hash using hash160 + let public_key_hash160 = { + use dash_sdk::dpp::dashcore::hashes::{Hash, hash160}; + hash160::Hash::hash(&public_key_bytes[..]).to_byte_array().to_vec() + }; + + // Find matching master key + let master_key = identity.public_keys().iter() + .find(|(_, key)| { + key.purpose() == Purpose::AUTHENTICATION && + key.security_level() == SecurityLevel::MASTER && + key.key_type() == KeyType::ECDSA_HASH160 && + key.data().as_slice() == public_key_hash160.as_slice() + }) + .map(|(id, _)| *id) + .ok_or_else(|| JsValue::from_str("Provided private key does not match any master key"))?; + + // Parse and prepare keys to add + let keys_to_add: Vec = if let Some(keys_json) = add_public_keys { + // Parse JSON array of keys + let keys_data: serde_json::Value = serde_json::from_str(&keys_json) + .map_err(|e| JsValue::from_str(&format!("Invalid JSON for add_public_keys: {}", e)))?; + + let keys_array = keys_data.as_array() + .ok_or_else(|| JsValue::from_str("add_public_keys must be a JSON array"))?; + + // Get the current max key ID + let mut next_key_id = identity.public_keys().keys().max().copied().unwrap_or(0) + 1; + + keys_array.iter() + .map(|key_data| { + let key_type_str = key_data["keyType"].as_str() + .ok_or_else(|| JsValue::from_str("keyType is required"))?; + let purpose_str = key_data["purpose"].as_str() + .ok_or_else(|| JsValue::from_str("purpose is required"))?; + let security_level_str = key_data["securityLevel"].as_str() + .unwrap_or("HIGH"); + let data_str = key_data["data"].as_str() + .ok_or_else(|| JsValue::from_str("data is required"))?; + + // Parse key type + let key_type = match key_type_str { + "ECDSA_SECP256K1" => KeyType::ECDSA_SECP256K1, + "BLS12_381" => KeyType::BLS12_381, + "ECDSA_HASH160" => KeyType::ECDSA_HASH160, + "BIP13_SCRIPT_HASH" => KeyType::BIP13_SCRIPT_HASH, + "EDDSA_25519_HASH160" => KeyType::EDDSA_25519_HASH160, + _ => return Err(JsValue::from_str(&format!("Unknown key type: {}", key_type_str))) + }; + + // Parse purpose + let purpose = match purpose_str { + "AUTHENTICATION" => Purpose::AUTHENTICATION, + "ENCRYPTION" => Purpose::ENCRYPTION, + "DECRYPTION" => Purpose::DECRYPTION, + "TRANSFER" => Purpose::TRANSFER, + "SYSTEM" => Purpose::SYSTEM, + "VOTING" => Purpose::VOTING, + _ => return Err(JsValue::from_str(&format!("Unknown purpose: {}", purpose_str))) + }; + + // Parse security level + let security_level = match security_level_str { + "MASTER" => SecurityLevel::MASTER, + "CRITICAL" => SecurityLevel::CRITICAL, + "HIGH" => SecurityLevel::HIGH, + "MEDIUM" => SecurityLevel::MEDIUM, + _ => SecurityLevel::HIGH + }; + + // Decode key data from base64 + let key_data = dash_sdk::dpp::dashcore::base64::decode(data_str) + .map_err(|e| JsValue::from_str(&format!("Invalid base64 key data: {}", e)))?; + + // Create the identity public key + use dash_sdk::dpp::identity::identity_public_key::v0::IdentityPublicKeyV0; + let public_key = IdentityPublicKey::V0(IdentityPublicKeyV0 { + id: next_key_id, + key_type, + purpose, + security_level, + contract_bounds: None, + read_only: false, + data: BinaryData::new(key_data), + disabled_at: None, + }); + + next_key_id += 1; + Ok(public_key) + }) + .collect::, _>>()? + } else { + Vec::new() + }; + + // Get keys to disable + let keys_to_disable = disable_public_keys.unwrap_or_default(); + + // Save counts before moving + let added_keys_count = keys_to_add.len(); + let disabled_keys_count = keys_to_disable.len(); + + // Validate keys to disable (cannot disable master, critical auth, or transfer keys) + for key_id in &keys_to_disable { + if let Some(key) = identity.public_keys().get(key_id) { + if key.security_level() == SecurityLevel::MASTER { + return Err(JsValue::from_str(&format!("Cannot disable master key {}", key_id))); + } + if key.purpose() == Purpose::AUTHENTICATION && + key.security_level() == SecurityLevel::CRITICAL && + key.key_type() == KeyType::ECDSA_SECP256K1 { + return Err(JsValue::from_str(&format!("Cannot disable critical authentication key {}", key_id))); + } + if key.purpose() == Purpose::TRANSFER { + return Err(JsValue::from_str(&format!("Cannot disable transfer key {}", key_id))); + } + } else { + return Err(JsValue::from_str(&format!("Key {} not found", key_id))); + } + } + + // Get identity nonce + let identity_nonce = sdk + .get_identity_nonce(identifier, true, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to get identity nonce: {}", e)))?; + + // Create signer + let signer = SingleKeySigner::from_string(&private_key_wif, dash_sdk::dpp::dashcore::Network::Testnet) + .map_err(|e| JsValue::from_str(&e))?; + + // Create the identity update transition + use dash_sdk::dpp::state_transition::identity_update_transition::methods::IdentityUpdateTransitionMethodsV0; + use dash_sdk::dpp::state_transition::identity_update_transition::IdentityUpdateTransition; + + let state_transition = IdentityUpdateTransition::try_from_identity_with_signer( + &identity, + &master_key, + keys_to_add, + keys_to_disable, + identity_nonce, + UserFeeIncrease::default(), + &signer, + sdk.version(), + None, // No version override + ) + .map_err(|e| JsValue::from_str(&format!("Failed to create update transition: {}", e)))?; + + // Broadcast the transition + use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; + let result = state_transition + .broadcast_and_wait::(&sdk, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to broadcast update: {}", e)))?; + + // Extract updated identity from result + let updated_revision = match result { + StateTransitionProofResult::VerifiedIdentity(updated_identity) => { + updated_identity.revision() + } + StateTransitionProofResult::VerifiedPartialIdentity(partial_identity) => { + partial_identity.revision.unwrap_or(current_revision + 1) + } + _ => current_revision + 1, + }; + + // Create JavaScript result object + let result_obj = js_sys::Object::new(); + + js_sys::Reflect::set(&result_obj, &JsValue::from_str("status"), &JsValue::from_str("success")) + .map_err(|e| JsValue::from_str(&format!("Failed to set status: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("identityId"), &JsValue::from_str(&identity_id)) + .map_err(|e| JsValue::from_str(&format!("Failed to set identityId: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("revision"), &JsValue::from_f64(updated_revision as f64)) + .map_err(|e| JsValue::from_str(&format!("Failed to set revision: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("addedKeys"), &JsValue::from_f64(added_keys_count as f64)) + .map_err(|e| JsValue::from_str(&format!("Failed to set addedKeys: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("disabledKeys"), &JsValue::from_f64(disabled_keys_count as f64)) + .map_err(|e| JsValue::from_str(&format!("Failed to set disabledKeys: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("message"), &JsValue::from_str("Identity updated successfully")) + .map_err(|e| JsValue::from_str(&format!("Failed to set message: {:?}", e)))?; + + Ok(result_obj.into()) + } + + /// Submit a masternode vote for a contested resource. + /// + /// # Arguments + /// + /// * `pro_tx_hash` - The ProTxHash of the masternode + /// * `contract_id` - The data contract ID containing the contested resource + /// * `document_type_name` - The document type name (e.g., "domain") + /// * `index_name` - The index name (e.g., "parentNameAndLabel") + /// * `index_values` - JSON array of index values (e.g., ["dash", "username"]) + /// * `vote_choice` - The vote choice: "towardsIdentity:", "abstain", or "lock" + /// * `private_key_wif` - The masternode voting key in WIF format + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the vote result + #[wasm_bindgen(js_name = masternodeVote)] + pub async fn masternode_vote( + &self, + masternode_pro_tx_hash: String, + contract_id: String, + document_type_name: String, + index_name: String, + index_values: String, + vote_choice: String, + voting_key_wif: String, + ) -> Result { + let sdk = self.inner_clone(); + + // Parse ProTxHash (try hex first, then base58) + let pro_tx_hash = if masternode_pro_tx_hash.len() == 64 && masternode_pro_tx_hash.chars().all(|c| c.is_ascii_hexdigit()) { + // Looks like hex + Identifier::from_string(&masternode_pro_tx_hash, Encoding::Hex) + .map_err(|e| JsValue::from_str(&format!("Invalid ProTxHash (hex): {}", e)))? + } else { + // Try base58 + Identifier::from_string(&masternode_pro_tx_hash, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid ProTxHash (base58): {}", e)))? + }; + + // Parse contract ID + let data_contract_id = Identifier::from_string(&contract_id, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid contract ID: {}", e)))?; + + // Parse index values from JSON + let index_values_json: serde_json::Value = serde_json::from_str(&index_values) + .map_err(|e| JsValue::from_str(&format!("Invalid index values JSON: {}", e)))?; + + let index_values_array = index_values_json.as_array() + .ok_or_else(|| JsValue::from_str("index_values must be a JSON array"))?; + + let index_values_vec: Vec = index_values_array.iter() + .map(|v| { + match v { + serde_json::Value::String(s) => Ok(dash_sdk::dpp::platform_value::Value::Text(s.clone())), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + Ok(dash_sdk::dpp::platform_value::Value::I64(i)) + } else if let Some(u) = n.as_u64() { + Ok(dash_sdk::dpp::platform_value::Value::U64(u)) + } else { + Ok(dash_sdk::dpp::platform_value::Value::Float(n.as_f64().unwrap())) + } + } + serde_json::Value::Bool(b) => Ok(dash_sdk::dpp::platform_value::Value::Bool(*b)), + _ => Err(JsValue::from_str("Unsupported index value type")) + } + }) + .collect::, _>>()?; + + // Parse vote choice + use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; + let resource_vote_choice = if vote_choice == "abstain" { + ResourceVoteChoice::Abstain + } else if vote_choice == "lock" { + ResourceVoteChoice::Lock + } else if vote_choice.starts_with("towardsIdentity:") { + let identity_id_str = vote_choice.strip_prefix("towardsIdentity:") + .ok_or_else(|| JsValue::from_str("Invalid vote choice format"))?; + let identity_id = Identifier::from_string(identity_id_str, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid identity ID in vote choice: {}", e)))?; + ResourceVoteChoice::TowardsIdentity(identity_id) + } else { + return Err(JsValue::from_str("Invalid vote choice. Must be 'abstain', 'lock', or 'towardsIdentity:'")); + }; + + // Parse private key (try WIF first, then hex) + let private_key = if voting_key_wif.len() == 64 && voting_key_wif.chars().all(|c| c.is_ascii_hexdigit()) { + // Looks like hex + let key_bytes = hex::decode(&voting_key_wif) + .map_err(|e| JsValue::from_str(&format!("Invalid hex private key: {}", e)))?; + if key_bytes.len() != 32 { + return Err(JsValue::from_str("Private key must be 32 bytes")); + } + PrivateKey::from_slice(&key_bytes, dash_sdk::dpp::dashcore::Network::Testnet) + .map_err(|e| JsValue::from_str(&format!("Invalid private key bytes: {}", e)))? + } else { + // Try WIF + PrivateKey::from_wif(&voting_key_wif) + .map_err(|e| JsValue::from_str(&format!("Invalid WIF private key: {}", e)))? + }; + + // Create the voting public key from the private key + let secp = dash_sdk::dpp::dashcore::secp256k1::Secp256k1::new(); + let secret_key = dash_sdk::dpp::dashcore::secp256k1::SecretKey::from_slice(&private_key.inner.secret_bytes()) + .map_err(|e| JsValue::from_str(&format!("Invalid secret key: {}", e)))?; + let public_key = dash_sdk::dpp::dashcore::secp256k1::PublicKey::from_secret_key(&secp, &secret_key); + let public_key_bytes = public_key.serialize(); + + // Create voting public key hash using hash160 + let voting_key_hash = { + use dash_sdk::dpp::dashcore::hashes::{Hash, hash160}; + hash160::Hash::hash(&public_key_bytes[..]).to_byte_array().to_vec() + }; + + // Create the voting identity public key + use dash_sdk::dpp::identity::identity_public_key::v0::IdentityPublicKeyV0; + let voting_public_key = IdentityPublicKey::V0(IdentityPublicKeyV0 { + id: 0, // The ID doesn't matter for voting keys + key_type: KeyType::ECDSA_HASH160, + purpose: Purpose::VOTING, + security_level: SecurityLevel::HIGH, // Voting keys should be HIGH, not MASTER + contract_bounds: None, + read_only: false, + data: BinaryData::new(voting_key_hash), + disabled_at: None, + }); + + // Create the contested document resource vote poll + use dash_sdk::dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; + let vote_poll = dash_sdk::dpp::voting::vote_polls::VotePoll::ContestedDocumentResourceVotePoll( + ContestedDocumentResourceVotePoll { + contract_id: data_contract_id, + document_type_name: document_type_name.clone(), + index_name: index_name.clone(), + index_values: index_values_vec, + } + ); + + // Create the resource vote + use dash_sdk::dpp::voting::votes::resource_vote::v0::ResourceVoteV0; + use dash_sdk::dpp::voting::votes::resource_vote::ResourceVote; + let resource_vote = ResourceVote::V0(ResourceVoteV0 { + vote_poll, + resource_vote_choice, + }); + + // Create the vote + use dash_sdk::dpp::voting::votes::Vote; + let vote = Vote::ResourceVote(resource_vote); + + // Create signer + let signer = SingleKeySigner::from_string(&voting_key_wif, dash_sdk::dpp::dashcore::Network::Testnet) + .map_err(|e| JsValue::from_str(&e))?; + + // Submit the vote using PutVote trait + use dash_sdk::platform::transition::vote::PutVote; + + vote.put_to_platform( + pro_tx_hash, + &voting_public_key, + &sdk, + &signer, + None, + ) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to submit vote: {}", e)))?; + + // Create JavaScript result object + let result_obj = js_sys::Object::new(); + + js_sys::Reflect::set(&result_obj, &JsValue::from_str("status"), &JsValue::from_str("success")) + .map_err(|e| JsValue::from_str(&format!("Failed to set status: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("proTxHash"), &JsValue::from_str(&masternode_pro_tx_hash)) + .map_err(|e| JsValue::from_str(&format!("Failed to set proTxHash: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("contractId"), &JsValue::from_str(&contract_id)) + .map_err(|e| JsValue::from_str(&format!("Failed to set contractId: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("documentType"), &JsValue::from_str(&document_type_name)) + .map_err(|e| JsValue::from_str(&format!("Failed to set documentType: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("voteChoice"), &JsValue::from_str(&vote_choice)) + .map_err(|e| JsValue::from_str(&format!("Failed to set voteChoice: {:?}", e)))?; + js_sys::Reflect::set(&result_obj, &JsValue::from_str("message"), &JsValue::from_str("Vote submitted successfully")) + .map_err(|e| JsValue::from_str(&format!("Failed to set message: {:?}", e)))?; + + Ok(result_obj.into()) + } +} \ No newline at end of file diff --git a/packages/wasm-sdk/src/state_transitions/mod.rs b/packages/wasm-sdk/src/state_transitions/mod.rs index 487a38d50d..52a67de9d1 100644 --- a/packages/wasm-sdk/src/state_transitions/mod.rs +++ b/packages/wasm-sdk/src/state_transitions/mod.rs @@ -1 +1,6 @@ +pub mod contracts; pub mod documents; +pub mod identity; +pub mod tokens; + +// Re-export functions for easy access diff --git a/packages/wasm-sdk/src/state_transitions/tokens/mod.rs b/packages/wasm-sdk/src/state_transitions/tokens/mod.rs new file mode 100644 index 0000000000..9a8d93dfd3 --- /dev/null +++ b/packages/wasm-sdk/src/state_transitions/tokens/mod.rs @@ -0,0 +1,438 @@ +//! Token state transition implementations for the WASM SDK. +//! +//! This module provides WASM bindings for token operations like mint, burn, transfer, etc. + +use crate::sdk::WasmSdk; +use dash_sdk::dpp::balances::credits::TokenAmount; +use dash_sdk::dpp::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel}; +use dash_sdk::dpp::identity::identity_public_key::v0::IdentityPublicKeyV0; +use dash_sdk::dpp::platform_value::{Identifier, BinaryData, string_encoding::Encoding}; +use dash_sdk::dpp::prelude::UserFeeIncrease; +use dash_sdk::dpp::state_transition::batch_transition::BatchTransition; +use dash_sdk::dpp::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; +use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; +use dash_sdk::dpp::tokens::calculate_token_id; +use dash_sdk::dpp::document::DocumentV0Getters; +use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; +use dash_sdk::platform::Fetch; +use simple_signer::SingleKeySigner; +use serde_wasm_bindgen::to_value; +use serde_json; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; + +// WasmSigner has been replaced with SingleKeySigner from simple-signer crate + +// Helper functions for token operations +impl WasmSdk { + /// Parse and validate token operation parameters + async fn parse_token_params( + &self, + data_contract_id: &str, + identity_id: &str, + amount: &str, + recipient_id: Option, + ) -> Result<(Identifier, Identifier, TokenAmount, Option), JsValue> { + // Parse identifiers + let contract_id = Identifier::from_string(data_contract_id, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid contract ID: {}", e)))?; + + let identity_identifier = Identifier::from_string(identity_id, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid identity ID: {}", e)))?; + + let recipient = if let Some(recipient_str) = recipient_id { + Some(Identifier::from_string(&recipient_str, Encoding::Base58) + .map_err(|e| JsValue::from_str(&format!("Invalid recipient ID: {}", e)))?) + } else { + None + }; + + // Parse amount + let token_amount = amount.parse::() + .map_err(|e| JsValue::from_str(&format!("Invalid amount: {}", e)))?; + + Ok((contract_id, identity_identifier, token_amount, recipient)) + } + + /// Fetch and cache data contract in trusted context + async fn fetch_and_cache_token_contract( + &self, + contract_id: Identifier, + ) -> Result { + let sdk = self.inner_clone(); + + // Fetch the data contract + let data_contract = dash_sdk::platform::DataContract::fetch(&sdk, contract_id) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch data contract: {}", e)))? + .ok_or_else(|| JsValue::from_str("Data contract not found"))?; + + // Add the contract to the context provider's cache if using trusted mode + match sdk.network { + dash_sdk::dpp::dashcore::Network::Testnet => { + if let Some(ref context) = *crate::sdk::TESTNET_TRUSTED_CONTEXT.lock().unwrap() { + context.add_known_contract(data_contract.clone()); + } + } + dash_sdk::dpp::dashcore::Network::Dash => { + if let Some(ref context) = *crate::sdk::MAINNET_TRUSTED_CONTEXT.lock().unwrap() { + context.add_known_contract(data_contract.clone()); + } + } + _ => {} // Other networks don't use trusted context + } + + Ok(data_contract) + } + + /// Create signer and derive public key from private key + fn create_signer_and_public_key( + &self, + private_key_wif: &str, + key_id: u32, + ) -> Result<(SingleKeySigner, IdentityPublicKey), JsValue> { + let sdk = self.inner_clone(); + + // Create signer + let signer = SingleKeySigner::from_string(private_key_wif, sdk.network) + .map_err(|e| JsValue::from_str(&e))?; + + // Derive public key + let private_key_bytes = signer.private_key().to_bytes(); + let public_key_bytes = dash_sdk::dpp::dashcore::secp256k1::PublicKey::from_secret_key( + &dash_sdk::dpp::dashcore::secp256k1::Secp256k1::new(), + &dash_sdk::dpp::dashcore::secp256k1::SecretKey::from_slice(&private_key_bytes) + .map_err(|e| JsValue::from_str(&format!("Invalid private key: {}", e)))? + ).serialize().to_vec(); + + let public_key = IdentityPublicKey::V0(IdentityPublicKeyV0 { + id: key_id, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::CRITICAL, + contract_bounds: None, + key_type: KeyType::ECDSA_SECP256K1, + read_only: false, + data: BinaryData::new(public_key_bytes), + disabled_at: None, + }); + + Ok((signer, public_key)) + } + + /// Convert state transition proof result to JsValue + fn format_token_result( + &self, + proof_result: StateTransitionProofResult, + ) -> Result { + match proof_result { + StateTransitionProofResult::VerifiedTokenBalance(recipient_id, new_balance) => { + to_value(&serde_json::json!({ + "type": "VerifiedTokenBalance", + "recipientId": recipient_id.to_string(Encoding::Base58), + "newBalance": new_balance.to_string() + })).map_err(|e| JsValue::from_str(&format!("Failed to serialize result: {}", e))) + } + StateTransitionProofResult::VerifiedTokenActionWithDocument(doc) => { + to_value(&serde_json::json!({ + "type": "VerifiedTokenActionWithDocument", + "documentId": doc.id().to_string(Encoding::Base58), + "message": "Token operation recorded successfully" + })).map_err(|e| JsValue::from_str(&format!("Failed to serialize result: {}", e))) + } + StateTransitionProofResult::VerifiedTokenGroupActionWithDocument(power, doc) => { + to_value(&serde_json::json!({ + "type": "VerifiedTokenGroupActionWithDocument", + "groupPower": power, + "document": doc.is_some() + })).map_err(|e| JsValue::from_str(&format!("Failed to serialize result: {}", e))) + } + StateTransitionProofResult::VerifiedTokenGroupActionWithTokenBalance(power, status, balance) => { + to_value(&serde_json::json!({ + "type": "VerifiedTokenGroupActionWithTokenBalance", + "groupPower": power, + "status": format!("{:?}", status), + "balance": balance.map(|b| b.to_string()) + })).map_err(|e| JsValue::from_str(&format!("Failed to serialize result: {}", e))) + } + _ => Err(JsValue::from_str("Unexpected result type for token transition")) + } + } +} + +#[wasm_bindgen] +impl WasmSdk { + /// Mint new tokens according to the token's configuration. + /// + /// # Arguments + /// + /// * `data_contract_id` - The ID of the data contract containing the token + /// * `token_position` - The position of the token in the contract (0-indexed) + /// * `amount` - The amount of tokens to mint + /// * `identity_id` - The identity ID of the minter + /// * `private_key_wif` - The private key in WIF format for signing + /// * `key_id` - The key ID to use for signing + /// * `recipient_id` - Optional recipient identity ID (if None, mints to issuer) + /// * `public_note` - Optional public note for the mint operation + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the state transition result + #[wasm_bindgen(js_name = tokenMint)] + pub async fn token_mint( + &self, + data_contract_id: String, + token_position: u16, + amount: String, + identity_id: String, + private_key_wif: String, + key_id: u32, + recipient_id: Option, + public_note: Option, + ) -> Result { + let sdk = self.inner_clone(); + + // Parse and validate parameters + let (contract_id, issuer_id, token_amount, recipient) = self.parse_token_params( + &data_contract_id, + &identity_id, + &amount, + recipient_id, + ).await?; + + // Fetch and cache the data contract + let _data_contract = self.fetch_and_cache_token_contract(contract_id).await?; + + // Get identity to construct public key (still needed for mint-specific logic) + let _identity = dash_sdk::platform::Identity::fetch(&sdk, issuer_id) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch identity: {}", e)))? + .ok_or_else(|| JsValue::from_str("Identity not found"))?; + + // Get identity contract nonce + let identity_contract_nonce = sdk + .get_identity_contract_nonce(issuer_id, contract_id, true, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch nonce: {}", e)))?; + + // Create signer and public key + let (signer, public_key) = self.create_signer_and_public_key(&private_key_wif, key_id)?; + + // Calculate token ID + let token_id = Identifier::from(calculate_token_id( + contract_id.as_bytes(), + token_position, + )); + + // Create the state transition + let platform_version = sdk.version(); + let state_transition = BatchTransition::new_token_mint_transition( + token_id, + issuer_id, + contract_id, + token_position, + token_amount, + recipient, + public_note, + None, // using_group_info + &public_key, + identity_contract_nonce, + UserFeeIncrease::default(), + &signer, + platform_version, + None, // state_transition_creation_options + ).map_err(|e| JsValue::from_str(&format!("Failed to create mint transition: {}", e)))?; + + // Broadcast the transition + let proof_result = state_transition + .broadcast_and_wait::(&sdk, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to broadcast transition: {}", e)))?; + + // Format and return result + self.format_token_result(proof_result) + } + + /// Burn tokens, permanently removing them from circulation. + /// + /// # Arguments + /// + /// * `data_contract_id` - The ID of the data contract containing the token + /// * `token_position` - The position of the token in the contract (0-indexed) + /// * `amount` - The amount of tokens to burn + /// * `identity_id` - The identity ID of the burner + /// * `private_key_wif` - The private key in WIF format for signing + /// * `key_id` - The key ID to use for signing + /// * `public_note` - Optional public note for the burn operation + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the state transition result + #[wasm_bindgen(js_name = tokenBurn)] + pub async fn token_burn( + &self, + data_contract_id: String, + token_position: u16, + amount: String, + identity_id: String, + private_key_wif: String, + key_id: u32, + public_note: Option, + ) -> Result { + let sdk = self.inner_clone(); + + // Parse and validate parameters (no recipient for burn) + let (contract_id, burner_id, token_amount, _) = self.parse_token_params( + &data_contract_id, + &identity_id, + &amount, + None, + ).await?; + + // Fetch and cache the data contract + let _data_contract = self.fetch_and_cache_token_contract(contract_id).await?; + + // Get identity contract nonce + let identity_contract_nonce = sdk + .get_identity_contract_nonce(burner_id, contract_id, true, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to fetch nonce: {}", e)))?; + + // Create signer and public key + let (signer, public_key) = self.create_signer_and_public_key(&private_key_wif, key_id)?; + + // Calculate token ID + let token_id = Identifier::from(calculate_token_id( + contract_id.as_bytes(), + token_position, + )); + + // Create the state transition + let platform_version = sdk.version(); + let state_transition = BatchTransition::new_token_burn_transition( + token_id, + burner_id, + contract_id, + token_position, + token_amount, + public_note, + None, // using_group_info + &public_key, + identity_contract_nonce, + UserFeeIncrease::default(), + &signer, + platform_version, + None, // state_transition_creation_options + ).map_err(|e| JsValue::from_str(&format!("Failed to create burn transition: {}", e)))?; + + // Broadcast the transition + let proof_result = state_transition + .broadcast_and_wait::(&sdk, None) + .await + .map_err(|e| JsValue::from_str(&format!("Failed to broadcast transition: {}", e)))?; + + // Format and return result + self.format_token_result(proof_result) + } + + /// Transfer tokens between identities. + /// + /// # Arguments + /// + /// * `data_contract_id` - The ID of the data contract containing the token + /// * `token_position` - The position of the token in the contract (0-indexed) + /// * `amount` - The amount of tokens to transfer + /// * `sender_id` - The identity ID of the sender + /// * `recipient_id` - The identity ID of the recipient + /// * `private_key_wif` - The private key in WIF format for signing + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the state transition result + #[wasm_bindgen(js_name = tokenTransfer)] + pub async fn token_transfer( + &self, + data_contract_id: String, + token_position: u16, + amount: String, + sender_id: String, + recipient_id: String, + private_key_wif: String, + ) -> Result { + Err(JsValue::from_str("Token transfer not yet implemented - similar pattern to mint/burn")) + } + + /// Freeze tokens for a specific identity. + /// + /// # Arguments + /// + /// * `data_contract_id` - The ID of the data contract containing the token + /// * `token_position` - The position of the token in the contract (0-indexed) + /// * `identity_to_freeze` - The identity ID whose tokens to freeze + /// * `freezer_id` - The identity ID of the freezer (must have permission) + /// * `private_key_wif` - The private key in WIF format for signing + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the state transition result + #[wasm_bindgen(js_name = tokenFreeze)] + pub async fn token_freeze( + &self, + data_contract_id: String, + token_position: u16, + identity_to_freeze: String, + freezer_id: String, + private_key_wif: String, + ) -> Result { + Err(JsValue::from_str("Token freeze not yet implemented")) + } + + /// Unfreeze tokens for a specific identity. + /// + /// # Arguments + /// + /// * `data_contract_id` - The ID of the data contract containing the token + /// * `token_position` - The position of the token in the contract (0-indexed) + /// * `identity_to_unfreeze` - The identity ID whose tokens to unfreeze + /// * `unfreezer_id` - The identity ID of the unfreezer (must have permission) + /// * `private_key_wif` - The private key in WIF format for signing + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the state transition result + #[wasm_bindgen(js_name = tokenUnfreeze)] + pub async fn token_unfreeze( + &self, + data_contract_id: String, + token_position: u16, + identity_to_unfreeze: String, + unfreezer_id: String, + private_key_wif: String, + ) -> Result { + Err(JsValue::from_str("Token unfreeze not yet implemented")) + } + + /// Destroy frozen tokens. + /// + /// # Arguments + /// + /// * `data_contract_id` - The ID of the data contract containing the token + /// * `token_position` - The position of the token in the contract (0-indexed) + /// * `identity_id` - The identity ID whose frozen tokens to destroy + /// * `destroyer_id` - The identity ID of the destroyer (must have permission) + /// * `private_key_wif` - The private key in WIF format for signing + /// + /// # Returns + /// + /// Returns a Promise that resolves to a JsValue containing the state transition result + #[wasm_bindgen(js_name = tokenDestroyFrozen)] + pub async fn token_destroy_frozen( + &self, + data_contract_id: String, + token_position: u16, + identity_id: String, + destroyer_id: String, + private_key_wif: String, + ) -> Result { + Err(JsValue::from_str("Token destroy frozen not yet implemented")) + } +} \ No newline at end of file