diff --git a/packages/wasm-sdk/CACHING.md b/packages/wasm-sdk/CACHING.md
new file mode 100644
index 00000000000..21aec736267
--- /dev/null
+++ b/packages/wasm-sdk/CACHING.md
@@ -0,0 +1,146 @@
+# WASM SDK Caching
+
+The WASM SDK implements multiple caching strategies to improve performance and reduce network requests.
+
+## How Cache Invalidation Works
+
+The cache system uses two mechanisms to ensure users get updates:
+
+### 1. ETag Support (with server.py)
+
+When using `python3 server.py`:
+- Server generates **ETags** based on file content
+- Browser validates cache using ETags
+- Returns `304 Not Modified` if unchanged
+- Automatic cache invalidation when files change
+
+### 2. Cache-First with Background Updates
+
+The service worker:
+- **Serves from cache immediately** (fast!)
+- **Updates cache in background**
+- Next reload gets the updated version
+- No manual cache busting needed
+
+### How It Works
+
+1. **First visit**: Downloads and caches WASM files
+2. **Subsequent visits**: Loads from cache instantly
+3. **Background**: Checks for updates
+4. **File changes**: New ETag → Cache updated
+5. **Next visit**: New version served
+
+## Service Worker Caching
+
+The SDK uses a Service Worker to cache the WASM binary and JavaScript files. This provides:
+
+- **Offline capability**: Once cached, the SDK can work offline
+- **Faster loading**: Subsequent visits load from cache instantly
+- **Automatic updates**: The service worker checks for updates hourly
+
+### Files Cached
+
+- `/pkg/wasm_sdk.js` - JavaScript bindings
+- `/pkg/wasm_sdk_bg.wasm` - WASM binary (several MB)
+- `/pkg/wasm_sdk.d.ts` - TypeScript definitions
+- `/index.html` - Main page
+
+## Using the Cache
+
+### Automatic Caching
+
+The service worker automatically caches files on first load. Subsequent loads will use the cached version.
+
+### Manual Cache Control
+
+The UI provides a "Clear Cache" button that:
+1. Clears the service worker cache
+2. Clears all browser caches
+3. Reloads the page with fresh resources
+
+### Using the Python Server with Cache Headers
+
+For better cache control, use the provided Python server:
+
+```bash
+# Instead of:
+python3 -m http.server 8888
+
+# Use:
+python3 server.py
+```
+
+This server adds proper cache headers:
+- WASM files: Cached for 1 week
+- JS files in /pkg/: Cached for 1 week
+- HTML files: Not cached
+- Other files: Cached for 1 hour
+
+## Browser Developer Tips
+
+### Force Refresh
+- **Chrome/Edge**: Ctrl+Shift+R (Cmd+Shift+R on Mac)
+- **Firefox**: Ctrl+F5 (Cmd+Shift+R on Mac)
+- **Safari**: Cmd+Option+R
+
+### Disable Cache in DevTools
+1. Open Developer Tools (F12)
+2. Go to Network tab
+3. Check "Disable cache"
+
+### View Service Worker
+1. Open Developer Tools
+2. Go to Application tab
+3. Click on "Service Workers" in sidebar
+4. You can manually unregister the worker here
+
+## How It Works
+
+### Automatic Cache Updates
+
+1. **Build**: Just run `./build.sh` or `wasm-pack build`
+2. **Deploy**: Upload the new files
+3. **Automatic Detection**: Service worker detects the new WASM file hash
+4. **User Notification**: Users see "New version detected!"
+5. **Update**: Users refresh to get the new version
+
+### Example Flow
+
+```bash
+# Make changes to Rust code
+vim src/lib.rs
+
+# Build (cache updates automatically!)
+./build.sh
+
+# That's it! No version increment needed
+```
+
+### Cache Names
+
+The cache is named based on content hash:
+- `wasm-sdk-cache-a1b2c3d4` (old version)
+- `wasm-sdk-cache-e5f6g7h8` (new version after rebuild)
+
+The service worker:
+- Detects the hash changed
+- Creates new cache with new name
+- Deletes old cache
+- Notifies users
+
+## Troubleshooting
+
+### Cache not working?
+- Check if service worker registered (see console)
+- Ensure HTTPS or localhost (service workers require secure context)
+- Check browser support for service workers
+
+### Old version still loading?
+1. Click "Clear Cache" button
+2. Or manually unregister service worker in DevTools
+3. Force refresh the page
+
+### Service worker errors?
+- Check console for registration errors
+- Ensure `/service-worker.js` is accessible
+- Try incognito/private mode to bypass cache
\ No newline at end of file
diff --git a/packages/wasm-sdk/index.html b/packages/wasm-sdk/index.html
index c188c223f93..54ae01aabfe 100644
--- a/packages/wasm-sdk/index.html
+++ b/packages/wasm-sdk/index.html
@@ -201,6 +201,20 @@
input:checked + .toggle-slider:before {
transform: translateX(26px);
}
+
+ /* Style for SDK config details */
+ details.sdk-config summary::-webkit-details-marker {
+ display: none;
+ }
+
+ details.sdk-config[open] summary span:last-child {
+ transform: rotate(180deg);
+ transition: transform 0.2s;
+ }
+
+ details.sdk-config summary span:last-child {
+ transition: transform 0.2s;
+ }
.query-container {
@@ -813,6 +827,48 @@
@@ -1948,6 +2004,9 @@ Results
const contractId = contractIdInput.value;
const documentType = documentTypeInput.value;
+ // Start timing
+ const startTime = performance.now();
+
try {
updateStatus('Loading data contract...', 'loading');
@@ -1999,7 +2058,7 @@ Results
}
});
- updateStatus('Data contract loaded successfully', 'success');
+ updateStatusWithTime('Data contract loaded successfully', 'success', startTime);
} catch (error) {
console.error('Error loading data contract:', error);
updateStatus(`Error loading data contract: ${error.message}`, 'error');
@@ -2835,6 +2894,13 @@ Results
statusBanner.textContent = message;
statusBanner.className = `status-banner ${type}`;
}
+
+ // Helper to update status with execution time
+ function updateStatusWithTime(message, type, startTime) {
+ const endTime = performance.now();
+ const executionTime = ((endTime - startTime) / 1000).toFixed(1);
+ updateStatus(`${message} (${executionTime}s)`, type);
+ }
async function initializeSdk(network) {
const currentRequestToken = ++initRequestCounter;
@@ -2857,7 +2923,28 @@ Results
console.log(`Discarding stale SDK initialization request ${currentRequestToken}`);
shouldContinue = false;
} else {
- newSdk = await WasmSdkBuilder.new_mainnet_trusted().build();
+ let builder = WasmSdkBuilder.new_mainnet_trusted();
+
+ // Apply configuration options
+ const platformVersion = document.getElementById('platformVersion').value;
+ if (platformVersion) {
+ builder = builder.with_version(parseInt(platformVersion, 10));
+ }
+
+ // Apply request settings
+ const connectTimeout = document.getElementById('connectTimeout').value;
+ const requestTimeout = document.getElementById('requestTimeout').value;
+ const retries = document.getElementById('retries').value;
+ const banFailedAddress = document.getElementById('banFailedAddress').checked;
+
+ builder = builder.with_settings(
+ connectTimeout ? parseInt(connectTimeout, 10) : undefined,
+ requestTimeout ? parseInt(requestTimeout, 10) : undefined,
+ retries ? parseInt(retries, 10) : undefined,
+ banFailedAddress || undefined
+ );
+
+ newSdk = await builder.build();
}
} else {
await prefetch_trusted_quorums_testnet();
@@ -2865,7 +2952,28 @@ Results
console.log(`Discarding stale SDK initialization request ${currentRequestToken}`);
shouldContinue = false;
} else {
- newSdk = await WasmSdkBuilder.new_testnet_trusted().build();
+ let builder = WasmSdkBuilder.new_testnet_trusted();
+
+ // Apply configuration options
+ const platformVersion = document.getElementById('platformVersion').value;
+ if (platformVersion) {
+ builder = builder.with_version(parseInt(platformVersion, 10));
+ }
+
+ // Apply request settings
+ const connectTimeout = document.getElementById('connectTimeout').value;
+ const requestTimeout = document.getElementById('requestTimeout').value;
+ const retries = document.getElementById('retries').value;
+ const banFailedAddress = document.getElementById('banFailedAddress').checked;
+
+ builder = builder.with_settings(
+ connectTimeout ? parseInt(connectTimeout, 10) : undefined,
+ requestTimeout ? parseInt(requestTimeout, 10) : undefined,
+ retries ? parseInt(retries, 10) : undefined,
+ banFailedAddress || undefined
+ );
+
+ newSdk = await builder.build();
}
}
} else if (shouldContinue) {
@@ -2885,6 +2993,14 @@ Results
// When the proof toggle is ON (checked), queries return verified data WITH proof information displayed
// When the proof toggle is OFF (unchecked), queries return verified data WITHOUT proof information displayed
// Note: Both modes use proof verification internally; the toggle only controls whether proof details are shown
+
+ // Display latest platform version info
+ try {
+ const latestVersion = WasmSdkBuilder.getLatestVersionNumber();
+ document.getElementById('latestVersionInfo').textContent = `(Latest: v${latestVersion})`;
+ } catch (e) {
+ console.warn('Could not get latest platform version:', e);
+ }
}
} catch (error) {
if (currentRequestToken === initRequestCounter) {
@@ -3252,6 +3368,9 @@ Results
alert('Invalid query selection');
return;
}
+
+ // Start timing
+ const startTime = performance.now();
const button = document.getElementById('executeQuery');
const originalButtonText = button.textContent;
@@ -3832,7 +3951,11 @@ Results
}
displayResult(result);
- updateStatus(`${queryDef.label} executed successfully`, 'success');
+
+ // Calculate and display execution time
+ const endTime = performance.now();
+ const executionTime = ((endTime - startTime) / 1000).toFixed(1);
+ updateStatus(`${queryDef.label} executed successfully (${executionTime}s)`, 'success');
} catch (error) {
console.error(`Error executing ${queryType}:`, error);
displayResult(`Error executing query: ${error.message || error}`, true);
@@ -3849,6 +3972,9 @@ Results
button.disabled = true;
button.textContent = 'Processing...';
+ // Start timing
+ const startTime = performance.now();
+
updateStatus(`Executing ${transitionType} state transition...`, 'loading');
try {
@@ -3882,7 +4008,7 @@ Results
values.publicNote || null
);
displayResult(JSON.stringify(result, null, 2));
- updateStatus('Token mint executed successfully', 'success');
+ updateStatusWithTime('Token mint executed successfully', 'success', startTime);
} else if (transitionType === 'tokenBurn') {
result = await sdk.tokenBurn(
values.contractId,
@@ -3894,7 +4020,7 @@ Results
values.publicNote || null
);
displayResult(JSON.stringify(result, null, 2));
- updateStatus('Token burn executed successfully', 'success');
+ updateStatusWithTime('Token burn executed successfully', 'success', startTime);
} else if (transitionType === 'documentCreate') {
// Collect document fields from dynamic inputs
const documentData = collectDocumentFields();
@@ -3918,7 +4044,7 @@ Results
// Pass the result object directly to displayResult
displayResult(result);
- updateStatus('Document created successfully', 'success');
+ updateStatusWithTime('Document created successfully', 'success', startTime);
} else if (transitionType === 'documentReplace') {
// Collect document fields from dynamic inputs
const documentData = collectDocumentFields();
@@ -3942,7 +4068,7 @@ Results
// Pass the result object directly to displayResult
displayResult(result);
- updateStatus('Document replaced successfully', 'success');
+ updateStatusWithTime('Document replaced successfully', 'success', startTime);
// Reset loaded data
loadedDocumentData = null;
@@ -3960,7 +4086,7 @@ Results
// Pass the result object directly to displayResult
displayResult(result);
- updateStatus('Document deleted successfully', 'success');
+ updateStatusWithTime('Document deleted successfully', 'success', startTime);
} else if (transitionType === 'documentTransfer') {
// Handle document transfer
result = await sdk.documentTransfer(
@@ -3975,7 +4101,7 @@ Results
// Pass the result object directly to displayResult
displayResult(result);
- updateStatus('Document transferred successfully', 'success');
+ updateStatusWithTime('Document transferred successfully', 'success', startTime);
} else if (transitionType === 'documentSetPrice') {
// Handle document set price
result = await sdk.documentSetPrice(
@@ -3990,7 +4116,7 @@ Results
// Pass the result object directly to displayResult
displayResult(result);
- updateStatus('Document price set successfully', 'success');
+ updateStatusWithTime('Document price set successfully', 'success', startTime);
} else if (transitionType === 'dpnsRegister') {
// Handle DPNS registration
// First validate the username
@@ -4109,7 +4235,7 @@ Results
// Since our callback is called at the end, let's update the message
// to reflect that both documents were submitted
displayResult(JSON.stringify(result, null, 2));
- updateStatus('DPNS name registered successfully! Both preorder and domain documents submitted.', 'success');
+ updateStatusWithTime('DPNS name registered successfully! Both preorder and domain documents submitted.', 'success', startTime);
} else if (transitionType === 'documentPurchase') {
// Handle document purchase
result = await sdk.documentPurchase(
@@ -4124,7 +4250,7 @@ Results
// Pass the result object directly to displayResult
displayResult(result);
- updateStatus('Document purchased successfully', 'success');
+ updateStatusWithTime('Document purchased successfully', 'success', startTime);
} else if (transitionType === 'identityCreditTransfer') {
// Handle identity credit transfer
result = await sdk.identityCreditTransfer(
@@ -4137,7 +4263,7 @@ Results
// Pass the result object directly to displayResult
displayResult(result);
- updateStatus('Credits transferred successfully', 'success');
+ updateStatusWithTime('Credits transferred successfully', 'success', startTime);
} else if (transitionType === 'identityCreditWithdrawal') {
// Handle identity credit withdrawal
result = await sdk.identityCreditWithdrawal(
@@ -4151,7 +4277,7 @@ Results
// Pass the result object directly to displayResult
displayResult(result);
- updateStatus('Credits withdrawn successfully', 'success');
+ updateStatusWithTime('Credits withdrawn successfully', 'success', startTime);
} else if (transitionType === 'identityUpdate') {
// Handle identity update
@@ -4172,7 +4298,7 @@ Results
// Pass the result object directly to displayResult
displayResult(result);
- updateStatus('Identity updated successfully', 'success');
+ updateStatusWithTime('Identity updated successfully', 'success', startTime);
} else if (transitionType === 'dpnsUsername') {
// Handle DPNS Username vote (simplified version of masternode vote)
@@ -4202,7 +4328,7 @@ Results
// Pass the result object directly to displayResult
displayResult(result);
- updateStatus('DPNS username vote submitted successfully', 'success');
+ updateStatusWithTime('DPNS username vote submitted successfully', 'success', startTime);
} else if (transitionType === 'masternodeVote') {
// Handle masternode vote
@@ -4250,7 +4376,7 @@ Results
// Pass the result object directly to displayResult
displayResult(result);
- updateStatus('Vote submitted successfully', 'success');
+ updateStatusWithTime('Vote submitted successfully', 'success', startTime);
} else if (transitionType === 'dataContractCreate') {
// Get document schemas JSON
const schemasTextarea = document.querySelector('textarea[name="documentSchemas"]');
@@ -4330,7 +4456,7 @@ Results
);
displayResult(result);
- updateStatus('Data contract created successfully', 'success');
+ updateStatusWithTime('Data contract created successfully', 'success', startTime);
} else if (transitionType === 'dataContractUpdate') {
// First fetch the existing contract
updateStatus(`Fetching contract ${values.dataContractId}...`, 'loading');
@@ -4426,7 +4552,7 @@ Results
);
displayResult(result);
- updateStatus('Data contract updated successfully', 'success');
+ updateStatusWithTime('Data contract updated successfully', 'success', startTime);
} else {
// For other transitions not yet implemented
const transitionData = {
@@ -4679,6 +4805,14 @@ Results
});
// Trusted mode is always on, no need for event listener
+
+ // Apply Configuration button handler
+ document.getElementById('applyConfig').addEventListener('click', async () => {
+ const network = document.querySelector('input[name="network"]:checked')?.value || 'mainnet';
+ clearResults();
+ updateStatus('Applying new SDK configuration...', 'loading');
+ await initializeSdk(network);
+ });
// Store loaded document data globally for document replace
let loadedDocumentData = null;
@@ -4699,6 +4833,9 @@ Results
return;
}
+ // Start timing
+ const startTime = performance.now();
+
try {
updateStatus('Loading document and schema...', 'loading');
@@ -4738,7 +4875,7 @@ Results
populateDocumentFields(docType, fieldsContainer, loadedDocumentData);
}
- updateStatus(`Document loaded successfully (revision: ${loadedDocumentRevision})`, 'success');
+ updateStatusWithTime(`Document loaded successfully (revision: ${loadedDocumentRevision})`, 'success', startTime);
} catch (error) {
console.error('Error loading document:', error);
updateStatus(`Error loading document: ${error.message}`, 'error');
diff --git a/packages/wasm-sdk/src/sdk.rs b/packages/wasm-sdk/src/sdk.rs
index 2954713fde6..5b326a02a13 100644
--- a/packages/wasm-sdk/src/sdk.rs
+++ b/packages/wasm-sdk/src/sdk.rs
@@ -13,11 +13,15 @@ use dash_sdk::platform::transition::put_identity::PutIdentity;
use dash_sdk::platform::{DataContract, Document, DocumentQuery, Fetch, Identifier, Identity};
use dash_sdk::sdk::AddressList;
use dash_sdk::{Sdk, SdkBuilder};
+use dapi_grpc::platform::VersionedGrpcResponse;
use platform_value::platform_value;
+use dash_sdk::dpp::version::{PlatformVersion, PlatformVersionCurrentVersion};
+use rs_dapi_client::RequestSettings;
use std::collections::BTreeMap;
use std::fmt::Debug;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
+use std::time::Duration;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsError, JsValue};
use web_sys::{console, js_sys};
@@ -129,6 +133,11 @@ impl DerefMut for WasmSdkBuilder {
#[wasm_bindgen]
impl WasmSdkBuilder {
+ /// Get the latest platform version number
+ #[wasm_bindgen(js_name = "getLatestVersionNumber")]
+ pub fn get_latest_version_number() -> u32 {
+ PlatformVersion::latest().protocol_version
+ }
pub fn new_mainnet() -> Self {
// Mainnet addresses from mnowatch.org
let mainnet_addresses = vec![
@@ -655,6 +664,56 @@ impl WasmSdkBuilder {
WasmSdkBuilder(self.0.with_context_provider(context_provider))
}
+ /// Configure platform version to use.
+ ///
+ /// Available versions:
+ /// - 1: Platform version 1
+ /// - 2: Platform version 2
+ /// - ... up to latest version
+ ///
+ /// Defaults to latest version if not specified.
+ pub fn with_version(self, version_number: u32) -> Result {
+ let version = PlatformVersion::get(version_number)
+ .map_err(|e| JsError::new(&format!("Invalid platform version {}: {}", version_number, e)))?;
+
+ Ok(WasmSdkBuilder(self.0.with_version(version)))
+ }
+
+ /// Configure request settings for the SDK.
+ ///
+ /// Settings include:
+ /// - connect_timeout_ms: Timeout for establishing connection (in milliseconds)
+ /// - timeout_ms: Timeout for single request (in milliseconds)
+ /// - retries: Number of retries in case of failed requests
+ /// - ban_failed_address: Whether to ban DAPI address if node not responded or responded with error
+ pub fn with_settings(
+ self,
+ connect_timeout_ms: Option,
+ timeout_ms: Option,
+ retries: Option,
+ ban_failed_address: Option,
+ ) -> Self {
+ let mut settings = RequestSettings::default();
+
+ if let Some(connect_timeout) = connect_timeout_ms {
+ settings.connect_timeout = Some(Duration::from_millis(connect_timeout as u64));
+ }
+
+ if let Some(timeout) = timeout_ms {
+ settings.timeout = Some(Duration::from_millis(timeout as u64));
+ }
+
+ if let Some(retries) = retries {
+ settings.retries = Some(retries as usize);
+ }
+
+ if let Some(ban) = ban_failed_address {
+ settings.ban_failed_address = Some(ban);
+ }
+
+ WasmSdkBuilder(self.0.with_settings(settings))
+ }
+
// TODO: Add with_proofs method when it's available in the SDK builder
// pub fn with_proofs(self, enable_proofs: bool) -> Self {
// WasmSdkBuilder(self.0.with_proofs(enable_proofs))