Skip to content

Commit 4e31884

Browse files
thephezclaude
andauthored
feat(wasm-sdk): add custom masternode address configuration (#2805)
Co-authored-by: Claude <[email protected]>
1 parent 430a523 commit 4e31884

File tree

4 files changed

+502
-5
lines changed

4 files changed

+502
-5
lines changed

packages/js-evo-sdk/src/sdk.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@ export interface ConnectionOptions {
2929
export interface EvoSDKOptions extends ConnectionOptions {
3030
network?: 'testnet' | 'mainnet';
3131
trusted?: boolean;
32+
// Custom masternode addresses. When provided, network and trusted options are ignored.
33+
// Example: ['https://127.0.0.1:1443', 'https://192.168.1.100:1443']
34+
addresses?: string[];
3235
}
3336

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

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

5356
this.documents = new DocumentsFacade(this);
5457
this.identities = new IdentitiesFacade(this);
@@ -80,10 +83,20 @@ export class EvoSDK {
8083
if (this.wasmSdk) return; // idempotent
8184
await initWasm();
8285

83-
const { network, trusted, version, proofs, settings, logs } = this.options;
86+
const { network, trusted, version, proofs, settings, logs, addresses } = this.options;
8487

8588
let builder: wasm.WasmSdkBuilder;
86-
if (network === 'mainnet') {
89+
90+
// If specific addresses are provided, use them instead of network presets
91+
if (addresses && addresses.length > 0) {
92+
// Prefetch trusted quorums for the network before creating builder with addresses
93+
if (network === 'mainnet') {
94+
await wasm.WasmSdk.prefetchTrustedQuorumsMainnet();
95+
} else if (network === 'testnet') {
96+
await wasm.WasmSdk.prefetchTrustedQuorumsTestnet();
97+
}
98+
builder = wasm.WasmSdkBuilder.withAddresses(addresses, network);
99+
} else if (network === 'mainnet') {
87100
await wasm.WasmSdk.prefetchTrustedQuorumsMainnet();
88101

89102
builder = trusted ? wasm.WasmSdkBuilder.mainnetTrusted() : wasm.WasmSdkBuilder.mainnet();
@@ -131,6 +144,24 @@ export class EvoSDK {
131144
static mainnet(options: ConnectionOptions = {}): EvoSDK { return new EvoSDK({ network: 'mainnet', ...options }); }
132145
static testnetTrusted(options: ConnectionOptions = {}): EvoSDK { return new EvoSDK({ network: 'testnet', trusted: true, ...options }); }
133146
static mainnetTrusted(options: ConnectionOptions = {}): EvoSDK { return new EvoSDK({ network: 'mainnet', trusted: true, ...options }); }
147+
148+
/**
149+
* Create an EvoSDK instance configured with specific masternode addresses.
150+
*
151+
* @param addresses - Array of HTTPS URLs to masternodes (e.g., ['https://127.0.0.1:1443'])
152+
* @param network - Network identifier: 'mainnet', 'testnet' (default: 'testnet')
153+
* @param options - Additional connection options
154+
* @returns A configured EvoSDK instance (not yet connected - call .connect() to establish connection)
155+
*
156+
* @example
157+
* ```typescript
158+
* const sdk = EvoSDK.withAddresses(['https://52.12.176.90:1443'], 'testnet');
159+
* await sdk.connect();
160+
* ```
161+
*/
162+
static withAddresses(addresses: string[], network: 'mainnet' | 'testnet' = 'testnet', options: ConnectionOptions = {}): EvoSDK {
163+
return new EvoSDK({ addresses, network, ...options });
164+
}
134165
}
135166

136167
export { DocumentsFacade } from './documents/facade.js';

packages/js-evo-sdk/tests/unit/sdk.spec.mjs

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import { EvoSDK } from '../../dist/evo-sdk.module.js';
22

3+
// Test addresses (RFC 6761 reserved test domain - no network calls in unit tests)
4+
const TEST_ADDRESS_1 = 'https://node-1.test:1443';
5+
const TEST_ADDRESS_2 = 'https://node-2.test:1443';
6+
const TEST_ADDRESS_3 = 'https://node-3.test:1443';
7+
const TEST_ADDRESSES = [TEST_ADDRESS_1, TEST_ADDRESS_2, TEST_ADDRESS_3];
8+
39
describe('EvoSDK', () => {
410
it('exposes constructor and factories', () => {
511
expect(EvoSDK).to.be.a('function');
612
expect(EvoSDK.testnet).to.be.a('function');
713
expect(EvoSDK.mainnet).to.be.a('function');
814
expect(EvoSDK.testnetTrusted).to.be.a('function');
915
expect(EvoSDK.mainnetTrusted).to.be.a('function');
16+
expect(EvoSDK.withAddresses).to.be.a('function');
1017
});
1118

1219
it('fromWasm() marks instance as connected', () => {
@@ -15,4 +22,194 @@ describe('EvoSDK', () => {
1522
expect(sdk.isConnected).to.equal(true);
1623
expect(sdk.wasm).to.equal(wasmStub);
1724
});
25+
26+
describe('EvoSDK.withAddresses()', () => {
27+
it('creates SDK instance with specific addresses', () => {
28+
const sdk = EvoSDK.withAddresses([TEST_ADDRESS_1], 'testnet');
29+
expect(sdk).to.be.instanceof(EvoSDK);
30+
expect(sdk.options.network).to.equal('testnet');
31+
expect(sdk.isConnected).to.equal(false);
32+
});
33+
34+
it('defaults to testnet when network not specified', () => {
35+
const sdk = EvoSDK.withAddresses([TEST_ADDRESS_1]);
36+
expect(sdk).to.be.instanceof(EvoSDK);
37+
expect(sdk.options.network).to.equal('testnet');
38+
expect(sdk.isConnected).to.equal(false);
39+
});
40+
41+
it('accepts mainnet network', () => {
42+
const sdk = EvoSDK.withAddresses([TEST_ADDRESS_2], 'mainnet');
43+
expect(sdk).to.be.instanceof(EvoSDK);
44+
expect(sdk.options.network).to.equal('mainnet');
45+
expect(sdk.isConnected).to.equal(false);
46+
});
47+
48+
it('accepts multiple addresses', () => {
49+
const sdk = EvoSDK.withAddresses(TEST_ADDRESSES, 'testnet');
50+
expect(sdk).to.be.instanceof(EvoSDK);
51+
expect(sdk.options.network).to.equal('testnet');
52+
expect(sdk.options.addresses).to.deep.equal(TEST_ADDRESSES);
53+
});
54+
55+
it('accepts additional connection options', () => {
56+
const sdk = EvoSDK.withAddresses(
57+
[TEST_ADDRESS_1],
58+
'testnet',
59+
{
60+
version: 1,
61+
proofs: true,
62+
logs: 'info',
63+
settings: {
64+
connectTimeoutMs: 10000,
65+
timeoutMs: 30000,
66+
retries: 3,
67+
banFailedAddress: false,
68+
},
69+
},
70+
);
71+
expect(sdk).to.be.instanceof(EvoSDK);
72+
expect(sdk.options.network).to.equal('testnet');
73+
expect(sdk.options.trusted).to.be.false();
74+
expect(sdk.options.addresses).to.deep.equal([TEST_ADDRESS_1]);
75+
expect(sdk.options.version).to.equal(1);
76+
expect(sdk.options.proofs).to.be.true();
77+
expect(sdk.options.logs).to.equal('info');
78+
expect(sdk.options.settings).to.exist();
79+
expect(sdk.options.settings.connectTimeoutMs).to.equal(10000);
80+
expect(sdk.options.settings.timeoutMs).to.equal(30000);
81+
expect(sdk.options.settings.retries).to.equal(3);
82+
expect(sdk.options.settings.banFailedAddress).to.be.false();
83+
});
84+
});
85+
86+
describe('constructor with addresses option', () => {
87+
it('accepts addresses in options', () => {
88+
const sdk = new EvoSDK({
89+
addresses: [TEST_ADDRESS_1],
90+
network: 'testnet',
91+
});
92+
expect(sdk).to.be.instanceof(EvoSDK);
93+
expect(sdk.options.network).to.equal('testnet');
94+
expect(sdk.options.trusted).to.be.false();
95+
expect(sdk.isConnected).to.equal(false);
96+
});
97+
98+
it('works with testnet default', () => {
99+
const sdk = new EvoSDK({
100+
addresses: [TEST_ADDRESS_1],
101+
});
102+
expect(sdk).to.be.instanceof(EvoSDK);
103+
expect(sdk.options.network).to.equal('testnet');
104+
expect(sdk.options.trusted).to.be.false();
105+
});
106+
107+
it('works with mainnet', () => {
108+
const sdk = new EvoSDK({
109+
addresses: [TEST_ADDRESS_2],
110+
network: 'mainnet',
111+
});
112+
expect(sdk).to.be.instanceof(EvoSDK);
113+
expect(sdk.options.network).to.equal('mainnet');
114+
expect(sdk.options.trusted).to.be.false();
115+
});
116+
117+
it('combines addresses with other options', () => {
118+
const sdk = new EvoSDK({
119+
addresses: [TEST_ADDRESS_1],
120+
network: 'testnet',
121+
version: 1,
122+
proofs: true,
123+
logs: 'debug',
124+
settings: {
125+
connectTimeoutMs: 5000,
126+
timeoutMs: 15000,
127+
retries: 5,
128+
banFailedAddress: true,
129+
},
130+
});
131+
expect(sdk).to.be.instanceof(EvoSDK);
132+
expect(sdk.options.network).to.equal('testnet');
133+
expect(sdk.options.trusted).to.be.false();
134+
expect(sdk.options.addresses).to.deep.equal([TEST_ADDRESS_1]);
135+
expect(sdk.options.version).to.equal(1);
136+
expect(sdk.options.proofs).to.be.true();
137+
expect(sdk.options.logs).to.equal('debug');
138+
expect(sdk.options.settings).to.exist();
139+
expect(sdk.options.settings.connectTimeoutMs).to.equal(5000);
140+
expect(sdk.options.settings.timeoutMs).to.equal(15000);
141+
expect(sdk.options.settings.retries).to.equal(5);
142+
expect(sdk.options.settings.banFailedAddress).to.be.true();
143+
});
144+
145+
it('prioritizes addresses over network presets when both provided', () => {
146+
// When addresses are provided, they should be used instead of default network addresses
147+
const sdk = new EvoSDK({
148+
addresses: [TEST_ADDRESS_3],
149+
network: 'testnet',
150+
trusted: true,
151+
});
152+
expect(sdk).to.be.instanceof(EvoSDK);
153+
expect(sdk.options.network).to.equal('testnet');
154+
expect(sdk.options.addresses).to.deep.equal([TEST_ADDRESS_3]);
155+
expect(sdk.options.trusted).to.be.true();
156+
});
157+
158+
it('withAddresses() and constructor with addresses produce equivalent SDKs', () => {
159+
const addresses = [TEST_ADDRESS_1];
160+
const options = { version: 1, proofs: true };
161+
162+
const sdk1 = EvoSDK.withAddresses(addresses, 'testnet', options);
163+
const sdk2 = new EvoSDK({ addresses, network: 'testnet', ...options });
164+
165+
expect(sdk1.options.addresses).to.deep.equal(sdk2.options.addresses);
166+
expect(sdk1.options.network).to.equal(sdk2.options.network);
167+
expect(sdk1.options.version).to.equal(sdk2.options.version);
168+
expect(sdk1.options.proofs).to.equal(sdk2.options.proofs);
169+
});
170+
});
171+
172+
describe('factory methods for standard configurations', () => {
173+
it('testnet() creates testnet instance', () => {
174+
const sdk = EvoSDK.testnet();
175+
expect(sdk).to.be.instanceof(EvoSDK);
176+
expect(sdk.options.network).to.equal('testnet');
177+
expect(sdk.options.trusted).to.be.false();
178+
expect(sdk.options.addresses).to.be.undefined();
179+
expect(sdk.isConnected).to.equal(false);
180+
});
181+
182+
it('mainnet() creates mainnet instance', () => {
183+
const sdk = EvoSDK.mainnet();
184+
expect(sdk).to.be.instanceof(EvoSDK);
185+
expect(sdk.options.network).to.equal('mainnet');
186+
expect(sdk.options.trusted).to.be.false();
187+
expect(sdk.isConnected).to.equal(false);
188+
});
189+
190+
it('testnetTrusted() creates trusted testnet instance', () => {
191+
const sdk = EvoSDK.testnetTrusted();
192+
expect(sdk).to.be.instanceof(EvoSDK);
193+
expect(sdk.options.network).to.equal('testnet');
194+
expect(sdk.options.trusted).to.be.true();
195+
expect(sdk.isConnected).to.equal(false);
196+
});
197+
198+
it('mainnetTrusted() creates trusted mainnet instance', () => {
199+
const sdk = EvoSDK.mainnetTrusted();
200+
expect(sdk).to.be.instanceof(EvoSDK);
201+
expect(sdk.options.network).to.equal('mainnet');
202+
expect(sdk.options.trusted).to.be.true();
203+
expect(sdk.isConnected).to.equal(false);
204+
});
205+
206+
it('factory methods accept connection options', () => {
207+
const sdk = EvoSDK.testnet({
208+
version: 1,
209+
proofs: false,
210+
logs: 'warn',
211+
});
212+
expect(sdk).to.be.instanceof(EvoSDK);
213+
});
214+
});
18215
});

packages/wasm-sdk/src/sdk.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,90 @@ impl WasmSdkBuilder {
127127
PlatformVersion::latest().protocol_version
128128
}
129129

130+
/// Create a new SdkBuilder with specific addresses and network.
131+
///
132+
/// # Arguments
133+
/// * `addresses` - Array of HTTPS URLs (e.g., ["https://127.0.0.1:1443"])
134+
/// * `network` - Network identifier: "mainnet" or "testnet"
135+
///
136+
/// # Example
137+
/// ```javascript
138+
/// const builder = WasmSdkBuilder.withAddresses(['https://127.0.0.1:1443'], 'testnet');
139+
/// const sdk = builder.build();
140+
/// ```
141+
#[wasm_bindgen(js_name = "withAddresses")]
142+
pub fn new_with_addresses(
143+
addresses: Vec<String>,
144+
network: String,
145+
) -> Result<Self, WasmSdkError> {
146+
use crate::context_provider::WasmTrustedContext;
147+
use dash_sdk::dpp::dashcore::Network;
148+
use dash_sdk::sdk::Uri;
149+
use rs_dapi_client::Address;
150+
151+
// Parse and validate addresses
152+
if addresses.is_empty() {
153+
return Err(WasmSdkError::invalid_argument(
154+
"Addresses must be a non-empty array",
155+
));
156+
}
157+
let parsed_addresses: Result<Vec<Address>, _> = addresses
158+
.into_iter()
159+
.map(|addr| {
160+
addr.parse::<Uri>()
161+
.map_err(|e| format!("Invalid URI '{}': {}", addr, e))
162+
.and_then(|uri| {
163+
Address::try_from(uri).map_err(|e| format!("Invalid address: {}", e))
164+
})
165+
})
166+
.collect();
167+
168+
let parsed_addresses = parsed_addresses.map_err(WasmSdkError::invalid_argument)?;
169+
170+
// Parse network - only mainnet and testnet are supported
171+
let network = match network.to_lowercase().as_str() {
172+
"mainnet" => Network::Dash,
173+
"testnet" => Network::Testnet,
174+
_ => {
175+
return Err(WasmSdkError::invalid_argument(format!(
176+
"Invalid network '{}'. Expected: mainnet or testnet",
177+
network
178+
)))
179+
}
180+
};
181+
182+
// Use the cached trusted context if available for the network, otherwise create a new one
183+
let trusted_context = match network {
184+
Network::Dash => {
185+
let guard = MAINNET_TRUSTED_CONTEXT.lock().unwrap();
186+
guard.clone()
187+
}
188+
.map(Ok)
189+
.unwrap_or_else(|| {
190+
WasmTrustedContext::new_mainnet()
191+
.map_err(|e| WasmSdkError::from(dash_sdk::Error::from(e)))
192+
})?,
193+
Network::Testnet => {
194+
let guard = TESTNET_TRUSTED_CONTEXT.lock().unwrap();
195+
guard.clone()
196+
}
197+
.map(Ok)
198+
.unwrap_or_else(|| {
199+
WasmTrustedContext::new_testnet()
200+
.map_err(|e| WasmSdkError::from(dash_sdk::Error::from(e)))
201+
})?,
202+
// Network was already validated above
203+
_ => unreachable!("Network already validated to mainnet or testnet"),
204+
};
205+
206+
let address_list = dash_sdk::sdk::AddressList::from_iter(parsed_addresses);
207+
let sdk_builder = SdkBuilder::new(address_list)
208+
.with_network(network)
209+
.with_context_provider(trusted_context);
210+
211+
Ok(Self(sdk_builder))
212+
}
213+
130214
#[wasm_bindgen(js_name = "mainnet")]
131215
pub fn new_mainnet() -> Self {
132216
// Mainnet addresses from mnowatch.org

0 commit comments

Comments
 (0)