Skip to content
41 changes: 36 additions & 5 deletions packages/js-evo-sdk/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,14 @@ export interface ConnectionOptions {
export interface EvoSDKOptions extends ConnectionOptions {
network?: 'testnet' | 'mainnet';
trusted?: boolean;
// Custom masternode addresses. When provided, network and trusted options are ignored.
// Example: ['https://127.0.0.1:1443', 'https://192.168.1.100:1443']
addresses?: string[];
}

export class EvoSDK {
private wasmSdk?: wasm.WasmSdk;
private options: Required<Pick<EvoSDKOptions, 'network' | 'trusted'>> & ConnectionOptions;
private options: Required<Pick<EvoSDKOptions, 'network' | 'trusted'>> & ConnectionOptions & { addresses?: string[] };

public documents!: DocumentsFacade;
public identities!: IdentitiesFacade;
Expand All @@ -47,8 +50,8 @@ export class EvoSDK {
public voting!: VotingFacade;
constructor(options: EvoSDKOptions = {}) {
// Apply defaults while preserving any future connection options
const { network = 'testnet', trusted = false, ...connection } = options;
this.options = { network, trusted, ...connection };
const { network = 'testnet', trusted = false, addresses, ...connection } = options;
this.options = { network, trusted, addresses, ...connection };

this.documents = new DocumentsFacade(this);
this.identities = new IdentitiesFacade(this);
Expand Down Expand Up @@ -80,10 +83,20 @@ export class EvoSDK {
if (this.wasmSdk) return; // idempotent
await initWasm();

const { network, trusted, version, proofs, settings, logs } = this.options;
const { network, trusted, version, proofs, settings, logs, addresses } = this.options;

let builder: wasm.WasmSdkBuilder;
if (network === 'mainnet') {

// If specific addresses are provided, use them instead of network presets
if (addresses && addresses.length > 0) {
// Prefetch trusted quorums for the network before creating builder with addresses
if (network === 'mainnet') {
await wasm.WasmSdk.prefetchTrustedQuorumsMainnet();
} else if (network === 'testnet') {
await wasm.WasmSdk.prefetchTrustedQuorumsTestnet();
}
builder = wasm.WasmSdkBuilder.withAddresses(addresses, network);
} else if (network === 'mainnet') {
await wasm.WasmSdk.prefetchTrustedQuorumsMainnet();

builder = trusted ? wasm.WasmSdkBuilder.mainnetTrusted() : wasm.WasmSdkBuilder.mainnet();
Expand Down Expand Up @@ -131,6 +144,24 @@ export class EvoSDK {
static mainnet(options: ConnectionOptions = {}): EvoSDK { return new EvoSDK({ network: 'mainnet', ...options }); }
static testnetTrusted(options: ConnectionOptions = {}): EvoSDK { return new EvoSDK({ network: 'testnet', trusted: true, ...options }); }
static mainnetTrusted(options: ConnectionOptions = {}): EvoSDK { return new EvoSDK({ network: 'mainnet', 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 options - Additional connection options
* @returns A configured EvoSDK instance (not yet connected - call .connect() to establish connection)
*
* @example
* ```typescript
* const sdk = EvoSDK.withAddresses(['https://52.12.176.90:1443'], 'testnet');
* await sdk.connect();
* ```
*/
static withAddresses(addresses: string[], network: 'mainnet' | 'testnet' = 'testnet', options: ConnectionOptions = {}): EvoSDK {
return new EvoSDK({ addresses, network, ...options });
}
}

export { DocumentsFacade } from './documents/facade.js';
Expand Down
197 changes: 197 additions & 0 deletions packages/js-evo-sdk/tests/unit/sdk.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { EvoSDK } from '../../dist/evo-sdk.module.js';

// Test addresses (RFC 6761 reserved test domain - no network calls in unit tests)
const TEST_ADDRESS_1 = 'https://node-1.test:1443';
const TEST_ADDRESS_2 = 'https://node-2.test:1443';
const TEST_ADDRESS_3 = 'https://node-3.test:1443';
const TEST_ADDRESSES = [TEST_ADDRESS_1, TEST_ADDRESS_2, TEST_ADDRESS_3];

describe('EvoSDK', () => {
it('exposes constructor and factories', () => {
expect(EvoSDK).to.be.a('function');
expect(EvoSDK.testnet).to.be.a('function');
expect(EvoSDK.mainnet).to.be.a('function');
expect(EvoSDK.testnetTrusted).to.be.a('function');
expect(EvoSDK.mainnetTrusted).to.be.a('function');
expect(EvoSDK.withAddresses).to.be.a('function');
});

it('fromWasm() marks instance as connected', () => {
Expand All @@ -15,4 +22,194 @@ describe('EvoSDK', () => {
expect(sdk.isConnected).to.equal(true);
expect(sdk.wasm).to.equal(wasmStub);
});

describe('EvoSDK.withAddresses()', () => {
it('creates SDK instance with specific addresses', () => {
const sdk = EvoSDK.withAddresses([TEST_ADDRESS_1], 'testnet');
expect(sdk).to.be.instanceof(EvoSDK);
expect(sdk.options.network).to.equal('testnet');
expect(sdk.isConnected).to.equal(false);
});

it('defaults to testnet when network not specified', () => {
const sdk = EvoSDK.withAddresses([TEST_ADDRESS_1]);
expect(sdk).to.be.instanceof(EvoSDK);
expect(sdk.options.network).to.equal('testnet');
expect(sdk.isConnected).to.equal(false);
});

it('accepts mainnet network', () => {
const sdk = EvoSDK.withAddresses([TEST_ADDRESS_2], 'mainnet');
expect(sdk).to.be.instanceof(EvoSDK);
expect(sdk.options.network).to.equal('mainnet');
expect(sdk.isConnected).to.equal(false);
});

it('accepts multiple addresses', () => {
const sdk = EvoSDK.withAddresses(TEST_ADDRESSES, 'testnet');
expect(sdk).to.be.instanceof(EvoSDK);
expect(sdk.options.network).to.equal('testnet');
expect(sdk.options.addresses).to.deep.equal(TEST_ADDRESSES);
});

it('accepts additional connection options', () => {
const sdk = EvoSDK.withAddresses(
[TEST_ADDRESS_1],
'testnet',
{
version: 1,
proofs: true,
logs: 'info',
settings: {
connectTimeoutMs: 10000,
timeoutMs: 30000,
retries: 3,
banFailedAddress: false,
},
},
);
expect(sdk).to.be.instanceof(EvoSDK);
expect(sdk.options.network).to.equal('testnet');
expect(sdk.options.trusted).to.be.false();
expect(sdk.options.addresses).to.deep.equal([TEST_ADDRESS_1]);
expect(sdk.options.version).to.equal(1);
expect(sdk.options.proofs).to.be.true();
expect(sdk.options.logs).to.equal('info');
expect(sdk.options.settings).to.exist();
expect(sdk.options.settings.connectTimeoutMs).to.equal(10000);
expect(sdk.options.settings.timeoutMs).to.equal(30000);
expect(sdk.options.settings.retries).to.equal(3);
expect(sdk.options.settings.banFailedAddress).to.be.false();
});
});

describe('constructor with addresses option', () => {
it('accepts addresses in options', () => {
const sdk = new EvoSDK({
addresses: [TEST_ADDRESS_1],
network: 'testnet',
});
expect(sdk).to.be.instanceof(EvoSDK);
expect(sdk.options.network).to.equal('testnet');
expect(sdk.options.trusted).to.be.false();
expect(sdk.isConnected).to.equal(false);
});

it('works with testnet default', () => {
const sdk = new EvoSDK({
addresses: [TEST_ADDRESS_1],
});
expect(sdk).to.be.instanceof(EvoSDK);
expect(sdk.options.network).to.equal('testnet');
expect(sdk.options.trusted).to.be.false();
});

it('works with mainnet', () => {
const sdk = new EvoSDK({
addresses: [TEST_ADDRESS_2],
network: 'mainnet',
});
expect(sdk).to.be.instanceof(EvoSDK);
expect(sdk.options.network).to.equal('mainnet');
expect(sdk.options.trusted).to.be.false();
});

it('combines addresses with other options', () => {
const sdk = new EvoSDK({
addresses: [TEST_ADDRESS_1],
network: 'testnet',
version: 1,
proofs: true,
logs: 'debug',
settings: {
connectTimeoutMs: 5000,
timeoutMs: 15000,
retries: 5,
banFailedAddress: true,
},
});
expect(sdk).to.be.instanceof(EvoSDK);
expect(sdk.options.network).to.equal('testnet');
expect(sdk.options.trusted).to.be.false();
expect(sdk.options.addresses).to.deep.equal([TEST_ADDRESS_1]);
expect(sdk.options.version).to.equal(1);
expect(sdk.options.proofs).to.be.true();
expect(sdk.options.logs).to.equal('debug');
expect(sdk.options.settings).to.exist();
expect(sdk.options.settings.connectTimeoutMs).to.equal(5000);
expect(sdk.options.settings.timeoutMs).to.equal(15000);
expect(sdk.options.settings.retries).to.equal(5);
expect(sdk.options.settings.banFailedAddress).to.be.true();
});

it('prioritizes addresses over network presets when both provided', () => {
// When addresses are provided, they should be used instead of default network addresses
const sdk = new EvoSDK({
addresses: [TEST_ADDRESS_3],
network: 'testnet',
trusted: true,
});
expect(sdk).to.be.instanceof(EvoSDK);
expect(sdk.options.network).to.equal('testnet');
expect(sdk.options.addresses).to.deep.equal([TEST_ADDRESS_3]);
expect(sdk.options.trusted).to.be.true();
});

it('withAddresses() and constructor with addresses produce equivalent SDKs', () => {
const addresses = [TEST_ADDRESS_1];
const options = { version: 1, proofs: true };

const sdk1 = EvoSDK.withAddresses(addresses, 'testnet', options);
const sdk2 = new EvoSDK({ addresses, network: 'testnet', ...options });

expect(sdk1.options.addresses).to.deep.equal(sdk2.options.addresses);
expect(sdk1.options.network).to.equal(sdk2.options.network);
expect(sdk1.options.version).to.equal(sdk2.options.version);
expect(sdk1.options.proofs).to.equal(sdk2.options.proofs);
});
});

describe('factory methods for standard configurations', () => {
it('testnet() creates testnet instance', () => {
const sdk = EvoSDK.testnet();
expect(sdk).to.be.instanceof(EvoSDK);
expect(sdk.options.network).to.equal('testnet');
expect(sdk.options.trusted).to.be.false();
expect(sdk.options.addresses).to.be.undefined();
expect(sdk.isConnected).to.equal(false);
});

it('mainnet() creates mainnet instance', () => {
const sdk = EvoSDK.mainnet();
expect(sdk).to.be.instanceof(EvoSDK);
expect(sdk.options.network).to.equal('mainnet');
expect(sdk.options.trusted).to.be.false();
expect(sdk.isConnected).to.equal(false);
});

it('testnetTrusted() creates trusted testnet instance', () => {
const sdk = EvoSDK.testnetTrusted();
expect(sdk).to.be.instanceof(EvoSDK);
expect(sdk.options.network).to.equal('testnet');
expect(sdk.options.trusted).to.be.true();
expect(sdk.isConnected).to.equal(false);
});

it('mainnetTrusted() creates trusted mainnet instance', () => {
const sdk = EvoSDK.mainnetTrusted();
expect(sdk).to.be.instanceof(EvoSDK);
expect(sdk.options.network).to.equal('mainnet');
expect(sdk.options.trusted).to.be.true();
expect(sdk.isConnected).to.equal(false);
});

it('factory methods accept connection options', () => {
const sdk = EvoSDK.testnet({
version: 1,
proofs: false,
logs: 'warn',
});
expect(sdk).to.be.instanceof(EvoSDK);
});
});
});
84 changes: 84 additions & 0 deletions packages/wasm-sdk/src/sdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,90 @@ impl WasmSdkBuilder {
PlatformVersion::latest().protocol_version
}

/// Create a new SdkBuilder with specific addresses and network.
///
/// # Arguments
/// * `addresses` - Array of HTTPS URLs (e.g., ["https://127.0.0.1:1443"])
/// * `network` - Network identifier: "mainnet" or "testnet"
///
/// # Example
/// ```javascript
/// const builder = WasmSdkBuilder.withAddresses(['https://127.0.0.1:1443'], 'testnet');
/// const sdk = builder.build();
/// ```
#[wasm_bindgen(js_name = "withAddresses")]
pub fn new_with_addresses(
addresses: Vec<String>,
network: String,
) -> Result<Self, WasmSdkError> {
use crate::context_provider::WasmTrustedContext;
use dash_sdk::dpp::dashcore::Network;
use dash_sdk::sdk::Uri;
use rs_dapi_client::Address;

// Parse and validate addresses
if addresses.is_empty() {
return Err(WasmSdkError::invalid_argument(
"Addresses must be a non-empty array",
));
}
let parsed_addresses: Result<Vec<Address>, _> = addresses
.into_iter()
.map(|addr| {
addr.parse::<Uri>()
.map_err(|e| format!("Invalid URI '{}': {}", addr, e))
.and_then(|uri| {
Address::try_from(uri).map_err(|e| format!("Invalid address: {}", e))
})
})
.collect();

let parsed_addresses = parsed_addresses.map_err(WasmSdkError::invalid_argument)?;

// Parse network - only mainnet and testnet are supported
let network = match network.to_lowercase().as_str() {
"mainnet" => Network::Dash,
"testnet" => Network::Testnet,
_ => {
return Err(WasmSdkError::invalid_argument(format!(
"Invalid network '{}'. Expected: mainnet or testnet",
network
)))
}
};

// Use the cached trusted context if available for the network, otherwise create a new one
let trusted_context = match network {
Network::Dash => {
let guard = MAINNET_TRUSTED_CONTEXT.lock().unwrap();
guard.clone()
}
.map(Ok)
.unwrap_or_else(|| {
WasmTrustedContext::new_mainnet()
.map_err(|e| WasmSdkError::from(dash_sdk::Error::from(e)))
})?,
Network::Testnet => {
let guard = TESTNET_TRUSTED_CONTEXT.lock().unwrap();
guard.clone()
}
.map(Ok)
.unwrap_or_else(|| {
WasmTrustedContext::new_testnet()
.map_err(|e| WasmSdkError::from(dash_sdk::Error::from(e)))
})?,
// Network was already validated above
_ => unreachable!("Network already validated to mainnet or testnet"),
};

let address_list = dash_sdk::sdk::AddressList::from_iter(parsed_addresses);
let sdk_builder = SdkBuilder::new(address_list)
.with_network(network)
.with_context_provider(trusted_context);

Ok(Self(sdk_builder))
}

#[wasm_bindgen(js_name = "mainnet")]
pub fn new_mainnet() -> Self {
// Mainnet addresses from mnowatch.org
Expand Down
Loading
Loading