Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
38 changes: 37 additions & 1 deletion book/src/evo-sdk/networks-and-environments.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.<name>.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
Expand Down
19 changes: 17 additions & 2 deletions packages/js-evo-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
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