From eb1ef25fd5364179b03c3d701585cd3664b5e8eb Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 23 Dec 2025 20:33:29 +0700 Subject: [PATCH 01/11] feat(sdk): query platform addresses in JS SDK --- packages/js-evo-sdk/src/addresses/facade.ts | 56 +++ packages/js-evo-sdk/src/sdk.ts | 4 + .../js-evo-sdk/tests/fixtures/testnet.mjs | 8 + .../tests/functional/addresses.spec.mjs | 47 +++ .../tests/unit/facades/addresses.spec.mjs | 109 ++++++ packages/wasm-dpp2/src/lib.rs | 2 + packages/wasm-dpp2/src/platform_address.rs | 329 ++++++++++++++++++ packages/wasm-sdk/src/lib.rs | 2 +- packages/wasm-sdk/src/queries/address.rs | 198 +++++++++++ packages/wasm-sdk/src/queries/mod.rs | 2 + 10 files changed, 756 insertions(+), 1 deletion(-) create mode 100644 packages/js-evo-sdk/src/addresses/facade.ts create mode 100644 packages/js-evo-sdk/tests/functional/addresses.spec.mjs create mode 100644 packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs create mode 100644 packages/wasm-dpp2/src/platform_address.rs create mode 100644 packages/wasm-sdk/src/queries/address.rs diff --git a/packages/js-evo-sdk/src/addresses/facade.ts b/packages/js-evo-sdk/src/addresses/facade.ts new file mode 100644 index 00000000000..45d71226a09 --- /dev/null +++ b/packages/js-evo-sdk/src/addresses/facade.ts @@ -0,0 +1,56 @@ +import * as wasm from '../wasm.js'; +import type { EvoSDK } from '../sdk.js'; + +export class AddressesFacade { + private sdk: EvoSDK; + + constructor(sdk: EvoSDK) { + this.sdk = sdk; + } + + /** + * Fetches information about a Platform address including its nonce and balance. + * + * @param address - The platform address to query (PlatformAddress, Uint8Array, or bech32m string) + * @returns AddressInfo containing address, nonce, and balance, or undefined if not found + */ + async get(address: wasm.PlatformAddressLike): Promise { + const w = await this.sdk.getWasmSdkConnected(); + return w.getAddressInfo(address); + } + + /** + * Fetches information about a Platform address with proof. + * + * @param address - The platform address to query (PlatformAddress, Uint8Array, or bech32m string) + * @returns ProofMetadataResponse containing AddressInfo with proof information + */ + async getWithProof(address: wasm.PlatformAddressLike): Promise> { + const w = await this.sdk.getWasmSdkConnected(); + return w.getAddressInfoWithProofInfo(address); + } + + /** + * Fetches information about multiple Platform addresses. + * + * @param addresses - Array of platform addresses to query + * @returns Map of PlatformAddress to AddressInfo (or undefined for unfunded addresses) + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async getMany(addresses: wasm.PlatformAddressLike[]): Promise> { + const w = await this.sdk.getWasmSdkConnected(); + return w.getAddressesInfos(addresses); + } + + /** + * Fetches information about multiple Platform addresses with proof. + * + * @param addresses - Array of platform addresses to query + * @returns ProofMetadataResponse containing Map of PlatformAddress to AddressInfo + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async getManyWithProof(addresses: wasm.PlatformAddressLike[]): Promise>> { + const w = await this.sdk.getWasmSdkConnected(); + return w.getAddressesInfosWithProofInfo(addresses); + } +} diff --git a/packages/js-evo-sdk/src/sdk.ts b/packages/js-evo-sdk/src/sdk.ts index 7c5e34fe45b..e3182b0c71b 100644 --- a/packages/js-evo-sdk/src/sdk.ts +++ b/packages/js-evo-sdk/src/sdk.ts @@ -1,5 +1,6 @@ import * as wasm from './wasm.js'; import { ensureInitialized as initWasm } from './wasm.js'; +import { AddressesFacade } from './addresses/facade.js'; import { DocumentsFacade } from './documents/facade.js'; import { IdentitiesFacade } from './identities/facade.js'; import { ContractsFacade } from './contracts/facade.js'; @@ -38,6 +39,7 @@ export class EvoSDK { private wasmSdk?: wasm.WasmSdk; private options: Required> & ConnectionOptions & { addresses?: string[] }; + public addresses!: AddressesFacade; public documents!: DocumentsFacade; public identities!: IdentitiesFacade; public contracts!: ContractsFacade; @@ -53,6 +55,7 @@ export class EvoSDK { const { network = 'testnet', trusted = false, addresses, ...connection } = options; this.options = { network, trusted, addresses, ...connection }; + this.addresses = new AddressesFacade(this); this.documents = new DocumentsFacade(this); this.identities = new IdentitiesFacade(this); this.contracts = new ContractsFacade(this); @@ -164,6 +167,7 @@ export class EvoSDK { } } +export { AddressesFacade } from './addresses/facade.js'; export { DocumentsFacade } from './documents/facade.js'; export { IdentitiesFacade } from './identities/facade.js'; export { ContractsFacade } from './contracts/facade.js'; diff --git a/packages/js-evo-sdk/tests/fixtures/testnet.mjs b/packages/js-evo-sdk/tests/fixtures/testnet.mjs index b91b64c81cb..2a3fc072917 100644 --- a/packages/js-evo-sdk/tests/fixtures/testnet.mjs +++ b/packages/js-evo-sdk/tests/fixtures/testnet.mjs @@ -17,6 +17,14 @@ export const TEST_IDS = { username: 'alice', existingUsername: 'therealslimshaddy5', epoch: 8635, + // Platform address fixtures (21 bytes: type byte + 20-byte hash) + // Type 0x00 = P2PKH, Type 0x01 = P2SH + // These are test addresses with zero hashes - queries will return undefined if not funded + platformAddressBytes: new Uint8Array([0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + platformAddressesBytesArray: [ + new Uint8Array([0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + new Uint8Array([0x00, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + ], }; // Optional environment-driven secrets for state-transition tests (skipped by default). diff --git a/packages/js-evo-sdk/tests/functional/addresses.spec.mjs b/packages/js-evo-sdk/tests/functional/addresses.spec.mjs new file mode 100644 index 00000000000..3b4362aef68 --- /dev/null +++ b/packages/js-evo-sdk/tests/functional/addresses.spec.mjs @@ -0,0 +1,47 @@ +import { EvoSDK } from '../../dist/evo-sdk.module.js'; +import { TEST_IDS } from '../fixtures/testnet.mjs'; + +describe('Addresses', function addressesSuite() { + this.timeout(60000); + let sdk; + + before(async () => { + sdk = EvoSDK.testnetTrusted(); + await sdk.connect(); + }); + + // Note: Platform addresses need to be funded on testnet for these tests to return data. + // If no funded addresses exist, the tests will still pass but return undefined/empty results. + + it('get() returns address info or undefined for unfunded address', async () => { + const testAddress = TEST_IDS.platformAddressBytes; + const res = await sdk.addresses.get(testAddress); + // Result is undefined for unfunded/non-existent addresses + expect(res === undefined || res !== null).to.be.true(); + }); + + it('getWithProof() returns proof info for address query', async () => { + const testAddress = TEST_IDS.platformAddressBytes; + const res = await sdk.addresses.getWithProof(testAddress); + expect(res).to.exist(); + expect(res.proof).to.exist(); + expect(res.metadata).to.exist(); + // data may be undefined for unfunded addresses + }); + + it('getMany() returns map of address infos', async () => { + const testAddresses = TEST_IDS.platformAddressesBytesArray; + const res = await sdk.addresses.getMany(testAddresses); + expect(res).to.be.instanceOf(Map); + expect(res.size).to.equal(testAddresses.length); + }); + + it('getManyWithProof() returns proof info for multiple addresses', async () => { + const testAddresses = TEST_IDS.platformAddressesBytesArray; + const res = await sdk.addresses.getManyWithProof(testAddresses); + expect(res).to.exist(); + expect(res.proof).to.exist(); + expect(res.metadata).to.exist(); + expect(res.data).to.be.instanceOf(Map); + }); +}); diff --git a/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs b/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs new file mode 100644 index 00000000000..45f7da2a09d --- /dev/null +++ b/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs @@ -0,0 +1,109 @@ +import init, * as wasmSDKPackage from '@dashevo/wasm-sdk'; +import { EvoSDK } from '../../../dist/sdk.js'; + +describe('AddressesFacade', () => { + let wasmSdk; + let client; + + beforeEach(async function setup() { + await init(); + const builder = wasmSDKPackage.WasmSdkBuilder.testnetTrusted(); + wasmSdk = builder.build(); + client = EvoSDK.fromWasm(wasmSdk); + + this.sinon.stub(wasmSdk, 'getAddressInfo').resolves('ok'); + this.sinon.stub(wasmSdk, 'getAddressInfoWithProofInfo').resolves('ok'); + this.sinon.stub(wasmSdk, 'getAddressesInfos').resolves('ok'); + this.sinon.stub(wasmSdk, 'getAddressesInfosWithProofInfo').resolves('ok'); + }); + + it('get() forwards address to getAddressInfo', async () => { + const address = 'tdashevo1qr4nl2m5z7v7g7d2c4z6k8x9w3y2f5p6h0s1t4'; + await client.addresses.get(address); + expect(wasmSdk.getAddressInfo).to.be.calledOnceWithExactly(address); + }); + + it('getWithProof() forwards address to getAddressInfoWithProofInfo', async () => { + const address = 'tdashevo1qr4nl2m5z7v7g7d2c4z6k8x9w3y2f5p6h0s1t4'; + await client.addresses.getWithProof(address); + expect(wasmSdk.getAddressInfoWithProofInfo).to.be.calledOnceWithExactly(address); + }); + + it('getMany() forwards array of addresses to getAddressesInfos', async () => { + const addresses = [ + 'tdashevo1qr4nl2m5z7v7g7d2c4z6k8x9w3y2f5p6h0s1t4', + 'tdashevo1abc123def456ghi789jkl012mno345pqr678st', + ]; + await client.addresses.getMany(addresses); + expect(wasmSdk.getAddressesInfos).to.be.calledOnceWithExactly(addresses); + }); + + it('getManyWithProof() forwards array of addresses to getAddressesInfosWithProofInfo', async () => { + const addresses = [ + 'tdashevo1qr4nl2m5z7v7g7d2c4z6k8x9w3y2f5p6h0s1t4', + ]; + await client.addresses.getManyWithProof(addresses); + expect(wasmSdk.getAddressesInfosWithProofInfo).to.be.calledOnceWithExactly(addresses); + }); + + it('get() accepts Uint8Array address', async () => { + const addressBytes = new Uint8Array(21); + await client.addresses.get(addressBytes); + expect(wasmSdk.getAddressInfo).to.be.calledOnceWithExactly(addressBytes); + }); + + it('getMany() accepts mixed address formats', async () => { + const bech32Address = 'tdashevo1qr4nl2m5z7v7g7d2c4z6k8x9w3y2f5p6h0s1t4'; + const bytesAddress = new Uint8Array(21); + const mixedAddresses = [bech32Address, bytesAddress]; + await client.addresses.getMany(mixedAddresses); + expect(wasmSdk.getAddressesInfos).to.be.calledOnceWithExactly(mixedAddresses); + }); +}); + +describe('PlatformAddress', () => { + let PlatformAddress; + + before(async () => { + await init(); + PlatformAddress = wasmSDKPackage.PlatformAddress; + }); + + it('can be created from bytes', () => { + // Create P2PKH address from bytes (type 0x00) + const p2pkhBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const p2pkhAddr = PlatformAddress.fromBytes(Array.from(p2pkhBytes)); + expect(p2pkhAddr).to.exist(); + expect(p2pkhAddr.addressType).to.equal('P2PKH'); + expect(p2pkhAddr.isP2pkh).to.be.true(); + expect(p2pkhAddr.isP2sh).to.be.false(); + + // Create P2SH address from bytes (type 0x01) + const p2shBytes = new Uint8Array([0x01, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const p2shAddr = PlatformAddress.fromBytes(Array.from(p2shBytes)); + expect(p2shAddr).to.exist(); + expect(p2shAddr.addressType).to.equal('P2SH'); + expect(p2shAddr.isP2pkh).to.be.false(); + expect(p2shAddr.isP2sh).to.be.true(); + }); + + it('converts to bech32m and back', () => { + // Create address from bytes + const originalBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = PlatformAddress.fromBytes(Array.from(originalBytes)); + + // Convert to bech32m for testnet + const bech32m = addr.toBech32m('testnet'); + expect(bech32m).to.be.a('string'); + expect(bech32m.startsWith('tdashevo1')).to.be.true(); + + // Convert back from bech32m + const recoveredAddr = PlatformAddress.fromBech32m(bech32m); + expect(recoveredAddr).to.exist(); + expect(recoveredAddr.addressType).to.equal(addr.addressType); + + // Verify bytes match + const recoveredBytes = recoveredAddr.toBytes(); + expect(Array.from(recoveredBytes)).to.deep.equal(Array.from(originalBytes)); + }); +}); diff --git a/packages/wasm-dpp2/src/lib.rs b/packages/wasm-dpp2/src/lib.rs index 9610a67545a..78121574225 100644 --- a/packages/wasm-dpp2/src/lib.rs +++ b/packages/wasm-dpp2/src/lib.rs @@ -21,6 +21,7 @@ pub mod group; pub mod identifier; pub mod identity; pub mod mock_bls; +pub mod platform_address; pub mod private_key; pub mod public_key; pub mod state_transitions; @@ -42,6 +43,7 @@ pub use identity::{ }; pub use state_transitions::base::{GroupStateTransitionInfoWasm, StateTransitionWasm}; pub use tokens::*; +pub use platform_address::PlatformAddressWasm; pub use voting::{ ContenderWithSerializedDocumentWasm, ContestedDocumentVotePollWinnerInfoWasm, ResourceVoteChoiceWasm, VotePollWasm, VoteWasm, diff --git a/packages/wasm-dpp2/src/platform_address.rs b/packages/wasm-dpp2/src/platform_address.rs new file mode 100644 index 00000000000..f47a8a0cd75 --- /dev/null +++ b/packages/wasm-dpp2/src/platform_address.rs @@ -0,0 +1,329 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::utils::IntoWasm; +use dpp::address_funds::PlatformAddress; +use dpp::dashcore::Network; +use js_sys::Uint8Array; +use serde::de::{self, Error, Visitor}; +use serde::{Deserialize, Deserializer}; +use std::fmt; +use wasm_bindgen::prelude::*; + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[wasm_bindgen(js_name = "PlatformAddress")] +pub struct PlatformAddressWasm(PlatformAddress); + +#[wasm_bindgen(typescript_custom_section)] +const PLATFORM_ADDRESS_TS_HELPERS: &'static str = r#" +/** + * A Platform address can be provided as: + * - A PlatformAddress object + * - A Uint8Array (21 bytes: type byte + 20-byte hash) + * - A bech32m string (e.g., "dashevo1..." or "tdashevo1...") + */ +export type PlatformAddressLike = PlatformAddress | Uint8Array | string; +"#; + +impl From for PlatformAddress { + fn from(address: PlatformAddressWasm) -> Self { + address.0 + } +} + +impl From for PlatformAddressWasm { + fn from(address: PlatformAddress) -> Self { + PlatformAddressWasm(address) + } +} + +impl From<&PlatformAddressWasm> for PlatformAddress { + fn from(address: &PlatformAddressWasm) -> Self { + address.0 + } +} + +impl TryFrom<&[u8]> for PlatformAddressWasm { + type Error = WasmDppError; + + fn try_from(value: &[u8]) -> Result { + PlatformAddress::from_bytes(value) + .map(PlatformAddressWasm) + .map_err(|e| WasmDppError::invalid_argument(e.to_string())) + } +} + +impl TryFrom for PlatformAddressWasm { + type Error = WasmDppError; + + fn try_from(value: JsValue) -> Result { + if value.is_undefined() || value.is_null() { + return Err(WasmDppError::invalid_argument( + "the platform address cannot be null or undefined", + )); + } + + // Check if it's already a PlatformAddressWasm + if let Ok(existing) = value + .clone() + .to_wasm::("PlatformAddress") + { + return Ok(*existing); + } + + // Try parsing as string (bech32m) + if let Some(string) = value.as_string() { + return PlatformAddressWasm::try_from(string.as_str()); + } + + // Try parsing as bytes + if value.is_instance_of::() || value.is_array() || value.is_object() { + let uint8_array = Uint8Array::from(value.clone()); + let bytes = uint8_array.to_vec(); + + return PlatformAddress::from_bytes(&bytes) + .map(PlatformAddressWasm) + .map_err(|e| WasmDppError::invalid_argument(e.to_string())); + } + + Err(WasmDppError::invalid_argument( + "Invalid platform address. Expected PlatformAddress, Uint8Array, or bech32m string", + )) + } +} + +impl TryFrom<&JsValue> for PlatformAddressWasm { + type Error = WasmDppError; + + fn try_from(value: &JsValue) -> Result { + PlatformAddressWasm::try_from(value.clone()) + } +} + +impl TryFrom<&str> for PlatformAddressWasm { + type Error = WasmDppError; + + fn try_from(value: &str) -> Result { + // Try parsing as bech32m string + PlatformAddress::from_bech32m_string(value) + .map(|(addr, _network)| PlatformAddressWasm(addr)) + .map_err(|e| WasmDppError::invalid_argument(e.to_string())) + } +} + +struct PlatformAddressWasmVisitor; + +impl<'de> Visitor<'de> for PlatformAddressWasmVisitor { + type Value = PlatformAddressWasm; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("PlatformAddress or compatible representation") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + PlatformAddressWasm::try_from(value).map_err(|err| E::custom(err.to_string())) + } + + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + self.visit_str(&value) + } + + fn visit_bytes(self, value: &[u8]) -> Result + where + E: de::Error, + { + PlatformAddress::from_bytes(value) + .map(PlatformAddressWasm) + .map_err(|e| E::custom(e.to_string())) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + let mut bytes: Vec = Vec::new(); + while let Some(byte) = seq.next_element::()? { + bytes.push(byte); + } + PlatformAddress::from_bytes(&bytes) + .map(PlatformAddressWasm) + .map_err(|e| A::Error::custom(e.to_string())) + } +} + +impl<'de> Deserialize<'de> for PlatformAddressWasm { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(PlatformAddressWasmVisitor) + } +} + +fn parse_network(network: &str) -> Result { + match network.to_lowercase().as_str() { + "mainnet" | "dash" => Ok(Network::Dash), + "testnet" => Ok(Network::Testnet), + "devnet" => Ok(Network::Devnet), + "regtest" => Ok(Network::Regtest), + _ => Err(WasmDppError::invalid_argument(format!( + "Invalid network: {}. Expected 'mainnet', 'testnet', 'devnet', or 'regtest'", + network + ))), + } +} + +#[wasm_bindgen(js_class = PlatformAddress)] +impl PlatformAddressWasm { + #[wasm_bindgen(getter = __type)] + pub fn type_name(&self) -> String { + "PlatformAddress".to_string() + } + + #[wasm_bindgen(getter = __struct)] + pub fn struct_name() -> String { + "PlatformAddress".to_string() + } + + /// Creates a new PlatformAddress from various input types. + /// + /// Accepts: + /// - A bech32m string (e.g., "dashevo1..." or "tdashevo1...") + /// - A Uint8Array (21 bytes: type byte + 20-byte hash) + /// - An existing PlatformAddress object + #[wasm_bindgen(constructor)] + pub fn new( + #[wasm_bindgen(unchecked_param_type = "PlatformAddress | Uint8Array | string")] + js_address: &JsValue, + ) -> WasmDppResult { + PlatformAddressWasm::try_from(js_address) + } + + /// Returns the bech32m-encoded address string for the specified network. + /// + /// @param network - "mainnet", "testnet", "devnet", or "regtest" + #[wasm_bindgen(js_name = "toBech32m")] + pub fn to_bech32m(&self, network: &str) -> WasmDppResult { + let net = parse_network(network)?; + Ok(self.0.to_bech32m_string(net)) + } + + /// Returns the raw bytes of the address (21 bytes: type byte + 20-byte hash). + #[wasm_bindgen(js_name = "toBytes")] + pub fn to_bytes(&self) -> Vec { + self.0.to_bytes() + } + + /// Returns the hex-encoded address bytes. + #[wasm_bindgen(js_name = "toHex")] + pub fn to_hex(&self) -> String { + hex::encode(self.0.to_bytes()) + } + + /// Returns the address type: "P2PKH" or "P2SH". + #[wasm_bindgen(js_name = "addressType", getter)] + pub fn address_type(&self) -> String { + match self.0 { + PlatformAddress::P2pkh(_) => "P2PKH".to_string(), + PlatformAddress::P2sh(_) => "P2SH".to_string(), + } + } + + /// Returns true if this is a P2PKH address. + #[wasm_bindgen(js_name = "isP2pkh", getter)] + pub fn is_p2pkh(&self) -> bool { + self.0.is_p2pkh() + } + + /// Returns true if this is a P2SH address. + #[wasm_bindgen(js_name = "isP2sh", getter)] + pub fn is_p2sh(&self) -> bool { + self.0.is_p2sh() + } + + /// Returns the 20-byte hash portion of the address. + #[wasm_bindgen(js_name = "hash")] + pub fn hash(&self) -> Vec { + self.0.hash().to_vec() + } + + /// Returns the hash as a hex string. + #[wasm_bindgen(js_name = "hashToHex")] + pub fn hash_to_hex(&self) -> String { + hex::encode(self.0.hash()) + } + + /// Creates a PlatformAddress from a bech32m-encoded string. + /// + /// Accepts addresses with either mainnet ("dashevo") or testnet ("tdashevo") HRP. + #[wasm_bindgen(js_name = "fromBech32m")] + pub fn from_bech32m(address: &str) -> WasmDppResult { + PlatformAddress::from_bech32m_string(address) + .map(|(addr, _)| PlatformAddressWasm(addr)) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()).into()) + } + + /// Creates a PlatformAddress from raw bytes (21 bytes: type byte + 20-byte hash). + #[wasm_bindgen(js_name = "fromBytes")] + pub fn from_bytes(bytes: Vec) -> WasmDppResult { + PlatformAddress::from_bytes(&bytes) + .map(PlatformAddressWasm) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()).into()) + } + + /// Creates a PlatformAddress from a hex-encoded string. + #[wasm_bindgen(js_name = "fromHex")] + pub fn from_hex(hex_string: &str) -> WasmDppResult { + let bytes = hex::decode(hex_string) + .map_err(|e| WasmDppError::invalid_argument(format!("Invalid hex: {}", e)))?; + PlatformAddress::from_bytes(&bytes) + .map(PlatformAddressWasm) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()).into()) + } + + /// Creates a P2PKH address from a 20-byte public key hash. + #[wasm_bindgen(js_name = "fromP2pkhHash")] + pub fn from_p2pkh_hash(hash: Vec) -> WasmDppResult { + if hash.len() != 20 { + return Err(WasmDppError::invalid_argument(format!( + "P2PKH hash must be 20 bytes, got {}", + hash.len() + )) + .into()); + } + let mut arr = [0u8; 20]; + arr.copy_from_slice(&hash); + Ok(PlatformAddressWasm(PlatformAddress::P2pkh(arr))) + } + + /// Creates a P2SH address from a 20-byte script hash. + #[wasm_bindgen(js_name = "fromP2shHash")] + pub fn from_p2sh_hash(hash: Vec) -> WasmDppResult { + if hash.len() != 20 { + return Err(WasmDppError::invalid_argument(format!( + "P2SH hash must be 20 bytes, got {}", + hash.len() + )) + .into()); + } + let mut arr = [0u8; 20]; + arr.copy_from_slice(&hash); + Ok(PlatformAddressWasm(PlatformAddress::P2sh(arr))) + } +} + +impl PlatformAddressWasm { + /// Returns the inner PlatformAddress. + pub fn inner(&self) -> &PlatformAddress { + &self.0 + } + + /// Consumes self and returns the inner PlatformAddress. + pub fn into_inner(self) -> PlatformAddress { + self.0 + } +} diff --git a/packages/wasm-sdk/src/lib.rs b/packages/wasm-sdk/src/lib.rs index 790da0f6589..9ef1ca97fb5 100644 --- a/packages/wasm-sdk/src/lib.rs +++ b/packages/wasm-sdk/src/lib.rs @@ -13,7 +13,7 @@ pub mod wallet; // Re-export commonly used items pub use dpns::*; pub use error::{WasmSdkError, WasmSdkErrorKind}; -pub use queries::{ProofInfoWasm, ProofMetadataResponseWasm, ResponseMetadataWasm}; +pub use queries::{AddressInfoWasm, ProofInfoWasm, ProofMetadataResponseWasm, ResponseMetadataWasm}; pub use state_transitions::identity as state_transition_identity; pub use wallet::*; pub use wasm_dpp2::*; diff --git a/packages/wasm-sdk/src/queries/address.rs b/packages/wasm-sdk/src/queries/address.rs new file mode 100644 index 00000000000..8a4e64db193 --- /dev/null +++ b/packages/wasm-sdk/src/queries/address.rs @@ -0,0 +1,198 @@ +use crate::error::WasmSdkError; +use crate::queries::ProofMetadataResponseWasm; +use crate::sdk::WasmSdk; +use dash_sdk::dpp::address_funds::PlatformAddress; +use dash_sdk::platform::{Fetch, FetchMany}; +use drive_proof_verifier::types::{AddressInfo, AddressInfos}; +use js_sys::{BigInt, Map}; +use std::collections::BTreeSet; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; +use wasm_dpp2::platform_address::PlatformAddressWasm; + +/// Information about a Platform address including its nonce and balance. +#[wasm_bindgen(js_name = "AddressInfo")] +#[derive(Clone)] +pub struct AddressInfoWasm { + address: PlatformAddressWasm, + nonce: u32, + balance: u64, +} + +impl AddressInfoWasm { + fn from_address_info(info: AddressInfo) -> Self { + AddressInfoWasm { + address: info.address.into(), + nonce: info.nonce, + balance: info.balance, + } + } +} + +#[wasm_bindgen(js_class = AddressInfo)] +impl AddressInfoWasm { + /// Returns the platform address. + #[wasm_bindgen(getter = "address")] + pub fn address(&self) -> PlatformAddressWasm { + self.address + } + + /// Returns the nonce associated with the address. + #[wasm_bindgen(getter = "nonce")] + pub fn nonce(&self) -> BigInt { + BigInt::from(self.nonce) + } + + /// Returns the balance stored for the address in credits. + #[wasm_bindgen(getter = "balance")] + pub fn balance(&self) -> BigInt { + BigInt::from(self.balance) + } +} + +#[wasm_bindgen] +impl WasmSdk { + /// Fetches information about a Platform address including its nonce and balance. + /// + /// @param address - The platform address to query (PlatformAddress, Uint8Array, or bech32m string) + /// @returns AddressInfo containing address, nonce, and balance + #[wasm_bindgen(js_name = "getAddressInfo")] + pub async fn get_address_info( + &self, + #[wasm_bindgen(unchecked_param_type = "PlatformAddressLike")] address: JsValue, + ) -> Result, WasmSdkError> { + let platform_address: PlatformAddress = PlatformAddressWasm::try_from(&address) + .map_err(|err| { + WasmSdkError::invalid_argument(format!("Invalid platform address: {}", err)) + })? + .into(); + + let address_info = AddressInfo::fetch(self.as_ref(), platform_address).await?; + + Ok(address_info.map(AddressInfoWasm::from_address_info)) + } + + /// Fetches information about a Platform address including its nonce and balance, with proof. + /// + /// @param address - The platform address to query (PlatformAddress, Uint8Array, or bech32m string) + /// @returns ProofMetadataResponse containing AddressInfo with proof information + #[wasm_bindgen( + js_name = "getAddressInfoWithProofInfo", + unchecked_return_type = "ProofMetadataResponseTyped" + )] + pub async fn get_address_info_with_proof_info( + &self, + #[wasm_bindgen(unchecked_param_type = "PlatformAddressLike")] address: JsValue, + ) -> Result { + let platform_address: PlatformAddress = PlatformAddressWasm::try_from(&address) + .map_err(|err| { + WasmSdkError::invalid_argument(format!("Invalid platform address: {}", err)) + })? + .into(); + + let (address_info, metadata, proof) = + AddressInfo::fetch_with_metadata_and_proof(self.as_ref(), platform_address, None) + .await?; + + address_info + .map(|info| { + ProofMetadataResponseWasm::from_sdk_parts( + JsValue::from(AddressInfoWasm::from_address_info(info)), + metadata, + proof, + ) + }) + .ok_or_else(|| WasmSdkError::not_found("Address info not found")) + } + + /// Fetches information about multiple Platform addresses. + /// + /// @param addresses - Array of platform addresses to query + /// @returns Map of PlatformAddress to AddressInfo (or undefined for unfunded addresses) + #[wasm_bindgen( + js_name = "getAddressesInfos", + unchecked_return_type = "Map" + )] + pub async fn get_addresses_infos( + &self, + #[wasm_bindgen(unchecked_param_type = "Array")] addresses: Vec, + ) -> Result { + let platform_addresses: BTreeSet = addresses + .into_iter() + .map(|value| { + PlatformAddressWasm::try_from(&value) + .map(PlatformAddress::from) + .map_err(|err| { + WasmSdkError::invalid_argument(format!("Invalid platform address: {}", err)) + }) + }) + .collect::, _>>()?; + + let address_infos: AddressInfos = + AddressInfo::fetch_many(self.as_ref(), platform_addresses.clone()).await?; + + let results_map = Map::new(); + + for address in platform_addresses { + let key = JsValue::from(PlatformAddressWasm::from(address)); + let value = address_infos + .get(&address) + .and_then(|opt| opt.as_ref()) + .map(|info| JsValue::from(AddressInfoWasm::from_address_info(info.clone()))) + .unwrap_or(JsValue::UNDEFINED); + results_map.set(&key, &value); + } + + Ok(results_map) + } + + /// Fetches information about multiple Platform addresses with proof. + /// + /// @param addresses - Array of platform addresses to query + /// @returns ProofMetadataResponse containing Map of PlatformAddress to AddressInfo + #[wasm_bindgen( + js_name = "getAddressesInfosWithProofInfo", + unchecked_return_type = "ProofMetadataResponseTyped>" + )] + pub async fn get_addresses_infos_with_proof_info( + &self, + #[wasm_bindgen(unchecked_param_type = "Array")] addresses: Vec, + ) -> Result { + let platform_addresses: BTreeSet = addresses + .into_iter() + .map(|value| { + PlatformAddressWasm::try_from(&value) + .map(PlatformAddress::from) + .map_err(|err| { + WasmSdkError::invalid_argument(format!("Invalid platform address: {}", err)) + }) + }) + .collect::, _>>()?; + + let (address_infos, metadata, proof): (AddressInfos, _, _) = + AddressInfo::fetch_many_with_metadata_and_proof( + self.as_ref(), + platform_addresses.clone(), + None, + ) + .await?; + + let results_map = Map::new(); + + for address in platform_addresses { + let key = JsValue::from(PlatformAddressWasm::from(address)); + let value = address_infos + .get(&address) + .and_then(|opt| opt.as_ref()) + .map(|info| JsValue::from(AddressInfoWasm::from_address_info(info.clone()))) + .unwrap_or(JsValue::UNDEFINED); + results_map.set(&key, &value); + } + + Ok(ProofMetadataResponseWasm::from_sdk_parts( + results_map, + metadata, + proof, + )) + } +} diff --git a/packages/wasm-sdk/src/queries/mod.rs b/packages/wasm-sdk/src/queries/mod.rs index e9d6536be84..e4f931b7af8 100644 --- a/packages/wasm-sdk/src/queries/mod.rs +++ b/packages/wasm-sdk/src/queries/mod.rs @@ -1,3 +1,4 @@ +pub mod address; pub mod data_contract; pub mod document; pub mod epoch; @@ -10,6 +11,7 @@ pub(crate) mod utils; pub mod voting; // Re-export all query functions for easy access +pub use address::AddressInfoWasm; pub use group::*; use js_sys::Uint8Array; From b8ec67bf7cf0881a0e8f84274d65ffc1218373cb Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 25 Dec 2025 19:11:42 +0700 Subject: [PATCH 02/11] feat: add other state transitions --- packages/js-evo-sdk/src/addresses/facade.ts | 238 ++++ packages/js-evo-sdk/src/wasm.ts | 1 - .../tests/unit/facades/addresses.spec.mjs | 217 +++- packages/wasm-dpp2/src/enums/withdrawal.rs | 39 + packages/wasm-dpp2/src/identity/model.rs | 12 +- packages/wasm-dpp2/src/identity_signer.rs | 213 ++++ packages/wasm-dpp2/src/lib.rs | 12 +- .../address.rs} | 2 +- .../src/platform_address/fee_strategy.rs | 152 +++ .../src/platform_address/input_output.rs | 469 +++++++ .../wasm-dpp2/src/platform_address/mod.rs | 15 + .../wasm-dpp2/src/platform_address/signer.rs | 175 +++ packages/wasm-dpp2/src/private_key.rs | 13 + .../base/state_transition.rs | 6 + .../tests/unit/AddressFundsTransfer.spec.mjs | 228 ++++ .../tests/unit/IdentitySigner.spec.mjs | 90 ++ .../tests/unit/PlatformAddress.spec.mjs | 239 ++++ .../tests/unit/PlatformAddressInput.spec.mjs | 110 ++ .../tests/unit/PlatformAddressOutput.spec.mjs | 87 ++ .../tests/unit/PlatformAddressSigner.spec.mjs | 158 +++ packages/wasm-sdk/src/lib.rs | 4 +- packages/wasm-sdk/src/queries/address.rs | 14 +- packages/wasm-sdk/src/settings.rs | 284 +++++ .../src/state_transitions/addresses.rs | 1104 +++++++++++++++++ .../src/state_transitions/broadcast.rs | 102 ++ .../wasm-sdk/src/state_transitions/mod.rs | 2 + .../IdentityTopUpFromAddressesResult.spec.mjs | 120 ++ ...IdentityTransferToAddressesResult.spec.mjs | 203 +++ 28 files changed, 4288 insertions(+), 21 deletions(-) create mode 100644 packages/wasm-dpp2/src/identity_signer.rs rename packages/wasm-dpp2/src/{platform_address.rs => platform_address/address.rs} (99%) create mode 100644 packages/wasm-dpp2/src/platform_address/fee_strategy.rs create mode 100644 packages/wasm-dpp2/src/platform_address/input_output.rs create mode 100644 packages/wasm-dpp2/src/platform_address/mod.rs create mode 100644 packages/wasm-dpp2/src/platform_address/signer.rs create mode 100644 packages/wasm-dpp2/tests/unit/AddressFundsTransfer.spec.mjs create mode 100644 packages/wasm-dpp2/tests/unit/IdentitySigner.spec.mjs create mode 100644 packages/wasm-dpp2/tests/unit/PlatformAddress.spec.mjs create mode 100644 packages/wasm-dpp2/tests/unit/PlatformAddressInput.spec.mjs create mode 100644 packages/wasm-dpp2/tests/unit/PlatformAddressOutput.spec.mjs create mode 100644 packages/wasm-dpp2/tests/unit/PlatformAddressSigner.spec.mjs create mode 100644 packages/wasm-sdk/src/settings.rs create mode 100644 packages/wasm-sdk/src/state_transitions/addresses.rs create mode 100644 packages/wasm-sdk/src/state_transitions/broadcast.rs create mode 100644 packages/wasm-sdk/tests/unit/IdentityTopUpFromAddressesResult.spec.mjs create mode 100644 packages/wasm-sdk/tests/unit/IdentityTransferToAddressesResult.spec.mjs diff --git a/packages/js-evo-sdk/src/addresses/facade.ts b/packages/js-evo-sdk/src/addresses/facade.ts index 5f4e1bc8c64..b41351f8255 100644 --- a/packages/js-evo-sdk/src/addresses/facade.ts +++ b/packages/js-evo-sdk/src/addresses/facade.ts @@ -55,4 +55,242 @@ export class AddressesFacade { const w = await this.sdk.getWasmSdkConnected(); return w.getAddressesInfosWithProofInfo(addresses); } + + /** + * Transfers credits between Platform addresses. + * + * This method handles the complete transfer flow: + * 1. Fetches current nonces for all input addresses + * 2. Builds and signs the transfer transition + * 3. Broadcasts and waits for confirmation + * + * @param options - Transfer options including inputs, outputs, and signer + * @returns Promise resolving to transfer result with updated address information + * + * @example + * ```typescript + * const senderAddr = PlatformAddress.fromBech32m("tdashevo1..."); + * const recipientAddr = PlatformAddress.fromBech32m("tdashevo1..."); + * const privateKey = PrivateKey.fromWIF("cPrivateKeyWif..."); + * + * const input = new PlatformAddressInput(senderAddr, 0n, 100000n); + * const output = new PlatformAddressOutput(recipientAddr, 90000n); + * + * const signer = new PlatformAddressSigner(); + * signer.addKey(senderAddr, privateKey); + * + * const result = await sdk.addresses.transfer({ + * inputs: [input], + * outputs: [output], + * signer + * }); + * ``` + */ + async transfer(options: wasm.AddressFundsTransferOptions): Promise> { + const w = await this.sdk.getWasmSdkConnected(); + return w.addressFundsTransfer(options); + } + + /** + * Top up an identity from Platform addresses. + * + * This method handles the complete top up flow: + * 1. Fetches the identity from Platform + * 2. Fetches current nonces for all input addresses + * 3. Builds and signs the identity top up transition + * 4. Broadcasts and waits for confirmation + * + * @param options - Top up options including identity ID, inputs, and signer + * @returns Promise resolving to result with updated address infos and new identity balance + * + * @example + * ```typescript + * const identityId = Identifier.from("..."); + * const sourceAddr = PlatformAddress.fromBech32m("tdashevo1..."); + * const privateKey = PrivateKey.fromWIF("cPrivateKeyWif..."); + * + * const input = new PlatformAddressInput(sourceAddr, 0n, 50000n); + * + * const signer = new PlatformAddressSigner(); + * signer.addKey(sourceAddr, privateKey); + * + * const result = await sdk.addresses.topUpIdentity({ + * identityId, + * inputs: [input], + * signer + * }); + * + * console.log('New identity balance:', result.newBalance); + * ``` + */ + async topUpIdentity(options: wasm.IdentityTopUpFromAddressesOptions): Promise { + const w = await this.sdk.getWasmSdkConnected(); + return w.identityTopUpFromAddresses(options); + } + + /** + * Withdraws Platform address credits to Core (L1). + * + * This method handles the complete withdrawal flow: + * 1. Fetches current nonces for all input addresses + * 2. Builds and signs the withdrawal transition + * 3. Broadcasts and waits for confirmation + * 4. The withdrawal may be pooled with others depending on the pooling strategy + * + * @param options - Withdrawal options including inputs, output script, pooling, and signer + * @returns Promise resolving to Map of PlatformAddress to PlatformAddressInfo + * + * @example + * ```typescript + * const platformAddr = PlatformAddress.fromBech32m("tdashevo1..."); + * const privateKey = PrivateKey.fromWIF("cPrivateKeyWif..."); + * + * // Create Core output script for L1 destination + * const coreScript = CoreScript.newP2PKH(coreAddressHash); + * + * const input = new PlatformAddressInput(platformAddr, 0n, 100000n); + * + * const signer = new PlatformAddressSigner(); + * signer.addKey(platformAddr, privateKey); + * + * const result = await sdk.addresses.withdraw({ + * inputs: [input], + * coreFeePerByte: 1, + * pooling: PoolingWasm.Standard, + * outputScript: coreScript, + * signer + * }); + * ``` + */ + async withdraw(options: wasm.AddressFundsWithdrawOptions): Promise> { + const w = await this.sdk.getWasmSdkConnected(); + return w.addressFundsWithdraw(options); + } + + /** + * Transfer credits from an identity to Platform addresses. + * + * This method handles the complete transfer flow: + * 1. Fetches the identity from Platform + * 2. Finds the appropriate transfer key to use for signing (if signingTransferKeyId specified) + * 3. Builds and signs the identity credit transfer to addresses transition + * 4. Broadcasts and waits for confirmation + * + * @param options - Transfer options including identity ID, outputs, and signer + * @returns Result with updated address information and new identity balance + * + * @example + * ```typescript + * const identityId = Identifier.from("..."); + * const recipientAddr = PlatformAddress.fromBech32m("tdashevo1..."); + * const privateKey = PrivateKey.fromWIF("cPrivateKeyWif..."); // Identity transfer key + * + * const output = new PlatformAddressOutput(recipientAddr, 100000n); + * + * // Create identity signer and add the transfer key + * const signer = new IdentitySigner(); + * signer.addKey(privateKey); + * + * const result = await sdk.addresses.transferFromIdentity({ + * identityId, + * outputs: [output], + * signer + * }); + * + * console.log(`New identity balance: ${result.newBalance}`); + * console.log(`Updated addresses:`, result.addressInfos); + * ``` + */ + async transferFromIdentity(options: wasm.IdentityTransferToAddressesOptions): Promise { + const w = await this.sdk.getWasmSdkConnected(); + return w.identityTransferToAddresses(options); + } + + /** + * Fund Platform addresses from an asset lock. + * + * This method handles the complete funding flow: + * 1. Validates the asset lock proof + * 2. Builds and signs the address funding transition + * 3. Broadcasts and waits for confirmation + * + * @param options - Funding options including asset lock proof, outputs, and signer + * @returns Promise resolving to Map of PlatformAddress to PlatformAddressInfo + * + * @example + * ```typescript + * const platformAddr = PlatformAddress.fromBech32m("tdashevo1..."); + * const assetLockPrivateKey = PrivateKey.fromWIF("cPrivateKeyWif..."); + * const addressPrivateKey = PrivateKey.fromWIF("cPrivateKeyWif..."); + * + * // Create asset lock proof from L1 transaction + * const assetLockProof = AssetLockProof.createInstantAssetLockProof( + * instantLockBytes, + * transactionBytes, + * outputIndex + * ); + * + * const output = new PlatformAddressOutput(platformAddr, 100000n); + * + * const signer = new PlatformAddressSigner(); + * signer.addKey(platformAddr, addressPrivateKey); + * + * const result = await sdk.addresses.fundFromAssetLock({ + * assetLockProof, + * assetLockPrivateKey, + * outputs: [output], + * signer + * }); + * ``` + */ + async fundFromAssetLock(options: wasm.AddressFundingFromAssetLockOptions): Promise> { + const w = await this.sdk.getWasmSdkConnected(); + return w.addressFundingFromAssetLock(options); + } + + /** + * Create an identity funded from Platform addresses. + * + * This method handles the complete identity creation flow: + * 1. Fetches current nonces for all input addresses + * 2. Builds and signs the identity create from addresses transition + * 3. Broadcasts and waits for confirmation + * + * @param options - Creation options including identity, inputs, and signers + * @returns Promise resolving to result with created identity and updated address infos + * + * @example + * ```typescript + * const sourceAddr = PlatformAddress.fromBech32m("tdashevo1..."); + * const addressPrivateKey = PrivateKey.fromWIF("cAddressPrivateKeyWif..."); + * const identityPrivateKey = PrivateKey.fromWIF("cIdentityKeyWif..."); + * + * // Create identity structure with public keys + * const identity = new Identity(Identifier.random()); + * identity.addPublicKey(identityPublicKey); + * + * const input = new PlatformAddressInput(sourceAddr, 0n, 100000n); + * + * // Create signers + * const addressSigner = new PlatformAddressSigner(); + * addressSigner.addKey(sourceAddr, addressPrivateKey); + * + * const identitySigner = new IdentitySigner(); + * identitySigner.addKey(identityPrivateKey); + * + * const result = await sdk.addresses.createIdentity({ + * identity, + * inputs: [input], + * identitySigner, + * addressSigner + * }); + * + * console.log('Created identity:', result.identity.id()); + * console.log('Updated addresses:', result.addressInfos); + * ``` + */ + async createIdentity(options: wasm.IdentityCreateFromAddressesOptions): Promise { + const w = await this.sdk.getWasmSdkConnected(); + return w.identityCreateFromAddresses(options); + } } diff --git a/packages/js-evo-sdk/src/wasm.ts b/packages/js-evo-sdk/src/wasm.ts index f2f0fbb62d6..f8d6272de67 100644 --- a/packages/js-evo-sdk/src/wasm.ts +++ b/packages/js-evo-sdk/src/wasm.ts @@ -13,4 +13,3 @@ export async function ensureInitialized(): Promise { // Re-export all wasm SDK symbols for convenience export * from '@dashevo/wasm-sdk/compressed'; export { default } from '@dashevo/wasm-sdk/compressed'; -export type { DataContract } from '@dashevo/wasm-sdk/compressed'; diff --git a/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs b/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs index 3e250738d6b..f9aa7890bef 100644 --- a/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs +++ b/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs @@ -15,6 +15,14 @@ describe('AddressesFacade', () => { this.sinon.stub(wasmSdk, 'getAddressInfoWithProofInfo').resolves('ok'); this.sinon.stub(wasmSdk, 'getAddressesInfos').resolves('ok'); this.sinon.stub(wasmSdk, 'getAddressesInfosWithProofInfo').resolves('ok'); + // Create a mock PlatformAddress for test results + const mockAddress = wasmSDKPackage.PlatformAddress.fromBytes( + new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]), + ); + this.sinon.stub(wasmSdk, 'addressFundsTransfer').resolves({ + type: 'VerifiedAddressInfos', + addressInfos: [{ address: mockAddress, nonce: 1, balance: '90000' }], + }); }); it('get() forwards address to getAddressInfo', async () => { @@ -59,6 +67,206 @@ describe('AddressesFacade', () => { await client.addresses.getMany(mixedAddresses); expect(wasmSdk.getAddressesInfos).to.be.calledOnceWithExactly(mixedAddresses); }); + + it('transfer() forwards options to addressFundsTransfer with PlatformAddressSigner', async () => { + // Create proper PlatformAddress objects + const senderAddr = wasmSDKPackage.PlatformAddress.fromBytes( + new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]), + ); + const recipientAddr = wasmSDKPackage.PlatformAddress.fromBytes( + new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]), + ); + + // Create a PrivateKey object from bytes (32 bytes for testnet) + const privateKeyBytes = new Uint8Array(32).fill(1); // Test key bytes + const privateKey = wasmSDKPackage.PrivateKey.fromBytes(privateKeyBytes, 'testnet'); + + // Create signer and add the private key + const signer = new wasmSDKPackage.PlatformAddressSigner(); + signer.addKey(senderAddr, privateKey); + + // Create typed input and output objects (address, nonce, amount) + const input = new wasmSDKPackage.PlatformAddressInput(senderAddr, 0n, 100000n); + const output = new wasmSDKPackage.PlatformAddressOutput(recipientAddr, 90000n); + + const options = { + inputs: [input], + outputs: [output], + signer, + }; + const result = await client.addresses.transfer(options); + expect(wasmSdk.addressFundsTransfer).to.be.calledOnce; + expect(result.type).to.equal('VerifiedAddressInfos'); + expect(result.addressInfos).to.have.lengthOf(1); + expect(result.addressInfos[0].address.addressType).to.equal('P2PKH'); + }); + + it('transfer() handles success result type', async () => { + wasmSdk.addressFundsTransfer.resolves({ + type: 'Success', + message: 'Address funds transfer completed successfully', + }); + + // Create proper PlatformAddress objects + const senderAddr = wasmSDKPackage.PlatformAddress.fromBytes( + new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]), + ); + const recipientAddr = wasmSDKPackage.PlatformAddress.fromBytes( + new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]), + ); + + // Create a PrivateKey object from bytes + const privateKeyBytes = new Uint8Array(32).fill(1); // Test key bytes + const privateKey = wasmSDKPackage.PrivateKey.fromBytes(privateKeyBytes, 'testnet'); + + // Create signer and add the private key + const signer = new wasmSDKPackage.PlatformAddressSigner(); + signer.addKey(senderAddr, privateKey); + + // Create typed input and output objects (address, nonce, amount) + const input = new wasmSDKPackage.PlatformAddressInput(senderAddr, 0n, 100000n); + const output = new wasmSDKPackage.PlatformAddressOutput(recipientAddr, 90000n); + + const options = { + inputs: [input], + outputs: [output], + signer, + }; + const result = await client.addresses.transfer(options); + expect(result.type).to.equal('Success'); + expect(result.message).to.include('successfully'); + }); + + it('topUpIdentity() forwards options to identityTopUpFromAddresses', async function () { + // Create mock address and result + const mockAddress = wasmSDKPackage.PlatformAddress.fromBytes( + new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]), + ); + + this.sinon.stub(wasmSdk, 'identityTopUpFromAddresses').resolves({ + addressInfos: new Map([[mockAddress, { address: mockAddress, nonce: 1n, balance: 50000n }]]), + newBalance: 150000n, + }); + + // Create a mock identity ID (32 bytes) + const identityId = wasmSDKPackage.Identifier.fromBytes(new Uint8Array(32).fill(42)); + + // Mock options - the actual implementation will process these + const options = { + identityId, + inputs: [], + signer: {}, + }; + + const result = await client.addresses.topUpIdentity(options); + expect(wasmSdk.identityTopUpFromAddresses).to.be.calledOnce; + expect(result.newBalance).to.equal(150000n); + }); + + it('withdraw() forwards options to addressFundsWithdraw', async function () { + // Create mock address and result map + const mockAddress = wasmSDKPackage.PlatformAddress.fromBytes( + new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]), + ); + + const resultMap = new Map(); + resultMap.set(mockAddress, { address: mockAddress, nonce: 1n, balance: 0n }); + + this.sinon.stub(wasmSdk, 'addressFundsWithdraw').resolves(resultMap); + + // Create Core output script (P2PKH) + const coreScript = wasmSDKPackage.CoreScript.newP2PKH(new Uint8Array(20).fill(5)); + + // Mock options - the actual implementation will process these + const options = { + inputs: [], + coreFeePerByte: 1, + pooling: wasmSDKPackage.PoolingWasm.Standard, + outputScript: coreScript, + signer: {}, + }; + + const result = await client.addresses.withdraw(options); + expect(wasmSdk.addressFundsWithdraw).to.be.calledOnce; + expect(result).to.be.instanceOf(Map); + }); + + it('transferFromIdentity() forwards options to identityTransferToAddresses', async function () { + // Create mock address + const mockAddress = wasmSDKPackage.PlatformAddress.fromBytes( + new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]), + ); + + this.sinon.stub(wasmSdk, 'identityTransferToAddresses').resolves({ + addressInfos: new Map([[mockAddress, { address: mockAddress, nonce: 0n, balance: 100000n }]]), + newBalance: 400000n, + }); + + // Create a mock identity ID + const identityId = wasmSDKPackage.Identifier.fromBytes(new Uint8Array(32).fill(42)); + + // Mock options - the actual implementation will process these + const options = { + identityId, + outputs: [], + signer: {}, + }; + + const result = await client.addresses.transferFromIdentity(options); + expect(wasmSdk.identityTransferToAddresses).to.be.calledOnce; + expect(result.newBalance).to.equal(400000n); + }); + + it('fundFromAssetLock() forwards options to addressFundingFromAssetLock', async function () { + // Create mock address and result map + const mockAddress = wasmSDKPackage.PlatformAddress.fromBytes( + new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]), + ); + + const resultMap = new Map(); + resultMap.set(mockAddress, { address: mockAddress, nonce: 0n, balance: 100000n }); + + this.sinon.stub(wasmSdk, 'addressFundingFromAssetLock').resolves(resultMap); + + // Mock options - the actual implementation will process these + const options = { + assetLockProof: {}, + assetLockPrivateKey: {}, + outputs: [], + signer: {}, + }; + + const result = await client.addresses.fundFromAssetLock(options); + expect(wasmSdk.addressFundingFromAssetLock).to.be.calledOnce; + expect(result).to.be.instanceOf(Map); + }); + + it('createIdentity() forwards options to identityCreateFromAddresses', async function () { + // Create mock address + const mockAddress = wasmSDKPackage.PlatformAddress.fromBytes( + new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]), + ); + + // Create mock identity ID + const mockIdentityId = wasmSDKPackage.Identifier.fromBytes(new Uint8Array(32).fill(99)); + + this.sinon.stub(wasmSdk, 'identityCreateFromAddresses').resolves({ + identity: { id: () => mockIdentityId }, + addressInfos: new Map([[mockAddress, { address: mockAddress, nonce: 1n, balance: 0n }]]), + }); + + // Mock options - the actual implementation will process these + const options = { + identity: {}, + inputs: [], + identitySigner: {}, + addressSigner: {}, + }; + + const result = await client.addresses.createIdentity(options); + expect(wasmSdk.identityCreateFromAddresses).to.be.calledOnce; + expect(result.identity).to.exist(); + }); }); describe('PlatformAddress', () => { @@ -71,18 +279,16 @@ describe('PlatformAddress', () => { it('can be created from bytes', () => { // Create P2PKH address from bytes (type 0x00) - // eslint-disable-next-line max-len const p2pkhBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const p2pkhAddr = PlatformAddress.fromBytes(Array.from(p2pkhBytes)); + const p2pkhAddr = PlatformAddress.fromBytes(p2pkhBytes); expect(p2pkhAddr).to.exist(); expect(p2pkhAddr.addressType).to.equal('P2PKH'); expect(p2pkhAddr.isP2pkh).to.be.true(); expect(p2pkhAddr.isP2sh).to.be.false(); // Create P2SH address from bytes (type 0x01) - // eslint-disable-next-line max-len const p2shBytes = new Uint8Array([0x01, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const p2shAddr = PlatformAddress.fromBytes(Array.from(p2shBytes)); + const p2shAddr = PlatformAddress.fromBytes(p2shBytes); expect(p2shAddr).to.exist(); expect(p2shAddr.addressType).to.equal('P2SH'); expect(p2shAddr.isP2pkh).to.be.false(); @@ -91,9 +297,8 @@ describe('PlatformAddress', () => { it('converts to bech32m and back', () => { // Create address from bytes - // eslint-disable-next-line max-len const originalBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const addr = PlatformAddress.fromBytes(Array.from(originalBytes)); + const addr = PlatformAddress.fromBytes(originalBytes); // Convert to bech32m for testnet const bech32m = addr.toBech32m('testnet'); diff --git a/packages/wasm-dpp2/src/enums/withdrawal.rs b/packages/wasm-dpp2/src/enums/withdrawal.rs index 4d24ca10d33..d774baad46b 100644 --- a/packages/wasm-dpp2/src/enums/withdrawal.rs +++ b/packages/wasm-dpp2/src/enums/withdrawal.rs @@ -1,15 +1,54 @@ use crate::error::WasmDppError; use dpp::withdrawal::Pooling; +use serde::{Deserialize, Deserializer}; use wasm_bindgen::JsValue; use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen] +#[derive(Clone, Copy)] pub enum PoolingWasm { Never = 0, IfAvailable = 1, Standard = 2, } +impl<'de> Deserialize<'de> for PoolingWasm { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value: serde_json::Value = Deserialize::deserialize(deserializer)?; + + // Try as string first + if let Some(s) = value.as_str() { + return match s.to_lowercase().as_str() { + "never" => Ok(PoolingWasm::Never), + "ifavailable" => Ok(PoolingWasm::IfAvailable), + "standard" => Ok(PoolingWasm::Standard), + _ => Err(serde::de::Error::custom(format!( + "unsupported pooling value ({})", + s + ))), + }; + } + + // Try as number + if let Some(n) = value.as_u64() { + return match n { + 0 => Ok(PoolingWasm::Never), + 1 => Ok(PoolingWasm::IfAvailable), + 2 => Ok(PoolingWasm::Standard), + _ => Err(serde::de::Error::custom(format!( + "unsupported pooling value ({})", + n + ))), + }; + } + + Err(serde::de::Error::custom("pooling must be a string or number")) + } +} + impl From for Pooling { fn from(pooling: PoolingWasm) -> Self { match pooling { diff --git a/packages/wasm-dpp2/src/identity/model.rs b/packages/wasm-dpp2/src/identity/model.rs index 103575d3903..465f47017bb 100644 --- a/packages/wasm-dpp2/src/identity/model.rs +++ b/packages/wasm-dpp2/src/identity/model.rs @@ -23,6 +23,12 @@ impl From for IdentityWasm { } } +impl From for Identity { + fn from(wasm: IdentityWasm) -> Self { + wasm.0 + } +} + #[wasm_bindgen(js_class = Identity)] impl IdentityWasm { #[wasm_bindgen(getter = __type)] @@ -91,9 +97,9 @@ impl IdentityWasm { } #[wasm_bindgen(js_name = "getPublicKeyById")] - pub fn get_public_key_by_id(&self, key_id: KeyID) -> IdentityPublicKeyWasm { - let identity_public_key = self.0.get_public_key_by_id(key_id); - IdentityPublicKeyWasm::from(identity_public_key.unwrap().clone()) + pub fn get_public_key_by_id(&self, key_id: KeyID) -> Option { + self.0.get_public_key_by_id(key_id) + .map(|key| IdentityPublicKeyWasm::from(key.clone())) } #[wasm_bindgen(js_name = "getPublicKeys")] diff --git a/packages/wasm-dpp2/src/identity_signer.rs b/packages/wasm-dpp2/src/identity_signer.rs new file mode 100644 index 00000000000..f9db6c2e3c1 --- /dev/null +++ b/packages/wasm-dpp2/src/identity_signer.rs @@ -0,0 +1,213 @@ +//! Identity signer for WASM bindings. +//! +//! This module provides a signer for identity-based state transitions that implements +//! `Signer`. + +use crate::error::{WasmDppError, WasmDppResult}; +use crate::private_key::PrivateKeyWasm; +use crate::utils::IntoWasm; +use dpp::dashcore::hashes::{hash160, Hash}; +use dpp::dashcore::secp256k1::{PublicKey, Secp256k1, SecretKey}; +use dpp::dashcore::signer; +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::address_funds::AddressWitness; +use dpp::ProtocolError; +use std::collections::BTreeMap; +use std::fmt; +use wasm_bindgen::prelude::*; + +/// A signer for identity-based state transitions. +/// +/// This signer holds private keys mapped by their public key hash (for ECDSA_HASH160 keys) +/// and can sign state transitions that require identity keys. +#[wasm_bindgen(js_name = "IdentitySigner")] +#[derive(Clone, Default)] +pub struct IdentitySignerWasm { + /// Maps public key hash (20 bytes) to private key bytes (32 bytes) + private_keys: BTreeMap<[u8; 20], [u8; 32]>, +} + +impl fmt::Debug for IdentitySignerWasm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("IdentitySigner") + .field("key_count", &self.private_keys.len()) + .finish() + } +} + +#[wasm_bindgen(js_class = IdentitySigner)] +impl IdentitySignerWasm { + /// Creates a new empty IdentitySigner. + #[wasm_bindgen(constructor)] + pub fn new() -> IdentitySignerWasm { + IdentitySignerWasm { + private_keys: BTreeMap::new(), + } + } + + /// Adds a private key to the signer. + /// + /// The public key hash is derived automatically from the private key. + /// + /// @param privateKey - The PrivateKey object + #[wasm_bindgen(js_name = "addKey")] + pub fn add_key(&mut self, private_key: &PrivateKeyWasm) -> WasmDppResult<()> { + let key_bytes = private_key.to_bytes(); + if key_bytes.len() != 32 { + return Err(WasmDppError::invalid_argument(format!( + "Private key must be 32 bytes, got {}", + key_bytes.len() + ))); + } + + // Derive public key hash from private key + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&key_bytes).map_err(|e| { + WasmDppError::invalid_argument(format!("Invalid secret key: {}", e)) + })?; + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + let public_key_bytes = public_key.serialize(); + let public_key_hash = hash160::Hash::hash(&public_key_bytes[..]) + .to_byte_array(); + + let mut key_array = [0u8; 32]; + key_array.copy_from_slice(&key_bytes); + + self.private_keys.insert(public_key_hash, key_array); + Ok(()) + } + + /// Adds a private key from WIF format. + /// + /// @param wif - The private key in WIF format + #[wasm_bindgen(js_name = "addKeyFromWif")] + pub fn add_key_from_wif(&mut self, wif: &str) -> WasmDppResult<()> { + let private_key = PrivateKeyWasm::from_wif(wif)?; + self.add_key(&private_key) + } + + #[wasm_bindgen(getter = __struct)] + pub fn struct_name() -> String { + "IdentitySigner".to_string() + } + + /// Returns the number of keys in this signer. + #[wasm_bindgen(getter = keyCount)] + pub fn key_count(&self) -> usize { + self.private_keys.len() + } +} + +impl Signer for IdentitySignerWasm { + fn sign( + &self, + identity_public_key: &IdentityPublicKey, + data: &[u8], + ) -> Result { + // Only support ECDSA_HASH160 keys for now + if identity_public_key.key_type() != KeyType::ECDSA_HASH160 { + return Err(ProtocolError::Generic(format!( + "IdentitySigner only supports ECDSA_HASH160 keys, got {:?}", + identity_public_key.key_type() + ))); + } + + // The key data is the public key hash (20 bytes) + let key_data = identity_public_key.data().as_slice(); + if key_data.len() != 20 { + return Err(ProtocolError::Generic(format!( + "Expected 20-byte public key hash, got {} bytes", + key_data.len() + ))); + } + + let mut key_hash = [0u8; 20]; + key_hash.copy_from_slice(key_data); + + let private_key = self.private_keys.get(&key_hash).ok_or_else(|| { + ProtocolError::Generic(format!( + "No private key found for public key hash {}", + hex::encode(key_hash) + )) + })?; + + let signature = signer::sign(data, private_key)?; + Ok(signature.to_vec().into()) + } + + fn sign_create_witness( + &self, + key: &IdentityPublicKey, + data: &[u8], + ) -> Result { + // First, sign the data to get the signature + let signature = self.sign(key, data)?; + + // Create the appropriate AddressWitness based on the key type + // IdentitySigner only supports ECDSA_HASH160 keys + match key.key_type() { + KeyType::ECDSA_HASH160 => { + // P2PKH witness only needs the signature - the public key is recovered + // during verification + Ok(AddressWitness::P2pkh { signature }) + } + _ => Err(ProtocolError::Generic(format!( + "IdentitySigner only supports ECDSA_HASH160 keys, got {:?}", + key.key_type() + ))), + } + } + + fn can_sign_with(&self, identity_public_key: &IdentityPublicKey) -> bool { + if identity_public_key.key_type() != KeyType::ECDSA_HASH160 { + return false; + } + + let key_data = identity_public_key.data().as_slice(); + if key_data.len() != 20 { + return false; + } + + let mut key_hash = [0u8; 20]; + key_hash.copy_from_slice(key_data); + + self.private_keys.contains_key(&key_hash) + } +} + +impl IdentitySignerWasm { + /// Returns a reference to the inner signer for use in Rust code. + pub fn inner(&self) -> &Self { + self + } + + /// Extracts an IdentitySigner from a JS options object. + /// + /// This helper reads the "signer" field from an options object and converts it + /// to an IdentitySignerWasm. Useful for state transition functions that + /// need a signer from their options. + pub fn try_from_options(options: &JsValue) -> WasmDppResult { + let signer_js = js_sys::Reflect::get(options, &JsValue::from_str("signer")) + .map_err(|_| WasmDppError::invalid_argument("Missing 'signer' field"))?; + + if signer_js.is_undefined() || signer_js.is_null() { + return Err(WasmDppError::invalid_argument("'signer' is required")); + } + + Self::try_from(&signer_js) + } +} + +impl TryFrom<&JsValue> for IdentitySignerWasm { + type Error = WasmDppError; + + fn try_from(value: &JsValue) -> Result { + value + .to_wasm::("IdentitySigner") + .map(|boxed| (*boxed).clone()) + .map_err(|_| WasmDppError::invalid_argument("Expected an IdentitySigner object")) + } +} diff --git a/packages/wasm-dpp2/src/lib.rs b/packages/wasm-dpp2/src/lib.rs index 52ca1a3c33b..241fd142575 100644 --- a/packages/wasm-dpp2/src/lib.rs +++ b/packages/wasm-dpp2/src/lib.rs @@ -20,6 +20,7 @@ pub mod error; pub mod group; pub mod identifier; pub mod identity; +pub mod identity_signer; pub mod mock_bls; pub mod platform_address; pub mod private_key; @@ -30,6 +31,10 @@ pub mod tokens; pub mod utils; pub mod voting; +pub use core_script::CoreScriptWasm; +pub use enums::withdrawal::PoolingWasm; +pub use identity_signer::IdentitySignerWasm; + pub use data_contract::{ ContractBoundsWasm, DataContractCreateTransitionWasm, DataContractUpdateTransitionWasm, DataContractWasm, DocumentWasm, tokens_configuration_from_js_value, @@ -42,7 +47,12 @@ pub use identity::{ IdentityTopUpTransitionWasm, IdentityUpdateTransitionWasm, IdentityWasm, MasternodeVoteTransitionWasm, PartialIdentityWasm, }; -pub use platform_address::PlatformAddressWasm; +pub use platform_address::{ + default_fee_strategy, extract_addresses, extract_amounts, fee_strategy_from_steps, + fee_strategy_from_steps_or_default, inputs_to_btree_map, outputs_to_btree_map, + FeeStrategyStepWasm, PlatformAddressInputWasm, PlatformAddressOutputWasm, + PlatformAddressSignerWasm, PlatformAddressWasm, +}; pub use state_transitions::base::{GroupStateTransitionInfoWasm, StateTransitionWasm}; pub use tokens::*; pub use voting::{ diff --git a/packages/wasm-dpp2/src/platform_address.rs b/packages/wasm-dpp2/src/platform_address/address.rs similarity index 99% rename from packages/wasm-dpp2/src/platform_address.rs rename to packages/wasm-dpp2/src/platform_address/address.rs index 2673dde2fd0..244a5e994c9 100644 --- a/packages/wasm-dpp2/src/platform_address.rs +++ b/packages/wasm-dpp2/src/platform_address/address.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Deserializer}; use std::fmt; use wasm_bindgen::prelude::*; -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] #[wasm_bindgen(js_name = "PlatformAddress")] pub struct PlatformAddressWasm(PlatformAddress); diff --git a/packages/wasm-dpp2/src/platform_address/fee_strategy.rs b/packages/wasm-dpp2/src/platform_address/fee_strategy.rs new file mode 100644 index 00000000000..72aa9eceed4 --- /dev/null +++ b/packages/wasm-dpp2/src/platform_address/fee_strategy.rs @@ -0,0 +1,152 @@ +use dpp::address_funds::{AddressFundsFeeStrategy, AddressFundsFeeStrategyStep}; +use serde::de::{self, Deserializer, MapAccess, Visitor}; +use serde::Deserialize; +use std::fmt; +use wasm_bindgen::prelude::*; + +/// Defines how fees are paid in address-based state transitions. +/// +/// Fee strategy is a sequence of steps that determine which inputs or outputs +/// should be reduced to cover the transaction fee. +#[wasm_bindgen(js_name = "FeeStrategyStep")] +#[derive(Clone, Debug)] +pub struct FeeStrategyStepWasm(AddressFundsFeeStrategyStep); + +#[wasm_bindgen(js_class = FeeStrategyStep)] +impl FeeStrategyStepWasm { + /// Creates a step that deducts the fee from the input at the given index. + /// + /// The input must have remaining balance after its contribution to outputs. + /// + /// @param index - The index of the input address to deduct fee from + #[wasm_bindgen(js_name = "deductFromInput")] + pub fn deduct_from_input(index: u16) -> FeeStrategyStepWasm { + FeeStrategyStepWasm(AddressFundsFeeStrategyStep::DeductFromInput(index)) + } + + /// Creates a step that reduces the output at the given index by the fee amount. + /// + /// The output amount will be reduced to cover the fee. + /// + /// @param index - The index of the output address to reduce + #[wasm_bindgen(js_name = "reduceOutput")] + pub fn reduce_output(index: u16) -> FeeStrategyStepWasm { + FeeStrategyStepWasm(AddressFundsFeeStrategyStep::ReduceOutput(index)) + } + + /// Returns true if this step deducts from an input. + #[wasm_bindgen(js_name = "isDeductFromInput", getter)] + pub fn is_deduct_from_input(&self) -> bool { + matches!(self.0, AddressFundsFeeStrategyStep::DeductFromInput(_)) + } + + /// Returns true if this step reduces an output. + #[wasm_bindgen(js_name = "isReduceOutput", getter)] + pub fn is_reduce_output(&self) -> bool { + matches!(self.0, AddressFundsFeeStrategyStep::ReduceOutput(_)) + } + + /// Returns the index associated with this step. + #[wasm_bindgen(getter)] + pub fn index(&self) -> u16 { + match self.0 { + AddressFundsFeeStrategyStep::DeductFromInput(i) => i, + AddressFundsFeeStrategyStep::ReduceOutput(i) => i, + } + } +} + +impl From for AddressFundsFeeStrategyStep { + fn from(step: FeeStrategyStepWasm) -> Self { + step.0 + } +} + +impl From for FeeStrategyStepWasm { + fn from(step: AddressFundsFeeStrategyStep) -> Self { + FeeStrategyStepWasm(step) + } +} + +/// Converts a vector of FeeStrategyStepWasm to AddressFundsFeeStrategy. +pub fn fee_strategy_from_steps(steps: Vec) -> AddressFundsFeeStrategy { + steps.into_iter().map(|s| s.0).collect() +} + +/// Returns the default fee strategy (deduct from first input). +pub fn default_fee_strategy() -> AddressFundsFeeStrategy { + vec![AddressFundsFeeStrategyStep::DeductFromInput(0)] +} + +/// Converts optional fee strategy steps to AddressFundsFeeStrategy, using default if None. +pub fn fee_strategy_from_steps_or_default( + steps: Option>, +) -> AddressFundsFeeStrategy { + steps + .map(fee_strategy_from_steps) + .unwrap_or_else(default_fee_strategy) +} + +impl<'de> Deserialize<'de> for FeeStrategyStepWasm { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "camelCase")] + enum Field { + Type, + Index, + } + + struct FeeStrategyStepVisitor; + + impl<'de> Visitor<'de> for FeeStrategyStepVisitor { + type Value = FeeStrategyStepWasm; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct FeeStrategyStep with type and index") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut step_type: Option = None; + let mut index: Option = None; + + while let Some(key) = map.next_key()? { + match key { + Field::Type => { + if step_type.is_some() { + return Err(de::Error::duplicate_field("type")); + } + step_type = Some(map.next_value()?); + } + Field::Index => { + if index.is_some() { + return Err(de::Error::duplicate_field("index")); + } + index = Some(map.next_value()?); + } + } + } + + let step_type = step_type.ok_or_else(|| de::Error::missing_field("type"))?; + let index = index.ok_or_else(|| de::Error::missing_field("index"))?; + + match step_type.as_str() { + "deductFromInput" => Ok(FeeStrategyStepWasm::deduct_from_input(index)), + "reduceOutput" => Ok(FeeStrategyStepWasm::reduce_output(index)), + _ => Err(de::Error::unknown_variant( + &step_type, + &["deductFromInput", "reduceOutput"], + )), + } + } + } + + const FIELDS: &[&str] = &["type", "index"]; + deserializer.deserialize_struct("FeeStrategyStep", FIELDS, FeeStrategyStepVisitor) + } +} diff --git a/packages/wasm-dpp2/src/platform_address/input_output.rs b/packages/wasm-dpp2/src/platform_address/input_output.rs new file mode 100644 index 00000000000..dd729c93949 --- /dev/null +++ b/packages/wasm-dpp2/src/platform_address/input_output.rs @@ -0,0 +1,469 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use super::PlatformAddressWasm; +use dpp::address_funds::PlatformAddress; +use dpp::fee::Credits; +use dpp::prelude::AddressNonce; +use js_sys::BigInt; +use serde::de::{self, Deserializer, MapAccess, Visitor}; +use serde::Deserialize; +use std::collections::BTreeMap; +use std::fmt; +use wasm_bindgen::prelude::*; + +/// Represents an input address for address-based state transitions. +/// +/// An input specifies a Platform address that will spend credits, +/// along with its current nonce and the amount to spend. +#[wasm_bindgen(js_name = "PlatformAddressInput")] +#[derive(Clone, Debug)] +pub struct PlatformAddressInputWasm { + address: PlatformAddressWasm, + nonce: AddressNonce, + amount: Credits, +} + +/// Helper to convert BigInt to u32 +fn bigint_to_u32(value: BigInt) -> Result { + let value_u64: u64 = value + .try_into() + .map_err(|_| WasmDppError::invalid_argument("value must be a valid u64"))?; + + if value_u64 > u32::MAX as u64 { + return Err(WasmDppError::invalid_argument("value exceeds u32 max")); + } + + Ok(value_u64 as u32) +} + +/// Helper to convert BigInt to u64 +fn bigint_to_u64(value: BigInt) -> Result { + value + .try_into() + .map_err(|_| WasmDppError::invalid_argument("value must be a valid u64")) +} + +#[wasm_bindgen(js_class = PlatformAddressInput)] +impl PlatformAddressInputWasm { + /// Creates a new PlatformAddressInput. + /// + /// @param address - The Platform address (PlatformAddress, Uint8Array, or bech32m string) + /// @param nonce - The current nonce of the address (will be incremented for the transaction) + /// @param amount - The amount of credits to spend from this address + #[wasm_bindgen(constructor)] + pub fn new( + #[wasm_bindgen(unchecked_param_type = "PlatformAddressLike")] address: &JsValue, + nonce: BigInt, + amount: BigInt, + ) -> WasmDppResult { + let platform_address = PlatformAddressWasm::try_from(address)?; + let nonce_u32 = bigint_to_u32(nonce)?; + let amount_u64 = bigint_to_u64(amount)?; + + Ok(PlatformAddressInputWasm { + address: platform_address, + nonce: nonce_u32, + amount: amount_u64, + }) + } + + /// Returns the Platform address. + #[wasm_bindgen(getter)] + pub fn address(&self) -> PlatformAddressWasm { + self.address + } + + /// Returns the nonce. + #[wasm_bindgen(getter)] + pub fn nonce(&self) -> BigInt { + BigInt::from(self.nonce) + } + + /// Returns the amount. + #[wasm_bindgen(getter)] + pub fn amount(&self) -> BigInt { + BigInt::from(self.amount) + } +} + +impl PlatformAddressInputWasm { + /// Returns the inner values as a tuple suitable for BTreeMap insertion. + pub fn into_inner(self) -> (PlatformAddress, (AddressNonce, Credits)) { + (self.address.into(), (self.nonce, self.amount)) + } + + /// Returns a reference to the inner PlatformAddress. + pub fn platform_address(&self) -> PlatformAddress { + self.address.into() + } + + /// Returns the nonce value. + pub fn nonce_value(&self) -> AddressNonce { + self.nonce + } + + /// Returns the amount value. + pub fn amount_value(&self) -> Credits { + self.amount + } +} + +/// Custom deserializer for Credits that handles number, string, or bigint. +fn deserialize_credits<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + struct CreditsVisitor; + + impl<'de> Visitor<'de> for CreditsVisitor { + type Value = Credits; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a number, string, or bigint representing credits") + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + Ok(value) + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + if value < 0 { + Err(E::custom("credits cannot be negative")) + } else { + Ok(value as Credits) + } + } + + fn visit_f64(self, value: f64) -> Result + where + E: de::Error, + { + if value < 0.0 { + Err(E::custom("credits cannot be negative")) + } else { + Ok(value as Credits) + } + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + // Handle bigint string format (e.g., "1000000n" or just "1000000") + let clean = value.trim_end_matches('n'); + clean + .parse::() + .map_err(|_| E::custom(format!("invalid credits value: {}", value))) + } + + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + self.visit_str(&value) + } + } + + deserializer.deserialize_any(CreditsVisitor) +} + +/// Custom deserializer for AddressNonce that handles number or bigint. +fn deserialize_nonce<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + struct NonceVisitor; + + impl<'de> Visitor<'de> for NonceVisitor { + type Value = AddressNonce; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a number or bigint representing a nonce") + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + if value > u32::MAX as u64 { + Err(E::custom("nonce exceeds u32 max")) + } else { + Ok(value as AddressNonce) + } + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + if value < 0 { + Err(E::custom("nonce cannot be negative")) + } else if value > u32::MAX as i64 { + Err(E::custom("nonce exceeds u32 max")) + } else { + Ok(value as AddressNonce) + } + } + + fn visit_f64(self, value: f64) -> Result + where + E: de::Error, + { + if value < 0.0 { + Err(E::custom("nonce cannot be negative")) + } else if value > u32::MAX as f64 { + Err(E::custom("nonce exceeds u32 max")) + } else { + Ok(value as AddressNonce) + } + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + let clean = value.trim_end_matches('n'); + clean + .parse::() + .map_err(|_| E::custom(format!("invalid nonce value: {}", value))) + } + + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + self.visit_str(&value) + } + } + + deserializer.deserialize_any(NonceVisitor) +} + +impl<'de> Deserialize<'de> for PlatformAddressInputWasm { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "camelCase")] + enum Field { + Address, + Nonce, + Amount, + } + + struct PlatformAddressInputVisitor; + + impl<'de> Visitor<'de> for PlatformAddressInputVisitor { + type Value = PlatformAddressInputWasm; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct PlatformAddressInput") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut address: Option = None; + let mut nonce: Option = None; + let mut amount: Option = None; + + while let Some(key) = map.next_key()? { + match key { + Field::Address => { + if address.is_some() { + return Err(de::Error::duplicate_field("address")); + } + address = Some(map.next_value()?); + } + Field::Nonce => { + if nonce.is_some() { + return Err(de::Error::duplicate_field("nonce")); + } + // Use a helper struct to deserialize nonce + #[derive(Deserialize)] + struct NonceHelper(#[serde(deserialize_with = "deserialize_nonce")] AddressNonce); + let helper: NonceHelper = map.next_value()?; + nonce = Some(helper.0); + } + Field::Amount => { + if amount.is_some() { + return Err(de::Error::duplicate_field("amount")); + } + // Use a helper struct to deserialize amount + #[derive(Deserialize)] + struct AmountHelper(#[serde(deserialize_with = "deserialize_credits")] Credits); + let helper: AmountHelper = map.next_value()?; + amount = Some(helper.0); + } + } + } + + let address = address.ok_or_else(|| de::Error::missing_field("address"))?; + let nonce = nonce.ok_or_else(|| de::Error::missing_field("nonce"))?; + let amount = amount.ok_or_else(|| de::Error::missing_field("amount"))?; + + Ok(PlatformAddressInputWasm { + address, + nonce, + amount, + }) + } + } + + const FIELDS: &[&str] = &["address", "nonce", "amount"]; + deserializer.deserialize_struct("PlatformAddressInput", FIELDS, PlatformAddressInputVisitor) + } +} + +impl<'de> Deserialize<'de> for PlatformAddressOutputWasm { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "camelCase")] + enum Field { + Address, + Amount, + } + + struct PlatformAddressOutputVisitor; + + impl<'de> Visitor<'de> for PlatformAddressOutputVisitor { + type Value = PlatformAddressOutputWasm; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct PlatformAddressOutput") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut address: Option = None; + let mut amount: Option = None; + + while let Some(key) = map.next_key()? { + match key { + Field::Address => { + if address.is_some() { + return Err(de::Error::duplicate_field("address")); + } + address = Some(map.next_value()?); + } + Field::Amount => { + if amount.is_some() { + return Err(de::Error::duplicate_field("amount")); + } + // Use a helper struct to deserialize amount + #[derive(Deserialize)] + struct AmountHelper(#[serde(deserialize_with = "deserialize_credits")] Credits); + let helper: AmountHelper = map.next_value()?; + amount = Some(helper.0); + } + } + } + + let address = address.ok_or_else(|| de::Error::missing_field("address"))?; + let amount = amount.ok_or_else(|| de::Error::missing_field("amount"))?; + + Ok(PlatformAddressOutputWasm { address, amount }) + } + } + + const FIELDS: &[&str] = &["address", "amount"]; + deserializer.deserialize_struct("PlatformAddressOutput", FIELDS, PlatformAddressOutputVisitor) + } +} + +/// Represents an output address for address-based state transitions. +/// +/// An output specifies a Platform address that will receive credits, +/// along with the amount to receive. +#[wasm_bindgen(js_name = "PlatformAddressOutput")] +#[derive(Clone, Debug)] +pub struct PlatformAddressOutputWasm { + address: PlatformAddressWasm, + amount: Credits, +} + +#[wasm_bindgen(js_class = PlatformAddressOutput)] +impl PlatformAddressOutputWasm { + /// Creates a new PlatformAddressOutput. + /// + /// @param address - The Platform address (PlatformAddress, Uint8Array, or bech32m string) + /// @param amount - The amount of credits to send to this address + #[wasm_bindgen(constructor)] + pub fn new( + #[wasm_bindgen(unchecked_param_type = "PlatformAddressLike")] address: &JsValue, + amount: BigInt, + ) -> WasmDppResult { + let platform_address = PlatformAddressWasm::try_from(address)?; + let amount_u64 = bigint_to_u64(amount)?; + + Ok(PlatformAddressOutputWasm { + address: platform_address, + amount: amount_u64, + }) + } + + /// Returns the Platform address. + #[wasm_bindgen(getter)] + pub fn address(&self) -> PlatformAddressWasm { + self.address + } + + /// Returns the amount. + #[wasm_bindgen(getter)] + pub fn amount(&self) -> BigInt { + BigInt::from(self.amount) + } +} + +impl PlatformAddressOutputWasm { + /// Returns the inner values as a tuple suitable for BTreeMap insertion. + pub fn into_inner(self) -> (PlatformAddress, Credits) { + (self.address.into(), self.amount) + } + + /// Returns a reference to the inner PlatformAddress. + pub fn platform_address(&self) -> PlatformAddress { + self.address.into() + } + + /// Returns the amount value. + pub fn amount_value(&self) -> Credits { + self.amount + } +} + +/// Converts a vector of PlatformAddressInput into a BTreeMap. +pub fn inputs_to_btree_map( + inputs: Vec, +) -> BTreeMap { + inputs.into_iter().map(|i| i.into_inner()).collect() +} + +/// Converts a vector of PlatformAddressOutput into a BTreeMap. +pub fn outputs_to_btree_map( + outputs: Vec, +) -> BTreeMap { + outputs.into_iter().map(|o| o.into_inner()).collect() +} + +/// Extracts addresses from a slice of PlatformAddressOutput. +pub fn extract_addresses(outputs: &[PlatformAddressOutputWasm]) -> Vec { + outputs.iter().map(|o| o.platform_address()).collect() +} + +/// Extracts amounts from a slice of PlatformAddressOutput. +pub fn extract_amounts(outputs: &[PlatformAddressOutputWasm]) -> Vec { + outputs.iter().map(|o| o.amount_value()).collect() +} diff --git a/packages/wasm-dpp2/src/platform_address/mod.rs b/packages/wasm-dpp2/src/platform_address/mod.rs new file mode 100644 index 00000000000..1dcb8f088e8 --- /dev/null +++ b/packages/wasm-dpp2/src/platform_address/mod.rs @@ -0,0 +1,15 @@ +mod address; +mod fee_strategy; +mod input_output; +mod signer; + +pub use address::PlatformAddressWasm; +pub use fee_strategy::{ + default_fee_strategy, fee_strategy_from_steps, fee_strategy_from_steps_or_default, + FeeStrategyStepWasm, +}; +pub use input_output::{ + extract_addresses, extract_amounts, inputs_to_btree_map, outputs_to_btree_map, + PlatformAddressInputWasm, PlatformAddressOutputWasm, +}; +pub use signer::PlatformAddressSignerWasm; diff --git a/packages/wasm-dpp2/src/platform_address/signer.rs b/packages/wasm-dpp2/src/platform_address/signer.rs new file mode 100644 index 00000000000..303df7b6bdf --- /dev/null +++ b/packages/wasm-dpp2/src/platform_address/signer.rs @@ -0,0 +1,175 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::private_key::PrivateKeyWasm; +use crate::utils::IntoWasm; +use super::PlatformAddressWasm; +use dpp::address_funds::{AddressWitness, PlatformAddress}; +use dpp::dashcore::signer; +use dpp::identity::signer::Signer; +use dpp::platform_value::BinaryData; +use dpp::ProtocolError; +use std::collections::BTreeMap; +use std::fmt; +use wasm_bindgen::prelude::*; + +/// A signer for Platform address-based state transitions. +/// +/// This signer holds private keys for Platform addresses and can sign +/// state transitions that spend from those addresses. +#[wasm_bindgen(js_name = "PlatformAddressSigner")] +#[derive(Clone, Default)] +pub struct PlatformAddressSignerWasm { + /// Maps platform address to private key + private_keys: BTreeMap, +} + +impl fmt::Debug for PlatformAddressSignerWasm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PlatformAddressSigner") + .field("key_count", &self.private_keys.len()) + .finish() + } +} + +#[wasm_bindgen(js_class = PlatformAddressSigner)] +impl PlatformAddressSignerWasm { + /// Creates a new empty PlatformAddressSigner. + #[wasm_bindgen(constructor)] + pub fn new() -> PlatformAddressSignerWasm { + PlatformAddressSignerWasm { + private_keys: BTreeMap::new(), + } + } + + /// Adds a private key for the given Platform address. + /// + /// @param address - The Platform address (PlatformAddress, Uint8Array, or bech32m string) + /// @param privateKey - The PrivateKey object + #[wasm_bindgen(js_name = "addKey")] + pub fn add_key( + &mut self, + #[wasm_bindgen(unchecked_param_type = "PlatformAddressLike")] address: &JsValue, + private_key: &PrivateKeyWasm, + ) -> WasmDppResult<()> { + let platform_address = PlatformAddressWasm::try_from(address)?; + self.private_keys.insert(platform_address, private_key.clone()); + Ok(()) + } + + #[wasm_bindgen(getter = __struct)] + pub fn struct_name() -> String { + "PlatformAddressSigner".to_string() + } + + /// Returns the number of keys in this signer. + #[wasm_bindgen(getter = keyCount)] + pub fn key_count(&self) -> usize { + self.private_keys.len() + } + + /// Returns true if this signer has a key for the given address. + #[wasm_bindgen(js_name = "hasKey")] + pub fn has_key( + &self, + #[wasm_bindgen(unchecked_param_type = "PlatformAddressLike")] address: &JsValue, + ) -> WasmDppResult { + let platform_address = PlatformAddressWasm::try_from(address)?; + Ok(self.private_keys.contains_key(&platform_address)) + } + + /// Returns all private keys as an array of {addressHash: Uint8Array, privateKey: Uint8Array}. + /// This is used internally for cross-package access. + #[wasm_bindgen(js_name = "getPrivateKeysBytes")] + pub fn get_private_keys_bytes(&self) -> js_sys::Array { + let result = js_sys::Array::new(); + for (addr, key) in &self.private_keys { + let entry = js_sys::Object::new(); + let addr_inner: PlatformAddress = (*addr).into(); + let hash_array = js_sys::Uint8Array::from(addr_inner.hash().as_slice()); + let key_bytes = key.to_bytes(); + let key_array = js_sys::Uint8Array::from(key_bytes.as_slice()); + let _ = js_sys::Reflect::set(&entry, &JsValue::from_str("addressHash"), &hash_array); + let _ = js_sys::Reflect::set(&entry, &JsValue::from_str("privateKey"), &key_array); + result.push(&entry); + } + result + } +} + +impl Signer for PlatformAddressSignerWasm { + fn sign(&self, address: &PlatformAddress, data: &[u8]) -> Result { + let wasm_address = PlatformAddressWasm::from(*address); + + let private_key = self + .private_keys + .get(&wasm_address) + .ok_or_else(|| { + ProtocolError::Generic(format!( + "No private key found for address hash {}", + hex::encode(address.hash()) + )) + })?; + + let key_bytes = private_key.to_bytes(); + let signature = signer::sign(data, &key_bytes)?; + Ok(signature.to_vec().into()) + } + + fn sign_create_witness( + &self, + address: &PlatformAddress, + data: &[u8], + ) -> Result { + let signature = self.sign(address, data)?; + + match address { + PlatformAddress::P2pkh(_) => Ok(AddressWitness::P2pkh { signature }), + PlatformAddress::P2sh(_) => Err(ProtocolError::Generic( + "P2SH addresses not supported for signing".to_string(), + )), + } + } + + fn can_sign_with(&self, address: &PlatformAddress) -> bool { + let wasm_address = PlatformAddressWasm::from(*address); + self.private_keys.contains_key(&wasm_address) + } +} + +impl PlatformAddressSignerWasm { + /// Returns a reference to the inner signer for use in Rust code. + pub fn inner(&self) -> &Self { + self + } + + /// Returns a reference to the private keys map. + pub fn private_keys(&self) -> &BTreeMap { + &self.private_keys + } + + /// Extracts a PlatformAddressSigner from a JS options object. + /// + /// This helper reads the "signer" field from an options object and converts it + /// to a PlatformAddressSignerWasm. Useful for state transition functions that + /// need a signer from their options. + pub fn try_from_options(options: &JsValue) -> WasmDppResult { + let signer_js = js_sys::Reflect::get(options, &JsValue::from_str("signer")) + .map_err(|_| WasmDppError::invalid_argument("Missing 'signer' field"))?; + + if signer_js.is_undefined() || signer_js.is_null() { + return Err(WasmDppError::invalid_argument("'signer' is required")); + } + + Self::try_from(&signer_js) + } +} + +impl TryFrom<&JsValue> for PlatformAddressSignerWasm { + type Error = WasmDppError; + + fn try_from(value: &JsValue) -> Result { + value + .to_wasm::("PlatformAddressSigner") + .map(|boxed| (*boxed).clone()) + .map_err(|_| WasmDppError::invalid_argument("Expected a PlatformAddressSigner object")) + } +} diff --git a/packages/wasm-dpp2/src/private_key.rs b/packages/wasm-dpp2/src/private_key.rs index e9172ee7d76..798c2877dc9 100644 --- a/packages/wasm-dpp2/src/private_key.rs +++ b/packages/wasm-dpp2/src/private_key.rs @@ -11,8 +11,21 @@ use wasm_bindgen::JsValue; use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen(js_name = "PrivateKey")] +#[derive(Clone)] pub struct PrivateKeyWasm(PrivateKey); +impl From for PrivateKey { + fn from(key: PrivateKeyWasm) -> Self { + key.0 + } +} + +impl From for PrivateKeyWasm { + fn from(key: PrivateKey) -> Self { + PrivateKeyWasm(key) + } +} + #[wasm_bindgen(js_class = PrivateKey)] impl PrivateKeyWasm { #[wasm_bindgen(getter = __type)] diff --git a/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs b/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs index 8c75a71a8ce..d3eec313fa0 100644 --- a/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs @@ -59,6 +59,12 @@ impl From for StateTransition { } } +impl From<&StateTransitionWasm> for StateTransition { + fn from(transition: &StateTransitionWasm) -> Self { + transition.0.clone() + } +} + #[wasm_bindgen(js_class = StateTransition)] impl StateTransitionWasm { #[wasm_bindgen(getter = __type)] diff --git a/packages/wasm-dpp2/tests/unit/AddressFundsTransfer.spec.mjs b/packages/wasm-dpp2/tests/unit/AddressFundsTransfer.spec.mjs new file mode 100644 index 00000000000..cb30a51642c --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/AddressFundsTransfer.spec.mjs @@ -0,0 +1,228 @@ +import getWasm from './helpers/wasm.js'; + +let wasm; + +before(async () => { + wasm = await getWasm(); +}); + +describe('FeeStrategyStep', () => { + describe('construction', () => { + it('should create deductFromInput step', () => { + const step = wasm.FeeStrategyStep.deductFromInput(0); + expect(step).to.exist; + expect(step.isDeductFromInput).to.be.true; + expect(step.isReduceOutput).to.be.false; + expect(step.index).to.equal(0); + }); + + it('should create deductFromInput step with non-zero index', () => { + const step = wasm.FeeStrategyStep.deductFromInput(5); + expect(step.isDeductFromInput).to.be.true; + expect(step.index).to.equal(5); + }); + + it('should create reduceOutput step', () => { + const step = wasm.FeeStrategyStep.reduceOutput(0); + expect(step).to.exist; + expect(step.isDeductFromInput).to.be.false; + expect(step.isReduceOutput).to.be.true; + expect(step.index).to.equal(0); + }); + + it('should create reduceOutput step with non-zero index', () => { + const step = wasm.FeeStrategyStep.reduceOutput(3); + expect(step.isReduceOutput).to.be.true; + expect(step.index).to.equal(3); + }); + }); +}); + +// TODO: Implement AddressFundsTransferTransition in wasm-dpp2 +describe.skip('AddressFundsTransferTransition', () => { + // Valid test private key + const testPrivateKeyHex = 'c9d9d0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfd'; + const testPrivateKeyHex2 = 'a9d9d0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfd'; + + describe('build', () => { + it('should build a transfer transition with single input and output', () => { + // Create input address + const inputAddrBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const inputAddr = wasm.PlatformAddress.fromBytes(Array.from(inputAddrBytes)); + + // Create output address + const outputAddrBytes = new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); + const outputAddr = wasm.PlatformAddress.fromBytes(Array.from(outputAddrBytes)); + + // Create input and output + const input = new wasm.PlatformAddressInput(inputAddr, BigInt(0), BigInt(100000)); + const output = new wasm.PlatformAddressOutput(outputAddr, BigInt(90000)); + + // Create signer + const signer = new wasm.PlatformAddressSigner(); + const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + signer.addKey(inputAddr, privateKey); + + // Build transition + const transition = wasm.AddressFundsTransferTransition.build({ + inputs: [input], + outputs: [output], + signer, + }); + + expect(transition).to.exist; + expect(transition.__wbg_ptr).to.not.equal(0); + }); + + it('should build a transfer transition with multiple inputs and outputs', () => { + // Create input addresses + const input1AddrBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const input1Addr = wasm.PlatformAddress.fromBytes(Array.from(input1AddrBytes)); + + const input2AddrBytes = new Uint8Array([0x00, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40]); + const input2Addr = wasm.PlatformAddress.fromBytes(Array.from(input2AddrBytes)); + + // Create output addresses + const output1AddrBytes = new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); + const output1Addr = wasm.PlatformAddress.fromBytes(Array.from(output1AddrBytes)); + + const output2AddrBytes = new Uint8Array([0x00, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21]); + const output2Addr = wasm.PlatformAddress.fromBytes(Array.from(output2AddrBytes)); + + // Create inputs + const input1 = new wasm.PlatformAddressInput(input1Addr, BigInt(0), BigInt(100000)); + const input2 = new wasm.PlatformAddressInput(input2Addr, BigInt(0), BigInt(50000)); + + // Create outputs + const output1 = new wasm.PlatformAddressOutput(output1Addr, BigInt(80000)); + const output2 = new wasm.PlatformAddressOutput(output2Addr, BigInt(60000)); + + // Create signer with keys for both inputs + const signer = new wasm.PlatformAddressSigner(); + const privateKey1 = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + const privateKey2 = wasm.PrivateKey.fromHex(testPrivateKeyHex2, 'testnet'); + signer.addKey(input1Addr, privateKey1); + signer.addKey(input2Addr, privateKey2); + + // Build transition + const transition = wasm.AddressFundsTransferTransition.build({ + inputs: [input1, input2], + outputs: [output1, output2], + signer, + }); + + expect(transition).to.exist; + }); + + it('should build with custom fee strategy', () => { + const inputAddrBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const inputAddr = wasm.PlatformAddress.fromBytes(Array.from(inputAddrBytes)); + + const outputAddrBytes = new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); + const outputAddr = wasm.PlatformAddress.fromBytes(Array.from(outputAddrBytes)); + + const input = new wasm.PlatformAddressInput(inputAddr, BigInt(0), BigInt(100000)); + const output = new wasm.PlatformAddressOutput(outputAddr, BigInt(100000)); + + const signer = new wasm.PlatformAddressSigner(); + const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + signer.addKey(inputAddr, privateKey); + + // Use reduceOutput fee strategy + const feeStrategy = [wasm.FeeStrategyStep.reduceOutput(0)]; + + const transition = wasm.AddressFundsTransferTransition.build({ + inputs: [input], + outputs: [output], + signer, + feeStrategy, + }); + + expect(transition).to.exist; + }); + + it('should build with user fee increase', () => { + const inputAddrBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const inputAddr = wasm.PlatformAddress.fromBytes(Array.from(inputAddrBytes)); + + const outputAddrBytes = new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); + const outputAddr = wasm.PlatformAddress.fromBytes(Array.from(outputAddrBytes)); + + const input = new wasm.PlatformAddressInput(inputAddr, BigInt(0), BigInt(100000)); + const output = new wasm.PlatformAddressOutput(outputAddr, BigInt(90000)); + + const signer = new wasm.PlatformAddressSigner(); + const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + signer.addKey(inputAddr, privateKey); + + const transition = wasm.AddressFundsTransferTransition.build({ + inputs: [input], + outputs: [output], + signer, + userFeeIncrease: 100, + }); + + expect(transition).to.exist; + }); + + it('should fail without inputs', () => { + const outputAddrBytes = new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); + const outputAddr = wasm.PlatformAddress.fromBytes(Array.from(outputAddrBytes)); + const output = new wasm.PlatformAddressOutput(outputAddr, BigInt(90000)); + + const signer = new wasm.PlatformAddressSigner(); + + try { + wasm.AddressFundsTransferTransition.build({ + inputs: [], + outputs: [output], + signer, + }); + expect.fail('Should have thrown error for missing inputs'); + } catch (error) { + expect(error.message).to.include('input'); + } + }); + + it('should fail without outputs', () => { + const inputAddrBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const inputAddr = wasm.PlatformAddress.fromBytes(Array.from(inputAddrBytes)); + const input = new wasm.PlatformAddressInput(inputAddr, BigInt(0), BigInt(100000)); + + const signer = new wasm.PlatformAddressSigner(); + const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + signer.addKey(inputAddr, privateKey); + + try { + wasm.AddressFundsTransferTransition.build({ + inputs: [input], + outputs: [], + signer, + }); + expect.fail('Should have thrown error for missing outputs'); + } catch (error) { + expect(error.message).to.include('output'); + } + }); + + it('should fail without signer', () => { + const inputAddrBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const inputAddr = wasm.PlatformAddress.fromBytes(Array.from(inputAddrBytes)); + const input = new wasm.PlatformAddressInput(inputAddr, BigInt(0), BigInt(100000)); + + const outputAddrBytes = new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); + const outputAddr = wasm.PlatformAddress.fromBytes(Array.from(outputAddrBytes)); + const output = new wasm.PlatformAddressOutput(outputAddr, BigInt(90000)); + + try { + wasm.AddressFundsTransferTransition.build({ + inputs: [input], + outputs: [output], + }); + expect.fail('Should have thrown error for missing signer'); + } catch (error) { + expect(error.message).to.include('signer'); + } + }); + }); +}); diff --git a/packages/wasm-dpp2/tests/unit/IdentitySigner.spec.mjs b/packages/wasm-dpp2/tests/unit/IdentitySigner.spec.mjs new file mode 100644 index 00000000000..35fc0686f04 --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/IdentitySigner.spec.mjs @@ -0,0 +1,90 @@ +import getWasm from './helpers/wasm.js'; + +let wasm; + +before(async () => { + wasm = await getWasm(); +}); + +describe('IdentitySigner', () => { + // Test private key (WIF format for testnet) + const testPrivateKeyWif = 'cR4EZ2nAvCmn2cFepKn7UgSSQFgFTjkySAchvcoiEVdm48eWjQGn'; + // Same key in hex format (32 bytes) + const testPrivateKeyHex = '67ad1669d882da256b6fa05e1b0ae384a6ac8aed146ea53602b8ff0e1e9c18e9'; + + describe('construction', () => { + it('should create empty signer', () => { + const signer = new wasm.IdentitySigner(); + expect(signer).to.exist; + expect(signer.keyCount).to.equal(0); + }); + }); + + describe('addKey', () => { + it('should add key from PrivateKey created from WIF', () => { + const signer = new wasm.IdentitySigner(); + const privateKey = wasm.PrivateKey.fromWIF(testPrivateKeyWif); + + signer.addKey(privateKey); + expect(signer.keyCount).to.equal(1); + }); + + it('should add key from PrivateKey created from hex', () => { + const signer = new wasm.IdentitySigner(); + const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + + signer.addKey(privateKey); + expect(signer.keyCount).to.equal(1); + }); + + it('should add key from PrivateKey created from bytes', () => { + const signer = new wasm.IdentitySigner(); + const keyBytes = new Uint8Array(32).fill(1); + const privateKey = wasm.PrivateKey.fromBytes(keyBytes, 'testnet'); + + signer.addKey(privateKey); + expect(signer.keyCount).to.equal(1); + }); + + it('should add multiple keys', () => { + const signer = new wasm.IdentitySigner(); + + const privateKey1 = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + const privateKey2 = wasm.PrivateKey.fromHex('a9d9d0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfd', 'testnet'); + + signer.addKey(privateKey1); + signer.addKey(privateKey2); + + expect(signer.keyCount).to.equal(2); + }); + + it('should replace key for same public key hash', () => { + const signer = new wasm.IdentitySigner(); + const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + + signer.addKey(privateKey); + expect(signer.keyCount).to.equal(1); + + // Add same key again + signer.addKey(privateKey); + expect(signer.keyCount).to.equal(1); // Still 1, replaced + }); + }); + + describe('addKeyFromWif', () => { + it('should add key from WIF string', () => { + const signer = new wasm.IdentitySigner(); + + signer.addKeyFromWif(testPrivateKeyWif); + expect(signer.keyCount).to.equal(1); + }); + + it('should throw for invalid WIF', () => { + const signer = new wasm.IdentitySigner(); + + expect(() => { + signer.addKeyFromWif('invalidWif'); + }).to.throw(); + }); + }); +}); diff --git a/packages/wasm-dpp2/tests/unit/PlatformAddress.spec.mjs b/packages/wasm-dpp2/tests/unit/PlatformAddress.spec.mjs new file mode 100644 index 00000000000..1836364411c --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/PlatformAddress.spec.mjs @@ -0,0 +1,239 @@ +import getWasm from './helpers/wasm.js'; + +let wasm; + +before(async () => { + wasm = await getWasm(); +}); + +describe('PlatformAddress', () => { + describe('fromBytes', () => { + it('should create P2PKH address from bytes', () => { + const p2pkhBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(p2pkhBytes); + expect(addr).to.exist; + expect(addr.addressType).to.equal('P2PKH'); + expect(addr.isP2pkh).to.be.true; + expect(addr.isP2sh).to.be.false; + }); + + it('should create P2SH address from bytes', () => { + const p2shBytes = new Uint8Array([0x01, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(p2shBytes); + expect(addr).to.exist; + expect(addr.addressType).to.equal('P2SH'); + expect(addr.isP2pkh).to.be.false; + expect(addr.isP2sh).to.be.true; + }); + + it('should reject invalid address type', () => { + const invalidBytes = new Uint8Array([0x02, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + try { + wasm.PlatformAddress.fromBytes(invalidBytes); + expect.fail('Should have thrown error for invalid address type'); + } catch (error) { + expect(error.message).to.include('type'); + } + }); + + it('should reject wrong length', () => { + const shortBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5]); + try { + wasm.PlatformAddress.fromBytes(shortBytes); + expect.fail('Should have thrown error for wrong length'); + } catch (error) { + expect(error.message).to.include('21'); + } + }); + }); + + describe('fromBech32m', () => { + it('should create address from testnet bech32m string', () => { + // First create an address and get its bech32m + const bytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const originalAddr = wasm.PlatformAddress.fromBytes(bytes); + const bech32m = originalAddr.toBech32m('testnet'); + + expect(bech32m.startsWith('tdashevo1')).to.be.true; + + // Parse it back + const parsedAddr = wasm.PlatformAddress.fromBech32m(bech32m); + expect(parsedAddr).to.exist; + expect(parsedAddr.addressType).to.equal('P2PKH'); + }); + + it('should create address from mainnet bech32m string', () => { + const bytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const originalAddr = wasm.PlatformAddress.fromBytes(bytes); + const bech32m = originalAddr.toBech32m('mainnet'); + + expect(bech32m.startsWith('dashevo1')).to.be.true; + + const parsedAddr = wasm.PlatformAddress.fromBech32m(bech32m); + expect(parsedAddr).to.exist; + expect(parsedAddr.addressType).to.equal('P2PKH'); + }); + + it('should reject invalid bech32m string', () => { + try { + wasm.PlatformAddress.fromBech32m('invalid-address'); + expect.fail('Should have thrown error for invalid bech32m'); + } catch (error) { + expect(error).to.exist; + } + }); + }); + + describe('fromHex', () => { + it('should create address from hex string', () => { + const hexString = '00' + '01020304050607080910111213141516171819'.padEnd(40, '0').substring(0, 40); + const addr = wasm.PlatformAddress.fromHex(hexString); + expect(addr).to.exist; + expect(addr.addressType).to.equal('P2PKH'); + }); + + it('should reject invalid hex', () => { + try { + wasm.PlatformAddress.fromHex('not-valid-hex'); + expect.fail('Should have thrown error for invalid hex'); + } catch (error) { + expect(error.message).to.include('hex'); + } + }); + }); + + describe('fromP2pkhHash', () => { + it('should create P2PKH address from 20-byte hash', () => { + const hash = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromP2pkhHash(hash); + expect(addr).to.exist; + expect(addr.addressType).to.equal('P2PKH'); + expect(addr.isP2pkh).to.be.true; + }); + + it('should reject wrong hash length', () => { + const shortHash = new Uint8Array([1, 2, 3, 4, 5]); + try { + wasm.PlatformAddress.fromP2pkhHash(shortHash); + expect.fail('Should have thrown error for wrong length'); + } catch (error) { + expect(error.message).to.include('20 bytes'); + } + }); + }); + + describe('fromP2shHash', () => { + it('should create P2SH address from 20-byte hash', () => { + const hash = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromP2shHash(hash); + expect(addr).to.exist; + expect(addr.addressType).to.equal('P2SH'); + expect(addr.isP2sh).to.be.true; + }); + + it('should reject wrong hash length', () => { + const shortHash = new Uint8Array([1, 2, 3, 4, 5]); + try { + wasm.PlatformAddress.fromP2shHash(shortHash); + expect.fail('Should have thrown error for wrong length'); + } catch (error) { + expect(error.message).to.include('20 bytes'); + } + }); + }); + + describe('constructor', () => { + it('should accept PlatformAddress object', () => { + const bytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const original = wasm.PlatformAddress.fromBytes(bytes); + const copy = new wasm.PlatformAddress(original); + expect(copy).to.exist; + expect(copy.addressType).to.equal(original.addressType); + }); + + it('should accept Uint8Array', () => { + const bytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = new wasm.PlatformAddress(bytes); + expect(addr).to.exist; + expect(addr.addressType).to.equal('P2PKH'); + }); + + it('should accept bech32m string', () => { + const bytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const original = wasm.PlatformAddress.fromBytes(bytes); + const bech32m = original.toBech32m('testnet'); + + const addr = new wasm.PlatformAddress(bech32m); + expect(addr).to.exist; + expect(addr.addressType).to.equal('P2PKH'); + }); + }); + + describe('toBytes', () => { + it('should return 21-byte array', () => { + const inputBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(inputBytes); + const outputBytes = addr.toBytes(); + expect(outputBytes.length).to.equal(21); + expect(Array.from(outputBytes)).to.deep.equal(Array.from(inputBytes)); + }); + }); + + describe('toHex', () => { + it('should return hex string', () => { + const bytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(bytes); + const hex = addr.toHex(); + expect(hex).to.be.a('string'); + expect(hex.length).to.equal(42); // 21 bytes * 2 + }); + }); + + describe('hash', () => { + it('should return 20-byte hash', () => { + const bytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(bytes); + const hash = addr.hash(); + expect(hash.length).to.equal(20); + expect(Array.from(hash)).to.deep.equal([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + }); + }); + + describe('hashToHex', () => { + it('should return hex string of hash', () => { + const bytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(bytes); + const hashHex = addr.hashToHex(); + expect(hashHex).to.be.a('string'); + expect(hashHex.length).to.equal(40); // 20 bytes * 2 + }); + }); + + describe('toBech32m', () => { + it('should convert to testnet bech32m', () => { + const bytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(bytes); + const bech32m = addr.toBech32m('testnet'); + expect(bech32m).to.be.a('string'); + expect(bech32m.startsWith('tdashevo1')).to.be.true; + }); + + it('should convert to mainnet bech32m', () => { + const bytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(bytes); + const bech32m = addr.toBech32m('mainnet'); + expect(bech32m).to.be.a('string'); + expect(bech32m.startsWith('dashevo1')).to.be.true; + }); + + it('should roundtrip through bech32m', () => { + const originalBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(originalBytes); + const bech32m = addr.toBech32m('testnet'); + + const recovered = wasm.PlatformAddress.fromBech32m(bech32m); + const recoveredBytes = recovered.toBytes(); + expect(Array.from(recoveredBytes)).to.deep.equal(Array.from(originalBytes)); + }); + }); +}); diff --git a/packages/wasm-dpp2/tests/unit/PlatformAddressInput.spec.mjs b/packages/wasm-dpp2/tests/unit/PlatformAddressInput.spec.mjs new file mode 100644 index 00000000000..a4a71be3fbc --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/PlatformAddressInput.spec.mjs @@ -0,0 +1,110 @@ +import getWasm from './helpers/wasm.js'; + +let wasm; + +before(async () => { + wasm = await getWasm(); +}); + +describe('PlatformAddressInput', () => { + describe('construction', () => { + it('should create from PlatformAddress object', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + + const input = new wasm.PlatformAddressInput(platformAddr, BigInt(5), BigInt(500000)); + expect(input).to.exist; + expect(input.nonce).to.equal(BigInt(5)); + expect(input.amount).to.equal(BigInt(500000)); + }); + + it('should create from bech32m address string', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + const bech32m = platformAddr.toBech32m('testnet'); + + const input = new wasm.PlatformAddressInput(bech32m, BigInt(1), BigInt(100000)); + expect(input).to.exist; + expect(input.nonce).to.equal(BigInt(1)); + expect(input.amount).to.equal(BigInt(100000)); + }); + + it('should create from Uint8Array', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + + const input = new wasm.PlatformAddressInput(addressBytes, BigInt(0), BigInt(100000)); + expect(input).to.exist; + expect(input.nonce).to.equal(BigInt(0)); + expect(input.amount).to.equal(BigInt(100000)); + }); + + it('should handle zero nonce', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + + const input = new wasm.PlatformAddressInput(platformAddr, BigInt(0), BigInt(100000)); + expect(input.nonce).to.equal(BigInt(0)); + }); + + it('should handle large amounts', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + + const largeAmount = BigInt('10000000000000000'); // 10 quadrillion + const input = new wasm.PlatformAddressInput(platformAddr, BigInt(1), largeAmount); + expect(input.amount).to.equal(largeAmount); + }); + + it('should reject negative nonce', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + + try { + new wasm.PlatformAddressInput(platformAddr, BigInt(-1), BigInt(100000)); + expect.fail('Should have thrown error for negative nonce'); + } catch (error) { + expect(error).to.exist; + } + }); + + it('should reject nonce exceeding u32 max', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + + try { + new wasm.PlatformAddressInput(platformAddr, BigInt('4294967296'), BigInt(100000)); // u32::MAX + 1 + expect.fail('Should have thrown error for nonce exceeding u32 max'); + } catch (error) { + expect(error.message).to.include('u32'); + } + }); + }); + + describe('getters', () => { + it('should return the address', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + + const input = new wasm.PlatformAddressInput(platformAddr, BigInt(1), BigInt(100000)); + const addr = input.address; + expect(addr).to.exist; + expect(addr.addressType).to.equal('P2PKH'); + }); + + it('should return the nonce', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + + const input = new wasm.PlatformAddressInput(platformAddr, BigInt(42), BigInt(100000)); + expect(input.nonce).to.equal(BigInt(42)); + }); + + it('should return the amount', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + + const input = new wasm.PlatformAddressInput(platformAddr, BigInt(1), BigInt(123456)); + expect(input.amount).to.equal(BigInt(123456)); + }); + }); +}); diff --git a/packages/wasm-dpp2/tests/unit/PlatformAddressOutput.spec.mjs b/packages/wasm-dpp2/tests/unit/PlatformAddressOutput.spec.mjs new file mode 100644 index 00000000000..e187813aac0 --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/PlatformAddressOutput.spec.mjs @@ -0,0 +1,87 @@ +import getWasm from './helpers/wasm.js'; + +let wasm; + +before(async () => { + wasm = await getWasm(); +}); + +describe('PlatformAddressOutput', () => { + describe('construction', () => { + it('should create from PlatformAddress object', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + + const output = new wasm.PlatformAddressOutput(platformAddr, BigInt(500000)); + expect(output).to.exist; + expect(output.amount).to.equal(BigInt(500000)); + }); + + it('should create from bech32m address string', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + const bech32m = platformAddr.toBech32m('testnet'); + + const output = new wasm.PlatformAddressOutput(bech32m, BigInt(90000)); + expect(output).to.exist; + expect(output.amount).to.equal(BigInt(90000)); + }); + + it('should create from Uint8Array', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + + const output = new wasm.PlatformAddressOutput(addressBytes, BigInt(100000)); + expect(output).to.exist; + expect(output.amount).to.equal(BigInt(100000)); + }); + + it('should handle large amounts', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + + const largeAmount = BigInt('10000000000000000'); + const output = new wasm.PlatformAddressOutput(platformAddr, largeAmount); + expect(output.amount).to.equal(largeAmount); + }); + + it('should handle zero amount', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + + const output = new wasm.PlatformAddressOutput(platformAddr, BigInt(0)); + expect(output.amount).to.equal(BigInt(0)); + }); + + it('should reject negative amount', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + + try { + new wasm.PlatformAddressOutput(platformAddr, BigInt(-1)); + expect.fail('Should have thrown error for negative amount'); + } catch (error) { + expect(error).to.exist; + } + }); + }); + + describe('getters', () => { + it('should return the address', () => { + const addressBytes = new Uint8Array([0x01, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + + const output = new wasm.PlatformAddressOutput(platformAddr, BigInt(100000)); + const addr = output.address; + expect(addr).to.exist; + expect(addr.addressType).to.equal('P2SH'); + }); + + it('should return the amount', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); + + const output = new wasm.PlatformAddressOutput(platformAddr, BigInt(999999)); + expect(output.amount).to.equal(BigInt(999999)); + }); + }); +}); diff --git a/packages/wasm-dpp2/tests/unit/PlatformAddressSigner.spec.mjs b/packages/wasm-dpp2/tests/unit/PlatformAddressSigner.spec.mjs new file mode 100644 index 00000000000..6d6b40a66f1 --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/PlatformAddressSigner.spec.mjs @@ -0,0 +1,158 @@ +import getWasm from './helpers/wasm.js'; + +let wasm; + +before(async () => { + wasm = await getWasm(); +}); + +describe('PlatformAddressSigner', () => { + // Test private key (WIF format for testnet) + const testPrivateKeyWif = 'cR4EZ2nAvCmn2cFepKn7UgSSQFgFTjkySAchvcoiEVdm48eWjQGn'; + // Same key in hex format (32 bytes) + const testPrivateKeyHex = '67ad1669d882da256b6fa05e1b0ae384a6ac8aed146ea53602b8ff0e1e9c18e9'; + + describe('construction', () => { + it('should create empty signer', () => { + const signer = new wasm.PlatformAddressSigner(); + expect(signer).to.exist; + expect(signer.keyCount).to.equal(0); + }); + }); + + describe('addKey', () => { + it('should add key from PrivateKey created from WIF', () => { + const signer = new wasm.PlatformAddressSigner(); + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(addressBytes); + const privateKey = wasm.PrivateKey.fromWIF(testPrivateKeyWif); + + signer.addKey(addr, privateKey); + expect(signer.keyCount).to.equal(1); + }); + + it('should add key from PrivateKey created from hex', () => { + const signer = new wasm.PlatformAddressSigner(); + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(addressBytes); + const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + + signer.addKey(addr, privateKey); + expect(signer.keyCount).to.equal(1); + }); + + it('should add key from PrivateKey created from bytes', () => { + const signer = new wasm.PlatformAddressSigner(); + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(addressBytes); + const keyBytes = new Uint8Array(32).fill(1); + const privateKey = wasm.PrivateKey.fromBytes(keyBytes, 'testnet'); + + signer.addKey(addr, privateKey); + expect(signer.keyCount).to.equal(1); + }); + + it('should add multiple keys', () => { + const signer = new wasm.PlatformAddressSigner(); + + const addr1Bytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr1 = wasm.PlatformAddress.fromBytes(addr1Bytes); + + const addr2Bytes = new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); + const addr2 = wasm.PlatformAddress.fromBytes(addr2Bytes); + + const privateKey1 = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + const privateKey2 = wasm.PrivateKey.fromHex('a9d9d0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfd', 'testnet'); + + signer.addKey(addr1, privateKey1); + signer.addKey(addr2, privateKey2); + + expect(signer.keyCount).to.equal(2); + }); + + it('should accept address as bech32m string', () => { + const signer = new wasm.PlatformAddressSigner(); + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(addressBytes); + const bech32m = addr.toBech32m('testnet'); + + const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + signer.addKey(bech32m, privateKey); + + expect(signer.keyCount).to.equal(1); + }); + + it('should replace key for same address', () => { + const signer = new wasm.PlatformAddressSigner(); + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(addressBytes); + + const privateKey1 = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + const privateKey2 = wasm.PrivateKey.fromHex('a9d9d0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfd', 'testnet'); + + signer.addKey(addr, privateKey1); + expect(signer.keyCount).to.equal(1); + + // Add different key for same address + signer.addKey(addr, privateKey2); + expect(signer.keyCount).to.equal(1); // Still 1, replaced + }); + }); + + describe('hasKey', () => { + it('should return true for added address', () => { + const signer = new wasm.PlatformAddressSigner(); + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(addressBytes); + const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + + signer.addKey(addr, privateKey); + expect(signer.hasKey(addr)).to.be.true; + }); + + it('should return false for unknown address', () => { + const signer = new wasm.PlatformAddressSigner(); + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(addressBytes); + + expect(signer.hasKey(addr)).to.be.false; + }); + + it('should accept address as bech32m string', () => { + const signer = new wasm.PlatformAddressSigner(); + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(addressBytes); + const bech32m = addr.toBech32m('testnet'); + const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + + signer.addKey(addr, privateKey); + expect(signer.hasKey(bech32m)).to.be.true; + }); + }); + + describe('getPrivateKeysBytes', () => { + it('should return empty array for empty signer', () => { + const signer = new wasm.PlatformAddressSigner(); + const keys = signer.getPrivateKeysBytes(); + expect(keys).to.be.an('array'); + expect(keys.length).to.equal(0); + }); + + it('should return array of key entries', () => { + const signer = new wasm.PlatformAddressSigner(); + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr = wasm.PlatformAddress.fromBytes(addressBytes); + const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + + signer.addKey(addr, privateKey); + + const keys = signer.getPrivateKeysBytes(); + expect(keys).to.be.an('array'); + expect(keys.length).to.equal(1); + expect(keys[0].addressHash).to.be.instanceOf(Uint8Array); + expect(keys[0].privateKey).to.be.instanceOf(Uint8Array); + expect(keys[0].addressHash.length).to.equal(20); + expect(keys[0].privateKey.length).to.equal(32); + }); + }); +}); diff --git a/packages/wasm-sdk/src/lib.rs b/packages/wasm-sdk/src/lib.rs index d55a46b0d76..a8e0b9d6c74 100644 --- a/packages/wasm-sdk/src/lib.rs +++ b/packages/wasm-sdk/src/lib.rs @@ -7,6 +7,7 @@ pub mod logging; pub mod queries; pub mod sdk; pub mod serialization; +pub mod settings; pub mod state_transitions; pub mod utils; pub mod wallet; @@ -14,8 +15,7 @@ pub mod wallet; // Re-export commonly used items pub use dpns::*; pub use error::{WasmSdkError, WasmSdkErrorKind}; -pub use queries::{AddressInfoWasm, PlatformAddressInfoWasm, ProofInfoWasm, ProofMetadataResponseWasm, ResponseMetadataWasm, -}; +pub use queries::{PlatformAddressInfoWasm, ProofInfoWasm, ProofMetadataResponseWasm, ResponseMetadataWasm}; pub use state_transitions::identity as state_transition_identity; pub use wallet::*; pub use wasm_dpp2::*; diff --git a/packages/wasm-sdk/src/queries/address.rs b/packages/wasm-sdk/src/queries/address.rs index 8f6ee85d6c5..526a0a0dc19 100644 --- a/packages/wasm-sdk/src/queries/address.rs +++ b/packages/wasm-sdk/src/queries/address.rs @@ -8,7 +8,7 @@ use js_sys::{BigInt, Map}; use std::collections::BTreeSet; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsValue; -use wasm_dpp2::platform_address::PlatformAddressWasm; +use wasm_dpp2::PlatformAddressWasm; /// Information about a Platform address including its nonce and balance. #[wasm_bindgen(js_name = "PlatformAddressInfo")] @@ -19,8 +19,8 @@ pub struct PlatformAddressInfoWasm { balance: u64, } -impl PlatformAddressInfoWasm { - fn from_address_info(info: AddressInfo) -> Self { +impl From for PlatformAddressInfoWasm { + fn from(info: AddressInfo) -> Self { PlatformAddressInfoWasm { address: info.address.into(), nonce: info.nonce, @@ -69,7 +69,7 @@ impl WasmSdk { let address_info = AddressInfo::fetch(self.as_ref(), platform_address).await?; - Ok(address_info.map(PlatformAddressInfoWasm::from_address_info)) + Ok(address_info.map(PlatformAddressInfoWasm::from)) } /// Fetches information about a Platform address including its nonce and balance, with proof. @@ -95,7 +95,7 @@ impl WasmSdk { .await?; let data = address_info - .map(|info| JsValue::from(PlatformAddressInfoWasm::from_address_info(info))) + .map(|info| JsValue::from(PlatformAddressInfoWasm::from(info))) .unwrap_or(JsValue::UNDEFINED); Ok(ProofMetadataResponseWasm::from_sdk_parts( @@ -138,7 +138,7 @@ impl WasmSdk { let value = address_infos .get(&address) .and_then(|opt| opt.as_ref()) - .map(|info| JsValue::from(PlatformAddressInfoWasm::from_address_info(info.clone()))) + .map(|info| JsValue::from(PlatformAddressInfoWasm::from(info.clone()))) .unwrap_or(JsValue::UNDEFINED); results_map.set(&key, &value); } @@ -186,7 +186,7 @@ impl WasmSdk { let value = address_infos .get(&address) .and_then(|opt| opt.as_ref()) - .map(|info| JsValue::from(PlatformAddressInfoWasm::from_address_info(info.clone()))) + .map(|info| JsValue::from(PlatformAddressInfoWasm::from(info.clone()))) .unwrap_or(JsValue::UNDEFINED); results_map.set(&key, &value); } diff --git a/packages/wasm-sdk/src/settings.rs b/packages/wasm-sdk/src/settings.rs new file mode 100644 index 00000000000..d434ebcdcd8 --- /dev/null +++ b/packages/wasm-sdk/src/settings.rs @@ -0,0 +1,284 @@ +//! Common settings types for SDK operations. +//! +//! This module provides WASM bindings for settings used across various SDK methods +//! including queries, broadcasts, and state transitions. + +use dash_sdk::platform::transition::put_settings::PutSettings; +use rs_dapi_client::RequestSettings; +use std::time::Duration; +use wasm_bindgen::prelude::*; + +// ============================================================================ +// RequestSettings - for queries and basic requests +// ============================================================================ + +/// TypeScript interface for request settings (queries) +#[wasm_bindgen(typescript_custom_section)] +const REQUEST_SETTINGS_TS: &'static str = r#" +/** + * Settings for SDK query/request operations. + */ +export interface RequestSettings { + /** + * Number of retries for the request. + * @default 5 + */ + retries?: number; + + /** + * Request timeout in milliseconds. + * @default 10000 + */ + timeoutMs?: number; + + /** + * Connection timeout in milliseconds. + */ + connectTimeoutMs?: number; + + /** + * Whether to ban failed addresses. + * @default true + */ + banFailedAddress?: boolean; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "RequestSettings")] + pub type RequestSettingsJs; +} + +/// Parse request settings from JavaScript into RequestSettings. +/// +/// Used for query operations. +pub fn parse_request_settings(settings: Option) -> Option { + let settings_js = settings?; + let settings_value: JsValue = settings_js.into(); + + if settings_value.is_undefined() || settings_value.is_null() { + return None; + } + + let mut request_settings = RequestSettings::default(); + + // Parse retries + if let Ok(retries_js) = js_sys::Reflect::get(&settings_value, &JsValue::from_str("retries")) { + if let Some(retries) = retries_js.as_f64() { + request_settings.retries = Some(retries as usize); + } + } + + // Parse timeoutMs + if let Ok(timeout_js) = js_sys::Reflect::get(&settings_value, &JsValue::from_str("timeoutMs")) { + if let Some(ms) = timeout_js.as_f64() { + request_settings.timeout = Some(Duration::from_millis(ms as u64)); + } + } + + // Parse connectTimeoutMs + if let Ok(connect_timeout_js) = + js_sys::Reflect::get(&settings_value, &JsValue::from_str("connectTimeoutMs")) + { + if let Some(ms) = connect_timeout_js.as_f64() { + request_settings.connect_timeout = Some(Duration::from_millis(ms as u64)); + } + } + + // Parse banFailedAddress + if let Ok(ban_js) = + js_sys::Reflect::get(&settings_value, &JsValue::from_str("banFailedAddress")) + { + if let Some(ban) = ban_js.as_bool() { + request_settings.ban_failed_address = Some(ban); + } + } + + Some(request_settings) +} + +// ============================================================================ +// PutSettings - for state transitions (broadcasts) +// ============================================================================ + +/// TypeScript interface for put/broadcast settings (state transitions) +#[wasm_bindgen(typescript_custom_section)] +const PUT_SETTINGS_TS: &'static str = r#" +/** + * Settings for state transition broadcast operations. + * Extends RequestSettings with additional options for waiting. + */ +export interface PutSettings { + /** + * Number of retries for the request. + * @default 5 + */ + retries?: number; + + /** + * Request timeout in milliseconds. + * @default 10000 + */ + timeoutMs?: number; + + /** + * Connection timeout in milliseconds. + */ + connectTimeoutMs?: number; + + /** + * Whether to ban failed addresses. + * @default true + */ + banFailedAddress?: boolean; + + /** + * Timeout in milliseconds for waiting for the state transition result. + * Only applies to broadcast and wait operations. + */ + waitTimeoutMs?: number; + + /** + * Fee increase multiplier (0-65535) to prioritize transaction processing. + * Higher values result in higher fees and faster processing. + * @default 0 + */ + userFeeIncrease?: number; + + /** + * Time in seconds after which identity nonces are considered stale. + * Used for nonce management in state transitions. + */ + identityNonceStaleTimeS?: number; + + /** + * Options for state transition creation (debugging). + */ + stateTransitionCreationOptions?: { + /** + * Allow signing with any security level (debugging only). + */ + allowSigningWithAnySecurityLevel?: boolean; + /** + * Allow signing with any purpose (debugging only). + */ + allowSigningWithAnyPurpose?: boolean; + }; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "PutSettings")] + pub type PutSettingsJs; +} + +/// Parse put settings from JavaScript into PutSettings. +/// +/// Used for state transition broadcast operations. +pub fn parse_put_settings(settings: Option) -> Option { + let settings_js = settings?; + let settings_value: JsValue = settings_js.into(); + + if settings_value.is_undefined() || settings_value.is_null() { + return None; + } + + let mut put_settings = PutSettings::default(); + + // Parse retries + if let Ok(retries_js) = js_sys::Reflect::get(&settings_value, &JsValue::from_str("retries")) { + if let Some(retries) = retries_js.as_f64() { + put_settings.request_settings.retries = Some(retries as usize); + } + } + + // Parse timeoutMs + if let Ok(timeout_js) = js_sys::Reflect::get(&settings_value, &JsValue::from_str("timeoutMs")) { + if let Some(ms) = timeout_js.as_f64() { + put_settings.request_settings.timeout = Some(Duration::from_millis(ms as u64)); + } + } + + // Parse connectTimeoutMs + if let Ok(connect_timeout_js) = + js_sys::Reflect::get(&settings_value, &JsValue::from_str("connectTimeoutMs")) + { + if let Some(ms) = connect_timeout_js.as_f64() { + put_settings.request_settings.connect_timeout = Some(Duration::from_millis(ms as u64)); + } + } + + // Parse banFailedAddress + if let Ok(ban_js) = + js_sys::Reflect::get(&settings_value, &JsValue::from_str("banFailedAddress")) + { + if let Some(ban) = ban_js.as_bool() { + put_settings.request_settings.ban_failed_address = Some(ban); + } + } + + // Parse waitTimeoutMs + if let Ok(wait_timeout_js) = + js_sys::Reflect::get(&settings_value, &JsValue::from_str("waitTimeoutMs")) + { + if let Some(ms) = wait_timeout_js.as_f64() { + put_settings.wait_timeout = Some(Duration::from_millis(ms as u64)); + } + } + + // Parse userFeeIncrease + if let Ok(fee_increase_js) = + js_sys::Reflect::get(&settings_value, &JsValue::from_str("userFeeIncrease")) + { + if let Some(fee) = fee_increase_js.as_f64() { + put_settings.user_fee_increase = Some(fee as u16); + } + } + + // Parse identityNonceStaleTimeS + if let Ok(nonce_stale_js) = + js_sys::Reflect::get(&settings_value, &JsValue::from_str("identityNonceStaleTimeS")) + { + if let Some(secs) = nonce_stale_js.as_f64() { + put_settings.identity_nonce_stale_time_s = Some(secs as u64); + } + } + + // Parse stateTransitionCreationOptions + if let Ok(creation_options_js) = js_sys::Reflect::get( + &settings_value, + &JsValue::from_str("stateTransitionCreationOptions"), + ) { + if !creation_options_js.is_undefined() && !creation_options_js.is_null() { + use dash_sdk::dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; + + let mut creation_options = StateTransitionCreationOptions::default(); + + // Parse allowSigningWithAnySecurityLevel + if let Ok(allow_sec_level_js) = js_sys::Reflect::get( + &creation_options_js, + &JsValue::from_str("allowSigningWithAnySecurityLevel"), + ) { + if let Some(allow) = allow_sec_level_js.as_bool() { + creation_options.signing_options.allow_signing_with_any_security_level = allow; + } + } + + // Parse allowSigningWithAnyPurpose + if let Ok(allow_purpose_js) = js_sys::Reflect::get( + &creation_options_js, + &JsValue::from_str("allowSigningWithAnyPurpose"), + ) { + if let Some(allow) = allow_purpose_js.as_bool() { + creation_options.signing_options.allow_signing_with_any_purpose = allow; + } + } + + put_settings.state_transition_creation_options = Some(creation_options); + } + } + + Some(put_settings) +} diff --git a/packages/wasm-sdk/src/state_transitions/addresses.rs b/packages/wasm-sdk/src/state_transitions/addresses.rs new file mode 100644 index 00000000000..48c8e8160f3 --- /dev/null +++ b/packages/wasm-sdk/src/state_transitions/addresses.rs @@ -0,0 +1,1104 @@ +//! Address funds state transition implementations for the WASM SDK. +//! +//! This module provides WASM bindings for address fund operations like transfers and withdrawals. + +use crate::error::WasmSdkError; +use crate::queries::address::PlatformAddressInfoWasm; +use crate::queries::utils::deserialize_required_query; +use crate::sdk::WasmSdk; +use crate::settings::{parse_put_settings, PutSettingsJs}; +use dash_sdk::dpp::identity::core_script::CoreScript; +use dash_sdk::platform::transition::address_credit_withdrawal::WithdrawAddressFunds; +use dash_sdk::platform::transition::transfer_address_funds::TransferAddressFunds; +use dash_sdk::platform::transition::top_up_identity_from_addresses::TopUpIdentityFromAddresses; +use dash_sdk::platform::transition::transfer_to_addresses::TransferToAddresses; +use dash_sdk::platform::{Fetch, Identifier, Identity}; +use js_sys::{BigInt, Map}; +use serde::Deserialize; +use wasm_bindgen::prelude::*; +use wasm_dpp2::identifier::IdentifierWasm; +use wasm_dpp2::utils::IntoWasm; +use wasm_dpp2::{ + fee_strategy_from_steps_or_default, outputs_to_btree_map, CoreScriptWasm, FeeStrategyStepWasm, + IdentitySignerWasm, PlatformAddressOutputWasm, PlatformAddressSignerWasm, PlatformAddressWasm, + PoolingWasm, +}; + +/// Main input struct for address funds transfer options. +/// Uses types from wasm-dpp2 for inputs, outputs, and fee strategy. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct AddressFundsTransferOptionsInput { + inputs: Vec, + outputs: Vec, + #[serde(default)] + fee_strategy: Option>, +} + +fn deserialize_transfer_options( + options: JsValue, +) -> Result { + let parsed: AddressFundsTransferOptionsInput = deserialize_required_query( + options, + "Options object is required", + "address funds transfer options", + )?; + + if parsed.inputs.is_empty() { + return Err(WasmSdkError::invalid_argument( + "At least one input is required", + )); + } + + if parsed.outputs.is_empty() { + return Err(WasmSdkError::invalid_argument( + "At least one output is required", + )); + } + + Ok(parsed) +} + +/// Extracts PutSettings from the 'settings' field of an options object. +fn extract_settings_from_options( + options: &JsValue, +) -> Result, WasmSdkError> { + let settings_js = js_sys::Reflect::get(options, &JsValue::from_str("settings")) + .map_err(|e| WasmSdkError::generic(format!("Failed to extract settings: {:?}", e)))?; + + if settings_js.is_undefined() || settings_js.is_null() { + return Ok(None); + } + + // Convert JsValue to PutSettingsJs and parse + let settings_typed: PutSettingsJs = settings_js.into(); + Ok(parse_put_settings(Some(settings_typed))) +} + +/// TypeScript interface for address transfer options +#[wasm_bindgen(typescript_custom_section)] +const ADDRESS_TRANSFER_OPTIONS_TS: &'static str = r#" +/** + * Options for transferring funds between Platform addresses. + */ +export interface AddressFundsTransferOptions { + /** + * Array of input addresses with amounts to spend. + * Use PlatformAddressInput for typed inputs (nonces fetched automatically). + */ + inputs: PlatformAddressInput[]; + + /** + * Array of output addresses with amounts to receive. + * Use PlatformAddressOutput for typed outputs. + */ + outputs: PlatformAddressOutput[]; + + /** + * Signer containing private keys for all input addresses. + * Use PlatformAddressSigner to add keys before calling transfer. + */ + signer: PlatformAddressSigner; + + /** + * Fee strategy defining how transaction fees are paid. + * Array of FeeStrategyStep, each specifying to deduct from an input or reduce an output. + * @default [FeeStrategyStep.deductFromInput(0)] + */ + feeStrategy?: FeeStrategyStep[]; + + /** + * Optional settings for the broadcast operation. + * Includes retries, timeouts, userFeeIncrease, etc. + */ + settings?: PutSettings; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "AddressFundsTransferOptions")] + pub type AddressFundsTransferOptionsJs; +} + +#[wasm_bindgen] +impl WasmSdk { + /// Transfers credits between Platform addresses. + /// + /// This method handles the complete transfer flow: + /// 1. Fetches current nonces for all input addresses + /// 2. Builds and signs the transfer transition + /// 3. Broadcasts and waits for confirmation + /// + /// @param options - Transfer options including inputs, outputs, and private keys + /// @returns Promise resolving to Map of PlatformAddress to PlatformAddressInfo + #[wasm_bindgen( + js_name = "addressFundsTransfer", + unchecked_return_type = "Map" + )] + pub async fn address_funds_transfer( + &self, + options: AddressFundsTransferOptionsJs, + ) -> Result { + let options_value: JsValue = options.into(); + + // Deserialize and validate options + let parsed = deserialize_transfer_options(options_value.clone())?; + + // Convert inputs and outputs to maps + let inputs_map = outputs_to_btree_map(parsed.inputs); + let outputs_map = outputs_to_btree_map(parsed.outputs); + + // Extract signer from options + let signer = PlatformAddressSignerWasm::try_from_options(&options_value)?; + + // Convert fee strategy from input using wasm-dpp2 helper + let fee_strategy = fee_strategy_from_steps_or_default(parsed.fee_strategy); + + // Extract settings from options + let settings = extract_settings_from_options(&options_value)?; + + // Use the SDK's transfer_address_funds method which handles nonces, building, and broadcasting + let address_infos = self + .inner_sdk() + .transfer_address_funds(inputs_map, outputs_map, fee_strategy, &signer, settings) + .await + .map_err(|e| WasmSdkError::generic(format!("Failed to transfer funds: {}", e)))?; + + // Convert to Map + let results_map = Map::new(); + + for (address, info_opt) in address_infos { + let info = info_opt.ok_or_else(|| { + WasmSdkError::generic(format!( + "Address {} has no info after transfer", + address + )) + })?; + let key = JsValue::from(PlatformAddressWasm::from(address)); + let value = JsValue::from(PlatformAddressInfoWasm::from(info)); + results_map.set(&key, &value); + } + + Ok(results_map) + } +} + +/// Main input struct for identity top up from addresses options. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct IdentityTopUpFromAddressesOptionsInput { + identity_id: IdentifierWasm, + inputs: Vec, +} + +fn deserialize_identity_top_up_options( + options: JsValue, +) -> Result { + let parsed: IdentityTopUpFromAddressesOptionsInput = deserialize_required_query( + options, + "Options object is required", + "identity top up from addresses options", + )?; + + if parsed.inputs.is_empty() { + return Err(WasmSdkError::invalid_argument( + "At least one input is required", + )); + } + + Ok(parsed) +} + +/// Result of topping up an identity from Platform addresses. +#[wasm_bindgen(js_name = "IdentityTopUpFromAddressesResult")] +pub struct IdentityTopUpFromAddressesResultWasm { + address_infos: Map, + new_balance: u64, +} + +#[wasm_bindgen(js_class = IdentityTopUpFromAddressesResult)] +impl IdentityTopUpFromAddressesResultWasm { + /// Map of addresses to their updated info after the top up. + #[wasm_bindgen(getter = "addressInfos")] + pub fn address_infos(&self) -> Map { + self.address_infos.clone() + } + + /// New balance of the identity after top up. + #[wasm_bindgen(getter = "newBalance")] + pub fn new_balance(&self) -> BigInt { + BigInt::from(self.new_balance) + } +} + +/// TypeScript interface for identity top up from addresses options +#[wasm_bindgen(typescript_custom_section)] +const IDENTITY_TOP_UP_OPTIONS_TS: &'static str = r#" +/** + * Options for topping up an identity from Platform addresses. + */ +export interface IdentityTopUpFromAddressesOptions { + /** + * The identity ID to top up. + */ + identityId: Identifier; + + /** + * Array of input addresses with amounts to use for top up. + * Use PlatformAddressInput for typed inputs (nonces fetched automatically). + */ + inputs: PlatformAddressInput[]; + + /** + * Signer containing private keys for all input addresses. + * Use PlatformAddressSigner to add keys before calling top up. + */ + signer: PlatformAddressSigner; + + /** + * Optional settings for the broadcast operation. + * Includes retries, timeouts, userFeeIncrease, etc. + */ + settings?: PutSettings; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IdentityTopUpFromAddressesOptions")] + pub type IdentityTopUpFromAddressesOptionsJs; +} + +#[wasm_bindgen] +impl WasmSdk { + /// Top up an identity from Platform addresses. + /// + /// This method handles the complete top up flow: + /// 1. Fetches the identity from Platform + /// 2. Fetches current nonces for all input addresses + /// 3. Builds and signs the identity top up transition + /// 4. Broadcasts and waits for confirmation + /// + /// @param options - Top up options including identity ID, inputs, and signer + /// @returns Promise resolving to result with updated address infos and new identity balance + #[wasm_bindgen(js_name = "identityTopUpFromAddresses")] + pub async fn identity_top_up_from_addresses( + &self, + options: IdentityTopUpFromAddressesOptionsJs, + ) -> Result { + let options_value: JsValue = options.into(); + + // Deserialize and validate options + let parsed = deserialize_identity_top_up_options(options_value.clone())?; + + // Convert identity ID + let identity_id: Identifier = parsed.identity_id.into(); + + // Fetch the identity + let identity = Identity::fetch_by_identifier(self.as_ref(), identity_id) + .await? + .ok_or_else(|| { + WasmSdkError::not_found(format!("Identity {} not found", identity_id)) + })?; + + // Convert inputs to map + let inputs_map = outputs_to_btree_map(parsed.inputs); + + // Extract signer from options + let signer = PlatformAddressSignerWasm::try_from_options(&options_value)?; + + // Extract settings from options + let settings = extract_settings_from_options(&options_value)?; + + // Use the SDK's top_up_from_addresses method + let (address_infos, new_balance) = identity + .top_up_from_addresses(self.inner_sdk(), inputs_map, &signer, settings) + .await + .map_err(|e| WasmSdkError::generic(format!("Failed to top up identity: {}", e)))?; + + // Convert to Map + let address_infos_map = Map::new(); + + for (address, info_opt) in address_infos { + let info = info_opt.ok_or_else(|| { + WasmSdkError::generic(format!("Address {} has no info after top up", address)) + })?; + let key = JsValue::from(PlatformAddressWasm::from(address)); + let value = JsValue::from(PlatformAddressInfoWasm::from(info)); + address_infos_map.set(&key, &value); + } + + Ok(IdentityTopUpFromAddressesResultWasm { + address_infos: address_infos_map, + new_balance, + }) + } +} + +/// Main input struct for address funds withdraw options. +/// Uses types from wasm-dpp2 for inputs, change output, and fee strategy. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct AddressFundsWithdrawOptionsInput { + inputs: Vec, + #[serde(default)] + change_output: Option, + #[serde(default)] + fee_strategy: Option>, + core_fee_per_byte: u32, + pooling: PoolingWasm, +} + +fn deserialize_withdraw_options( + options: JsValue, +) -> Result { + let parsed: AddressFundsWithdrawOptionsInput = deserialize_required_query( + options, + "Options object is required", + "address funds withdraw options", + )?; + + if parsed.inputs.is_empty() { + return Err(WasmSdkError::invalid_argument( + "At least one input is required", + )); + } + + Ok(parsed) +} + +/// TypeScript interface for address withdraw options +#[wasm_bindgen(typescript_custom_section)] +const ADDRESS_WITHDRAW_OPTIONS_TS: &'static str = r#" +/** + * Options for withdrawing Platform address credits to Core (L1). + */ +export interface AddressFundsWithdrawOptions { + /** + * Array of input addresses with amounts to withdraw. + * Use PlatformAddressInput for typed inputs (nonces fetched automatically). + */ + inputs: PlatformAddressInput[]; + + /** + * Optional change output address and amount. + * If provided, specifies where to send any change from the withdrawal. + */ + changeOutput?: PlatformAddressOutput; + + /** + * Fee strategy defining how transaction fees are paid. + * Array of FeeStrategyStep, each specifying to deduct from an input or reduce an output. + * @default [FeeStrategyStep.deductFromInput(0)] + */ + feeStrategy?: FeeStrategyStep[]; + + /** + * Core (L1) fee per byte for the withdrawal transaction. + * This determines the mining fee for the Core blockchain transaction. + */ + coreFeePerByte: number; + + /** + * Pooling strategy for the withdrawal. + * - Pooling.Never: Create individual withdrawal transaction + * - Pooling.IfAvailable: Join pool if available, otherwise individual + * - Pooling.Standard: Wait to join pool (may take longer) + */ + pooling: Pooling; + + /** + * Core output script specifying the L1 destination address. + * Use CoreScript.newP2PKH() or CoreScript.newP2SH() to create. + */ + outputScript: CoreScript; + + /** + * Signer containing private keys for all input addresses. + * Use PlatformAddressSigner to add keys before calling withdraw. + */ + signer: PlatformAddressSigner; + + /** + * Optional settings for the broadcast operation. + * Includes retries, timeouts, userFeeIncrease, etc. + */ + settings?: PutSettings; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "AddressFundsWithdrawOptions")] + pub type AddressFundsWithdrawOptionsJs; +} + +#[wasm_bindgen] +impl WasmSdk { + /// Withdraws Platform address credits to Core (L1). + /// + /// This method handles the complete withdrawal flow: + /// 1. Fetches current nonces for all input addresses + /// 2. Builds and signs the withdrawal transition + /// 3. Broadcasts and waits for confirmation + /// 4. The withdrawal may be pooled with others depending on the pooling strategy + /// + /// @param options - Withdrawal options including inputs, output script, and private keys + /// @returns Promise resolving to Map of PlatformAddress to PlatformAddressInfo + #[wasm_bindgen( + js_name = "addressFundsWithdraw", + unchecked_return_type = "Map" + )] + pub async fn address_funds_withdraw( + &self, + options: AddressFundsWithdrawOptionsJs, + ) -> Result { + let options_value: JsValue = options.into(); + + // Deserialize and validate options + let parsed = deserialize_withdraw_options(options_value.clone())?; + + // Convert inputs to map + let inputs_map = outputs_to_btree_map(parsed.inputs); + + // Convert change output if provided + let change_output = parsed.change_output.map(|output| { + let address = output.address().clone().into(); + let amount = output.amount_value(); + (address, amount) + }); + + // Extract output script from options + let output_script_js = + js_sys::Reflect::get(&options_value, &JsValue::from_str("outputScript")) + .map_err(|_| WasmSdkError::invalid_argument("outputScript is required"))?; + + let output_script: CoreScript = output_script_js + .to_wasm::("CoreScript")? + .clone() + .into(); + + // Extract signer from options + let signer = PlatformAddressSignerWasm::try_from_options(&options_value)?; + + // Convert fee strategy from input using wasm-dpp2 helper + let fee_strategy = fee_strategy_from_steps_or_default(parsed.fee_strategy); + + // Convert pooling + let pooling = parsed.pooling.into(); + + // Extract settings from options + let settings = extract_settings_from_options(&options_value)?; + + // Use the SDK's withdraw_address_funds method which handles nonces, building, and broadcasting + let address_infos = self + .inner_sdk() + .withdraw_address_funds( + inputs_map, + change_output, + fee_strategy, + parsed.core_fee_per_byte, + pooling, + output_script, + &signer, + settings, + ) + .await + .map_err(|e| WasmSdkError::generic(format!("Failed to withdraw funds: {}", e)))?; + + // Convert to Map + let results_map = Map::new(); + + for (address, info_opt) in address_infos { + let info = info_opt.ok_or_else(|| { + WasmSdkError::generic(format!( + "Address {} has no info after withdrawal", + address + )) + })?; + let key = JsValue::from(PlatformAddressWasm::from(address)); + let value = JsValue::from(PlatformAddressInfoWasm::from(info)); + results_map.set(&key, &value); + } + + Ok(results_map) + } + + /// Transfer credits from an identity to Platform addresses. + /// + /// This method handles the complete transfer flow: + /// 1. Fetches the identity from Platform + /// 2. Finds the appropriate transfer key to use for signing (if signingTransferKeyId specified) + /// 3. Builds and signs the identity credit transfer to addresses transition + /// 4. Broadcasts and waits for confirmation + /// + /// @param options - Transfer options including identity ID, outputs, and signer + /// @returns Promise resolving to result with updated address infos and new identity balance + #[wasm_bindgen(js_name = "identityTransferToAddresses")] + pub async fn identity_transfer_to_addresses( + &self, + options: IdentityTransferToAddressesOptionsJs, + ) -> Result { + let options_value: JsValue = options.into(); + + // Deserialize and validate options + let parsed = deserialize_identity_transfer_options(options_value.clone())?; + + // Convert identity ID + let identity_id: Identifier = parsed.identity_id.into(); + + // Fetch the identity + let identity = Identity::fetch_by_identifier(self.as_ref(), identity_id) + .await? + .ok_or_else(|| { + WasmSdkError::not_found(format!("Identity {} not found", identity_id)) + })?; + + // Convert outputs to map (recipient addresses with amounts) + let outputs_map = outputs_to_btree_map(parsed.outputs); + + // Get the signing key if a specific key ID was requested + use dash_sdk::dpp::identity::accessors::IdentityGettersV0; + let signing_key = parsed + .signing_transfer_key_id + .map(|key_id| { + identity.get_public_key_by_id(key_id).ok_or_else(|| { + WasmSdkError::not_found(format!("Key with ID {} not found", key_id)) + }) + }) + .transpose()?; + + // Extract signer from options + let signer = IdentitySignerWasm::try_from_options(&options_value)?; + + // Extract settings from options + let settings = extract_settings_from_options(&options_value)?; + + // Use the SDK's transfer_credits_to_addresses method + let (address_infos, new_balance) = identity + .transfer_credits_to_addresses( + self.inner_sdk(), + outputs_map, + signing_key, + &signer, + settings, + ) + .await + .map_err(|e| { + WasmSdkError::generic(format!("Failed to transfer credits to addresses: {}", e)) + })?; + + // Convert to Map + let address_infos_map = Map::new(); + + for (address, info_opt) in address_infos { + let info = info_opt.ok_or_else(|| { + WasmSdkError::generic(format!( + "Address {} has no info after transfer", + address + )) + })?; + let key = JsValue::from(PlatformAddressWasm::from(address)); + let value = JsValue::from(PlatformAddressInfoWasm::from(info)); + address_infos_map.set(&key, &value); + } + + Ok(IdentityTransferToAddressesResultWasm { + address_infos: address_infos_map, + new_balance, + }) + } +} + +/// Main input struct for identity transfer to addresses options. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct IdentityTransferToAddressesOptionsInput { + identity_id: IdentifierWasm, + outputs: Vec, + #[serde(default)] + signing_transfer_key_id: Option, +} + +fn deserialize_identity_transfer_options( + options: JsValue, +) -> Result { + let parsed: IdentityTransferToAddressesOptionsInput = deserialize_required_query( + options, + "Options object is required", + "identity transfer to addresses options", + )?; + + if parsed.outputs.is_empty() { + return Err(WasmSdkError::invalid_argument( + "At least one output is required", + )); + } + + Ok(parsed) +} + +/// Result of transferring credits from an identity to Platform addresses. +#[wasm_bindgen(js_name = "IdentityTransferToAddressesResult")] +pub struct IdentityTransferToAddressesResultWasm { + address_infos: Map, + new_balance: u64, +} + +#[wasm_bindgen(js_class = IdentityTransferToAddressesResult)] +impl IdentityTransferToAddressesResultWasm { + /// Map of addresses to their updated info after the transfer. + #[wasm_bindgen(getter = "addressInfos")] + pub fn address_infos(&self) -> Map { + self.address_infos.clone() + } + + /// New balance of the identity after transfer. + #[wasm_bindgen(getter = "newBalance")] + pub fn new_balance(&self) -> BigInt { + BigInt::from(self.new_balance) + } +} + +/// TypeScript interface for identity transfer to addresses options +#[wasm_bindgen(typescript_custom_section)] +const IDENTITY_TRANSFER_OPTIONS_TS: &'static str = r#" +/** + * Options for transferring credits from an identity to Platform addresses. + */ +export interface IdentityTransferToAddressesOptions { + /** + * The identity ID to transfer credits from. + */ + identityId: Identifier; + + /** + * Array of output addresses with amounts to receive. + * Use PlatformAddressOutput for typed outputs. + */ + outputs: PlatformAddressOutput[]; + + /** + * Signer containing the private key(s) for signing with identity transfer key(s). + * Use IdentitySigner to add keys before calling transfer. + */ + signer: IdentitySigner; + + /** + * Optional key ID to use for signing. + * If not specified, will auto-select a matching transfer key. + */ + signingTransferKeyId?: number; + + /** + * Optional settings for the broadcast operation. + * Includes retries, timeouts, userFeeIncrease, etc. + */ + settings?: PutSettings; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IdentityTransferToAddressesOptions")] + pub type IdentityTransferToAddressesOptionsJs; +} + +// ============================================================================ +// Address Funding from Asset Lock +// ============================================================================ + +/// Main input struct for address funding from asset lock options. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct AddressFundingFromAssetLockOptionsInput { + outputs: Vec, + #[serde(default)] + fee_strategy: Option>, +} + +fn deserialize_address_funding_options( + options: JsValue, +) -> Result { + let parsed: AddressFundingFromAssetLockOptionsInput = deserialize_required_query( + options, + "Options object is required", + "address funding from asset lock options", + )?; + + if parsed.outputs.is_empty() { + return Err(WasmSdkError::invalid_argument( + "At least one output is required", + )); + } + + Ok(parsed) +} + +/// TypeScript interface for address funding from asset lock options +#[wasm_bindgen(typescript_custom_section)] +const ADDRESS_FUNDING_OPTIONS_TS: &'static str = r#" +/** + * Options for funding Platform addresses from an asset lock. + */ +export interface AddressFundingFromAssetLockOptions { + /** + * Asset lock proof from the Core chain. + * Use AssetLockProof.createInstantAssetLockProof() or AssetLockProof.createChainAssetLockProof(). + */ + assetLockProof: AssetLockProof; + + /** + * Private key for signing the asset lock proof. + * This is the private key that controls the asset lock output. + */ + assetLockPrivateKey: PrivateKey; + + /** + * Array of output addresses with amounts to fund. + * Use PlatformAddressOutput for typed outputs. + */ + outputs: PlatformAddressOutput[]; + + /** + * Signer containing private keys for all output addresses. + * Use PlatformAddressSigner to add keys before calling fund. + */ + signer: PlatformAddressSigner; + + /** + * Fee strategy defining how transaction fees are paid. + * Array of FeeStrategyStep, each specifying to deduct from an input or reduce an output. + * @default [FeeStrategyStep.deductFromInput(0)] + */ + feeStrategy?: FeeStrategyStep[]; + + /** + * Optional settings for the broadcast operation. + * Includes retries, timeouts, userFeeIncrease, etc. + */ + settings?: PutSettings; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "AddressFundingFromAssetLockOptions")] + pub type AddressFundingFromAssetLockOptionsJs; +} + +#[wasm_bindgen] +impl WasmSdk { + /// Fund Platform addresses from an asset lock. + /// + /// This method handles the complete funding flow: + /// 1. Validates the asset lock proof + /// 2. Builds and signs the address funding transition + /// 3. Broadcasts and waits for confirmation + /// + /// @param options - Funding options including asset lock, outputs, and signer + /// @returns Promise resolving to Map of PlatformAddress to PlatformAddressInfo + #[wasm_bindgen( + js_name = "addressFundingFromAssetLock", + unchecked_return_type = "Map" + )] + pub async fn address_funding_from_asset_lock( + &self, + options: AddressFundingFromAssetLockOptionsJs, + ) -> Result { + use dash_sdk::platform::transition::top_up_address::TopUpAddress; + use wasm_dpp2::asset_lock_proof::AssetLockProofWasm; + use wasm_dpp2::private_key::PrivateKeyWasm; + use wasm_dpp2::utils::IntoWasm; + + let options_value: JsValue = options.into(); + + // Deserialize and validate options + let parsed = deserialize_address_funding_options(options_value.clone())?; + + // Extract asset lock proof from options + let asset_lock_proof_js = + js_sys::Reflect::get(&options_value, &JsValue::from_str("assetLockProof")) + .map_err(|_| WasmSdkError::invalid_argument("assetLockProof is required"))?; + let asset_lock_proof: dash_sdk::dpp::prelude::AssetLockProof = asset_lock_proof_js + .to_wasm::("AssetLockProof")? + .clone() + .into(); + + // Extract asset lock private key from options + let asset_lock_private_key_js = + js_sys::Reflect::get(&options_value, &JsValue::from_str("assetLockPrivateKey")) + .map_err(|_| WasmSdkError::invalid_argument("assetLockPrivateKey is required"))?; + let asset_lock_private_key: dash_sdk::dpp::dashcore::PrivateKey = asset_lock_private_key_js + .to_wasm::("PrivateKey")? + .clone() + .into(); + + // Convert outputs to map (address -> optional amount) + let outputs_map: std::collections::BTreeMap< + dash_sdk::dpp::address_funds::PlatformAddress, + Option, + > = parsed + .outputs + .into_iter() + .map(|output| { + let address = output.address().clone().into(); + let amount = Some(output.amount_value()); + (address, amount) + }) + .collect(); + + // Extract signer from options + let signer = PlatformAddressSignerWasm::try_from_options(&options_value)?; + + // Convert fee strategy from input using wasm-dpp2 helper + let fee_strategy = fee_strategy_from_steps_or_default(parsed.fee_strategy); + + // Extract settings from options + let settings = extract_settings_from_options(&options_value)?; + + // Use the SDK's top_up method for addresses + let address_infos = outputs_map + .top_up( + self.inner_sdk(), + asset_lock_proof, + asset_lock_private_key, + fee_strategy, + &signer, + settings, + ) + .await + .map_err(|e| WasmSdkError::generic(format!("Failed to fund addresses: {}", e)))?; + + // Convert to Map + let results_map = Map::new(); + + for (address, info_opt) in address_infos { + let info = info_opt.ok_or_else(|| { + WasmSdkError::generic(format!( + "Address {} has no info after funding", + address + )) + })?; + let key = JsValue::from(PlatformAddressWasm::from(address)); + let value = JsValue::from(PlatformAddressInfoWasm::from(info)); + results_map.set(&key, &value); + } + + Ok(results_map) + } +} + +// ============================================================================ +// Identity Create from Platform Addresses +// ============================================================================ + +/// Main input struct for identity create from addresses options. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct IdentityCreateFromAddressesOptionsInput { + inputs: Vec, + #[serde(default)] + change_output: Option, +} + +fn deserialize_identity_create_options( + options: JsValue, +) -> Result { + let parsed: IdentityCreateFromAddressesOptionsInput = deserialize_required_query( + options, + "Options object is required", + "identity create from addresses options", + )?; + + if parsed.inputs.is_empty() { + return Err(WasmSdkError::invalid_argument( + "At least one input is required", + )); + } + + Ok(parsed) +} + +/// Result of creating an identity from Platform addresses. +#[wasm_bindgen(js_name = "IdentityCreateFromAddressesResult")] +pub struct IdentityCreateFromAddressesResultWasm { + identity: wasm_dpp2::IdentityWasm, + address_infos: Map, +} + +#[wasm_bindgen(js_class = IdentityCreateFromAddressesResult)] +impl IdentityCreateFromAddressesResultWasm { + /// The newly created identity. + #[wasm_bindgen(getter)] + pub fn identity(&self) -> wasm_dpp2::IdentityWasm { + self.identity.clone() + } + + /// Map of addresses to their updated info after the identity creation. + #[wasm_bindgen(getter = "addressInfos")] + pub fn address_infos(&self) -> Map { + self.address_infos.clone() + } +} + +/// TypeScript interface for identity create from addresses options +#[wasm_bindgen(typescript_custom_section)] +const IDENTITY_CREATE_FROM_ADDRESSES_OPTIONS_TS: &'static str = r#" +/** + * Options for creating an identity funded from Platform addresses. + */ +export interface IdentityCreateFromAddressesOptions { + /** + * The identity to create (with public keys set up). + * Use Identity.create() to build the identity structure first. + */ + identity: Identity; + + /** + * Array of input addresses with amounts to use for funding. + * Use PlatformAddressInput for typed inputs (nonces fetched automatically). + */ + inputs: PlatformAddressInput[]; + + /** + * Optional change output address and amount. + * If provided, remaining credits will be sent to this address. + */ + changeOutput?: PlatformAddressOutput; + + /** + * Signer containing private keys for the identity's public keys. + * Use IdentitySigner to add keys for signing identity key proofs. + */ + identitySigner: IdentitySigner; + + /** + * Signer containing private keys for all input addresses. + * Use PlatformAddressSigner to add keys for signing address inputs. + */ + addressSigner: PlatformAddressSigner; + + /** + * Optional settings for the broadcast operation. + * Includes retries, timeouts, userFeeIncrease, etc. + */ + settings?: PutSettings; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IdentityCreateFromAddressesOptions")] + pub type IdentityCreateFromAddressesOptionsJs; +} + +#[wasm_bindgen] +impl WasmSdk { + /// Create an identity funded from Platform addresses. + /// + /// This method handles the complete identity creation flow: + /// 1. Fetches current nonces for all input addresses + /// 2. Builds and signs the identity create from addresses transition + /// 3. Broadcasts and waits for confirmation + /// + /// @param options - Creation options including identity, inputs, and signers + /// @returns Promise resolving to result with created identity and updated address infos + #[wasm_bindgen(js_name = "identityCreateFromAddresses")] + pub async fn identity_create_from_addresses( + &self, + options: IdentityCreateFromAddressesOptionsJs, + ) -> Result { + use dash_sdk::platform::transition::put_identity::PutIdentity; + use wasm_dpp2::utils::IntoWasm; + + let options_value: JsValue = options.into(); + + // Deserialize and validate options + let parsed = deserialize_identity_create_options(options_value.clone())?; + + // Extract identity from options + let identity_js = js_sys::Reflect::get(&options_value, &JsValue::from_str("identity")) + .map_err(|_| WasmSdkError::invalid_argument("identity is required"))?; + let identity: Identity = identity_js + .to_wasm::("Identity")? + .clone() + .into(); + + // Convert inputs to map (address -> amount) + let inputs_map: std::collections::BTreeMap< + dash_sdk::dpp::address_funds::PlatformAddress, + dash_sdk::dpp::fee::Credits, + > = parsed + .inputs + .into_iter() + .map(|input| { + let address = input.address().clone().into(); + let amount = input.amount_value(); + (address, amount) + }) + .collect(); + + // Convert change output if provided + let change_output = parsed.change_output.map(|output| { + let address = output.address().clone().into(); + let amount = output.amount_value(); + (address, amount) + }); + + // Extract identity signer from options + let identity_signer_js = + js_sys::Reflect::get(&options_value, &JsValue::from_str("identitySigner")) + .map_err(|_| WasmSdkError::invalid_argument("identitySigner is required"))?; + let identity_signer = identity_signer_js + .to_wasm::("IdentitySigner")? + .clone(); + + // Extract address signer from options + let address_signer_js = + js_sys::Reflect::get(&options_value, &JsValue::from_str("addressSigner")) + .map_err(|_| WasmSdkError::invalid_argument("addressSigner is required"))?; + let address_signer = address_signer_js + .to_wasm::("PlatformAddressSigner")? + .clone(); + + // Extract settings from options + let settings = extract_settings_from_options(&options_value)?; + + // Use the SDK's put_with_address_funding method + let (created_identity, address_infos) = identity + .put_with_address_funding( + self.inner_sdk(), + inputs_map, + change_output, + &identity_signer, + &address_signer, + settings, + ) + .await + .map_err(|e| { + WasmSdkError::generic(format!("Failed to create identity from addresses: {}", e)) + })?; + + // Convert to Map + let address_infos_map = Map::new(); + + for (address, info_opt) in address_infos { + let info = info_opt.ok_or_else(|| { + WasmSdkError::generic(format!( + "Address {} has no info after identity creation", + address + )) + })?; + let key = JsValue::from(PlatformAddressWasm::from(address)); + let value = JsValue::from(PlatformAddressInfoWasm::from(info)); + address_infos_map.set(&key, &value); + } + + Ok(IdentityCreateFromAddressesResultWasm { + identity: created_identity.into(), + address_infos: address_infos_map, + }) + } +} diff --git a/packages/wasm-sdk/src/state_transitions/broadcast.rs b/packages/wasm-sdk/src/state_transitions/broadcast.rs new file mode 100644 index 00000000000..5b75c1316af --- /dev/null +++ b/packages/wasm-sdk/src/state_transitions/broadcast.rs @@ -0,0 +1,102 @@ +//! Generic state transition broadcast functionality. +//! +//! This module provides methods to broadcast any state transition +//! to the network and wait for the result. + +use crate::error::WasmSdkError; +use crate::sdk::WasmSdk; +use crate::settings::{parse_put_settings, PutSettingsJs}; +use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; +use dash_sdk::dpp::state_transition::StateTransition; +use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; +use wasm_bindgen::prelude::*; +use wasm_dpp2::StateTransitionWasm; + +#[wasm_bindgen] +impl WasmSdk { + /// Broadcasts a state transition to the network. + /// + /// This method only broadcasts but does not wait for the result. + /// Use `waitForResponse` to wait for confirmation after broadcasting, + /// or use `broadcastAndWait` to do both in one call. + /// + /// @param stateTransition - The state transition to broadcast + /// @param settings - Optional put settings (retries, timeout) + #[wasm_bindgen(js_name = "broadcastStateTransition")] + pub async fn broadcast_state_transition( + &self, + #[wasm_bindgen(js_name = "stateTransition")] state_transition: &StateTransitionWasm, + settings: Option, + ) -> Result<(), WasmSdkError> { + let st: StateTransition = state_transition.into(); + let put_settings = parse_put_settings(settings); + + st.broadcast(self.as_ref(), put_settings) + .await + .map_err(|e| WasmSdkError::generic(format!("Failed to broadcast: {}", e)))?; + + Ok(()) + } + + /// Waits for a state transition response after it has been broadcast. + /// + /// Use this after calling `broadcastStateTransition` to wait for the transition + /// to be processed by the network. This is useful when you want to broadcast + /// and wait separately (e.g., for monitoring or progress tracking). + /// + /// Note: This differs from `waitForStateTransitionResult` which takes a hash string. + /// This method takes the full state transition object and performs proof verification. + /// + /// @param stateTransition - The state transition that was broadcast + /// @param settings - Optional put settings (retries, timeout, waitTimeoutMs) + /// @returns JSON representation of the state transition result + #[wasm_bindgen(js_name = "waitForResponse")] + pub async fn wait_for_response( + &self, + #[wasm_bindgen(js_name = "stateTransition")] state_transition: &StateTransitionWasm, + settings: Option, + ) -> Result { + let st: StateTransition = state_transition.into(); + let put_settings = parse_put_settings(settings); + + let result = st + .wait_for_response::(self.as_ref(), put_settings) + .await + .map_err(|e| { + WasmSdkError::generic(format!("Failed to wait for state transition result: {}", e)) + })?; + + // Convert result to a JsValue representation + let result_str = format!("{:?}", result); + Ok(JsValue::from_str(&result_str)) + } + + /// Broadcasts a state transition and waits for the result. + /// + /// This method broadcasts the transition and waits for confirmation from the network. + /// Returns once the transition has been processed or fails. + /// This is equivalent to calling `broadcastStateTransition` followed by + /// `waitForResponse`. + /// + /// @param stateTransition - The state transition to broadcast + /// @param settings - Optional put settings (retries, timeout, waitTimeoutMs) + /// @returns JSON representation of the state transition result + #[wasm_bindgen(js_name = "broadcastAndWait")] + pub async fn broadcast_and_wait( + &self, + #[wasm_bindgen(js_name = "stateTransition")] state_transition: &StateTransitionWasm, + settings: Option, + ) -> Result { + let st: StateTransition = state_transition.into(); + let put_settings = parse_put_settings(settings); + + let result = st + .broadcast_and_wait::(self.as_ref(), put_settings) + .await + .map_err(|e| WasmSdkError::generic(format!("Failed to broadcast: {}", e)))?; + + // Convert result to a JsValue representation + let result_str = format!("{:?}", result); + Ok(JsValue::from_str(&result_str)) + } +} diff --git a/packages/wasm-sdk/src/state_transitions/mod.rs b/packages/wasm-sdk/src/state_transitions/mod.rs index 52a67de9d18..87ac05722e2 100644 --- a/packages/wasm-sdk/src/state_transitions/mod.rs +++ b/packages/wasm-sdk/src/state_transitions/mod.rs @@ -1,3 +1,5 @@ +pub mod addresses; +pub mod broadcast; pub mod contracts; pub mod documents; pub mod identity; diff --git a/packages/wasm-sdk/tests/unit/IdentityTopUpFromAddressesResult.spec.mjs b/packages/wasm-sdk/tests/unit/IdentityTopUpFromAddressesResult.spec.mjs new file mode 100644 index 00000000000..645c9a838d3 --- /dev/null +++ b/packages/wasm-sdk/tests/unit/IdentityTopUpFromAddressesResult.spec.mjs @@ -0,0 +1,120 @@ +import init, * as sdk from '../../dist/sdk.compressed.js'; + +describe('IdentityTopUpFromAddressesResult', () => { + before(async () => { + await init(); + }); + + describe('construction and getters', () => { + it('should have addressInfos getter that returns a Map', () => { + // Note: PlatformAddressInfo has a private constructor and cannot be directly constructed. + // It is only returned by SDK methods like getAddressInfo. + // This test verifies the class exists in the SDK exports. + expect(sdk.IdentityTopUpFromAddressesResult).to.exist; + expect(sdk.PlatformAddressInfo).to.exist; + }); + + it('should have newBalance getter that returns BigInt', () => { + // Verify the class exists and has the expected interface + expect(sdk.IdentityTopUpFromAddressesResult).to.be.a('function'); + + // The actual functionality will be tested in integration tests + // where we can get a real result from identityTopUpFromAddresses + }); + }); + + describe('interface validation', () => { + it('should be a constructor function', () => { + expect(sdk.IdentityTopUpFromAddressesResult).to.be.a('function'); + }); + + it('should have addressInfos and newBalance in prototype', () => { + const proto = sdk.IdentityTopUpFromAddressesResult.prototype; + expect(proto).to.exist; + + // Check that getters are defined (they will be on the prototype) + const descriptors = Object.getOwnPropertyDescriptors(proto); + + // In WASM bindings, getters are defined on the prototype + // We just verify the constructor exists for now + expect(sdk.IdentityTopUpFromAddressesResult.name).to.equal('IdentityTopUpFromAddressesResult'); + }); + }); + + describe('type checking', () => { + it('should export IdentityTopUpFromAddressesResult class', () => { + expect(sdk).to.have.property('IdentityTopUpFromAddressesResult'); + expect(typeof sdk.IdentityTopUpFromAddressesResult).to.equal('function'); + }); + }); + + describe('expected usage pattern', () => { + it('should document that addressInfos returns Map', () => { + // This is a documentation test showing the expected return type + // The actual Map structure is: + // - Key: PlatformAddress instance + // - Value: PlatformAddressInfo instance (returned by SDK, not constructed directly) + + // PlatformAddress can be created directly + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const address = sdk.PlatformAddress.fromBytes(addressBytes); + + expect(address).to.exist; + expect(address.toBytes()).to.deep.equal(addressBytes); + + // PlatformAddressInfo has a private constructor - it can only be obtained + // from SDK methods like getAddressInfo() or from result objects. + // This is by design to ensure data integrity. + expect(sdk.PlatformAddressInfo).to.exist; + }); + + it('should document that newBalance returns BigInt', () => { + // This is a documentation test showing the expected return type + const sampleBalance = 500000n; + + expect(typeof sampleBalance).to.equal('bigint'); + expect(sampleBalance > 0n).to.be.true; + }); + }); + + describe('integration with other SDK types', () => { + it('should work with PlatformAddress from result', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const address = sdk.PlatformAddress.fromBytes(addressBytes); + + expect(address).to.exist; + expect(address.toBytes()).to.deep.equal(addressBytes); + }); + + it('should export PlatformAddressInfo class', () => { + // PlatformAddressInfo has a private constructor and can only be obtained + // from SDK methods. This test verifies the class is exported. + expect(sdk.PlatformAddressInfo).to.exist; + expect(sdk.PlatformAddressInfo).to.be.a('function'); + + // The class has getters for address, balance, and nonce + const proto = sdk.PlatformAddressInfo.prototype; + expect(proto).to.exist; + }); + }); + + describe('BigInt handling', () => { + it('should handle large balance values as BigInt', () => { + // Platform balances can exceed Number.MAX_SAFE_INTEGER + const largeBalance = 23522425453263151n; + + expect(typeof largeBalance).to.equal('bigint'); + expect(largeBalance > Number.MAX_SAFE_INTEGER).to.be.true; + }); + + it('should handle zero and small values', () => { + const zero = 0n; + const small = 100n; + + expect(typeof zero).to.equal('bigint'); + expect(typeof small).to.equal('bigint'); + expect(zero === 0n).to.be.true; + expect(small === 100n).to.be.true; + }); + }); +}); diff --git a/packages/wasm-sdk/tests/unit/IdentityTransferToAddressesResult.spec.mjs b/packages/wasm-sdk/tests/unit/IdentityTransferToAddressesResult.spec.mjs new file mode 100644 index 00000000000..cf90d77cd86 --- /dev/null +++ b/packages/wasm-sdk/tests/unit/IdentityTransferToAddressesResult.spec.mjs @@ -0,0 +1,203 @@ +import init, * as sdk from '../../dist/sdk.compressed.js'; + +describe('IdentityTransferToAddressesResult', () => { + before(async () => { + await init(); + }); + + describe('construction and getters', () => { + it('should have addressInfos getter that returns a Map', () => { + // Note: PlatformAddressInfo has a private constructor and cannot be directly constructed. + // It is only returned by SDK methods like getAddressInfo. + // This test verifies the class exists in the SDK exports. + expect(sdk.IdentityTransferToAddressesResult).to.exist; + expect(sdk.PlatformAddressInfo).to.exist; + }); + + it('should have newBalance getter that returns BigInt', () => { + // Verify the class exists and has the expected interface + expect(sdk.IdentityTransferToAddressesResult).to.be.a('function'); + + // The actual functionality will be tested in integration tests + // where we can get a real result from identityTransferToAddresses + }); + }); + + describe('interface validation', () => { + it('should be a constructor function', () => { + expect(sdk.IdentityTransferToAddressesResult).to.be.a('function'); + }); + + it('should have addressInfos and newBalance in prototype', () => { + const proto = sdk.IdentityTransferToAddressesResult.prototype; + expect(proto).to.exist; + + // Check that getters are defined (they will be on the prototype) + const descriptors = Object.getOwnPropertyDescriptors(proto); + + // In WASM bindings, getters are defined on the prototype + // We just verify the constructor exists for now + expect(sdk.IdentityTransferToAddressesResult.name).to.equal('IdentityTransferToAddressesResult'); + }); + }); + + describe('type checking', () => { + it('should export IdentityTransferToAddressesResult class', () => { + expect(sdk).to.have.property('IdentityTransferToAddressesResult'); + expect(typeof sdk.IdentityTransferToAddressesResult).to.equal('function'); + }); + }); + + describe('expected usage pattern', () => { + it('should document that addressInfos returns Map', () => { + // This is a documentation test showing the expected return type + // The actual Map structure is: + // - Key: PlatformAddress instance + // - Value: PlatformAddressInfo instance (returned by SDK, not constructed directly) + + // PlatformAddress can be created directly + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const address = sdk.PlatformAddress.fromBytes(addressBytes); + + expect(address).to.exist; + expect(address.toBytes()).to.deep.equal(addressBytes); + + // PlatformAddressInfo has a private constructor - it can only be obtained + // from SDK methods like getAddressInfo() or from result objects. + // This is by design to ensure data integrity. + expect(sdk.PlatformAddressInfo).to.exist; + }); + + it('should document that newBalance returns BigInt representing identity balance after transfer', () => { + // This is a documentation test showing the expected return type + // newBalance represents the identity's remaining balance after transferring to addresses + const sampleBalance = 250000n; + + expect(typeof sampleBalance).to.equal('bigint'); + expect(sampleBalance > 0n).to.be.true; + }); + }); + + describe('integration with other SDK types', () => { + it('should work with PlatformAddress from result', () => { + const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const address = sdk.PlatformAddress.fromBytes(addressBytes); + + expect(address).to.exist; + expect(address.toBytes()).to.deep.equal(addressBytes); + }); + + it('should export PlatformAddressInfo class', () => { + // PlatformAddressInfo has a private constructor and can only be obtained + // from SDK methods. This test verifies the class is exported. + expect(sdk.PlatformAddressInfo).to.exist; + expect(sdk.PlatformAddressInfo).to.be.a('function'); + + // The class has getters for address, balance, and nonce + const proto = sdk.PlatformAddressInfo.prototype; + expect(proto).to.exist; + }); + + it('should work with multiple addresses in Map', () => { + const resultMap = new Map(); + + // Create multiple addresses + const addr1Bytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + const addr1 = sdk.PlatformAddress.fromBytes(addr1Bytes); + + const addr2Bytes = new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); + const addr2 = sdk.PlatformAddress.fromBytes(addr2Bytes); + + // Note: PlatformAddressInfo objects would come from SDK methods. + // Here we just verify the Map structure works with PlatformAddress keys. + resultMap.set(addr1, { balance: 50000n, nonce: 0n }); + resultMap.set(addr2, { balance: 25000n, nonce: 0n }); + + expect(resultMap.size).to.equal(2); + expect(resultMap.get(addr1).balance).to.equal(50000n); + expect(resultMap.get(addr2).balance).to.equal(25000n); + }); + }); + + describe('BigInt handling', () => { + it('should handle large balance values as BigInt', () => { + // Platform balances can exceed Number.MAX_SAFE_INTEGER + const largeBalance = 23522425453263151n; + + expect(typeof largeBalance).to.equal('bigint'); + expect(largeBalance > Number.MAX_SAFE_INTEGER).to.be.true; + }); + + it('should handle zero balance (identity fully transferred)', () => { + const zero = 0n; + + expect(typeof zero).to.equal('bigint'); + expect(zero === 0n).to.be.true; + }); + + it('should handle arithmetic on balance values', () => { + const initialBalance = 1000000n; + const transferred = 250000n; + const expectedRemaining = 750000n; + + const remaining = initialBalance - transferred; + + expect(remaining).to.equal(expectedRemaining); + expect(typeof remaining).to.equal('bigint'); + }); + }); + + describe('semantic differences from IdentityTopUpFromAddressesResult', () => { + it('should have same structure but opposite flow semantics', () => { + // Both results have the same structure: + // - addressInfos: Map + // - newBalance: BigInt + + // But different semantics: + // IdentityTopUpFromAddressesResult: + // - addressInfos shows addresses that FUNDED the identity (reduced balance) + // - newBalance is identity balance AFTER receiving funds (increased) + // + // IdentityTransferToAddressesResult: + // - addressInfos shows addresses that RECEIVED from identity (increased balance) + // - newBalance is identity balance AFTER sending funds (decreased) + + expect(sdk.IdentityTopUpFromAddressesResult).to.exist; + expect(sdk.IdentityTransferToAddressesResult).to.exist; + expect(sdk.IdentityTopUpFromAddressesResult).to.not.equal(sdk.IdentityTransferToAddressesResult); + }); + + it('should document the transfer flow direction', () => { + // Transfer flow: Identity -> Platform Addresses + // The identity balance decreases, address balances increase + + const identityInitialBalance = 1000000n; + const transferAmount = 100000n; + const identityFinalBalance = identityInitialBalance - transferAmount; + + const addressInitialBalance = 0n; + const addressFinalBalance = addressInitialBalance + transferAmount; + + expect(identityFinalBalance < identityInitialBalance).to.be.true; + expect(addressFinalBalance > addressInitialBalance).to.be.true; + }); + }); + + describe('use with IdentitySigner', () => { + it('should document that IdentitySigner is used for signing transfer transitions', () => { + // IdentityTransferToAddresses requires an IdentitySigner + // because it signs with identity keys, not address keys + + // Create an IdentitySigner (from wasm-dpp2 package) + const signer = new sdk.IdentitySigner(); + expect(signer).to.exist; + expect(signer.keyCount).to.equal(0); + + // The signer would hold private keys corresponding to identity public keys + const testPrivateKeyWif = 'cR4EZ2nAvCmn2cFepKn7UgSSQFgFTjkySAchvcoiEVdm48eWjQGn'; + signer.addKeyFromWif(testPrivateKeyWif); + + expect(signer.keyCount).to.equal(1); + }); + }); +}); From bdb10803b35255304d5891a78151c080929c4d77 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 25 Dec 2025 21:29:30 +0700 Subject: [PATCH 03/11] docs: update doc blocks --- packages/js-evo-sdk/src/addresses/facade.ts | 35 +-------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/packages/js-evo-sdk/src/addresses/facade.ts b/packages/js-evo-sdk/src/addresses/facade.ts index b41351f8255..73042f7a053 100644 --- a/packages/js-evo-sdk/src/addresses/facade.ts +++ b/packages/js-evo-sdk/src/addresses/facade.ts @@ -59,11 +59,6 @@ export class AddressesFacade { /** * Transfers credits between Platform addresses. * - * This method handles the complete transfer flow: - * 1. Fetches current nonces for all input addresses - * 2. Builds and signs the transfer transition - * 3. Broadcasts and waits for confirmation - * * @param options - Transfer options including inputs, outputs, and signer * @returns Promise resolving to transfer result with updated address information * @@ -94,12 +89,6 @@ export class AddressesFacade { /** * Top up an identity from Platform addresses. * - * This method handles the complete top up flow: - * 1. Fetches the identity from Platform - * 2. Fetches current nonces for all input addresses - * 3. Builds and signs the identity top up transition - * 4. Broadcasts and waits for confirmation - * * @param options - Top up options including identity ID, inputs, and signer * @returns Promise resolving to result with updated address infos and new identity balance * @@ -129,13 +118,7 @@ export class AddressesFacade { } /** - * Withdraws Platform address credits to Core (L1). - * - * This method handles the complete withdrawal flow: - * 1. Fetches current nonces for all input addresses - * 2. Builds and signs the withdrawal transition - * 3. Broadcasts and waits for confirmation - * 4. The withdrawal may be pooled with others depending on the pooling strategy + * Withdraws Platform address credits to Dash Core. * * @param options - Withdrawal options including inputs, output script, pooling, and signer * @returns Promise resolving to Map of PlatformAddress to PlatformAddressInfo @@ -170,12 +153,6 @@ export class AddressesFacade { /** * Transfer credits from an identity to Platform addresses. * - * This method handles the complete transfer flow: - * 1. Fetches the identity from Platform - * 2. Finds the appropriate transfer key to use for signing (if signingTransferKeyId specified) - * 3. Builds and signs the identity credit transfer to addresses transition - * 4. Broadcasts and waits for confirmation - * * @param options - Transfer options including identity ID, outputs, and signer * @returns Result with updated address information and new identity balance * @@ -209,11 +186,6 @@ export class AddressesFacade { /** * Fund Platform addresses from an asset lock. * - * This method handles the complete funding flow: - * 1. Validates the asset lock proof - * 2. Builds and signs the address funding transition - * 3. Broadcasts and waits for confirmation - * * @param options - Funding options including asset lock proof, outputs, and signer * @returns Promise resolving to Map of PlatformAddress to PlatformAddressInfo * @@ -251,11 +223,6 @@ export class AddressesFacade { /** * Create an identity funded from Platform addresses. * - * This method handles the complete identity creation flow: - * 1. Fetches current nonces for all input addresses - * 2. Builds and signs the identity create from addresses transition - * 3. Broadcasts and waits for confirmation - * * @param options - Creation options including identity, inputs, and signers * @returns Promise resolving to result with created identity and updated address infos * From bf23e8ec520625558e3a35eb23fbf059915a1179 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 25 Dec 2025 21:29:46 +0700 Subject: [PATCH 04/11] test: remove reduntant test --- .../tests/unit/facades/addresses.spec.mjs | 47 ------------------- 1 file changed, 47 deletions(-) diff --git a/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs b/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs index f9aa7890bef..a21b9b53e2e 100644 --- a/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs +++ b/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs @@ -268,50 +268,3 @@ describe('AddressesFacade', () => { expect(result.identity).to.exist(); }); }); - -describe('PlatformAddress', () => { - let PlatformAddress; - - before(async () => { - await init(); - PlatformAddress = wasmSDKPackage.PlatformAddress; - }); - - it('can be created from bytes', () => { - // Create P2PKH address from bytes (type 0x00) - const p2pkhBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const p2pkhAddr = PlatformAddress.fromBytes(p2pkhBytes); - expect(p2pkhAddr).to.exist(); - expect(p2pkhAddr.addressType).to.equal('P2PKH'); - expect(p2pkhAddr.isP2pkh).to.be.true(); - expect(p2pkhAddr.isP2sh).to.be.false(); - - // Create P2SH address from bytes (type 0x01) - const p2shBytes = new Uint8Array([0x01, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const p2shAddr = PlatformAddress.fromBytes(p2shBytes); - expect(p2shAddr).to.exist(); - expect(p2shAddr.addressType).to.equal('P2SH'); - expect(p2shAddr.isP2pkh).to.be.false(); - expect(p2shAddr.isP2sh).to.be.true(); - }); - - it('converts to bech32m and back', () => { - // Create address from bytes - const originalBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const addr = PlatformAddress.fromBytes(originalBytes); - - // Convert to bech32m for testnet - const bech32m = addr.toBech32m('testnet'); - expect(bech32m).to.be.a('string'); - expect(bech32m.startsWith('tdashevo1')).to.be.true(); - - // Convert back from bech32m - const recoveredAddr = PlatformAddress.fromBech32m(bech32m); - expect(recoveredAddr).to.exist(); - expect(recoveredAddr.addressType).to.equal(addr.addressType); - - // Verify bytes match - const recoveredBytes = recoveredAddr.toBytes(); - expect(Array.from(recoveredBytes)).to.deep.equal(Array.from(originalBytes)); - }); -}); From 7976d6f840fdd89c203b8985af9c94be4a3e1fc3 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 25 Dec 2025 22:12:38 +0700 Subject: [PATCH 05/11] refactor: remove unnecessary code --- .../tests/unit/facades/addresses.spec.mjs | 4 +- packages/wasm-dpp2/src/enums/mod.rs | 1 - .../credit_withdrawal_transition.rs | 2 +- .../wasm-dpp2/src/identity/transitions/mod.rs | 1 + .../transitions/pooling.rs} | 0 packages/wasm-dpp2/src/lib.rs | 2 +- .../src/platform_address/input_output.rs | 306 +----------------- .../tests/unit/PlatformAddressInput.spec.mjs | 49 +-- 8 files changed, 30 insertions(+), 335 deletions(-) rename packages/wasm-dpp2/src/{enums/withdrawal.rs => identity/transitions/pooling.rs} (100%) diff --git a/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs b/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs index a21b9b53e2e..ba44c247e06 100644 --- a/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs +++ b/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs @@ -86,7 +86,7 @@ describe('AddressesFacade', () => { signer.addKey(senderAddr, privateKey); // Create typed input and output objects (address, nonce, amount) - const input = new wasmSDKPackage.PlatformAddressInput(senderAddr, 0n, 100000n); + const input = new wasmSDKPackage.PlatformAddressInput(senderAddr, 0, 100000n); const output = new wasmSDKPackage.PlatformAddressOutput(recipientAddr, 90000n); const options = { @@ -124,7 +124,7 @@ describe('AddressesFacade', () => { signer.addKey(senderAddr, privateKey); // Create typed input and output objects (address, nonce, amount) - const input = new wasmSDKPackage.PlatformAddressInput(senderAddr, 0n, 100000n); + const input = new wasmSDKPackage.PlatformAddressInput(senderAddr, 0, 100000n); const output = new wasmSDKPackage.PlatformAddressOutput(recipientAddr, 90000n); const options = { diff --git a/packages/wasm-dpp2/src/enums/mod.rs b/packages/wasm-dpp2/src/enums/mod.rs index b8303a599d5..0f7c0624464 100644 --- a/packages/wasm-dpp2/src/enums/mod.rs +++ b/packages/wasm-dpp2/src/enums/mod.rs @@ -5,4 +5,3 @@ pub mod lock_types; pub mod network; pub mod platform; pub mod token; -pub mod withdrawal; diff --git a/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs b/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs index 006de993ca8..3a4ad324be6 100644 --- a/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs +++ b/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs @@ -1,7 +1,7 @@ use crate::asset_lock_proof::AssetLockProofWasm; use crate::core_script::CoreScriptWasm; use crate::enums::keys::purpose::PurposeWasm; -use crate::enums::withdrawal::PoolingWasm; +use super::pooling::PoolingWasm; use crate::error::{WasmDppError, WasmDppResult}; use crate::identifier::IdentifierWasm; use crate::impl_wasm_conversions; diff --git a/packages/wasm-dpp2/src/identity/transitions/mod.rs b/packages/wasm-dpp2/src/identity/transitions/mod.rs index a7119f74ccf..4ff31f3d0e5 100644 --- a/packages/wasm-dpp2/src/identity/transitions/mod.rs +++ b/packages/wasm-dpp2/src/identity/transitions/mod.rs @@ -2,6 +2,7 @@ pub mod create_transition; pub mod credit_withdrawal_transition; pub mod identity_credit_transfer_transition; pub mod masternode_vote_transition; +pub mod pooling; pub mod public_key_in_creation; pub mod top_up_transition; pub mod update_transition; diff --git a/packages/wasm-dpp2/src/enums/withdrawal.rs b/packages/wasm-dpp2/src/identity/transitions/pooling.rs similarity index 100% rename from packages/wasm-dpp2/src/enums/withdrawal.rs rename to packages/wasm-dpp2/src/identity/transitions/pooling.rs diff --git a/packages/wasm-dpp2/src/lib.rs b/packages/wasm-dpp2/src/lib.rs index 241fd142575..005679d4c09 100644 --- a/packages/wasm-dpp2/src/lib.rs +++ b/packages/wasm-dpp2/src/lib.rs @@ -32,7 +32,7 @@ pub mod utils; pub mod voting; pub use core_script::CoreScriptWasm; -pub use enums::withdrawal::PoolingWasm; +pub use identity::transitions::pooling::PoolingWasm; pub use identity_signer::IdentitySignerWasm; pub use data_contract::{ diff --git a/packages/wasm-dpp2/src/platform_address/input_output.rs b/packages/wasm-dpp2/src/platform_address/input_output.rs index dd729c93949..131fc727076 100644 --- a/packages/wasm-dpp2/src/platform_address/input_output.rs +++ b/packages/wasm-dpp2/src/platform_address/input_output.rs @@ -4,10 +4,8 @@ use dpp::address_funds::PlatformAddress; use dpp::fee::Credits; use dpp::prelude::AddressNonce; use js_sys::BigInt; -use serde::de::{self, Deserializer, MapAccess, Visitor}; use serde::Deserialize; use std::collections::BTreeMap; -use std::fmt; use wasm_bindgen::prelude::*; /// Represents an input address for address-based state transitions. @@ -15,26 +13,14 @@ use wasm_bindgen::prelude::*; /// An input specifies a Platform address that will spend credits, /// along with its current nonce and the amount to spend. #[wasm_bindgen(js_name = "PlatformAddressInput")] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct PlatformAddressInputWasm { address: PlatformAddressWasm, nonce: AddressNonce, amount: Credits, } -/// Helper to convert BigInt to u32 -fn bigint_to_u32(value: BigInt) -> Result { - let value_u64: u64 = value - .try_into() - .map_err(|_| WasmDppError::invalid_argument("value must be a valid u64"))?; - - if value_u64 > u32::MAX as u64 { - return Err(WasmDppError::invalid_argument("value exceeds u32 max")); - } - - Ok(value_u64 as u32) -} - /// Helper to convert BigInt to u64 fn bigint_to_u64(value: BigInt) -> Result { value @@ -52,16 +38,15 @@ impl PlatformAddressInputWasm { #[wasm_bindgen(constructor)] pub fn new( #[wasm_bindgen(unchecked_param_type = "PlatformAddressLike")] address: &JsValue, - nonce: BigInt, + nonce: u32, amount: BigInt, ) -> WasmDppResult { let platform_address = PlatformAddressWasm::try_from(address)?; - let nonce_u32 = bigint_to_u32(nonce)?; let amount_u64 = bigint_to_u64(amount)?; Ok(PlatformAddressInputWasm { address: platform_address, - nonce: nonce_u32, + nonce, amount: amount_u64, }) } @@ -74,8 +59,8 @@ impl PlatformAddressInputWasm { /// Returns the nonce. #[wasm_bindgen(getter)] - pub fn nonce(&self) -> BigInt { - BigInt::from(self.nonce) + pub fn nonce(&self) -> u32 { + self.nonce } /// Returns the amount. @@ -107,288 +92,13 @@ impl PlatformAddressInputWasm { } } -/// Custom deserializer for Credits that handles number, string, or bigint. -fn deserialize_credits<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - struct CreditsVisitor; - - impl<'de> Visitor<'de> for CreditsVisitor { - type Value = Credits; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a number, string, or bigint representing credits") - } - - fn visit_u64(self, value: u64) -> Result - where - E: de::Error, - { - Ok(value) - } - - fn visit_i64(self, value: i64) -> Result - where - E: de::Error, - { - if value < 0 { - Err(E::custom("credits cannot be negative")) - } else { - Ok(value as Credits) - } - } - - fn visit_f64(self, value: f64) -> Result - where - E: de::Error, - { - if value < 0.0 { - Err(E::custom("credits cannot be negative")) - } else { - Ok(value as Credits) - } - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - // Handle bigint string format (e.g., "1000000n" or just "1000000") - let clean = value.trim_end_matches('n'); - clean - .parse::() - .map_err(|_| E::custom(format!("invalid credits value: {}", value))) - } - - fn visit_string(self, value: String) -> Result - where - E: de::Error, - { - self.visit_str(&value) - } - } - - deserializer.deserialize_any(CreditsVisitor) -} - -/// Custom deserializer for AddressNonce that handles number or bigint. -fn deserialize_nonce<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - struct NonceVisitor; - - impl<'de> Visitor<'de> for NonceVisitor { - type Value = AddressNonce; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a number or bigint representing a nonce") - } - - fn visit_u64(self, value: u64) -> Result - where - E: de::Error, - { - if value > u32::MAX as u64 { - Err(E::custom("nonce exceeds u32 max")) - } else { - Ok(value as AddressNonce) - } - } - - fn visit_i64(self, value: i64) -> Result - where - E: de::Error, - { - if value < 0 { - Err(E::custom("nonce cannot be negative")) - } else if value > u32::MAX as i64 { - Err(E::custom("nonce exceeds u32 max")) - } else { - Ok(value as AddressNonce) - } - } - - fn visit_f64(self, value: f64) -> Result - where - E: de::Error, - { - if value < 0.0 { - Err(E::custom("nonce cannot be negative")) - } else if value > u32::MAX as f64 { - Err(E::custom("nonce exceeds u32 max")) - } else { - Ok(value as AddressNonce) - } - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - let clean = value.trim_end_matches('n'); - clean - .parse::() - .map_err(|_| E::custom(format!("invalid nonce value: {}", value))) - } - - fn visit_string(self, value: String) -> Result - where - E: de::Error, - { - self.visit_str(&value) - } - } - - deserializer.deserialize_any(NonceVisitor) -} - -impl<'de> Deserialize<'de> for PlatformAddressInputWasm { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(field_identifier, rename_all = "camelCase")] - enum Field { - Address, - Nonce, - Amount, - } - - struct PlatformAddressInputVisitor; - - impl<'de> Visitor<'de> for PlatformAddressInputVisitor { - type Value = PlatformAddressInputWasm; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("struct PlatformAddressInput") - } - - fn visit_map(self, mut map: V) -> Result - where - V: MapAccess<'de>, - { - let mut address: Option = None; - let mut nonce: Option = None; - let mut amount: Option = None; - - while let Some(key) = map.next_key()? { - match key { - Field::Address => { - if address.is_some() { - return Err(de::Error::duplicate_field("address")); - } - address = Some(map.next_value()?); - } - Field::Nonce => { - if nonce.is_some() { - return Err(de::Error::duplicate_field("nonce")); - } - // Use a helper struct to deserialize nonce - #[derive(Deserialize)] - struct NonceHelper(#[serde(deserialize_with = "deserialize_nonce")] AddressNonce); - let helper: NonceHelper = map.next_value()?; - nonce = Some(helper.0); - } - Field::Amount => { - if amount.is_some() { - return Err(de::Error::duplicate_field("amount")); - } - // Use a helper struct to deserialize amount - #[derive(Deserialize)] - struct AmountHelper(#[serde(deserialize_with = "deserialize_credits")] Credits); - let helper: AmountHelper = map.next_value()?; - amount = Some(helper.0); - } - } - } - - let address = address.ok_or_else(|| de::Error::missing_field("address"))?; - let nonce = nonce.ok_or_else(|| de::Error::missing_field("nonce"))?; - let amount = amount.ok_or_else(|| de::Error::missing_field("amount"))?; - - Ok(PlatformAddressInputWasm { - address, - nonce, - amount, - }) - } - } - - const FIELDS: &[&str] = &["address", "nonce", "amount"]; - deserializer.deserialize_struct("PlatformAddressInput", FIELDS, PlatformAddressInputVisitor) - } -} - -impl<'de> Deserialize<'de> for PlatformAddressOutputWasm { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(field_identifier, rename_all = "camelCase")] - enum Field { - Address, - Amount, - } - - struct PlatformAddressOutputVisitor; - - impl<'de> Visitor<'de> for PlatformAddressOutputVisitor { - type Value = PlatformAddressOutputWasm; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("struct PlatformAddressOutput") - } - - fn visit_map(self, mut map: V) -> Result - where - V: MapAccess<'de>, - { - let mut address: Option = None; - let mut amount: Option = None; - - while let Some(key) = map.next_key()? { - match key { - Field::Address => { - if address.is_some() { - return Err(de::Error::duplicate_field("address")); - } - address = Some(map.next_value()?); - } - Field::Amount => { - if amount.is_some() { - return Err(de::Error::duplicate_field("amount")); - } - // Use a helper struct to deserialize amount - #[derive(Deserialize)] - struct AmountHelper(#[serde(deserialize_with = "deserialize_credits")] Credits); - let helper: AmountHelper = map.next_value()?; - amount = Some(helper.0); - } - } - } - - let address = address.ok_or_else(|| de::Error::missing_field("address"))?; - let amount = amount.ok_or_else(|| de::Error::missing_field("amount"))?; - - Ok(PlatformAddressOutputWasm { address, amount }) - } - } - - const FIELDS: &[&str] = &["address", "amount"]; - deserializer.deserialize_struct("PlatformAddressOutput", FIELDS, PlatformAddressOutputVisitor) - } -} - /// Represents an output address for address-based state transitions. /// /// An output specifies a Platform address that will receive credits, /// along with the amount to receive. #[wasm_bindgen(js_name = "PlatformAddressOutput")] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct PlatformAddressOutputWasm { address: PlatformAddressWasm, amount: Credits, diff --git a/packages/wasm-dpp2/tests/unit/PlatformAddressInput.spec.mjs b/packages/wasm-dpp2/tests/unit/PlatformAddressInput.spec.mjs index a4a71be3fbc..256bcf56a7f 100644 --- a/packages/wasm-dpp2/tests/unit/PlatformAddressInput.spec.mjs +++ b/packages/wasm-dpp2/tests/unit/PlatformAddressInput.spec.mjs @@ -12,9 +12,9 @@ describe('PlatformAddressInput', () => { const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); - const input = new wasm.PlatformAddressInput(platformAddr, BigInt(5), BigInt(500000)); + const input = new wasm.PlatformAddressInput(platformAddr, 5, BigInt(500000)); expect(input).to.exist; - expect(input.nonce).to.equal(BigInt(5)); + expect(input.nonce).to.equal(5); expect(input.amount).to.equal(BigInt(500000)); }); @@ -23,18 +23,18 @@ describe('PlatformAddressInput', () => { const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); const bech32m = platformAddr.toBech32m('testnet'); - const input = new wasm.PlatformAddressInput(bech32m, BigInt(1), BigInt(100000)); + const input = new wasm.PlatformAddressInput(bech32m, 1, BigInt(100000)); expect(input).to.exist; - expect(input.nonce).to.equal(BigInt(1)); + expect(input.nonce).to.equal(1); expect(input.amount).to.equal(BigInt(100000)); }); it('should create from Uint8Array', () => { const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const input = new wasm.PlatformAddressInput(addressBytes, BigInt(0), BigInt(100000)); + const input = new wasm.PlatformAddressInput(addressBytes, 0, BigInt(100000)); expect(input).to.exist; - expect(input.nonce).to.equal(BigInt(0)); + expect(input.nonce).to.equal(0); expect(input.amount).to.equal(BigInt(100000)); }); @@ -42,8 +42,8 @@ describe('PlatformAddressInput', () => { const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); - const input = new wasm.PlatformAddressInput(platformAddr, BigInt(0), BigInt(100000)); - expect(input.nonce).to.equal(BigInt(0)); + const input = new wasm.PlatformAddressInput(platformAddr, 0, BigInt(100000)); + expect(input.nonce).to.equal(0); }); it('should handle large amounts', () => { @@ -51,32 +51,17 @@ describe('PlatformAddressInput', () => { const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); const largeAmount = BigInt('10000000000000000'); // 10 quadrillion - const input = new wasm.PlatformAddressInput(platformAddr, BigInt(1), largeAmount); + const input = new wasm.PlatformAddressInput(platformAddr, 1, largeAmount); expect(input.amount).to.equal(largeAmount); }); - it('should reject negative nonce', () => { + it('should handle max u32 nonce', () => { const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); - try { - new wasm.PlatformAddressInput(platformAddr, BigInt(-1), BigInt(100000)); - expect.fail('Should have thrown error for negative nonce'); - } catch (error) { - expect(error).to.exist; - } - }); - - it('should reject nonce exceeding u32 max', () => { - const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); - - try { - new wasm.PlatformAddressInput(platformAddr, BigInt('4294967296'), BigInt(100000)); // u32::MAX + 1 - expect.fail('Should have thrown error for nonce exceeding u32 max'); - } catch (error) { - expect(error.message).to.include('u32'); - } + const maxU32 = 4294967295; // u32::MAX + const input = new wasm.PlatformAddressInput(platformAddr, maxU32, BigInt(100000)); + expect(input.nonce).to.equal(maxU32); }); }); @@ -85,7 +70,7 @@ describe('PlatformAddressInput', () => { const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); - const input = new wasm.PlatformAddressInput(platformAddr, BigInt(1), BigInt(100000)); + const input = new wasm.PlatformAddressInput(platformAddr, 1, BigInt(100000)); const addr = input.address; expect(addr).to.exist; expect(addr.addressType).to.equal('P2PKH'); @@ -95,15 +80,15 @@ describe('PlatformAddressInput', () => { const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); - const input = new wasm.PlatformAddressInput(platformAddr, BigInt(42), BigInt(100000)); - expect(input.nonce).to.equal(BigInt(42)); + const input = new wasm.PlatformAddressInput(platformAddr, 42, BigInt(100000)); + expect(input.nonce).to.equal(42); }); it('should return the amount', () => { const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); const platformAddr = wasm.PlatformAddress.fromBytes(addressBytes); - const input = new wasm.PlatformAddressInput(platformAddr, BigInt(1), BigInt(123456)); + const input = new wasm.PlatformAddressInput(platformAddr, 1, BigInt(123456)); expect(input.amount).to.equal(BigInt(123456)); }); }); From 1960141ca4c9c0555c823b21012eef443200002f Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 25 Dec 2025 22:21:51 +0700 Subject: [PATCH 06/11] refactor: move signer --- packages/wasm-dpp2/src/identity/mod.rs | 2 ++ .../wasm-dpp2/src/{identity_signer.rs => identity/signer.rs} | 0 packages/wasm-dpp2/src/lib.rs | 3 +-- 3 files changed, 3 insertions(+), 2 deletions(-) rename packages/wasm-dpp2/src/{identity_signer.rs => identity/signer.rs} (100%) diff --git a/packages/wasm-dpp2/src/identity/mod.rs b/packages/wasm-dpp2/src/identity/mod.rs index 4f32fef888a..2bbbac6a005 100644 --- a/packages/wasm-dpp2/src/identity/mod.rs +++ b/packages/wasm-dpp2/src/identity/mod.rs @@ -1,11 +1,13 @@ pub mod model; pub mod partial_identity; pub mod public_key; +pub mod signer; pub mod transitions; pub use model::IdentityWasm; pub use partial_identity::PartialIdentityWasm; pub use public_key::IdentityPublicKeyWasm; +pub use signer::IdentitySignerWasm; pub use transitions::create_transition::IdentityCreateTransitionWasm; pub use transitions::credit_withdrawal_transition::IdentityCreditWithdrawalTransitionWasm; pub use transitions::identity_credit_transfer_transition::IdentityCreditTransferWasm; diff --git a/packages/wasm-dpp2/src/identity_signer.rs b/packages/wasm-dpp2/src/identity/signer.rs similarity index 100% rename from packages/wasm-dpp2/src/identity_signer.rs rename to packages/wasm-dpp2/src/identity/signer.rs diff --git a/packages/wasm-dpp2/src/lib.rs b/packages/wasm-dpp2/src/lib.rs index 005679d4c09..ec9270d78b2 100644 --- a/packages/wasm-dpp2/src/lib.rs +++ b/packages/wasm-dpp2/src/lib.rs @@ -20,7 +20,6 @@ pub mod error; pub mod group; pub mod identifier; pub mod identity; -pub mod identity_signer; pub mod mock_bls; pub mod platform_address; pub mod private_key; @@ -32,8 +31,8 @@ pub mod utils; pub mod voting; pub use core_script::CoreScriptWasm; +pub use identity::signer::IdentitySignerWasm; pub use identity::transitions::pooling::PoolingWasm; -pub use identity_signer::IdentitySignerWasm; pub use data_contract::{ ContractBoundsWasm, DataContractCreateTransitionWasm, DataContractUpdateTransitionWasm, From fb78f887c85072a830af08a4a008c2390efcda92 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 25 Dec 2025 22:37:06 +0700 Subject: [PATCH 07/11] refactor: use serde to deserialize settings --- packages/wasm-sdk/src/settings.rs | 217 +++++++----------- .../IdentityTopUpFromAddressesResult.spec.mjs | 120 ---------- ...IdentityTransferToAddressesResult.spec.mjs | 203 ---------------- 3 files changed, 80 insertions(+), 460 deletions(-) delete mode 100644 packages/wasm-sdk/tests/unit/IdentityTopUpFromAddressesResult.spec.mjs delete mode 100644 packages/wasm-sdk/tests/unit/IdentityTransferToAddressesResult.spec.mjs diff --git a/packages/wasm-sdk/src/settings.rs b/packages/wasm-sdk/src/settings.rs index d434ebcdcd8..77a6f6233bb 100644 --- a/packages/wasm-sdk/src/settings.rs +++ b/packages/wasm-sdk/src/settings.rs @@ -5,6 +5,7 @@ use dash_sdk::platform::transition::put_settings::PutSettings; use rs_dapi_client::RequestSettings; +use serde::Deserialize; use std::time::Duration; use wasm_bindgen::prelude::*; @@ -50,52 +51,39 @@ extern "C" { pub type RequestSettingsJs; } +/// Internal struct for deserializing RequestSettings from JavaScript. +#[derive(Deserialize, Default)] +#[serde(rename_all = "camelCase")] +struct RequestSettingsInput { + retries: Option, + timeout_ms: Option, + connect_timeout_ms: Option, + ban_failed_address: Option, +} + +impl From for RequestSettings { + fn from(input: RequestSettingsInput) -> Self { + RequestSettings { + retries: input.retries.map(|r| r as usize), + timeout: input.timeout_ms.map(Duration::from_millis), + connect_timeout: input.connect_timeout_ms.map(Duration::from_millis), + ban_failed_address: input.ban_failed_address, + } + } +} + /// Parse request settings from JavaScript into RequestSettings. /// /// Used for query operations. pub fn parse_request_settings(settings: Option) -> Option { - let settings_js = settings?; - let settings_value: JsValue = settings_js.into(); + let js_value: JsValue = settings?.into(); - if settings_value.is_undefined() || settings_value.is_null() { + if js_value.is_undefined() || js_value.is_null() { return None; } - let mut request_settings = RequestSettings::default(); - - // Parse retries - if let Ok(retries_js) = js_sys::Reflect::get(&settings_value, &JsValue::from_str("retries")) { - if let Some(retries) = retries_js.as_f64() { - request_settings.retries = Some(retries as usize); - } - } - - // Parse timeoutMs - if let Ok(timeout_js) = js_sys::Reflect::get(&settings_value, &JsValue::from_str("timeoutMs")) { - if let Some(ms) = timeout_js.as_f64() { - request_settings.timeout = Some(Duration::from_millis(ms as u64)); - } - } - - // Parse connectTimeoutMs - if let Ok(connect_timeout_js) = - js_sys::Reflect::get(&settings_value, &JsValue::from_str("connectTimeoutMs")) - { - if let Some(ms) = connect_timeout_js.as_f64() { - request_settings.connect_timeout = Some(Duration::from_millis(ms as u64)); - } - } - - // Parse banFailedAddress - if let Ok(ban_js) = - js_sys::Reflect::get(&settings_value, &JsValue::from_str("banFailedAddress")) - { - if let Some(ban) = ban_js.as_bool() { - request_settings.ban_failed_address = Some(ban); - } - } - - Some(request_settings) + let input: RequestSettingsInput = serde_wasm_bindgen::from_value(js_value).ok()?; + Some(input.into()) } // ============================================================================ @@ -174,111 +162,66 @@ extern "C" { pub type PutSettingsJs; } -/// Parse put settings from JavaScript into PutSettings. -/// -/// Used for state transition broadcast operations. -pub fn parse_put_settings(settings: Option) -> Option { - let settings_js = settings?; - let settings_value: JsValue = settings_js.into(); - - if settings_value.is_undefined() || settings_value.is_null() { - return None; - } - - let mut put_settings = PutSettings::default(); - - // Parse retries - if let Ok(retries_js) = js_sys::Reflect::get(&settings_value, &JsValue::from_str("retries")) { - if let Some(retries) = retries_js.as_f64() { - put_settings.request_settings.retries = Some(retries as usize); - } - } - - // Parse timeoutMs - if let Ok(timeout_js) = js_sys::Reflect::get(&settings_value, &JsValue::from_str("timeoutMs")) { - if let Some(ms) = timeout_js.as_f64() { - put_settings.request_settings.timeout = Some(Duration::from_millis(ms as u64)); - } - } - - // Parse connectTimeoutMs - if let Ok(connect_timeout_js) = - js_sys::Reflect::get(&settings_value, &JsValue::from_str("connectTimeoutMs")) - { - if let Some(ms) = connect_timeout_js.as_f64() { - put_settings.request_settings.connect_timeout = Some(Duration::from_millis(ms as u64)); - } - } - - // Parse banFailedAddress - if let Ok(ban_js) = - js_sys::Reflect::get(&settings_value, &JsValue::from_str("banFailedAddress")) - { - if let Some(ban) = ban_js.as_bool() { - put_settings.request_settings.ban_failed_address = Some(ban); - } - } - - // Parse waitTimeoutMs - if let Ok(wait_timeout_js) = - js_sys::Reflect::get(&settings_value, &JsValue::from_str("waitTimeoutMs")) - { - if let Some(ms) = wait_timeout_js.as_f64() { - put_settings.wait_timeout = Some(Duration::from_millis(ms as u64)); - } - } +/// Internal struct for deserializing state transition creation options. +#[derive(Deserialize, Default)] +#[serde(rename_all = "camelCase")] +struct StateTransitionCreationOptionsInput { + #[serde(default)] + allow_signing_with_any_security_level: bool, + #[serde(default)] + allow_signing_with_any_purpose: bool, +} - // Parse userFeeIncrease - if let Ok(fee_increase_js) = - js_sys::Reflect::get(&settings_value, &JsValue::from_str("userFeeIncrease")) - { - if let Some(fee) = fee_increase_js.as_f64() { - put_settings.user_fee_increase = Some(fee as u16); - } - } +/// Internal struct for deserializing PutSettings from JavaScript. +#[derive(Deserialize, Default)] +#[serde(rename_all = "camelCase")] +struct PutSettingsInput { + #[serde(flatten)] + request: RequestSettingsInput, + // Put-specific fields + wait_timeout_ms: Option, + user_fee_increase: Option, + identity_nonce_stale_time_s: Option, + state_transition_creation_options: Option, +} - // Parse identityNonceStaleTimeS - if let Ok(nonce_stale_js) = - js_sys::Reflect::get(&settings_value, &JsValue::from_str("identityNonceStaleTimeS")) - { - if let Some(secs) = nonce_stale_js.as_f64() { - put_settings.identity_nonce_stale_time_s = Some(secs as u64); +impl From for PutSettings { + fn from(input: PutSettingsInput) -> Self { + use dash_sdk::dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; + + let state_transition_creation_options = + input.state_transition_creation_options.map(|opts| { + let mut creation_options = StateTransitionCreationOptions::default(); + creation_options + .signing_options + .allow_signing_with_any_security_level = + opts.allow_signing_with_any_security_level; + creation_options + .signing_options + .allow_signing_with_any_purpose = opts.allow_signing_with_any_purpose; + creation_options + }); + + PutSettings { + request_settings: input.request.into(), + wait_timeout: input.wait_timeout_ms.map(Duration::from_millis), + user_fee_increase: input.user_fee_increase, + identity_nonce_stale_time_s: input.identity_nonce_stale_time_s, + state_transition_creation_options, } } +} - // Parse stateTransitionCreationOptions - if let Ok(creation_options_js) = js_sys::Reflect::get( - &settings_value, - &JsValue::from_str("stateTransitionCreationOptions"), - ) { - if !creation_options_js.is_undefined() && !creation_options_js.is_null() { - use dash_sdk::dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; - - let mut creation_options = StateTransitionCreationOptions::default(); - - // Parse allowSigningWithAnySecurityLevel - if let Ok(allow_sec_level_js) = js_sys::Reflect::get( - &creation_options_js, - &JsValue::from_str("allowSigningWithAnySecurityLevel"), - ) { - if let Some(allow) = allow_sec_level_js.as_bool() { - creation_options.signing_options.allow_signing_with_any_security_level = allow; - } - } - - // Parse allowSigningWithAnyPurpose - if let Ok(allow_purpose_js) = js_sys::Reflect::get( - &creation_options_js, - &JsValue::from_str("allowSigningWithAnyPurpose"), - ) { - if let Some(allow) = allow_purpose_js.as_bool() { - creation_options.signing_options.allow_signing_with_any_purpose = allow; - } - } +/// Parse put settings from JavaScript into PutSettings. +/// +/// Used for state transition broadcast operations. +pub fn parse_put_settings(settings: Option) -> Option { + let js_value: JsValue = settings?.into(); - put_settings.state_transition_creation_options = Some(creation_options); - } + if js_value.is_undefined() || js_value.is_null() { + return None; } - Some(put_settings) + let input: PutSettingsInput = serde_wasm_bindgen::from_value(js_value).ok()?; + Some(input.into()) } diff --git a/packages/wasm-sdk/tests/unit/IdentityTopUpFromAddressesResult.spec.mjs b/packages/wasm-sdk/tests/unit/IdentityTopUpFromAddressesResult.spec.mjs deleted file mode 100644 index 645c9a838d3..00000000000 --- a/packages/wasm-sdk/tests/unit/IdentityTopUpFromAddressesResult.spec.mjs +++ /dev/null @@ -1,120 +0,0 @@ -import init, * as sdk from '../../dist/sdk.compressed.js'; - -describe('IdentityTopUpFromAddressesResult', () => { - before(async () => { - await init(); - }); - - describe('construction and getters', () => { - it('should have addressInfos getter that returns a Map', () => { - // Note: PlatformAddressInfo has a private constructor and cannot be directly constructed. - // It is only returned by SDK methods like getAddressInfo. - // This test verifies the class exists in the SDK exports. - expect(sdk.IdentityTopUpFromAddressesResult).to.exist; - expect(sdk.PlatformAddressInfo).to.exist; - }); - - it('should have newBalance getter that returns BigInt', () => { - // Verify the class exists and has the expected interface - expect(sdk.IdentityTopUpFromAddressesResult).to.be.a('function'); - - // The actual functionality will be tested in integration tests - // where we can get a real result from identityTopUpFromAddresses - }); - }); - - describe('interface validation', () => { - it('should be a constructor function', () => { - expect(sdk.IdentityTopUpFromAddressesResult).to.be.a('function'); - }); - - it('should have addressInfos and newBalance in prototype', () => { - const proto = sdk.IdentityTopUpFromAddressesResult.prototype; - expect(proto).to.exist; - - // Check that getters are defined (they will be on the prototype) - const descriptors = Object.getOwnPropertyDescriptors(proto); - - // In WASM bindings, getters are defined on the prototype - // We just verify the constructor exists for now - expect(sdk.IdentityTopUpFromAddressesResult.name).to.equal('IdentityTopUpFromAddressesResult'); - }); - }); - - describe('type checking', () => { - it('should export IdentityTopUpFromAddressesResult class', () => { - expect(sdk).to.have.property('IdentityTopUpFromAddressesResult'); - expect(typeof sdk.IdentityTopUpFromAddressesResult).to.equal('function'); - }); - }); - - describe('expected usage pattern', () => { - it('should document that addressInfos returns Map', () => { - // This is a documentation test showing the expected return type - // The actual Map structure is: - // - Key: PlatformAddress instance - // - Value: PlatformAddressInfo instance (returned by SDK, not constructed directly) - - // PlatformAddress can be created directly - const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const address = sdk.PlatformAddress.fromBytes(addressBytes); - - expect(address).to.exist; - expect(address.toBytes()).to.deep.equal(addressBytes); - - // PlatformAddressInfo has a private constructor - it can only be obtained - // from SDK methods like getAddressInfo() or from result objects. - // This is by design to ensure data integrity. - expect(sdk.PlatformAddressInfo).to.exist; - }); - - it('should document that newBalance returns BigInt', () => { - // This is a documentation test showing the expected return type - const sampleBalance = 500000n; - - expect(typeof sampleBalance).to.equal('bigint'); - expect(sampleBalance > 0n).to.be.true; - }); - }); - - describe('integration with other SDK types', () => { - it('should work with PlatformAddress from result', () => { - const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const address = sdk.PlatformAddress.fromBytes(addressBytes); - - expect(address).to.exist; - expect(address.toBytes()).to.deep.equal(addressBytes); - }); - - it('should export PlatformAddressInfo class', () => { - // PlatformAddressInfo has a private constructor and can only be obtained - // from SDK methods. This test verifies the class is exported. - expect(sdk.PlatformAddressInfo).to.exist; - expect(sdk.PlatformAddressInfo).to.be.a('function'); - - // The class has getters for address, balance, and nonce - const proto = sdk.PlatformAddressInfo.prototype; - expect(proto).to.exist; - }); - }); - - describe('BigInt handling', () => { - it('should handle large balance values as BigInt', () => { - // Platform balances can exceed Number.MAX_SAFE_INTEGER - const largeBalance = 23522425453263151n; - - expect(typeof largeBalance).to.equal('bigint'); - expect(largeBalance > Number.MAX_SAFE_INTEGER).to.be.true; - }); - - it('should handle zero and small values', () => { - const zero = 0n; - const small = 100n; - - expect(typeof zero).to.equal('bigint'); - expect(typeof small).to.equal('bigint'); - expect(zero === 0n).to.be.true; - expect(small === 100n).to.be.true; - }); - }); -}); diff --git a/packages/wasm-sdk/tests/unit/IdentityTransferToAddressesResult.spec.mjs b/packages/wasm-sdk/tests/unit/IdentityTransferToAddressesResult.spec.mjs deleted file mode 100644 index cf90d77cd86..00000000000 --- a/packages/wasm-sdk/tests/unit/IdentityTransferToAddressesResult.spec.mjs +++ /dev/null @@ -1,203 +0,0 @@ -import init, * as sdk from '../../dist/sdk.compressed.js'; - -describe('IdentityTransferToAddressesResult', () => { - before(async () => { - await init(); - }); - - describe('construction and getters', () => { - it('should have addressInfos getter that returns a Map', () => { - // Note: PlatformAddressInfo has a private constructor and cannot be directly constructed. - // It is only returned by SDK methods like getAddressInfo. - // This test verifies the class exists in the SDK exports. - expect(sdk.IdentityTransferToAddressesResult).to.exist; - expect(sdk.PlatformAddressInfo).to.exist; - }); - - it('should have newBalance getter that returns BigInt', () => { - // Verify the class exists and has the expected interface - expect(sdk.IdentityTransferToAddressesResult).to.be.a('function'); - - // The actual functionality will be tested in integration tests - // where we can get a real result from identityTransferToAddresses - }); - }); - - describe('interface validation', () => { - it('should be a constructor function', () => { - expect(sdk.IdentityTransferToAddressesResult).to.be.a('function'); - }); - - it('should have addressInfos and newBalance in prototype', () => { - const proto = sdk.IdentityTransferToAddressesResult.prototype; - expect(proto).to.exist; - - // Check that getters are defined (they will be on the prototype) - const descriptors = Object.getOwnPropertyDescriptors(proto); - - // In WASM bindings, getters are defined on the prototype - // We just verify the constructor exists for now - expect(sdk.IdentityTransferToAddressesResult.name).to.equal('IdentityTransferToAddressesResult'); - }); - }); - - describe('type checking', () => { - it('should export IdentityTransferToAddressesResult class', () => { - expect(sdk).to.have.property('IdentityTransferToAddressesResult'); - expect(typeof sdk.IdentityTransferToAddressesResult).to.equal('function'); - }); - }); - - describe('expected usage pattern', () => { - it('should document that addressInfos returns Map', () => { - // This is a documentation test showing the expected return type - // The actual Map structure is: - // - Key: PlatformAddress instance - // - Value: PlatformAddressInfo instance (returned by SDK, not constructed directly) - - // PlatformAddress can be created directly - const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const address = sdk.PlatformAddress.fromBytes(addressBytes); - - expect(address).to.exist; - expect(address.toBytes()).to.deep.equal(addressBytes); - - // PlatformAddressInfo has a private constructor - it can only be obtained - // from SDK methods like getAddressInfo() or from result objects. - // This is by design to ensure data integrity. - expect(sdk.PlatformAddressInfo).to.exist; - }); - - it('should document that newBalance returns BigInt representing identity balance after transfer', () => { - // This is a documentation test showing the expected return type - // newBalance represents the identity's remaining balance after transferring to addresses - const sampleBalance = 250000n; - - expect(typeof sampleBalance).to.equal('bigint'); - expect(sampleBalance > 0n).to.be.true; - }); - }); - - describe('integration with other SDK types', () => { - it('should work with PlatformAddress from result', () => { - const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const address = sdk.PlatformAddress.fromBytes(addressBytes); - - expect(address).to.exist; - expect(address.toBytes()).to.deep.equal(addressBytes); - }); - - it('should export PlatformAddressInfo class', () => { - // PlatformAddressInfo has a private constructor and can only be obtained - // from SDK methods. This test verifies the class is exported. - expect(sdk.PlatformAddressInfo).to.exist; - expect(sdk.PlatformAddressInfo).to.be.a('function'); - - // The class has getters for address, balance, and nonce - const proto = sdk.PlatformAddressInfo.prototype; - expect(proto).to.exist; - }); - - it('should work with multiple addresses in Map', () => { - const resultMap = new Map(); - - // Create multiple addresses - const addr1Bytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const addr1 = sdk.PlatformAddress.fromBytes(addr1Bytes); - - const addr2Bytes = new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); - const addr2 = sdk.PlatformAddress.fromBytes(addr2Bytes); - - // Note: PlatformAddressInfo objects would come from SDK methods. - // Here we just verify the Map structure works with PlatformAddress keys. - resultMap.set(addr1, { balance: 50000n, nonce: 0n }); - resultMap.set(addr2, { balance: 25000n, nonce: 0n }); - - expect(resultMap.size).to.equal(2); - expect(resultMap.get(addr1).balance).to.equal(50000n); - expect(resultMap.get(addr2).balance).to.equal(25000n); - }); - }); - - describe('BigInt handling', () => { - it('should handle large balance values as BigInt', () => { - // Platform balances can exceed Number.MAX_SAFE_INTEGER - const largeBalance = 23522425453263151n; - - expect(typeof largeBalance).to.equal('bigint'); - expect(largeBalance > Number.MAX_SAFE_INTEGER).to.be.true; - }); - - it('should handle zero balance (identity fully transferred)', () => { - const zero = 0n; - - expect(typeof zero).to.equal('bigint'); - expect(zero === 0n).to.be.true; - }); - - it('should handle arithmetic on balance values', () => { - const initialBalance = 1000000n; - const transferred = 250000n; - const expectedRemaining = 750000n; - - const remaining = initialBalance - transferred; - - expect(remaining).to.equal(expectedRemaining); - expect(typeof remaining).to.equal('bigint'); - }); - }); - - describe('semantic differences from IdentityTopUpFromAddressesResult', () => { - it('should have same structure but opposite flow semantics', () => { - // Both results have the same structure: - // - addressInfos: Map - // - newBalance: BigInt - - // But different semantics: - // IdentityTopUpFromAddressesResult: - // - addressInfos shows addresses that FUNDED the identity (reduced balance) - // - newBalance is identity balance AFTER receiving funds (increased) - // - // IdentityTransferToAddressesResult: - // - addressInfos shows addresses that RECEIVED from identity (increased balance) - // - newBalance is identity balance AFTER sending funds (decreased) - - expect(sdk.IdentityTopUpFromAddressesResult).to.exist; - expect(sdk.IdentityTransferToAddressesResult).to.exist; - expect(sdk.IdentityTopUpFromAddressesResult).to.not.equal(sdk.IdentityTransferToAddressesResult); - }); - - it('should document the transfer flow direction', () => { - // Transfer flow: Identity -> Platform Addresses - // The identity balance decreases, address balances increase - - const identityInitialBalance = 1000000n; - const transferAmount = 100000n; - const identityFinalBalance = identityInitialBalance - transferAmount; - - const addressInitialBalance = 0n; - const addressFinalBalance = addressInitialBalance + transferAmount; - - expect(identityFinalBalance < identityInitialBalance).to.be.true; - expect(addressFinalBalance > addressInitialBalance).to.be.true; - }); - }); - - describe('use with IdentitySigner', () => { - it('should document that IdentitySigner is used for signing transfer transitions', () => { - // IdentityTransferToAddresses requires an IdentitySigner - // because it signs with identity keys, not address keys - - // Create an IdentitySigner (from wasm-dpp2 package) - const signer = new sdk.IdentitySigner(); - expect(signer).to.exist; - expect(signer.keyCount).to.equal(0); - - // The signer would hold private keys corresponding to identity public keys - const testPrivateKeyWif = 'cR4EZ2nAvCmn2cFepKn7UgSSQFgFTjkySAchvcoiEVdm48eWjQGn'; - signer.addKeyFromWif(testPrivateKeyWif); - - expect(signer.keyCount).to.equal(1); - }); - }); -}); From 571a6f369172f17cda302c665bba440067dc252a Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 25 Dec 2025 22:57:02 +0700 Subject: [PATCH 08/11] refactor: make things better --- packages/wasm-dpp2/src/identity/signer.rs | 17 +++- packages/wasm-dpp2/src/lib.rs | 4 +- .../src/platform_address/input_output.rs | 16 ++++ .../wasm-dpp2/src/platform_address/mod.rs | 2 +- .../wasm-dpp2/src/platform_address/signer.rs | 17 +++- .../src/state_transitions/addresses.rs | 90 +++++-------------- 6 files changed, 67 insertions(+), 79 deletions(-) diff --git a/packages/wasm-dpp2/src/identity/signer.rs b/packages/wasm-dpp2/src/identity/signer.rs index f9db6c2e3c1..1eecf421d0d 100644 --- a/packages/wasm-dpp2/src/identity/signer.rs +++ b/packages/wasm-dpp2/src/identity/signer.rs @@ -190,11 +190,22 @@ impl IdentitySignerWasm { /// to an IdentitySignerWasm. Useful for state transition functions that /// need a signer from their options. pub fn try_from_options(options: &JsValue) -> WasmDppResult { - let signer_js = js_sys::Reflect::get(options, &JsValue::from_str("signer")) - .map_err(|_| WasmDppError::invalid_argument("Missing 'signer' field"))?; + Self::try_from_options_with_field(options, "signer") + } + + /// Extracts an IdentitySigner from a JS options object with a custom field name. + /// + /// This helper reads the specified field from an options object and converts it + /// to an IdentitySignerWasm. + pub fn try_from_options_with_field(options: &JsValue, field_name: &str) -> WasmDppResult { + let signer_js = js_sys::Reflect::get(options, &JsValue::from_str(field_name)) + .map_err(|_| WasmDppError::invalid_argument(format!("Missing '{}' field", field_name)))?; if signer_js.is_undefined() || signer_js.is_null() { - return Err(WasmDppError::invalid_argument("'signer' is required")); + return Err(WasmDppError::invalid_argument(format!( + "'{}' is required", + field_name + ))); } Self::try_from(&signer_js) diff --git a/packages/wasm-dpp2/src/lib.rs b/packages/wasm-dpp2/src/lib.rs index ec9270d78b2..29e00de089b 100644 --- a/packages/wasm-dpp2/src/lib.rs +++ b/packages/wasm-dpp2/src/lib.rs @@ -49,8 +49,8 @@ pub use identity::{ pub use platform_address::{ default_fee_strategy, extract_addresses, extract_amounts, fee_strategy_from_steps, fee_strategy_from_steps_or_default, inputs_to_btree_map, outputs_to_btree_map, - FeeStrategyStepWasm, PlatformAddressInputWasm, PlatformAddressOutputWasm, - PlatformAddressSignerWasm, PlatformAddressWasm, + outputs_to_optional_btree_map, FeeStrategyStepWasm, PlatformAddressInputWasm, + PlatformAddressOutputWasm, PlatformAddressSignerWasm, PlatformAddressWasm, }; pub use state_transitions::base::{GroupStateTransitionInfoWasm, StateTransitionWasm}; pub use tokens::*; diff --git a/packages/wasm-dpp2/src/platform_address/input_output.rs b/packages/wasm-dpp2/src/platform_address/input_output.rs index 131fc727076..065d4134ad2 100644 --- a/packages/wasm-dpp2/src/platform_address/input_output.rs +++ b/packages/wasm-dpp2/src/platform_address/input_output.rs @@ -168,6 +168,22 @@ pub fn outputs_to_btree_map( outputs.into_iter().map(|o| o.into_inner()).collect() } +/// Converts a vector of PlatformAddressOutput into a BTreeMap with optional amounts. +/// +/// Used for asset lock funding where the amount is optional (None means +/// the system distributes the asset lock funds automatically). +pub fn outputs_to_optional_btree_map( + outputs: Vec, +) -> BTreeMap> { + outputs + .into_iter() + .map(|o| { + let (addr, amount) = o.into_inner(); + (addr, Some(amount)) + }) + .collect() +} + /// Extracts addresses from a slice of PlatformAddressOutput. pub fn extract_addresses(outputs: &[PlatformAddressOutputWasm]) -> Vec { outputs.iter().map(|o| o.platform_address()).collect() diff --git a/packages/wasm-dpp2/src/platform_address/mod.rs b/packages/wasm-dpp2/src/platform_address/mod.rs index 1dcb8f088e8..122df32e144 100644 --- a/packages/wasm-dpp2/src/platform_address/mod.rs +++ b/packages/wasm-dpp2/src/platform_address/mod.rs @@ -10,6 +10,6 @@ pub use fee_strategy::{ }; pub use input_output::{ extract_addresses, extract_amounts, inputs_to_btree_map, outputs_to_btree_map, - PlatformAddressInputWasm, PlatformAddressOutputWasm, + outputs_to_optional_btree_map, PlatformAddressInputWasm, PlatformAddressOutputWasm, }; pub use signer::PlatformAddressSignerWasm; diff --git a/packages/wasm-dpp2/src/platform_address/signer.rs b/packages/wasm-dpp2/src/platform_address/signer.rs index 303df7b6bdf..90b8396f3f4 100644 --- a/packages/wasm-dpp2/src/platform_address/signer.rs +++ b/packages/wasm-dpp2/src/platform_address/signer.rs @@ -152,11 +152,22 @@ impl PlatformAddressSignerWasm { /// to a PlatformAddressSignerWasm. Useful for state transition functions that /// need a signer from their options. pub fn try_from_options(options: &JsValue) -> WasmDppResult { - let signer_js = js_sys::Reflect::get(options, &JsValue::from_str("signer")) - .map_err(|_| WasmDppError::invalid_argument("Missing 'signer' field"))?; + Self::try_from_options_with_field(options, "signer") + } + + /// Extracts a PlatformAddressSigner from a JS options object with a custom field name. + /// + /// This helper reads the specified field from an options object and converts it + /// to a PlatformAddressSignerWasm. + pub fn try_from_options_with_field(options: &JsValue, field_name: &str) -> WasmDppResult { + let signer_js = js_sys::Reflect::get(options, &JsValue::from_str(field_name)) + .map_err(|_| WasmDppError::invalid_argument(format!("Missing '{}' field", field_name)))?; if signer_js.is_undefined() || signer_js.is_null() { - return Err(WasmDppError::invalid_argument("'signer' is required")); + return Err(WasmDppError::invalid_argument(format!( + "'{}' is required", + field_name + ))); } Self::try_from(&signer_js) diff --git a/packages/wasm-sdk/src/state_transitions/addresses.rs b/packages/wasm-sdk/src/state_transitions/addresses.rs index 48c8e8160f3..eb8080d975f 100644 --- a/packages/wasm-sdk/src/state_transitions/addresses.rs +++ b/packages/wasm-sdk/src/state_transitions/addresses.rs @@ -9,8 +9,8 @@ use crate::sdk::WasmSdk; use crate::settings::{parse_put_settings, PutSettingsJs}; use dash_sdk::dpp::identity::core_script::CoreScript; use dash_sdk::platform::transition::address_credit_withdrawal::WithdrawAddressFunds; -use dash_sdk::platform::transition::transfer_address_funds::TransferAddressFunds; use dash_sdk::platform::transition::top_up_identity_from_addresses::TopUpIdentityFromAddresses; +use dash_sdk::platform::transition::transfer_address_funds::TransferAddressFunds; use dash_sdk::platform::transition::transfer_to_addresses::TransferToAddresses; use dash_sdk::platform::{Fetch, Identifier, Identity}; use js_sys::{BigInt, Map}; @@ -19,9 +19,9 @@ use wasm_bindgen::prelude::*; use wasm_dpp2::identifier::IdentifierWasm; use wasm_dpp2::utils::IntoWasm; use wasm_dpp2::{ - fee_strategy_from_steps_or_default, outputs_to_btree_map, CoreScriptWasm, FeeStrategyStepWasm, - IdentitySignerWasm, PlatformAddressOutputWasm, PlatformAddressSignerWasm, PlatformAddressWasm, - PoolingWasm, + fee_strategy_from_steps_or_default, outputs_to_btree_map, outputs_to_optional_btree_map, + CoreScriptWasm, FeeStrategyStepWasm, IdentitySignerWasm, PlatformAddressOutputWasm, + PlatformAddressSignerWasm, PlatformAddressWasm, PoolingWasm, }; /// Main input struct for address funds transfer options. @@ -170,10 +170,7 @@ impl WasmSdk { for (address, info_opt) in address_infos { let info = info_opt.ok_or_else(|| { - WasmSdkError::generic(format!( - "Address {} has no info after transfer", - address - )) + WasmSdkError::generic(format!("Address {} has no info after transfer", address)) })?; let key = JsValue::from(PlatformAddressWasm::from(address)); let value = JsValue::from(PlatformAddressInfoWasm::from(info)); @@ -463,11 +460,7 @@ impl WasmSdk { let inputs_map = outputs_to_btree_map(parsed.inputs); // Convert change output if provided - let change_output = parsed.change_output.map(|output| { - let address = output.address().clone().into(); - let amount = output.amount_value(); - (address, amount) - }); + let change_output = parsed.change_output.map(|output| output.into_inner()); // Extract output script from options let output_script_js = @@ -512,10 +505,7 @@ impl WasmSdk { for (address, info_opt) in address_infos { let info = info_opt.ok_or_else(|| { - WasmSdkError::generic(format!( - "Address {} has no info after withdrawal", - address - )) + WasmSdkError::generic(format!("Address {} has no info after withdrawal", address)) })?; let key = JsValue::from(PlatformAddressWasm::from(address)); let value = JsValue::from(PlatformAddressInfoWasm::from(info)); @@ -594,10 +584,7 @@ impl WasmSdk { for (address, info_opt) in address_infos { let info = info_opt.ok_or_else(|| { - WasmSdkError::generic(format!( - "Address {} has no info after transfer", - address - )) + WasmSdkError::generic(format!("Address {} has no info after transfer", address)) })?; let key = JsValue::from(PlatformAddressWasm::from(address)); let value = JsValue::from(PlatformAddressInfoWasm::from(info)); @@ -836,18 +823,7 @@ impl WasmSdk { .into(); // Convert outputs to map (address -> optional amount) - let outputs_map: std::collections::BTreeMap< - dash_sdk::dpp::address_funds::PlatformAddress, - Option, - > = parsed - .outputs - .into_iter() - .map(|output| { - let address = output.address().clone().into(); - let amount = Some(output.amount_value()); - (address, amount) - }) - .collect(); + let outputs_map = outputs_to_optional_btree_map(parsed.outputs); // Extract signer from options let signer = PlatformAddressSignerWasm::try_from_options(&options_value)?; @@ -876,10 +852,7 @@ impl WasmSdk { for (address, info_opt) in address_infos { let info = info_opt.ok_or_else(|| { - WasmSdkError::generic(format!( - "Address {} has no info after funding", - address - )) + WasmSdkError::generic(format!("Address {} has no info after funding", address)) })?; let key = JsValue::from(PlatformAddressWasm::from(address)); let value = JsValue::from(PlatformAddressInfoWasm::from(info)); @@ -1027,41 +1000,18 @@ impl WasmSdk { .into(); // Convert inputs to map (address -> amount) - let inputs_map: std::collections::BTreeMap< - dash_sdk::dpp::address_funds::PlatformAddress, - dash_sdk::dpp::fee::Credits, - > = parsed - .inputs - .into_iter() - .map(|input| { - let address = input.address().clone().into(); - let amount = input.amount_value(); - (address, amount) - }) - .collect(); + let inputs_map = outputs_to_btree_map(parsed.inputs); // Convert change output if provided - let change_output = parsed.change_output.map(|output| { - let address = output.address().clone().into(); - let amount = output.amount_value(); - (address, amount) - }); - - // Extract identity signer from options - let identity_signer_js = - js_sys::Reflect::get(&options_value, &JsValue::from_str("identitySigner")) - .map_err(|_| WasmSdkError::invalid_argument("identitySigner is required"))?; - let identity_signer = identity_signer_js - .to_wasm::("IdentitySigner")? - .clone(); - - // Extract address signer from options - let address_signer_js = - js_sys::Reflect::get(&options_value, &JsValue::from_str("addressSigner")) - .map_err(|_| WasmSdkError::invalid_argument("addressSigner is required"))?; - let address_signer = address_signer_js - .to_wasm::("PlatformAddressSigner")? - .clone(); + let change_output = parsed.change_output.map(|output| output.into_inner()); + + // Extract signers from options using helper methods + let identity_signer = + IdentitySignerWasm::try_from_options_with_field(&options_value, "identitySigner")?; + let address_signer = PlatformAddressSignerWasm::try_from_options_with_field( + &options_value, + "addressSigner", + )?; // Extract settings from options let settings = extract_settings_from_options(&options_value)?; From 91dfba030ffedd26801aa793a33e808738275f87 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 25 Dec 2025 23:18:10 +0700 Subject: [PATCH 09/11] refactor: remove unnecessary code --- packages/wasm-dpp2/src/lib.rs | 8 +- .../src/platform_address/input_output.rs | 42 ------- .../wasm-dpp2/src/platform_address/mod.rs | 4 +- .../wasm-dpp2/src/platform_address/signer.rs | 10 +- packages/wasm-sdk/src/settings.rs | 47 +++++-- .../src/state_transitions/addresses.rs | 116 +++++------------- .../src/state_transitions/broadcast.rs | 6 +- 7 files changed, 86 insertions(+), 147 deletions(-) diff --git a/packages/wasm-dpp2/src/lib.rs b/packages/wasm-dpp2/src/lib.rs index 29e00de089b..cf4f9a808dd 100644 --- a/packages/wasm-dpp2/src/lib.rs +++ b/packages/wasm-dpp2/src/lib.rs @@ -47,10 +47,10 @@ pub use identity::{ MasternodeVoteTransitionWasm, PartialIdentityWasm, }; pub use platform_address::{ - default_fee_strategy, extract_addresses, extract_amounts, fee_strategy_from_steps, - fee_strategy_from_steps_or_default, inputs_to_btree_map, outputs_to_btree_map, - outputs_to_optional_btree_map, FeeStrategyStepWasm, PlatformAddressInputWasm, - PlatformAddressOutputWasm, PlatformAddressSignerWasm, PlatformAddressWasm, + default_fee_strategy, fee_strategy_from_steps, fee_strategy_from_steps_or_default, + outputs_to_btree_map, outputs_to_optional_btree_map, FeeStrategyStepWasm, + PlatformAddressInputWasm, PlatformAddressOutputWasm, PlatformAddressSignerWasm, + PlatformAddressWasm, }; pub use state_transitions::base::{GroupStateTransitionInfoWasm, StateTransitionWasm}; pub use tokens::*; diff --git a/packages/wasm-dpp2/src/platform_address/input_output.rs b/packages/wasm-dpp2/src/platform_address/input_output.rs index 065d4134ad2..0025f1bb124 100644 --- a/packages/wasm-dpp2/src/platform_address/input_output.rs +++ b/packages/wasm-dpp2/src/platform_address/input_output.rs @@ -75,21 +75,6 @@ impl PlatformAddressInputWasm { pub fn into_inner(self) -> (PlatformAddress, (AddressNonce, Credits)) { (self.address.into(), (self.nonce, self.amount)) } - - /// Returns a reference to the inner PlatformAddress. - pub fn platform_address(&self) -> PlatformAddress { - self.address.into() - } - - /// Returns the nonce value. - pub fn nonce_value(&self) -> AddressNonce { - self.nonce - } - - /// Returns the amount value. - pub fn amount_value(&self) -> Credits { - self.amount - } } /// Represents an output address for address-based state transitions. @@ -142,23 +127,6 @@ impl PlatformAddressOutputWasm { pub fn into_inner(self) -> (PlatformAddress, Credits) { (self.address.into(), self.amount) } - - /// Returns a reference to the inner PlatformAddress. - pub fn platform_address(&self) -> PlatformAddress { - self.address.into() - } - - /// Returns the amount value. - pub fn amount_value(&self) -> Credits { - self.amount - } -} - -/// Converts a vector of PlatformAddressInput into a BTreeMap. -pub fn inputs_to_btree_map( - inputs: Vec, -) -> BTreeMap { - inputs.into_iter().map(|i| i.into_inner()).collect() } /// Converts a vector of PlatformAddressOutput into a BTreeMap. @@ -183,13 +151,3 @@ pub fn outputs_to_optional_btree_map( }) .collect() } - -/// Extracts addresses from a slice of PlatformAddressOutput. -pub fn extract_addresses(outputs: &[PlatformAddressOutputWasm]) -> Vec { - outputs.iter().map(|o| o.platform_address()).collect() -} - -/// Extracts amounts from a slice of PlatformAddressOutput. -pub fn extract_amounts(outputs: &[PlatformAddressOutputWasm]) -> Vec { - outputs.iter().map(|o| o.amount_value()).collect() -} diff --git a/packages/wasm-dpp2/src/platform_address/mod.rs b/packages/wasm-dpp2/src/platform_address/mod.rs index 122df32e144..fa6fea98ac5 100644 --- a/packages/wasm-dpp2/src/platform_address/mod.rs +++ b/packages/wasm-dpp2/src/platform_address/mod.rs @@ -9,7 +9,7 @@ pub use fee_strategy::{ FeeStrategyStepWasm, }; pub use input_output::{ - extract_addresses, extract_amounts, inputs_to_btree_map, outputs_to_btree_map, - outputs_to_optional_btree_map, PlatformAddressInputWasm, PlatformAddressOutputWasm, + outputs_to_btree_map, outputs_to_optional_btree_map, PlatformAddressInputWasm, + PlatformAddressOutputWasm, }; pub use signer::PlatformAddressSignerWasm; diff --git a/packages/wasm-dpp2/src/platform_address/signer.rs b/packages/wasm-dpp2/src/platform_address/signer.rs index 90b8396f3f4..03c5ad8989c 100644 --- a/packages/wasm-dpp2/src/platform_address/signer.rs +++ b/packages/wasm-dpp2/src/platform_address/signer.rs @@ -79,7 +79,7 @@ impl PlatformAddressSignerWasm { /// Returns all private keys as an array of {addressHash: Uint8Array, privateKey: Uint8Array}. /// This is used internally for cross-package access. #[wasm_bindgen(js_name = "getPrivateKeysBytes")] - pub fn get_private_keys_bytes(&self) -> js_sys::Array { + pub fn get_private_keys_bytes(&self) -> WasmDppResult { let result = js_sys::Array::new(); for (addr, key) in &self.private_keys { let entry = js_sys::Object::new(); @@ -87,11 +87,13 @@ impl PlatformAddressSignerWasm { let hash_array = js_sys::Uint8Array::from(addr_inner.hash().as_slice()); let key_bytes = key.to_bytes(); let key_array = js_sys::Uint8Array::from(key_bytes.as_slice()); - let _ = js_sys::Reflect::set(&entry, &JsValue::from_str("addressHash"), &hash_array); - let _ = js_sys::Reflect::set(&entry, &JsValue::from_str("privateKey"), &key_array); + js_sys::Reflect::set(&entry, &JsValue::from_str("addressHash"), &hash_array) + .map_err(|_| WasmDppError::generic("Failed to set addressHash property"))?; + js_sys::Reflect::set(&entry, &JsValue::from_str("privateKey"), &key_array) + .map_err(|_| WasmDppError::generic("Failed to set privateKey property"))?; result.push(&entry); } - result + Ok(result) } } diff --git a/packages/wasm-sdk/src/settings.rs b/packages/wasm-sdk/src/settings.rs index 77a6f6233bb..4f438c35aaa 100644 --- a/packages/wasm-sdk/src/settings.rs +++ b/packages/wasm-sdk/src/settings.rs @@ -3,6 +3,7 @@ //! This module provides WASM bindings for settings used across various SDK methods //! including queries, broadcasts, and state transitions. +use crate::error::WasmSdkError; use dash_sdk::platform::transition::put_settings::PutSettings; use rs_dapi_client::RequestSettings; use serde::Deserialize; @@ -75,15 +76,28 @@ impl From for RequestSettings { /// Parse request settings from JavaScript into RequestSettings. /// /// Used for query operations. -pub fn parse_request_settings(settings: Option) -> Option { - let js_value: JsValue = settings?.into(); +/// +/// Returns: +/// - `Ok(None)` if no settings were provided +/// - `Ok(Some(settings))` if valid settings were parsed +/// - `Err(...)` if settings were provided but malformed +pub fn parse_request_settings( + settings: Option, +) -> Result, WasmSdkError> { + let Some(settings_js) = settings else { + return Ok(None); + }; + + let js_value: JsValue = settings_js.into(); if js_value.is_undefined() || js_value.is_null() { - return None; + return Ok(None); } - let input: RequestSettingsInput = serde_wasm_bindgen::from_value(js_value).ok()?; - Some(input.into()) + let input: RequestSettingsInput = serde_wasm_bindgen::from_value(js_value).map_err(|e| { + WasmSdkError::serialization(format!("Failed to parse request settings: {}", e)) + })?; + Ok(Some(input.into())) } // ============================================================================ @@ -215,13 +229,26 @@ impl From for PutSettings { /// Parse put settings from JavaScript into PutSettings. /// /// Used for state transition broadcast operations. -pub fn parse_put_settings(settings: Option) -> Option { - let js_value: JsValue = settings?.into(); +/// +/// Returns: +/// - `Ok(None)` if no settings were provided +/// - `Ok(Some(settings))` if valid settings were parsed +/// - `Err(...)` if settings were provided but malformed +pub fn parse_put_settings( + settings: Option, +) -> Result, WasmSdkError> { + let Some(settings_js) = settings else { + return Ok(None); + }; + + let js_value: JsValue = settings_js.into(); if js_value.is_undefined() || js_value.is_null() { - return None; + return Ok(None); } - let input: PutSettingsInput = serde_wasm_bindgen::from_value(js_value).ok()?; - Some(input.into()) + let input: PutSettingsInput = serde_wasm_bindgen::from_value(js_value).map_err(|e| { + WasmSdkError::serialization(format!("Failed to parse put settings: {}", e)) + })?; + Ok(Some(input.into())) } diff --git a/packages/wasm-sdk/src/state_transitions/addresses.rs b/packages/wasm-sdk/src/state_transitions/addresses.rs index eb8080d975f..f681831b967 100644 --- a/packages/wasm-sdk/src/state_transitions/addresses.rs +++ b/packages/wasm-sdk/src/state_transitions/addresses.rs @@ -7,12 +7,14 @@ use crate::queries::address::PlatformAddressInfoWasm; use crate::queries::utils::deserialize_required_query; use crate::sdk::WasmSdk; use crate::settings::{parse_put_settings, PutSettingsJs}; +use dash_sdk::dpp::address_funds::PlatformAddress; use dash_sdk::dpp::identity::core_script::CoreScript; use dash_sdk::platform::transition::address_credit_withdrawal::WithdrawAddressFunds; use dash_sdk::platform::transition::top_up_identity_from_addresses::TopUpIdentityFromAddresses; use dash_sdk::platform::transition::transfer_address_funds::TransferAddressFunds; use dash_sdk::platform::transition::transfer_to_addresses::TransferToAddresses; use dash_sdk::platform::{Fetch, Identifier, Identity}; +use drive_proof_verifier::types::{AddressInfo, IndexMap}; use js_sys::{BigInt, Map}; use serde::Deserialize; use wasm_bindgen::prelude::*; @@ -24,6 +26,31 @@ use wasm_dpp2::{ PlatformAddressSignerWasm, PlatformAddressWasm, PoolingWasm, }; +/// Converts address infos from SDK response to a JavaScript Map. +/// +/// This helper handles the common pattern of converting IndexMap> +/// to Map for WASM bindings. +fn address_infos_to_js_map( + address_infos: IndexMap>, + operation_name: &str, +) -> Result { + let map = Map::new(); + + for (address, info_opt) in address_infos { + let info = info_opt.ok_or_else(|| { + WasmSdkError::generic(format!( + "Address {} has no info after {}", + address, operation_name + )) + })?; + let key = JsValue::from(PlatformAddressWasm::from(address)); + let value = JsValue::from(PlatformAddressInfoWasm::from(info)); + map.set(&key, &value); + } + + Ok(map) +} + /// Main input struct for address funds transfer options. /// Uses types from wasm-dpp2 for inputs, outputs, and fee strategy. #[derive(Deserialize)] @@ -72,7 +99,7 @@ fn extract_settings_from_options( // Convert JsValue to PutSettingsJs and parse let settings_typed: PutSettingsJs = settings_js.into(); - Ok(parse_put_settings(Some(settings_typed))) + parse_put_settings(Some(settings_typed)) } /// TypeScript interface for address transfer options @@ -165,19 +192,7 @@ impl WasmSdk { .await .map_err(|e| WasmSdkError::generic(format!("Failed to transfer funds: {}", e)))?; - // Convert to Map - let results_map = Map::new(); - - for (address, info_opt) in address_infos { - let info = info_opt.ok_or_else(|| { - WasmSdkError::generic(format!("Address {} has no info after transfer", address)) - })?; - let key = JsValue::from(PlatformAddressWasm::from(address)); - let value = JsValue::from(PlatformAddressInfoWasm::from(info)); - results_map.set(&key, &value); - } - - Ok(results_map) + address_infos_to_js_map(address_infos, "transfer") } } @@ -314,20 +329,8 @@ impl WasmSdk { .await .map_err(|e| WasmSdkError::generic(format!("Failed to top up identity: {}", e)))?; - // Convert to Map - let address_infos_map = Map::new(); - - for (address, info_opt) in address_infos { - let info = info_opt.ok_or_else(|| { - WasmSdkError::generic(format!("Address {} has no info after top up", address)) - })?; - let key = JsValue::from(PlatformAddressWasm::from(address)); - let value = JsValue::from(PlatformAddressInfoWasm::from(info)); - address_infos_map.set(&key, &value); - } - Ok(IdentityTopUpFromAddressesResultWasm { - address_infos: address_infos_map, + address_infos: address_infos_to_js_map(address_infos, "top up")?, new_balance, }) } @@ -500,19 +503,7 @@ impl WasmSdk { .await .map_err(|e| WasmSdkError::generic(format!("Failed to withdraw funds: {}", e)))?; - // Convert to Map - let results_map = Map::new(); - - for (address, info_opt) in address_infos { - let info = info_opt.ok_or_else(|| { - WasmSdkError::generic(format!("Address {} has no info after withdrawal", address)) - })?; - let key = JsValue::from(PlatformAddressWasm::from(address)); - let value = JsValue::from(PlatformAddressInfoWasm::from(info)); - results_map.set(&key, &value); - } - - Ok(results_map) + address_infos_to_js_map(address_infos, "withdrawal") } /// Transfer credits from an identity to Platform addresses. @@ -579,20 +570,8 @@ impl WasmSdk { WasmSdkError::generic(format!("Failed to transfer credits to addresses: {}", e)) })?; - // Convert to Map - let address_infos_map = Map::new(); - - for (address, info_opt) in address_infos { - let info = info_opt.ok_or_else(|| { - WasmSdkError::generic(format!("Address {} has no info after transfer", address)) - })?; - let key = JsValue::from(PlatformAddressWasm::from(address)); - let value = JsValue::from(PlatformAddressInfoWasm::from(info)); - address_infos_map.set(&key, &value); - } - Ok(IdentityTransferToAddressesResultWasm { - address_infos: address_infos_map, + address_infos: address_infos_to_js_map(address_infos, "transfer")?, new_balance, }) } @@ -847,19 +826,7 @@ impl WasmSdk { .await .map_err(|e| WasmSdkError::generic(format!("Failed to fund addresses: {}", e)))?; - // Convert to Map - let results_map = Map::new(); - - for (address, info_opt) in address_infos { - let info = info_opt.ok_or_else(|| { - WasmSdkError::generic(format!("Address {} has no info after funding", address)) - })?; - let key = JsValue::from(PlatformAddressWasm::from(address)); - let value = JsValue::from(PlatformAddressInfoWasm::from(info)); - results_map.set(&key, &value); - } - - Ok(results_map) + address_infos_to_js_map(address_infos, "funding") } } @@ -1031,24 +998,9 @@ impl WasmSdk { WasmSdkError::generic(format!("Failed to create identity from addresses: {}", e)) })?; - // Convert to Map - let address_infos_map = Map::new(); - - for (address, info_opt) in address_infos { - let info = info_opt.ok_or_else(|| { - WasmSdkError::generic(format!( - "Address {} has no info after identity creation", - address - )) - })?; - let key = JsValue::from(PlatformAddressWasm::from(address)); - let value = JsValue::from(PlatformAddressInfoWasm::from(info)); - address_infos_map.set(&key, &value); - } - Ok(IdentityCreateFromAddressesResultWasm { identity: created_identity.into(), - address_infos: address_infos_map, + address_infos: address_infos_to_js_map(address_infos, "identity creation")?, }) } } diff --git a/packages/wasm-sdk/src/state_transitions/broadcast.rs b/packages/wasm-sdk/src/state_transitions/broadcast.rs index 5b75c1316af..bc5289ef736 100644 --- a/packages/wasm-sdk/src/state_transitions/broadcast.rs +++ b/packages/wasm-sdk/src/state_transitions/broadcast.rs @@ -29,7 +29,7 @@ impl WasmSdk { settings: Option, ) -> Result<(), WasmSdkError> { let st: StateTransition = state_transition.into(); - let put_settings = parse_put_settings(settings); + let put_settings = parse_put_settings(settings)?; st.broadcast(self.as_ref(), put_settings) .await @@ -57,7 +57,7 @@ impl WasmSdk { settings: Option, ) -> Result { let st: StateTransition = state_transition.into(); - let put_settings = parse_put_settings(settings); + let put_settings = parse_put_settings(settings)?; let result = st .wait_for_response::(self.as_ref(), put_settings) @@ -88,7 +88,7 @@ impl WasmSdk { settings: Option, ) -> Result { let st: StateTransition = state_transition.into(); - let put_settings = parse_put_settings(settings); + let put_settings = parse_put_settings(settings)?; let result = st .broadcast_and_wait::(self.as_ref(), put_settings) From dc885f7a79b4c6525b2dc8e078c9fa754aee36f4 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 26 Dec 2025 14:01:18 +0700 Subject: [PATCH 10/11] feat: support optinal output amount --- .../src/platform_address/input_output.rs | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/wasm-dpp2/src/platform_address/input_output.rs b/packages/wasm-dpp2/src/platform_address/input_output.rs index 0025f1bb124..f7d31b30156 100644 --- a/packages/wasm-dpp2/src/platform_address/input_output.rs +++ b/packages/wasm-dpp2/src/platform_address/input_output.rs @@ -80,28 +80,30 @@ impl PlatformAddressInputWasm { /// Represents an output address for address-based state transitions. /// /// An output specifies a Platform address that will receive credits, -/// along with the amount to receive. +/// along with an optional amount to receive. When amount is None, +/// the system distributes funds automatically (used for asset lock funding). #[wasm_bindgen(js_name = "PlatformAddressOutput")] #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PlatformAddressOutputWasm { address: PlatformAddressWasm, - amount: Credits, + #[serde(default)] + amount: Option, } #[wasm_bindgen(js_class = PlatformAddressOutput)] impl PlatformAddressOutputWasm { - /// Creates a new PlatformAddressOutput. + /// Creates a new PlatformAddressOutput with a specific amount. /// /// @param address - The Platform address (PlatformAddress, Uint8Array, or bech32m string) - /// @param amount - The amount of credits to send to this address + /// @param amount - The amount of credits to send to this address (optional for asset lock funding) #[wasm_bindgen(constructor)] pub fn new( #[wasm_bindgen(unchecked_param_type = "PlatformAddressLike")] address: &JsValue, - amount: BigInt, + amount: Option, ) -> WasmDppResult { let platform_address = PlatformAddressWasm::try_from(address)?; - let amount_u64 = bigint_to_u64(amount)?; + let amount_u64 = amount.map(bigint_to_u64).transpose()?; Ok(PlatformAddressOutputWasm { address: platform_address, @@ -115,16 +117,25 @@ impl PlatformAddressOutputWasm { self.address } - /// Returns the amount. + /// Returns the amount, or undefined if not specified. #[wasm_bindgen(getter)] - pub fn amount(&self) -> BigInt { - BigInt::from(self.amount) + pub fn amount(&self) -> Option { + self.amount.map(BigInt::from) } } impl PlatformAddressOutputWasm { /// Returns the inner values as a tuple suitable for BTreeMap insertion. + /// Panics if amount is None - use `into_inner_optional` for optional amounts. pub fn into_inner(self) -> (PlatformAddress, Credits) { + ( + self.address.into(), + self.amount.expect("amount is required for this operation"), + ) + } + + /// Returns the inner values with optional amount. + pub fn into_inner_optional(self) -> (PlatformAddress, Option) { (self.address.into(), self.amount) } } @@ -145,9 +156,6 @@ pub fn outputs_to_optional_btree_map( ) -> BTreeMap> { outputs .into_iter() - .map(|o| { - let (addr, amount) = o.into_inner(); - (addr, Some(amount)) - }) + .map(|o| o.into_inner_optional()) .collect() } From da2a86ba9b95af3298eb83e4b2376d1cf33efc99 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 26 Dec 2025 14:49:34 +0700 Subject: [PATCH 11/11] feat: derive address from key --- packages/js-evo-sdk/src/addresses/facade.ts | 35 ++++---- packages/js-evo-sdk/tests/.eslintrc.yml | 5 +- .../tests/unit/facades/addresses.spec.mjs | 30 +++---- .../src/address_funds/platform_address.rs | 41 ++++++++- packages/wasm-dpp2/src/identity/model.rs | 3 +- packages/wasm-dpp2/src/identity/signer.rs | 43 ++++------ .../credit_withdrawal_transition.rs | 2 +- .../src/identity/transitions/pooling.rs | 4 +- packages/wasm-dpp2/src/lib.rs | 7 +- .../src/platform_address/fee_strategy.rs | 2 +- .../src/platform_address/input_output.rs | 2 +- .../wasm-dpp2/src/platform_address/mod.rs | 8 +- .../wasm-dpp2/src/platform_address/signer.rs | 45 +++++----- packages/wasm-dpp2/src/private_key.rs | 7 ++ .../tests/unit/AddressFundsTransfer.spec.mjs | 65 +++++--------- .../tests/unit/PlatformAddressSigner.spec.mjs | 85 +++++++------------ packages/wasm-sdk/src/lib.rs | 4 +- packages/wasm-sdk/src/settings.rs | 5 +- 18 files changed, 189 insertions(+), 204 deletions(-) diff --git a/packages/js-evo-sdk/src/addresses/facade.ts b/packages/js-evo-sdk/src/addresses/facade.ts index 73042f7a053..eea4f2aea0d 100644 --- a/packages/js-evo-sdk/src/addresses/facade.ts +++ b/packages/js-evo-sdk/src/addresses/facade.ts @@ -64,16 +64,15 @@ export class AddressesFacade { * * @example * ```typescript - * const senderAddr = PlatformAddress.fromBech32m("tdashevo1..."); * const recipientAddr = PlatformAddress.fromBech32m("tdashevo1..."); * const privateKey = PrivateKey.fromWIF("cPrivateKeyWif..."); * + * const signer = new PlatformAddressSigner(); + * const senderAddr = signer.addKey(privateKey); // Derives P2PKH address from key + * * const input = new PlatformAddressInput(senderAddr, 0n, 100000n); * const output = new PlatformAddressOutput(recipientAddr, 90000n); * - * const signer = new PlatformAddressSigner(); - * signer.addKey(senderAddr, privateKey); - * * const result = await sdk.addresses.transfer({ * inputs: [input], * outputs: [output], @@ -95,13 +94,12 @@ export class AddressesFacade { * @example * ```typescript * const identityId = Identifier.from("..."); - * const sourceAddr = PlatformAddress.fromBech32m("tdashevo1..."); * const privateKey = PrivateKey.fromWIF("cPrivateKeyWif..."); * - * const input = new PlatformAddressInput(sourceAddr, 0n, 50000n); - * * const signer = new PlatformAddressSigner(); - * signer.addKey(sourceAddr, privateKey); + * const sourceAddr = signer.addKey(privateKey); // Derives P2PKH address from key + * + * const input = new PlatformAddressInput(sourceAddr, 0n, 50000n); * * const result = await sdk.addresses.topUpIdentity({ * identityId, @@ -125,16 +123,15 @@ export class AddressesFacade { * * @example * ```typescript - * const platformAddr = PlatformAddress.fromBech32m("tdashevo1..."); * const privateKey = PrivateKey.fromWIF("cPrivateKeyWif..."); * * // Create Core output script for L1 destination * const coreScript = CoreScript.newP2PKH(coreAddressHash); * - * const input = new PlatformAddressInput(platformAddr, 0n, 100000n); - * * const signer = new PlatformAddressSigner(); - * signer.addKey(platformAddr, privateKey); + * const platformAddr = signer.addKey(privateKey); // Derives P2PKH address from key + * + * const input = new PlatformAddressInput(platformAddr, 0n, 100000n); * * const result = await sdk.addresses.withdraw({ * inputs: [input], @@ -191,7 +188,6 @@ export class AddressesFacade { * * @example * ```typescript - * const platformAddr = PlatformAddress.fromBech32m("tdashevo1..."); * const assetLockPrivateKey = PrivateKey.fromWIF("cPrivateKeyWif..."); * const addressPrivateKey = PrivateKey.fromWIF("cPrivateKeyWif..."); * @@ -202,10 +198,10 @@ export class AddressesFacade { * outputIndex * ); * - * const output = new PlatformAddressOutput(platformAddr, 100000n); - * * const signer = new PlatformAddressSigner(); - * signer.addKey(platformAddr, addressPrivateKey); + * const platformAddr = signer.addKey(addressPrivateKey); // Derives P2PKH address from key + * + * const output = new PlatformAddressOutput(platformAddr, 100000n); * * const result = await sdk.addresses.fundFromAssetLock({ * assetLockProof, @@ -228,7 +224,6 @@ export class AddressesFacade { * * @example * ```typescript - * const sourceAddr = PlatformAddress.fromBech32m("tdashevo1..."); * const addressPrivateKey = PrivateKey.fromWIF("cAddressPrivateKeyWif..."); * const identityPrivateKey = PrivateKey.fromWIF("cIdentityKeyWif..."); * @@ -236,11 +231,11 @@ export class AddressesFacade { * const identity = new Identity(Identifier.random()); * identity.addPublicKey(identityPublicKey); * - * const input = new PlatformAddressInput(sourceAddr, 0n, 100000n); - * * // Create signers * const addressSigner = new PlatformAddressSigner(); - * addressSigner.addKey(sourceAddr, addressPrivateKey); + * const sourceAddr = addressSigner.addKey(addressPrivateKey); // Derives P2PKH address from key + * + * const input = new PlatformAddressInput(sourceAddr, 0n, 100000n); * * const identitySigner = new IdentitySigner(); * identitySigner.addKey(identityPrivateKey); diff --git a/packages/js-evo-sdk/tests/.eslintrc.yml b/packages/js-evo-sdk/tests/.eslintrc.yml index 509d508a284..06d2a93e05a 100644 --- a/packages/js-evo-sdk/tests/.eslintrc.yml +++ b/packages/js-evo-sdk/tests/.eslintrc.yml @@ -2,9 +2,12 @@ extends: - airbnb-base - plugin:jsdoc/recommended env: - es2020: true + es2022: true node: true mocha: true +parserOptions: + ecmaVersion: 2022 + sourceType: module rules: eol-last: - error diff --git a/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs b/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs index ba44c247e06..87e701a665e 100644 --- a/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs +++ b/packages/js-evo-sdk/tests/unit/facades/addresses.spec.mjs @@ -70,9 +70,6 @@ describe('AddressesFacade', () => { it('transfer() forwards options to addressFundsTransfer with PlatformAddressSigner', async () => { // Create proper PlatformAddress objects - const senderAddr = wasmSDKPackage.PlatformAddress.fromBytes( - new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]), - ); const recipientAddr = wasmSDKPackage.PlatformAddress.fromBytes( new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]), ); @@ -81,12 +78,12 @@ describe('AddressesFacade', () => { const privateKeyBytes = new Uint8Array(32).fill(1); // Test key bytes const privateKey = wasmSDKPackage.PrivateKey.fromBytes(privateKeyBytes, 'testnet'); - // Create signer and add the private key + // Create signer and derive the sender address from the private key const signer = new wasmSDKPackage.PlatformAddressSigner(); - signer.addKey(senderAddr, privateKey); + const derivedSenderAddr = signer.addKey(privateKey); // Create typed input and output objects (address, nonce, amount) - const input = new wasmSDKPackage.PlatformAddressInput(senderAddr, 0, 100000n); + const input = new wasmSDKPackage.PlatformAddressInput(derivedSenderAddr, 0, 100000n); const output = new wasmSDKPackage.PlatformAddressOutput(recipientAddr, 90000n); const options = { @@ -95,7 +92,7 @@ describe('AddressesFacade', () => { signer, }; const result = await client.addresses.transfer(options); - expect(wasmSdk.addressFundsTransfer).to.be.calledOnce; + expect(wasmSdk.addressFundsTransfer).to.be.calledOnce(); expect(result.type).to.equal('VerifiedAddressInfos'); expect(result.addressInfos).to.have.lengthOf(1); expect(result.addressInfos[0].address.addressType).to.equal('P2PKH'); @@ -108,9 +105,6 @@ describe('AddressesFacade', () => { }); // Create proper PlatformAddress objects - const senderAddr = wasmSDKPackage.PlatformAddress.fromBytes( - new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]), - ); const recipientAddr = wasmSDKPackage.PlatformAddress.fromBytes( new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]), ); @@ -119,12 +113,12 @@ describe('AddressesFacade', () => { const privateKeyBytes = new Uint8Array(32).fill(1); // Test key bytes const privateKey = wasmSDKPackage.PrivateKey.fromBytes(privateKeyBytes, 'testnet'); - // Create signer and add the private key + // Create signer and derive the sender address from the private key const signer = new wasmSDKPackage.PlatformAddressSigner(); - signer.addKey(senderAddr, privateKey); + const derivedSenderAddr = signer.addKey(privateKey); // Create typed input and output objects (address, nonce, amount) - const input = new wasmSDKPackage.PlatformAddressInput(senderAddr, 0, 100000n); + const input = new wasmSDKPackage.PlatformAddressInput(derivedSenderAddr, 0, 100000n); const output = new wasmSDKPackage.PlatformAddressOutput(recipientAddr, 90000n); const options = { @@ -159,7 +153,7 @@ describe('AddressesFacade', () => { }; const result = await client.addresses.topUpIdentity(options); - expect(wasmSdk.identityTopUpFromAddresses).to.be.calledOnce; + expect(wasmSdk.identityTopUpFromAddresses).to.be.calledOnce(); expect(result.newBalance).to.equal(150000n); }); @@ -187,7 +181,7 @@ describe('AddressesFacade', () => { }; const result = await client.addresses.withdraw(options); - expect(wasmSdk.addressFundsWithdraw).to.be.calledOnce; + expect(wasmSdk.addressFundsWithdraw).to.be.calledOnce(); expect(result).to.be.instanceOf(Map); }); @@ -213,7 +207,7 @@ describe('AddressesFacade', () => { }; const result = await client.addresses.transferFromIdentity(options); - expect(wasmSdk.identityTransferToAddresses).to.be.calledOnce; + expect(wasmSdk.identityTransferToAddresses).to.be.calledOnce(); expect(result.newBalance).to.equal(400000n); }); @@ -237,7 +231,7 @@ describe('AddressesFacade', () => { }; const result = await client.addresses.fundFromAssetLock(options); - expect(wasmSdk.addressFundingFromAssetLock).to.be.calledOnce; + expect(wasmSdk.addressFundingFromAssetLock).to.be.calledOnce(); expect(result).to.be.instanceOf(Map); }); @@ -264,7 +258,7 @@ describe('AddressesFacade', () => { }; const result = await client.addresses.createIdentity(options); - expect(wasmSdk.identityCreateFromAddresses).to.be.calledOnce; + expect(wasmSdk.identityCreateFromAddresses).to.be.calledOnce(); expect(result.identity).to.exist(); }); }); diff --git a/packages/rs-dpp/src/address_funds/platform_address.rs b/packages/rs-dpp/src/address_funds/platform_address.rs index 5137326ca27..511255ee4a3 100644 --- a/packages/rs-dpp/src/address_funds/platform_address.rs +++ b/packages/rs-dpp/src/address_funds/platform_address.rs @@ -11,7 +11,7 @@ use dashcore::key::Secp256k1; use dashcore::secp256k1::ecdsa::RecoverableSignature; use dashcore::secp256k1::Message; use dashcore::signer::CompactSignature; -use dashcore::{Address, Network, PubkeyHash, PublicKey, ScriptHash}; +use dashcore::{Address, Network, PrivateKey, PubkeyHash, PublicKey, ScriptHash}; use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize}; #[cfg(feature = "state-transition-serde-conversion")] use serde::{Deserialize, Serialize}; @@ -63,6 +63,18 @@ impl TryFrom
for PlatformAddress { } } +impl From<&PrivateKey> for PlatformAddress { + /// Derives a P2PKH Platform address from a private key. + /// + /// The address is derived as: P2PKH(Hash160(compressed_public_key)) + /// where Hash160 = RIPEMD160(SHA256(x)), which is the standard Bitcoin P2PKH derivation. + fn from(private_key: &PrivateKey) -> Self { + let secp = Secp256k1::new(); + let pubkey_hash = private_key.public_key(&secp).pubkey_hash(); + PlatformAddress::P2pkh(*pubkey_hash.as_byte_array()) + } +} + impl Default for PlatformAddress { fn default() -> Self { PlatformAddress::P2pkh([0u8; 20]) @@ -618,6 +630,33 @@ mod tests { script } + #[test] + fn test_platform_address_from_private_key() { + // Create a keypair + let seed = [1u8; 32]; + let (secret_key, public_key) = create_keypair(seed); + + // Create PrivateKey from the secret key + let private_key = PrivateKey::new(secret_key, Network::Testnet); + + // Derive address using From<&PrivateKey> + let address_from_private = PlatformAddress::from(&private_key); + + // Derive address manually using pubkey_hash() which computes Hash160(pubkey) + // Hash160 = RIPEMD160(SHA256(x)), the standard Bitcoin P2PKH derivation + let pubkey_hash = public_key.pubkey_hash(); + let address_from_pubkey = PlatformAddress::P2pkh(*pubkey_hash.as_byte_array()); + + // Both addresses should be identical + assert_eq!( + address_from_private, address_from_pubkey, + "Address derived from private key should match Hash160(compressed_pubkey)" + ); + + // Verify it's a P2PKH address + assert!(address_from_private.is_p2pkh()); + } + #[test] fn test_p2pkh_verify_signature_success() { // Create a keypair diff --git a/packages/wasm-dpp2/src/identity/model.rs b/packages/wasm-dpp2/src/identity/model.rs index 465f47017bb..1c8075a35c5 100644 --- a/packages/wasm-dpp2/src/identity/model.rs +++ b/packages/wasm-dpp2/src/identity/model.rs @@ -98,7 +98,8 @@ impl IdentityWasm { #[wasm_bindgen(js_name = "getPublicKeyById")] pub fn get_public_key_by_id(&self, key_id: KeyID) -> Option { - self.0.get_public_key_by_id(key_id) + self.0 + .get_public_key_by_id(key_id) .map(|key| IdentityPublicKeyWasm::from(key.clone())) } diff --git a/packages/wasm-dpp2/src/identity/signer.rs b/packages/wasm-dpp2/src/identity/signer.rs index 1eecf421d0d..83f4d50f57e 100644 --- a/packages/wasm-dpp2/src/identity/signer.rs +++ b/packages/wasm-dpp2/src/identity/signer.rs @@ -6,15 +6,13 @@ use crate::error::{WasmDppError, WasmDppResult}; use crate::private_key::PrivateKeyWasm; use crate::utils::IntoWasm; -use dpp::dashcore::hashes::{hash160, Hash}; -use dpp::dashcore::secp256k1::{PublicKey, Secp256k1, SecretKey}; +use dpp::ProtocolError; +use dpp::address_funds::{AddressWitness, PlatformAddress}; use dpp::dashcore::signer; 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::address_funds::AddressWitness; -use dpp::ProtocolError; use std::collections::BTreeMap; use std::fmt; use wasm_bindgen::prelude::*; @@ -50,33 +48,22 @@ impl IdentitySignerWasm { /// Adds a private key to the signer. /// - /// The public key hash is derived automatically from the private key. + /// The public key hash is derived automatically from the private key using + /// Hash160(compressed_public_key) where Hash160 = RIPEMD160(SHA256(x)). /// /// @param privateKey - The PrivateKey object #[wasm_bindgen(js_name = "addKey")] pub fn add_key(&mut self, private_key: &PrivateKeyWasm) -> WasmDppResult<()> { - let key_bytes = private_key.to_bytes(); - if key_bytes.len() != 32 { - return Err(WasmDppError::invalid_argument(format!( - "Private key must be 32 bytes, got {}", - key_bytes.len() - ))); - } - - // Derive public key hash from private key - let secp = Secp256k1::new(); - let secret_key = SecretKey::from_slice(&key_bytes).map_err(|e| { - WasmDppError::invalid_argument(format!("Invalid secret key: {}", e)) - })?; - let public_key = PublicKey::from_secret_key(&secp, &secret_key); - let public_key_bytes = public_key.serialize(); - let public_key_hash = hash160::Hash::hash(&public_key_bytes[..]) - .to_byte_array(); + let private_key_bytes: [u8; 32] = private_key + .to_bytes() + .try_into() + .map_err(|_| WasmDppError::invalid_argument("Private key must be 32 bytes"))?; - let mut key_array = [0u8; 32]; - key_array.copy_from_slice(&key_bytes); + // Derive public key hash from private key using From<&PrivateKey> for PlatformAddress + let platform_address = PlatformAddress::from(private_key.inner()); + let public_key_hash = *platform_address.hash(); - self.private_keys.insert(public_key_hash, key_array); + self.private_keys.insert(public_key_hash, private_key_bytes); Ok(()) } @@ -198,8 +185,10 @@ impl IdentitySignerWasm { /// This helper reads the specified field from an options object and converts it /// to an IdentitySignerWasm. pub fn try_from_options_with_field(options: &JsValue, field_name: &str) -> WasmDppResult { - let signer_js = js_sys::Reflect::get(options, &JsValue::from_str(field_name)) - .map_err(|_| WasmDppError::invalid_argument(format!("Missing '{}' field", field_name)))?; + let signer_js = + js_sys::Reflect::get(options, &JsValue::from_str(field_name)).map_err(|_| { + WasmDppError::invalid_argument(format!("Missing '{}' field", field_name)) + })?; if signer_js.is_undefined() || signer_js.is_null() { return Err(WasmDppError::invalid_argument(format!( diff --git a/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs b/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs index 3a4ad324be6..3cac596b523 100644 --- a/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs +++ b/packages/wasm-dpp2/src/identity/transitions/credit_withdrawal_transition.rs @@ -1,7 +1,7 @@ +use super::pooling::PoolingWasm; use crate::asset_lock_proof::AssetLockProofWasm; use crate::core_script::CoreScriptWasm; use crate::enums::keys::purpose::PurposeWasm; -use super::pooling::PoolingWasm; use crate::error::{WasmDppError, WasmDppResult}; use crate::identifier::IdentifierWasm; use crate::impl_wasm_conversions; diff --git a/packages/wasm-dpp2/src/identity/transitions/pooling.rs b/packages/wasm-dpp2/src/identity/transitions/pooling.rs index d774baad46b..dc8751e964f 100644 --- a/packages/wasm-dpp2/src/identity/transitions/pooling.rs +++ b/packages/wasm-dpp2/src/identity/transitions/pooling.rs @@ -45,7 +45,9 @@ impl<'de> Deserialize<'de> for PoolingWasm { }; } - Err(serde::de::Error::custom("pooling must be a string or number")) + Err(serde::de::Error::custom( + "pooling must be a string or number", + )) } } diff --git a/packages/wasm-dpp2/src/lib.rs b/packages/wasm-dpp2/src/lib.rs index cf4f9a808dd..f22ffb59f5f 100644 --- a/packages/wasm-dpp2/src/lib.rs +++ b/packages/wasm-dpp2/src/lib.rs @@ -47,10 +47,9 @@ pub use identity::{ MasternodeVoteTransitionWasm, PartialIdentityWasm, }; pub use platform_address::{ - default_fee_strategy, fee_strategy_from_steps, fee_strategy_from_steps_or_default, - outputs_to_btree_map, outputs_to_optional_btree_map, FeeStrategyStepWasm, - PlatformAddressInputWasm, PlatformAddressOutputWasm, PlatformAddressSignerWasm, - PlatformAddressWasm, + FeeStrategyStepWasm, PlatformAddressInputWasm, PlatformAddressOutputWasm, + PlatformAddressSignerWasm, PlatformAddressWasm, default_fee_strategy, fee_strategy_from_steps, + fee_strategy_from_steps_or_default, outputs_to_btree_map, outputs_to_optional_btree_map, }; pub use state_transitions::base::{GroupStateTransitionInfoWasm, StateTransitionWasm}; pub use tokens::*; diff --git a/packages/wasm-dpp2/src/platform_address/fee_strategy.rs b/packages/wasm-dpp2/src/platform_address/fee_strategy.rs index 72aa9eceed4..8f815a0c5a8 100644 --- a/packages/wasm-dpp2/src/platform_address/fee_strategy.rs +++ b/packages/wasm-dpp2/src/platform_address/fee_strategy.rs @@ -1,6 +1,6 @@ use dpp::address_funds::{AddressFundsFeeStrategy, AddressFundsFeeStrategyStep}; -use serde::de::{self, Deserializer, MapAccess, Visitor}; use serde::Deserialize; +use serde::de::{self, Deserializer, MapAccess, Visitor}; use std::fmt; use wasm_bindgen::prelude::*; diff --git a/packages/wasm-dpp2/src/platform_address/input_output.rs b/packages/wasm-dpp2/src/platform_address/input_output.rs index f7d31b30156..be1aeabc597 100644 --- a/packages/wasm-dpp2/src/platform_address/input_output.rs +++ b/packages/wasm-dpp2/src/platform_address/input_output.rs @@ -1,5 +1,5 @@ -use crate::error::{WasmDppError, WasmDppResult}; use super::PlatformAddressWasm; +use crate::error::{WasmDppError, WasmDppResult}; use dpp::address_funds::PlatformAddress; use dpp::fee::Credits; use dpp::prelude::AddressNonce; diff --git a/packages/wasm-dpp2/src/platform_address/mod.rs b/packages/wasm-dpp2/src/platform_address/mod.rs index fa6fea98ac5..1ede35af35f 100644 --- a/packages/wasm-dpp2/src/platform_address/mod.rs +++ b/packages/wasm-dpp2/src/platform_address/mod.rs @@ -5,11 +5,11 @@ mod signer; pub use address::PlatformAddressWasm; pub use fee_strategy::{ - default_fee_strategy, fee_strategy_from_steps, fee_strategy_from_steps_or_default, - FeeStrategyStepWasm, + FeeStrategyStepWasm, default_fee_strategy, fee_strategy_from_steps, + fee_strategy_from_steps_or_default, }; pub use input_output::{ - outputs_to_btree_map, outputs_to_optional_btree_map, PlatformAddressInputWasm, - PlatformAddressOutputWasm, + PlatformAddressInputWasm, PlatformAddressOutputWasm, outputs_to_btree_map, + outputs_to_optional_btree_map, }; pub use signer::PlatformAddressSignerWasm; diff --git a/packages/wasm-dpp2/src/platform_address/signer.rs b/packages/wasm-dpp2/src/platform_address/signer.rs index 03c5ad8989c..05809449801 100644 --- a/packages/wasm-dpp2/src/platform_address/signer.rs +++ b/packages/wasm-dpp2/src/platform_address/signer.rs @@ -1,12 +1,12 @@ +use super::PlatformAddressWasm; use crate::error::{WasmDppError, WasmDppResult}; use crate::private_key::PrivateKeyWasm; use crate::utils::IntoWasm; -use super::PlatformAddressWasm; +use dpp::ProtocolError; use dpp::address_funds::{AddressWitness, PlatformAddress}; use dpp::dashcore::signer; use dpp::identity::signer::Signer; use dpp::platform_value::BinaryData; -use dpp::ProtocolError; use std::collections::BTreeMap; use std::fmt; use wasm_bindgen::prelude::*; @@ -40,19 +40,19 @@ impl PlatformAddressSignerWasm { } } - /// Adds a private key for the given Platform address. + /// Adds a private key and derives the Platform address from it. + /// + /// The address is derived as: P2PKH(Hash160(compressed_public_key)) /// - /// @param address - The Platform address (PlatformAddress, Uint8Array, or bech32m string) /// @param privateKey - The PrivateKey object + /// @returns The derived Platform address #[wasm_bindgen(js_name = "addKey")] - pub fn add_key( - &mut self, - #[wasm_bindgen(unchecked_param_type = "PlatformAddressLike")] address: &JsValue, - private_key: &PrivateKeyWasm, - ) -> WasmDppResult<()> { - let platform_address = PlatformAddressWasm::try_from(address)?; - self.private_keys.insert(platform_address, private_key.clone()); - Ok(()) + pub fn add_key(&mut self, private_key: &PrivateKeyWasm) -> WasmDppResult { + let platform_address = + PlatformAddressWasm::from(PlatformAddress::from(private_key.inner())); + self.private_keys + .insert(platform_address, private_key.clone()); + Ok(platform_address) } #[wasm_bindgen(getter = __struct)] @@ -101,15 +101,12 @@ impl Signer for PlatformAddressSignerWasm { fn sign(&self, address: &PlatformAddress, data: &[u8]) -> Result { let wasm_address = PlatformAddressWasm::from(*address); - let private_key = self - .private_keys - .get(&wasm_address) - .ok_or_else(|| { - ProtocolError::Generic(format!( - "No private key found for address hash {}", - hex::encode(address.hash()) - )) - })?; + let private_key = self.private_keys.get(&wasm_address).ok_or_else(|| { + ProtocolError::Generic(format!( + "No private key found for address hash {}", + hex::encode(address.hash()) + )) + })?; let key_bytes = private_key.to_bytes(); let signature = signer::sign(data, &key_bytes)?; @@ -162,8 +159,10 @@ impl PlatformAddressSignerWasm { /// This helper reads the specified field from an options object and converts it /// to a PlatformAddressSignerWasm. pub fn try_from_options_with_field(options: &JsValue, field_name: &str) -> WasmDppResult { - let signer_js = js_sys::Reflect::get(options, &JsValue::from_str(field_name)) - .map_err(|_| WasmDppError::invalid_argument(format!("Missing '{}' field", field_name)))?; + let signer_js = + js_sys::Reflect::get(options, &JsValue::from_str(field_name)).map_err(|_| { + WasmDppError::invalid_argument(format!("Missing '{}' field", field_name)) + })?; if signer_js.is_undefined() || signer_js.is_null() { return Err(WasmDppError::invalid_argument(format!( diff --git a/packages/wasm-dpp2/src/private_key.rs b/packages/wasm-dpp2/src/private_key.rs index 798c2877dc9..fdd5569cd0e 100644 --- a/packages/wasm-dpp2/src/private_key.rs +++ b/packages/wasm-dpp2/src/private_key.rs @@ -26,6 +26,13 @@ impl From for PrivateKeyWasm { } } +impl PrivateKeyWasm { + /// Returns a reference to the inner PrivateKey + pub fn inner(&self) -> &PrivateKey { + &self.0 + } +} + #[wasm_bindgen(js_class = PrivateKey)] impl PrivateKeyWasm { #[wasm_bindgen(getter = __type)] diff --git a/packages/wasm-dpp2/tests/unit/AddressFundsTransfer.spec.mjs b/packages/wasm-dpp2/tests/unit/AddressFundsTransfer.spec.mjs index cb30a51642c..0533f0dfd42 100644 --- a/packages/wasm-dpp2/tests/unit/AddressFundsTransfer.spec.mjs +++ b/packages/wasm-dpp2/tests/unit/AddressFundsTransfer.spec.mjs @@ -46,23 +46,19 @@ describe.skip('AddressFundsTransferTransition', () => { describe('build', () => { it('should build a transfer transition with single input and output', () => { - // Create input address - const inputAddrBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const inputAddr = wasm.PlatformAddress.fromBytes(Array.from(inputAddrBytes)); - // Create output address const outputAddrBytes = new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); const outputAddr = wasm.PlatformAddress.fromBytes(Array.from(outputAddrBytes)); + // Create signer and derive input address from private key + const signer = new wasm.PlatformAddressSigner(); + const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + const inputAddr = signer.addKey(privateKey); + // Create input and output const input = new wasm.PlatformAddressInput(inputAddr, BigInt(0), BigInt(100000)); const output = new wasm.PlatformAddressOutput(outputAddr, BigInt(90000)); - // Create signer - const signer = new wasm.PlatformAddressSigner(); - const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); - signer.addKey(inputAddr, privateKey); - // Build transition const transition = wasm.AddressFundsTransferTransition.build({ inputs: [input], @@ -75,13 +71,6 @@ describe.skip('AddressFundsTransferTransition', () => { }); it('should build a transfer transition with multiple inputs and outputs', () => { - // Create input addresses - const input1AddrBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const input1Addr = wasm.PlatformAddress.fromBytes(Array.from(input1AddrBytes)); - - const input2AddrBytes = new Uint8Array([0x00, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40]); - const input2Addr = wasm.PlatformAddress.fromBytes(Array.from(input2AddrBytes)); - // Create output addresses const output1AddrBytes = new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); const output1Addr = wasm.PlatformAddress.fromBytes(Array.from(output1AddrBytes)); @@ -89,6 +78,13 @@ describe.skip('AddressFundsTransferTransition', () => { const output2AddrBytes = new Uint8Array([0x00, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21]); const output2Addr = wasm.PlatformAddress.fromBytes(Array.from(output2AddrBytes)); + // Create signer with keys and derive input addresses + const signer = new wasm.PlatformAddressSigner(); + const privateKey1 = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); + const privateKey2 = wasm.PrivateKey.fromHex(testPrivateKeyHex2, 'testnet'); + const input1Addr = signer.addKey(privateKey1); + const input2Addr = signer.addKey(privateKey2); + // Create inputs const input1 = new wasm.PlatformAddressInput(input1Addr, BigInt(0), BigInt(100000)); const input2 = new wasm.PlatformAddressInput(input2Addr, BigInt(0), BigInt(50000)); @@ -97,13 +93,6 @@ describe.skip('AddressFundsTransferTransition', () => { const output1 = new wasm.PlatformAddressOutput(output1Addr, BigInt(80000)); const output2 = new wasm.PlatformAddressOutput(output2Addr, BigInt(60000)); - // Create signer with keys for both inputs - const signer = new wasm.PlatformAddressSigner(); - const privateKey1 = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); - const privateKey2 = wasm.PrivateKey.fromHex(testPrivateKeyHex2, 'testnet'); - signer.addKey(input1Addr, privateKey1); - signer.addKey(input2Addr, privateKey2); - // Build transition const transition = wasm.AddressFundsTransferTransition.build({ inputs: [input1, input2], @@ -115,18 +104,15 @@ describe.skip('AddressFundsTransferTransition', () => { }); it('should build with custom fee strategy', () => { - const inputAddrBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const inputAddr = wasm.PlatformAddress.fromBytes(Array.from(inputAddrBytes)); - const outputAddrBytes = new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); const outputAddr = wasm.PlatformAddress.fromBytes(Array.from(outputAddrBytes)); - const input = new wasm.PlatformAddressInput(inputAddr, BigInt(0), BigInt(100000)); - const output = new wasm.PlatformAddressOutput(outputAddr, BigInt(100000)); - const signer = new wasm.PlatformAddressSigner(); const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); - signer.addKey(inputAddr, privateKey); + const inputAddr = signer.addKey(privateKey); + + const input = new wasm.PlatformAddressInput(inputAddr, BigInt(0), BigInt(100000)); + const output = new wasm.PlatformAddressOutput(outputAddr, BigInt(100000)); // Use reduceOutput fee strategy const feeStrategy = [wasm.FeeStrategyStep.reduceOutput(0)]; @@ -142,18 +128,15 @@ describe.skip('AddressFundsTransferTransition', () => { }); it('should build with user fee increase', () => { - const inputAddrBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const inputAddr = wasm.PlatformAddress.fromBytes(Array.from(inputAddrBytes)); - const outputAddrBytes = new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); const outputAddr = wasm.PlatformAddress.fromBytes(Array.from(outputAddrBytes)); - const input = new wasm.PlatformAddressInput(inputAddr, BigInt(0), BigInt(100000)); - const output = new wasm.PlatformAddressOutput(outputAddr, BigInt(90000)); - const signer = new wasm.PlatformAddressSigner(); const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); - signer.addKey(inputAddr, privateKey); + const inputAddr = signer.addKey(privateKey); + + const input = new wasm.PlatformAddressInput(inputAddr, BigInt(0), BigInt(100000)); + const output = new wasm.PlatformAddressOutput(outputAddr, BigInt(90000)); const transition = wasm.AddressFundsTransferTransition.build({ inputs: [input], @@ -185,13 +168,11 @@ describe.skip('AddressFundsTransferTransition', () => { }); it('should fail without outputs', () => { - const inputAddrBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const inputAddr = wasm.PlatformAddress.fromBytes(Array.from(inputAddrBytes)); - const input = new wasm.PlatformAddressInput(inputAddr, BigInt(0), BigInt(100000)); - const signer = new wasm.PlatformAddressSigner(); const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); - signer.addKey(inputAddr, privateKey); + const inputAddr = signer.addKey(privateKey); + + const input = new wasm.PlatformAddressInput(inputAddr, BigInt(0), BigInt(100000)); try { wasm.AddressFundsTransferTransition.build({ diff --git a/packages/wasm-dpp2/tests/unit/PlatformAddressSigner.spec.mjs b/packages/wasm-dpp2/tests/unit/PlatformAddressSigner.spec.mjs index 6d6b40a66f1..e3f88c80764 100644 --- a/packages/wasm-dpp2/tests/unit/PlatformAddressSigner.spec.mjs +++ b/packages/wasm-dpp2/tests/unit/PlatformAddressSigner.spec.mjs @@ -21,93 +21,72 @@ describe('PlatformAddressSigner', () => { }); describe('addKey', () => { - it('should add key from PrivateKey created from WIF', () => { + it('should add key and return derived address from PrivateKey created from WIF', () => { const signer = new wasm.PlatformAddressSigner(); - const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const addr = wasm.PlatformAddress.fromBytes(addressBytes); const privateKey = wasm.PrivateKey.fromWIF(testPrivateKeyWif); - signer.addKey(addr, privateKey); + const derivedAddr = signer.addKey(privateKey); expect(signer.keyCount).to.equal(1); + expect(derivedAddr).to.exist; + expect(derivedAddr.addressType).to.equal('P2PKH'); }); - it('should add key from PrivateKey created from hex', () => { + it('should add key and return derived address from PrivateKey created from hex', () => { const signer = new wasm.PlatformAddressSigner(); - const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const addr = wasm.PlatformAddress.fromBytes(addressBytes); const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); - signer.addKey(addr, privateKey); + const derivedAddr = signer.addKey(privateKey); expect(signer.keyCount).to.equal(1); + expect(derivedAddr).to.exist; + expect(derivedAddr.addressType).to.equal('P2PKH'); }); - it('should add key from PrivateKey created from bytes', () => { + it('should add key and return derived address from PrivateKey created from bytes', () => { const signer = new wasm.PlatformAddressSigner(); - const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const addr = wasm.PlatformAddress.fromBytes(addressBytes); const keyBytes = new Uint8Array(32).fill(1); const privateKey = wasm.PrivateKey.fromBytes(keyBytes, 'testnet'); - signer.addKey(addr, privateKey); + const derivedAddr = signer.addKey(privateKey); expect(signer.keyCount).to.equal(1); + expect(derivedAddr).to.exist; + expect(derivedAddr.addressType).to.equal('P2PKH'); }); - it('should add multiple keys', () => { + it('should add multiple keys with different derived addresses', () => { const signer = new wasm.PlatformAddressSigner(); - const addr1Bytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const addr1 = wasm.PlatformAddress.fromBytes(addr1Bytes); - - const addr2Bytes = new Uint8Array([0x00, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); - const addr2 = wasm.PlatformAddress.fromBytes(addr2Bytes); - const privateKey1 = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); const privateKey2 = wasm.PrivateKey.fromHex('a9d9d0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfd', 'testnet'); - signer.addKey(addr1, privateKey1); - signer.addKey(addr2, privateKey2); + const addr1 = signer.addKey(privateKey1); + const addr2 = signer.addKey(privateKey2); expect(signer.keyCount).to.equal(2); + // Addresses should be different since keys are different + expect(addr1.toBytes()).to.not.deep.equal(addr2.toBytes()); }); - it('should accept address as bech32m string', () => { + it('should return same address when adding same key twice', () => { const signer = new wasm.PlatformAddressSigner(); - const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const addr = wasm.PlatformAddress.fromBytes(addressBytes); - const bech32m = addr.toBech32m('testnet'); - const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); - signer.addKey(bech32m, privateKey); + const addr1 = signer.addKey(privateKey); expect(signer.keyCount).to.equal(1); - }); - it('should replace key for same address', () => { - const signer = new wasm.PlatformAddressSigner(); - const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const addr = wasm.PlatformAddress.fromBytes(addressBytes); - - const privateKey1 = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); - const privateKey2 = wasm.PrivateKey.fromHex('a9d9d0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfd', 'testnet'); - - signer.addKey(addr, privateKey1); - expect(signer.keyCount).to.equal(1); - - // Add different key for same address - signer.addKey(addr, privateKey2); - expect(signer.keyCount).to.equal(1); // Still 1, replaced + // Add same key again + const addr2 = signer.addKey(privateKey); + expect(signer.keyCount).to.equal(1); // Still 1, same address + expect(addr1.toBytes()).to.deep.equal(addr2.toBytes()); }); }); describe('hasKey', () => { - it('should return true for added address', () => { + it('should return true for derived address', () => { const signer = new wasm.PlatformAddressSigner(); - const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const addr = wasm.PlatformAddress.fromBytes(addressBytes); const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); - signer.addKey(addr, privateKey); - expect(signer.hasKey(addr)).to.be.true; + const derivedAddr = signer.addKey(privateKey); + expect(signer.hasKey(derivedAddr)).to.be.true; }); it('should return false for unknown address', () => { @@ -118,14 +97,12 @@ describe('PlatformAddressSigner', () => { expect(signer.hasKey(addr)).to.be.false; }); - it('should accept address as bech32m string', () => { + it('should accept derived address as bech32m string', () => { const signer = new wasm.PlatformAddressSigner(); - const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const addr = wasm.PlatformAddress.fromBytes(addressBytes); - const bech32m = addr.toBech32m('testnet'); const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); - signer.addKey(addr, privateKey); + const derivedAddr = signer.addKey(privateKey); + const bech32m = derivedAddr.toBech32m('testnet'); expect(signer.hasKey(bech32m)).to.be.true; }); }); @@ -140,11 +117,9 @@ describe('PlatformAddressSigner', () => { it('should return array of key entries', () => { const signer = new wasm.PlatformAddressSigner(); - const addressBytes = new Uint8Array([0x00, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); - const addr = wasm.PlatformAddress.fromBytes(addressBytes); const privateKey = wasm.PrivateKey.fromHex(testPrivateKeyHex, 'testnet'); - signer.addKey(addr, privateKey); + signer.addKey(privateKey); const keys = signer.getPrivateKeysBytes(); expect(keys).to.be.an('array'); diff --git a/packages/wasm-sdk/src/lib.rs b/packages/wasm-sdk/src/lib.rs index a8e0b9d6c74..d9a9bd80b88 100644 --- a/packages/wasm-sdk/src/lib.rs +++ b/packages/wasm-sdk/src/lib.rs @@ -15,7 +15,9 @@ pub mod wallet; // Re-export commonly used items pub use dpns::*; pub use error::{WasmSdkError, WasmSdkErrorKind}; -pub use queries::{PlatformAddressInfoWasm, ProofInfoWasm, ProofMetadataResponseWasm, ResponseMetadataWasm}; +pub use queries::{ + PlatformAddressInfoWasm, ProofInfoWasm, ProofMetadataResponseWasm, ResponseMetadataWasm, +}; pub use state_transitions::identity as state_transition_identity; pub use wallet::*; pub use wasm_dpp2::*; diff --git a/packages/wasm-sdk/src/settings.rs b/packages/wasm-sdk/src/settings.rs index 4f438c35aaa..9a7b916a937 100644 --- a/packages/wasm-sdk/src/settings.rs +++ b/packages/wasm-sdk/src/settings.rs @@ -247,8 +247,7 @@ pub fn parse_put_settings( return Ok(None); } - let input: PutSettingsInput = serde_wasm_bindgen::from_value(js_value).map_err(|e| { - WasmSdkError::serialization(format!("Failed to parse put settings: {}", e)) - })?; + let input: PutSettingsInput = serde_wasm_bindgen::from_value(js_value) + .map_err(|e| WasmSdkError::serialization(format!("Failed to parse put settings: {}", e)))?; Ok(Some(input.into())) }