diff --git a/book/src/evo-sdk/networks-and-environments.md b/book/src/evo-sdk/networks-and-environments.md index 964aa714f06..a900b42e5c0 100644 --- a/book/src/evo-sdk/networks-and-environments.md +++ b/book/src/evo-sdk/networks-and-environments.md @@ -1,6 +1,6 @@ # Networks and Environments -The Evo SDK supports three built-in network configurations plus custom +The Evo SDK supports four built-in network configurations plus custom addresses for private or development networks. ## Built-in networks @@ -9,12 +9,48 @@ addresses for private or development networks. |---------|---------|---------------|----------| | **Testnet** | `EvoSDK.testnetTrusted()` | Automatic via seed nodes | Development and testing | | **Mainnet** | `EvoSDK.mainnetTrusted()` | Automatic via seed nodes | Production applications | +| **Devnet** | `EvoSDK.devnetTrusted(name)` | Automatic via quorums server | Long-lived shared devnets (e.g. `'paloma'`) | | **Local** | `EvoSDK.localTrusted()` | `127.0.0.1:1443` | Docker-based local development | For each network, the SDK discovers DAPI endpoints from seed nodes and rotates between them automatically. Failed nodes are temporarily banned so the SDK retries against healthy nodes. +## Devnets + +Devnets are long-lived shared development networks identified by a short name +(e.g. `'paloma'`). The trusted context derives the quorum base URL from the +name as `https://quorums..networks.dash.org`: + +```typescript +const sdk = EvoSDK.devnetTrusted('paloma'); +await sdk.connect(); +``` + +If the public quorums DNS for a devnet isn't deployed yet, override the URL: + +```typescript +const sdk = EvoSDK.devnetTrusted('paloma', { + quorumUrl: 'https://quorums.staging.example/', +}); +await sdk.connect(); +``` + +For a devnet without any trusted context (no proof verification), supply +explicit DAPI addresses: + +```typescript +const sdk = EvoSDK.devnet('paloma', { + addresses: ['https://10.0.0.5:1443'], +}); +await sdk.connect(); +``` + +Behind the scenes these factories call `WasmTrustedContext.prefetchDevnet(name)` +or `prefetchDevnetWithUrl(url)`; the same shape is available on +`prefetchMainnetWithUrl` / `prefetchTestnetWithUrl` for staging endpoints +(production networks must use `https://`). + ## Local development with Docker When running a local Platform network via diff --git a/packages/js-evo-sdk/README.md b/packages/js-evo-sdk/README.md index 324a6c05413..1eac5c8ac31 100644 --- a/packages/js-evo-sdk/README.md +++ b/packages/js-evo-sdk/README.md @@ -45,14 +45,29 @@ console.log('Current epoch:', epoch.index); | Option | Type | Default | Notes | |--------|------|---------|-------| -| `network` | `'testnet' \| 'mainnet' \| 'local'` | `'testnet'` | Target network. | +| `network` | `'testnet' \| 'mainnet' \| 'local' \| 'devnet'` | `'testnet'` | Target network. | | `trusted` | `boolean` | `false` | When `true`, pre-fetches quorum keys for proof verification. Required for default query methods. | +| `addresses` | `string[]` | — | Seed masternode addresses. Required for non-trusted devnet; optional for other networks (replaces built-in defaults). | +| `devnetName` | `string` | — | Short name of the devnet (e.g. `'paloma'`). Required when `network: 'devnet'` and `trusted: true` (used to derive the quorum URL); ignored otherwise — only valid when `network === 'devnet'`. | +| `quorumUrl` | `string` | — | Override the trusted-context quorum base URL. Only meaningful when `trusted: true`. Useful for staging endpoints or devnets where the public DNS isn't deployed yet. | | `proofs` | `boolean` | `true` | Setting to `false` disables proof requests where supported, but unproved mode is limited — several query paths (e.g. document fetches) force proofs regardless, and some query builders reject the unproved path. Mainly intended for mock/offline replay. | | `version` | `number` | latest | Platform protocol version. | | `logs` | `string` | — | Tracing/log filter for the underlying Wasm SDK. Accepts simple levels (`'info'`, `'debug'`, …) or a full `EnvFilter` string. | | `settings` | `{ connectTimeoutMs?, timeoutMs?, retries?, banFailedAddress? }` | — | DAPI client transport settings. | -Preset factories are available as convenience: `EvoSDK.testnet()`, `EvoSDK.mainnet()`, `EvoSDK.testnetTrusted()`, `EvoSDK.mainnetTrusted()`, `EvoSDK.local()`, and `EvoSDK.localTrusted()` (the last two target a dashmate local node). +Preset factories are available as convenience: `EvoSDK.testnet()`, `EvoSDK.mainnet()`, `EvoSDK.testnetTrusted()`, `EvoSDK.mainnetTrusted()`, `EvoSDK.local()`, `EvoSDK.localTrusted()` (the last two target a dashmate local node), and the devnet factories `EvoSDK.devnet(name, options)` / `EvoSDK.devnetTrusted(name, options)`. + +```typescript +// Trusted devnet — quorum URL auto-derived from the devnet name. +const sdk = EvoSDK.devnetTrusted('paloma'); +await sdk.connect(); + +// Non-trusted devnet — explicit addresses required (no quorum context). +const local = EvoSDK.devnet('paloma', { + addresses: ['https://10.0.0.5:1443'], +}); +await local.connect(); +``` Two static helpers are also exported: diff --git a/packages/js-evo-sdk/src/sdk.ts b/packages/js-evo-sdk/src/sdk.ts index 8bfeb46574c..9cf8aba176f 100644 --- a/packages/js-evo-sdk/src/sdk.ts +++ b/packages/js-evo-sdk/src/sdk.ts @@ -30,16 +30,31 @@ export interface ConnectionOptions { } export interface EvoSDKOptions extends ConnectionOptions { - network?: 'testnet' | 'mainnet' | 'local'; + network?: 'testnet' | 'mainnet' | 'local' | 'devnet'; trusted?: boolean; - // Custom masternode addresses. When provided, network and trusted options are ignored. + // Custom masternode addresses to seed the SDK with. `network` still + // controls which Network enum the underlying builder uses (and, for + // trusted mode, which quorums endpoint is prefetched); the addresses + // here replace the network's built-in defaults at seed time. // Example: ['https://127.0.0.1:1443', 'https://192.168.1.100:1443'] addresses?: string[]; + // Short name of the devnet (e.g. 'paloma'). Required when network === 'devnet' + // AND trusted === true (used to derive the quorum URL). When trusted === false, + // explicit `addresses` are mandatory and `devnetName` alone is not sufficient + // — no masternode addresses can be discovered without a trusted context. + devnetName?: string; + // Optional override for the trusted-context quorum base URL. When omitted, + // the URL is the network's default (e.g. + // `https://quorums..networks.dash.org` for devnet, + // `https://quorums.testnet.networks.dash.org` for testnet, etc.). + // Only consulted when trusted === true. Useful for pointing at a staging, + // self-hosted, or not-yet-deployed quorums endpoint. + quorumUrl?: string; } export class EvoSDK { private wasmSdk?: wasm.WasmSdk; - private options: Required> & ConnectionOptions & { addresses?: string[] }; + private options: Required> & ConnectionOptions & { addresses?: string[]; devnetName?: string; quorumUrl?: string }; public addresses!: AddressesFacade; public documents!: DocumentsFacade; @@ -56,8 +71,27 @@ export class EvoSDK { public shielded!: ShieldedFacade; constructor(options: EvoSDKOptions = {}) { // Apply defaults while preserving any future connection options - const { network = 'testnet', trusted = false, addresses, ...connection } = options; - this.options = { network, trusted, addresses, ...connection }; + const { network = 'testnet', trusted = false, addresses, devnetName, quorumUrl, ...connection } = options; + + if (network === 'devnet') { + const hasAddresses = !!(addresses && addresses.length > 0); + if (trusted) { + if (!devnetName && !quorumUrl) { + throw new Error("EvoSDK: trusted devnet requires devnetName (to derive the quorum URL) or an explicit quorumUrl"); + } + } else if (!hasAddresses) { + throw new Error("EvoSDK: non-trusted devnet requires explicit addresses (no addresses can be discovered without a trusted context)"); + } + } else if (devnetName) { + // Surface a likely typo (e.g. network: 'testent' + devnetName: 'paloma') + // — devnetName has no effect outside network === 'devnet'. + throw new Error("EvoSDK: devnetName is only valid when network === 'devnet'"); + } + if (quorumUrl && !trusted) { + throw new Error("EvoSDK: quorumUrl is only meaningful when trusted === true"); + } + + this.options = { network, trusted, addresses, devnetName, quorumUrl, ...connection }; this.addresses = new AddressesFacade(this); this.documents = new DocumentsFacade(this); @@ -96,17 +130,31 @@ export class EvoSDK { } await initWasm(); - const { network, trusted, version, proofs, settings, logs, addresses } = this.options; + const { network, trusted, version, proofs, settings, logs, addresses, devnetName, quorumUrl } = this.options; // Prefetch trusted context only when trusted mode is requested let context: wasm.WasmTrustedContext | undefined; if (trusted) { if (network === 'mainnet') { - context = await wasm.WasmTrustedContext.prefetchMainnet(); + context = quorumUrl + ? await wasm.WasmTrustedContext.prefetchMainnetWithUrl(quorumUrl) + : await wasm.WasmTrustedContext.prefetchMainnet(); } else if (network === 'testnet') { - context = await wasm.WasmTrustedContext.prefetchTestnet(); + context = quorumUrl + ? await wasm.WasmTrustedContext.prefetchTestnetWithUrl(quorumUrl) + : await wasm.WasmTrustedContext.prefetchTestnet(); } else if (network === 'local') { - context = await wasm.WasmTrustedContext.prefetchLocal(); + context = quorumUrl + ? await wasm.WasmTrustedContext.prefetchLocalWithUrl(quorumUrl) + : await wasm.WasmTrustedContext.prefetchLocal(); + } else if (network === 'devnet') { + if (quorumUrl) { + context = await wasm.WasmTrustedContext.prefetchDevnetWithUrl(quorumUrl); + } else if (devnetName) { + context = await wasm.WasmTrustedContext.prefetchDevnet(devnetName); + } else { + throw new Error("EvoSDK: trusted devnet requires devnetName or quorumUrl"); + } } else { throw new Error(`Unknown network: ${network}`); } @@ -122,6 +170,8 @@ export class EvoSDK { builder = wasm.WasmSdkBuilder.testnet(); } else if (network === 'local') { builder = wasm.WasmSdkBuilder.local(); + } else if (network === 'devnet') { + builder = wasm.WasmSdkBuilder.newDevnet(); } else { throw new Error(`Unknown network: ${network}`); } @@ -181,11 +231,43 @@ export class EvoSDK { static local(options: ConnectionOptions = {}): EvoSDK { return new EvoSDK({ network: 'local', ...options }); } static localTrusted(options: ConnectionOptions = {}): EvoSDK { return new EvoSDK({ network: 'local', trusted: true, ...options }); } + /** + * Create an EvoSDK instance configured for a devnet, without trusted-context + * proof verification. Requires explicit `addresses` in `options` — + * `devnetName` alone is not sufficient in non-trusted mode, since no + * masternode addresses can be discovered without a trusted context. + * Proof-bearing queries will fail; for proof verification on devnet, use + * `EvoSDK.devnetTrusted` instead. + */ + static devnet(devnetName: string, options: ConnectionOptions & { addresses?: string[] } = {}): EvoSDK { + return new EvoSDK({ network: 'devnet', devnetName, ...options }); + } + + /** + * Create an EvoSDK instance configured for a devnet with a trusted context. + * + * The trusted context is prefetched from + * `https://quorums..networks.dash.org` by default. Pass + * `quorumUrl` to override (useful when the public DNS is not yet deployed). + * + * @example + * ```typescript + * const sdk = EvoSDK.devnetTrusted('paloma'); + * await sdk.connect(); + * ``` + */ + static devnetTrusted( + devnetName: string, + options: ConnectionOptions & { quorumUrl?: string } = {}, + ): EvoSDK { + return new EvoSDK({ network: 'devnet', devnetName, trusted: true, ...options }); + } + /** * Create an EvoSDK instance configured with specific masternode addresses. * * @param addresses - Array of HTTPS URLs to masternodes (e.g., ['https://127.0.0.1:1443']) - * @param network - Network identifier: 'mainnet', 'testnet' (default: 'testnet') + * @param network - Network identifier: 'mainnet', 'testnet', 'devnet', or 'local' (default: 'testnet') * @param options - Additional connection options * @returns A configured EvoSDK instance (not yet connected - call .connect() to establish connection) * @@ -195,7 +277,7 @@ export class EvoSDK { * await sdk.connect(); * ``` */ - static withAddresses(addresses: string[], network: 'mainnet' | 'testnet' | 'local' = 'testnet', options: ConnectionOptions = {}): EvoSDK { + static withAddresses(addresses: string[], network: 'mainnet' | 'testnet' | 'local' | 'devnet' = 'testnet', options: ConnectionOptions & { devnetName?: string } = {}): EvoSDK { return new EvoSDK({ addresses, network, ...options }); } } diff --git a/packages/js-evo-sdk/tests/unit/sdk.spec.ts b/packages/js-evo-sdk/tests/unit/sdk.spec.ts index 546b13343dd..b9ea275fc0f 100644 --- a/packages/js-evo-sdk/tests/unit/sdk.spec.ts +++ b/packages/js-evo-sdk/tests/unit/sdk.spec.ts @@ -220,4 +220,85 @@ describe('EvoSDK', () => { expect(sdk.isConnected).to.equal(false); }); }); + + describe('devnet()', () => { + it('should create non-trusted devnet instance with addresses + devnetName', () => { + const sdk = EvoSDK.devnet('paloma', { addresses: [TEST_ADDRESS_1] }); + expect(sdk).to.be.instanceof(EvoSDK); + expect(sdk.options.network).to.equal('devnet'); + expect(sdk.options.devnetName).to.equal('paloma'); + expect(sdk.options.addresses).to.deep.equal([TEST_ADDRESS_1]); + expect(sdk.options.trusted).to.be.false(); + expect(sdk.isConnected).to.equal(false); + }); + + it('should accept devnet with only addresses (no devnetName)', () => { + const sdk = new EvoSDK({ network: 'devnet', addresses: [TEST_ADDRESS_1] }); + expect(sdk.options.network).to.equal('devnet'); + expect(sdk.options.addresses).to.deep.equal([TEST_ADDRESS_1]); + expect(sdk.options.devnetName).to.be.undefined(); + }); + + it('should reject non-trusted devnet without addresses', () => { + // devnetName alone is not enough — without trusted context, no addresses can be discovered. + expect(() => EvoSDK.devnet('paloma')).to.throw(/addresses/); + }); + + it('should reject network=devnet without devnetName and without addresses', () => { + expect(() => new EvoSDK({ network: 'devnet' })).to.throw(/devnet/); + }); + }); + + describe('devnetTrusted()', () => { + it('should create trusted devnet instance', () => { + const sdk = EvoSDK.devnetTrusted('paloma'); + expect(sdk).to.be.instanceof(EvoSDK); + expect(sdk.options.network).to.equal('devnet'); + expect(sdk.options.devnetName).to.equal('paloma'); + expect(sdk.options.trusted).to.be.true(); + expect(sdk.isConnected).to.equal(false); + }); + + it('should preserve quorumUrl override', () => { + const sdk = EvoSDK.devnetTrusted('paloma', { quorumUrl: 'https://custom.example' }); + expect(sdk.options.quorumUrl).to.equal('https://custom.example'); + expect(sdk.options.trusted).to.be.true(); + }); + + it('should reject trusted devnet without devnetName', () => { + expect(() => new EvoSDK({ network: 'devnet', trusted: true })).to.throw(/devnetName/); + }); + + it('should reject quorumUrl when trusted is false', () => { + expect(() => new EvoSDK({ + network: 'devnet', + devnetName: 'paloma', + addresses: [TEST_ADDRESS_1], + quorumUrl: 'https://custom', + })).to.throw(/quorumUrl/); + }); + + it('should accept quorumUrl on trusted testnet (override)', () => { + const sdk = new EvoSDK({ network: 'testnet', trusted: true, quorumUrl: 'https://x' }); + expect(sdk.options.quorumUrl).to.equal('https://x'); + expect(sdk.options.trusted).to.be.true(); + }); + + it('should accept quorumUrl on trusted mainnet (override)', () => { + const sdk = new EvoSDK({ network: 'mainnet', trusted: true, quorumUrl: 'https://x' }); + expect(sdk.options.quorumUrl).to.equal('https://x'); + expect(sdk.options.network).to.equal('mainnet'); + }); + + it('should reject devnetName on non-devnet networks (typo guard)', () => { + expect(() => new EvoSDK({ network: 'testnet', devnetName: 'paloma' })) + .to.throw(/devnetName/); + }); + + it('should accept trusted devnet with only quorumUrl (no devnetName)', () => { + const sdk = new EvoSDK({ network: 'devnet', trusted: true, quorumUrl: 'https://x' }); + expect(sdk.options.quorumUrl).to.equal('https://x'); + expect(sdk.options.devnetName).to.be.undefined(); + }); + }); }); diff --git a/packages/rs-sdk-trusted-context-provider/README.md b/packages/rs-sdk-trusted-context-provider/README.md index b1c5dadcd13..78aecf3c068 100644 --- a/packages/rs-sdk-trusted-context-provider/README.md +++ b/packages/rs-sdk-trusted-context-provider/README.md @@ -15,7 +15,7 @@ This crate provides a trusted HTTP-based context provider for the Dash SDK that - **Mainnet**: Uses `https://quorums.mainnet.networks.dash.org/` - **Testnet**: Uses `https://quorums.testnet.networks.dash.org/` -- **Devnet**: Uses `https://quorums.devnet..networks.dash.org/` +- **Devnet**: Uses `https://quorums..networks.dash.org/` ## Usage diff --git a/packages/rs-sdk-trusted-context-provider/src/lib.rs b/packages/rs-sdk-trusted-context-provider/src/lib.rs index 3fc2c45c337..b1ec3f9afd0 100644 --- a/packages/rs-sdk-trusted-context-provider/src/lib.rs +++ b/packages/rs-sdk-trusted-context-provider/src/lib.rs @@ -6,7 +6,7 @@ //! ## Networks Supported //! - **Mainnet**: Uses `https://quorums.mainnet.networks.dash.org/` //! - **Testnet**: Uses `https://quorums.testnet.networks.dash.org/` -//! - **Devnet**: Uses `https://quorums.devnet..networks.dash.org/` +//! - **Devnet**: Uses `https://quorums..networks.dash.org/` pub mod error; pub mod provider; @@ -44,7 +44,21 @@ pub fn get_quorum_base_url( "Devnet name cannot start or end with a hyphen".to_string(), )); } - Ok(format!("https://quorums.devnet.{}.networks.dash.org", name)) + // Reserved names that would alias the production / non-devnet quorum + // hostnames (e.g. "mainnet" => https://quorums.mainnet.networks.dash.org). + // The URL pattern shares its namespace with mainnet/testnet/local, so + // the validator is the only line of defense against a cross-network + // trust-root mixup labeled `Network::Devnet`. + if matches!( + name.to_ascii_lowercase().as_str(), + "mainnet" | "testnet" | "devnet" | "local" | "regtest" + ) { + return Err(TrustedContextProviderError::InvalidDevnetName(format!( + "Devnet name '{}' is reserved (would alias a non-devnet quorum hostname)", + name + ))); + } + Ok(format!("https://quorums.{}.networks.dash.org", name)) } else { Err(TrustedContextProviderError::InvalidDevnetName( "Devnet name must be provided for devnet network".to_string(), diff --git a/packages/rs-sdk-trusted-context-provider/src/provider.rs b/packages/rs-sdk-trusted-context-provider/src/provider.rs index fa1dac57665..6769890dde6 100644 --- a/packages/rs-sdk-trusted-context-provider/src/provider.rs +++ b/packages/rs-sdk-trusted-context-provider/src/provider.rs @@ -139,6 +139,25 @@ impl TrustedHttpContextProvider { base_url: String, cache_size: NonZeroUsize, ) -> Result { + // The base URL is the SDK's root of trust for proof verification + // (quorum public keys) and network connectivity (discovered masternode + // addresses). For production networks (mainnet/testnet) there is no + // legitimate plaintext workflow — HTTPS is deployed — so refuse to + // hand a non-TLS quorum URL to the SDK. Devnet/Regtest keep the + // plaintext escape hatch (early-stage devnets without a cert yet, + // local sidecars on loopback). + if matches!(network, Network::Mainnet | Network::Testnet) { + let parsed = Url::parse(&base_url).map_err(|e| { + TrustedContextProviderError::NetworkError(format!("Invalid URL: {}", e)) + })?; + if !parsed.scheme().eq_ignore_ascii_case("https") { + return Err(TrustedContextProviderError::NetworkError(format!( + "Custom quorum URL for {:?} must use https://; got '{}'", + network, base_url + ))); + } + } + // Verify the domain resolves before proceeding (skip on WASM and iOS) #[cfg(all(not(target_arch = "wasm32"), not(target_os = "ios")))] Self::verify_domain_resolves(&base_url)?; @@ -785,7 +804,7 @@ mod tests { assert_eq!( get_quorum_base_url(Network::Devnet, Some("example")).unwrap(), - "https://quorums.devnet.example.networks.dash.org" + "https://quorums.example.networks.dash.org" ); } @@ -833,6 +852,115 @@ mod tests { assert!(get_quorum_base_url(Network::Devnet, Some("TEST123")).is_ok()); } + #[test] + fn test_new_with_url_rejects_plaintext_for_production_networks() { + // Mainnet and testnet have HTTPS deployed; reject plaintext URLs to + // prevent silently weakening the trust root via a typo or misconfig. + // Mixed-case scheme must also be rejected (case-insensitive match). + for url in ["http://example.com", "HTTP://example.com"] { + for network in [Network::Mainnet, Network::Testnet] { + let result = TrustedHttpContextProvider::new_with_url( + network, + url.to_string(), + NonZeroUsize::new(10).unwrap(), + ); + match result { + Ok(_) => panic!("expected {} to be rejected for {:?}", url, network), + Err(e) => { + let msg = e.to_string(); + assert!( + msg.contains("must use https://"), + "expected HTTPS-gate error for {:?} + {}, got: {}", + network, + url, + msg + ); + } + } + } + } + } + + #[test] + fn test_new_with_url_does_not_reject_https_on_production_networks() { + // Positive path: an HTTPS URL must not trip the new HTTPS gate. + // The constructor may still fail downstream (DNS, on non-wasm/non-iOS + // builds), but if so the error must NOT be the HTTPS-gate variant. + // Mixed-case `HTTPS://` must also be accepted by the gate. + for url in [ + "https://example.com", + "HTTPS://example.com", + "https://example.com:8443/sub/path", + ] { + for network in [Network::Mainnet, Network::Testnet] { + let result = TrustedHttpContextProvider::new_with_url( + network, + url.to_string(), + NonZeroUsize::new(10).unwrap(), + ); + if let Err(e) = result { + let msg = e.to_string(); + assert!( + !msg.contains("must use https://"), + "HTTPS gate incorrectly rejected {} for {:?}: {}", + url, + network, + msg + ); + } + } + } + } + + #[test] + fn test_new_with_url_does_not_reject_plaintext_on_devnet_or_regtest() { + // Positive path: plaintext stays acceptable for devnet/regtest (early + // devnets without certs, loopback sidecars). The HTTPS gate must not + // contribute to any failure here. + for network in [Network::Devnet, Network::Regtest] { + let result = TrustedHttpContextProvider::new_with_url( + network, + "http://127.0.0.1:22444".to_string(), + NonZeroUsize::new(10).unwrap(), + ); + if let Err(e) = result { + let msg = e.to_string(); + assert!( + !msg.contains("must use https://"), + "HTTPS gate incorrectly rejected http:// for {:?}: {}", + network, + msg + ); + } + } + } + + #[test] + fn test_reserved_devnet_names_rejected() { + // Names that would alias non-devnet quorum hostnames must be rejected. + for reserved in ["mainnet", "testnet", "devnet", "local", "regtest"] { + assert!( + matches!( + get_quorum_base_url(Network::Devnet, Some(reserved)), + Err(TrustedContextProviderError::InvalidDevnetName(_)) + ), + "expected '{}' to be rejected as a reserved devnet name", + reserved + ); + } + // Case-insensitive: uppercase variants must also be rejected. + for reserved in ["Mainnet", "TESTNET", "DevNet"] { + assert!( + matches!( + get_quorum_base_url(Network::Devnet, Some(reserved)), + Err(TrustedContextProviderError::InvalidDevnetName(_)) + ), + "expected '{}' to be rejected as a reserved devnet name (case-insensitive)", + reserved + ); + } + } + #[test] fn test_known_contracts() { use dpp::version::PlatformVersion; diff --git a/packages/wasm-sdk/src/context_provider.rs b/packages/wasm-sdk/src/context_provider.rs index fbb7fc74a70..fa210ce96d4 100644 --- a/packages/wasm-sdk/src/context_provider.rs +++ b/packages/wasm-sdk/src/context_provider.rs @@ -18,8 +18,9 @@ pub struct WasmContext {} /// /// Holds pre-fetched quorum keys and discovered masternode addresses for /// proof verification and network connectivity. Create one via the async -/// `prefetchMainnet()`, `prefetchTestnet()`, or `prefetchLocal()` factory -/// methods, then pass it to a builder via `withTrustedContext()`. +/// `prefetchMainnet()`, `prefetchTestnet()`, `prefetchDevnet()`, or +/// `prefetchLocal()` factory methods, then pass it to a builder via +/// `withTrustedContext()`. #[wasm_bindgen] #[derive(Clone)] pub struct WasmTrustedContext { @@ -35,7 +36,7 @@ impl ContextProvider for WasmContext { _core_chain_locked_height: u32, ) -> Result<[u8; 48], ContextProviderError> { Err(ContextProviderError::Generic( - "Non-trusted mode is not supported in WASM. Please use the trusted SDK builders (new_mainnet_trusted or new_testnet_trusted) instead.".to_string() + "Non-trusted mode is not supported in WASM. Please construct a WasmTrustedContext via prefetchMainnet/prefetchTestnet/prefetchDevnet/prefetchLocal and attach it with WasmSdkBuilder.withTrustedContext().".to_string() )) } @@ -114,27 +115,22 @@ impl WasmTrustedContext { /// `WasmSdkBuilder.mainnet().withTrustedContext(context)`. #[wasm_bindgen(js_name = "prefetchMainnet")] pub async fn prefetch_mainnet() -> Result { - let inner = rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new( + Self::prefetch_for(dash_sdk::dpp::dashcore::Network::Mainnet, None, None).await + } + + /// Pre-fetch quorum keys and masternode addresses for mainnet using a + /// fully-specified quorum base URL (useful for testing against a staging + /// or self-hosted quorums endpoint). + #[wasm_bindgen(js_name = "prefetchMainnetWithUrl")] + pub async fn prefetch_mainnet_with_url( + base_url: String, + ) -> Result { + Self::prefetch_for( dash_sdk::dpp::dashcore::Network::Mainnet, None, - std::num::NonZeroUsize::new(100).unwrap(), + Some(base_url), ) - .map_err(|e| WasmSdkError::generic(format!("Failed to create context provider: {}", e)))? - .with_refetch_if_not_found(false); - - let inner = Arc::new(inner); - - inner - .update_quorum_caches() - .await - .map_err(|e| WasmSdkError::generic(format!("Failed to prefetch quorums: {}", e)))?; - - let discovered_addresses = Self::fetch_addresses_from(&inner).await?; - - Ok(WasmTrustedContext { - inner, - discovered_addresses, - }) + .await } /// Pre-fetch quorum keys and masternode addresses for testnet. @@ -143,27 +139,58 @@ impl WasmTrustedContext { /// `WasmSdkBuilder.testnet().withTrustedContext(context)`. #[wasm_bindgen(js_name = "prefetchTestnet")] pub async fn prefetch_testnet() -> Result { - let inner = rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new( + Self::prefetch_for(dash_sdk::dpp::dashcore::Network::Testnet, None, None).await + } + + /// Pre-fetch quorum keys and masternode addresses for testnet using a + /// fully-specified quorum base URL (useful for testing against a staging + /// or self-hosted quorums endpoint). + #[wasm_bindgen(js_name = "prefetchTestnetWithUrl")] + pub async fn prefetch_testnet_with_url( + base_url: String, + ) -> Result { + Self::prefetch_for( dash_sdk::dpp::dashcore::Network::Testnet, None, - std::num::NonZeroUsize::new(100).unwrap(), + Some(base_url), ) - .map_err(|e| WasmSdkError::generic(format!("Failed to create context provider: {}", e)))? - .with_refetch_if_not_found(false); - - let inner = Arc::new(inner); - - inner - .update_quorum_caches() - .await - .map_err(|e| WasmSdkError::generic(format!("Failed to prefetch quorums: {}", e)))?; + .await + } - let discovered_addresses = Self::fetch_addresses_from(&inner).await?; + /// Pre-fetch quorum keys and masternode addresses for a devnet. + /// + /// `devnet_name` is the short name of the devnet (e.g. `"paloma"`). The + /// quorum base URL is derived as `https://quorums..networks.dash.org`. + /// + /// Returns a ready-to-use `WasmTrustedContext` that can be passed to + /// `WasmSdkBuilder.newDevnet().withTrustedContext(context)`. + #[wasm_bindgen(js_name = "prefetchDevnet")] + pub async fn prefetch_devnet(devnet_name: String) -> Result { + Self::prefetch_for( + dash_sdk::dpp::dashcore::Network::Devnet, + Some(devnet_name), + None, + ) + .await + } - Ok(WasmTrustedContext { - inner, - discovered_addresses, - }) + /// Pre-fetch quorum keys and masternode addresses for a devnet using a + /// fully-specified quorum base URL. + /// + /// Use this when the default + /// `https://quorums..networks.dash.org` URL produced by + /// `prefetchDevnet` is not yet deployed for a devnet, or when pointing + /// at a non-standard quorums endpoint. + #[wasm_bindgen(js_name = "prefetchDevnetWithUrl")] + pub async fn prefetch_devnet_with_url( + base_url: String, + ) -> Result { + Self::prefetch_for( + dash_sdk::dpp::dashcore::Network::Devnet, + None, + Some(base_url), + ) + .await } /// Pre-fetch quorum keys and masternode addresses for a local network. @@ -174,20 +201,44 @@ impl WasmTrustedContext { /// `WasmSdkBuilder.local().withTrustedContext(context)`. #[wasm_bindgen(js_name = "prefetchLocal")] pub async fn prefetch_local() -> Result { - Self::prefetch_local_with_url("http://127.0.0.1:22444").await + Self::prefetch_local_with_url("http://127.0.0.1:22444".to_string()).await } /// Pre-fetch quorum keys and masternode addresses for a local network /// using a custom quorum sidecar URL. #[wasm_bindgen(js_name = "prefetchLocalWithUrl")] pub async fn prefetch_local_with_url( - base_url: &str, + base_url: String, ) -> Result { - let inner = rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new_with_url( + Self::prefetch_for( dash_sdk::dpp::dashcore::Network::Regtest, - base_url.to_string(), - std::num::NonZeroUsize::new(100).unwrap(), + None, + Some(base_url), ) + .await + } +} + +impl WasmTrustedContext { + /// Shared constructor used by every `prefetch*` factory. When `base_url` + /// is `Some`, it overrides the default URL derived from `network` + + /// `devnet_name` (the validator inside `new_with_url` still runs). + async fn prefetch_for( + network: dash_sdk::dpp::dashcore::Network, + devnet_name: Option, + base_url: Option, + ) -> Result { + let cache_size = std::num::NonZeroUsize::new(100).unwrap(); + let inner = match base_url { + Some(url) => rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new_with_url( + network, url, cache_size, + ), + None => rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new( + network, + devnet_name, + cache_size, + ), + } .map_err(|e| WasmSdkError::generic(format!("Failed to create context provider: {}", e)))? .with_refetch_if_not_found(false); @@ -205,9 +256,7 @@ impl WasmTrustedContext { discovered_addresses, }) } -} -impl WasmTrustedContext { /// Fetch masternode addresses from the trusted provider and convert to `Vec
`. async fn fetch_addresses_from( inner: &rs_sdk_trusted_context_provider::TrustedHttpContextProvider, diff --git a/packages/wasm-sdk/src/sdk.rs b/packages/wasm-sdk/src/sdk.rs index d88a83844d8..57801f38475 100644 --- a/packages/wasm-sdk/src/sdk.rs +++ b/packages/wasm-sdk/src/sdk.rs @@ -175,7 +175,7 @@ impl WasmSdkBuilder { /// /// # Arguments /// * `addresses` - Array of HTTPS URLs (e.g., ["https://127.0.0.1:1443"]) - /// * `network` - Network identifier: "mainnet", "testnet" or "local" + /// * `network` - Network identifier: "mainnet", "testnet", "devnet", or "local" #[wasm_bindgen(js_name = "withAddresses")] pub fn new_with_addresses( addresses: Vec, @@ -205,10 +205,11 @@ impl WasmSdkBuilder { let network = match network.to_lowercase().as_str() { "mainnet" => Network::Mainnet, "testnet" => Network::Testnet, + "devnet" => Network::Devnet, "local" => Network::Regtest, _ => { return Err(WasmSdkError::invalid_argument(format!( - "Invalid network '{}'. Expected: mainnet, testnet or local", + "Invalid network '{}'. Expected: mainnet, testnet, devnet, or local", network ))); } @@ -245,6 +246,25 @@ impl WasmSdkBuilder { } } + /// Create a new SdkBuilder preconfigured for a devnet. + /// + /// Devnets have no built-in default address list. The returned builder + /// is expected to be paired with either explicit addresses (via the + /// `withAddresses` variant) or a `WasmTrustedContext` from + /// `WasmTrustedContext.prefetchDevnet(name)`, whose discovered addresses + /// will be substituted via `withTrustedContext`. + #[wasm_bindgen(js_name = "newDevnet")] + pub fn new_devnet() -> Self { + let sdk_builder = SdkBuilder::new(dash_sdk::sdk::AddressList::default()) + .with_network(dash_sdk::dpp::dashcore::Network::Devnet) + .with_context_provider(WasmContext {}); + + Self { + inner: sdk_builder, + trusted_context: None, + } + } + /// Create a new SdkBuilder preconfigured for a local network using default dashmate gateway. #[wasm_bindgen(js_name = "local")] pub fn new_local() -> Self { diff --git a/packages/wasm-sdk/tests/unit/builder-with-addresses.spec.ts b/packages/wasm-sdk/tests/unit/builder-with-addresses.spec.ts index 92f27be78ab..a60d2a42f1b 100644 --- a/packages/wasm-sdk/tests/unit/builder-with-addresses.spec.ts +++ b/packages/wasm-sdk/tests/unit/builder-with-addresses.spec.ts @@ -70,16 +70,12 @@ describe('WasmSdkBuilder', () => { }); describe('network validation', () => { - it('should reject devnet', async () => { - try { - sdk.WasmSdkBuilder.withAddresses( - [TEST_ADDRESS_1], - 'devnet', - ); - expect.fail('Should have thrown error for devnet'); - } catch (error) { - expect(error.message).to.include('mainnet, testnet or local'); - } + it('should accept devnet', () => { + const builder = sdk.WasmSdkBuilder.withAddresses( + [TEST_ADDRESS_1], + 'devnet', + ); + expect(builder).to.be.an.instanceof(sdk.WasmSdkBuilder); }); it('should reject invalid network name', async () => { @@ -90,7 +86,7 @@ describe('WasmSdkBuilder', () => { ); expect.fail('Should have thrown error for invalid network'); } catch (error) { - expect(error.message).to.include('mainnet, testnet or local'); + expect(error.message).to.include('mainnet, testnet, devnet, or local'); } });