Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 93 additions & 11 deletions packages/js-evo-sdk/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Optional override for the trusted-context quorum base URL. When omitted,
// the URL is the network's default (e.g.
// `https://quorums.<devnetName>.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<Pick<EvoSDKOptions, 'network' | 'trusted'>> & ConnectionOptions & { addresses?: string[] };
private options: Required<Pick<EvoSDKOptions, 'network' | 'trusted'>> & ConnectionOptions & { addresses?: string[]; devnetName?: string; quorumUrl?: string };

public addresses!: AddressesFacade;
public documents!: DocumentsFacade;
Expand All @@ -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");
}
Comment thread
PastaPastaPasta marked this conversation as resolved.

this.options = { network, trusted, addresses, devnetName, quorumUrl, ...connection };

this.addresses = new AddressesFacade(this);
this.documents = new DocumentsFacade(this);
Expand Down Expand Up @@ -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}`);
}
Expand All @@ -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}`);
}
Expand Down Expand Up @@ -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 });
}
Comment thread
PastaPastaPasta marked this conversation as resolved.

/**
* Create an EvoSDK instance configured for a devnet with a trusted context.
*
* The trusted context is prefetched from
* `https://quorums.<devnetName>.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)
*
Expand All @@ -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 });
}
}
Expand Down
81 changes: 81 additions & 0 deletions packages/js-evo-sdk/tests/unit/sdk.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
2 changes: 1 addition & 1 deletion packages/rs-sdk-trusted-context-provider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<devnet_name>.networks.dash.org/`
- **Devnet**: Uses `https://quorums.<devnet_name>.networks.dash.org/`

## Usage

Expand Down
18 changes: 16 additions & 2 deletions packages/rs-sdk-trusted-context-provider/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.<devnet_name>.networks.dash.org/`
//! - **Devnet**: Uses `https://quorums.<devnet_name>.networks.dash.org/`

pub mod error;
pub mod provider;
Expand Down Expand Up @@ -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(),
Expand Down
Loading
Loading