diff --git a/.github/workflows/test-tutorials.yml b/.github/workflows/test-tutorials.yml
index e50f4db2..2bd54fde 100644
--- a/.github/workflows/test-tutorials.yml
+++ b/.github/workflows/test-tutorials.yml
@@ -41,11 +41,11 @@ jobs:
node -v
npm -v
- run: npm ci
- - name: Run read-only tutorial tests
+ - name: Run PR-safe tests
env:
NETWORK: testnet
PLATFORM_MNEMONIC: ${{ secrets.PLATFORM_MNEMONIC }}
- run: npm run test:read-only
+ run: npm test
test-read-write:
runs-on: ubuntu-latest
diff --git a/CLAUDE.md b/CLAUDE.md
index 3c06af9d..74c52492 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -19,9 +19,10 @@ Run `node view-wallet.mjs` to confirm the identity is found before proceeding.
```bash
nvm use # Use the repo's tested Node 22.22.x toolchain
-npm test # Read-only tests (~2min, safe to run anytime)
+npm test # All PR-safe suites (~2min, safe to run anytime)
+npm run test:read-only # Just the tutorial subprocess suite
npm run test:read-write # Write tests (destructive, consumes testnet credits, ~5min)
-npm run test:all # Both suites sequentially
+npm run test:all # Every suite sequentially
npm run test:setup # Mocha tests for setupDashClient configuration
npm run lint # TypeScript type-check all JS files (tsc)
@@ -52,6 +53,8 @@ node 1-Identities-and-Names/identity-retrieve.mjs
node --test --test-timeout=120000 test/read-only.test.mjs
```
+**Protocol version:** `createClient()` deliberately leaves the platform protocol version unset so the SDK negotiates it with the network — it starts at a conservative version and ratchets up to whatever the network reports, capped at the newest version the SDK understands. Passing the `version` option pins it and disables negotiation outright, so a hardcoded value silently goes stale at the next network upgrade. Use `sdk.version()` when you need the negotiated value.
+
## Architecture
### Tutorial Structure
@@ -89,7 +92,7 @@ The central helper (~500 lines) that all tutorials import. It handles:
### Test Framework
-Tests use Node.js built-in test runner. Each test runs a tutorial as a **subprocess** via `test/run-tutorial.mjs` and validates:
+Tests use Node.js built-in test runner. The tutorial suites (`read-only`, `read-write`) run each tutorial as a **subprocess** via `test/run-tutorial.mjs` and validate:
- Exit code is 0
- `stdout`/`stderr` match expected regex patterns
@@ -99,6 +102,14 @@ Tests use Node.js built-in test runner. Each test runs a tutorial as a **subproc
**Read-write tests** maintain a shared state object to pass IDs (contract IDs, document IDs, etc.) between sequential dependent tests.
+The remaining suites are repo invariants rather than tutorial runs, which is why they live in their own files:
+
+| File | Checks | Network |
+| - | - | - |
+| `platform-version-config.test.mjs` | `createClient()` passes no `version` to the `*Trusted` factories, so the SDK negotiates the protocol version instead of using a pin | none (factories mocked) |
+| `platform-version.test.mjs` | A live client settles on `min(network active version, SDK ceiling)` after its first proof-bearing read | testnet + mainnet |
+| `lite-sdk-versions.test.mjs` | Each `*-lite.html` page imports exactly the `@dashevo/evo-sdk` version its companion app declares | none |
+
### Derivation Paths
All key derivation uses standard Dash paths. External wallets/tools must use the same paths for compatibility.
@@ -135,7 +146,7 @@ Read-only tests skip gracefully when `PLATFORM_MNEMONIC` is unset.
- **`1-Identities-and-Names/`** — identity registration, top-up, key management, DPNS name registration/lookup
- **`2-Contracts-and-Documents/`** — data contract variants (minimal, indexed, binary, timestamps, history, NFT), document CRUD, NFT operations
- **`3-Tokens/`** — token contract registration, info queries, minting, burning, and transfers
-- **`test/`** — test runner, assertions, read-only and read-write test suites
+- **`test/`** — test runner, assertions, tutorial suites (read-only, read-write), and repo-invariant suites (platform version, lite SDK versions)
- **`docs/`** — HTML/JS interactive tutorial runner (separate from Node tutorials)
- **`example-apps/`** — Standalone applications (Vite + React + TypeScript) that consume the tutorial SDK code. Each has its own `package.json`, tsconfig, and toolchain — the conventions in this file (Node16 modules, `airbnb-base`, etc.) describe the **root** tutorial code only and do not apply inside `example-apps/`. See each app's local `CLAUDE.md` for its conventions.
diff --git a/README.md b/README.md
index 41ff3784..09c91aee 100644
--- a/README.md
+++ b/README.md
@@ -105,12 +105,18 @@ setup instructions and dependencies but share repository-level SDK client helper
Tests run each tutorial as a subprocess and validate its output. No test framework dependencies are
required — tests use the Node.js built-in test runner.
-Ensure your `.env` file is configured (see [`.env.example`](./.env.example)) before running tests.
+`npm test` needs no wallet credentials. The tutorial suites do: configure your `.env` file (see
+[`.env.example`](./.env.example)) before running `test:read-only` or `test:read-write` — without a
+`PLATFORM_MNEMONIC` the read-only suite skips the tutorials that need one, and the write suite
+cannot run at all.
```shell
-# Read-only tests (default) — safe to run, no credits consumed
+# Default — safe to run, no credits consumed
npm test
+# Just the tutorial suite
+npm run test:read-only
+
# Write tests — registers identities/contracts/documents (consumes testnet credits)
npm run test:read-write
@@ -118,6 +124,11 @@ npm run test:read-write
npm run test:all
```
+Alongside the tutorial suite, `npm test` checks a couple of repository invariants that need no
+credits: that the SDK client negotiates the platform protocol version rather than using a hardcoded
+pin, and that each standalone `*-lite.html` page imports the same Evo SDK version as its companion
+app.
+
### Importing an existing wallet
If you already have a Dash identity created with another tool (e.g. [Dash
diff --git a/example-apps/dashmint-lab/public/dashmint-lite.html b/example-apps/dashmint-lab/public/dashmint-lite.html
index 084c924e..841166c6 100644
--- a/example-apps/dashmint-lab/public/dashmint-lite.html
+++ b/example-apps/dashmint-lab/public/dashmint-lite.html
@@ -119,7 +119,7 @@
Browse cards
// package and serves it as a browser-native ES module. Pinned to the same
// version the React app at ../package.json depends on so both UIs behave
// identically against the same testnet contract.
- import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@4.0.0';
+ import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@4.1.0';
// The token-enabled "card" data contract is already published on testnet by
// the React app. Anyone querying with the same contract id hits the same
@@ -131,12 +131,11 @@ Browse cards
// nodes — no node URL or config needed. connect() does the gRPC handshake
// + initial sync. No identity or signing is required for read-only queries.
//
- // Workaround: pin the platform protocol version for evo-sdk dev.6 so the
- // SDK doesn't ask testnet for a newer protocol it can't decode. Mirrors
- // PLATFORM_VERSION_OVERRIDE in setupDashClient-core.mjs. Remove once a
- // fixed SDK release lands.
+ // The platform protocol version is left unset on purpose: the SDK negotiates
+ // it with the network. Passing `version` pins it and disables that, so a
+ // hardcoded value silently goes stale at the next network upgrade.
async function connectSdk() {
- const sdk = EvoSDK.testnetTrusted({ version: 11 });
+ const sdk = EvoSDK.testnetTrusted();
await sdk.connect();
return sdk;
}
diff --git a/example-apps/dashnote-starter/src/dash/createNote.ts b/example-apps/dashnote-starter/src/dash/createNote.ts
index 64d74051..2c557b6d 100644
--- a/example-apps/dashnote-starter/src/dash/createNote.ts
+++ b/example-apps/dashnote-starter/src/dash/createNote.ts
@@ -4,7 +4,6 @@
* SDK method: sdk.documents.create({ document, identityKey, signer })
*/
import type { Logger } from "../lib/logger";
-import { PLATFORM_VERSION_OVERRIDE } from "../../../../platformVersion.mjs";
import { loadSdkModule } from "./sdkModule";
import type { DashKeyManager, DashSdk } from "./types";
@@ -47,7 +46,7 @@ export async function createNote({
const json =
typeof document.toJSON === "function"
- ? (document.toJSON(PLATFORM_VERSION_OVERRIDE) as Record)
+ ? (document.toJSON(sdk.version()) as Record)
: {};
const noteId = String(json.$id ?? json.id ?? "");
if (!noteId) {
diff --git a/example-apps/dashnote-starter/src/dash/types.ts b/example-apps/dashnote-starter/src/dash/types.ts
index bca572d5..b9fc3f35 100644
--- a/example-apps/dashnote-starter/src/dash/types.ts
+++ b/example-apps/dashnote-starter/src/dash/types.ts
@@ -22,6 +22,8 @@ export interface DashDocumentLike {
}
export interface DashSdk {
+ /** Platform protocol version negotiated with the network. */
+ version(): number;
contracts: {
fetch(contractId: string): Promise<{
toJSON?: () => Record;
diff --git a/example-apps/dashnote/public/dashnote-lite.html b/example-apps/dashnote/public/dashnote-lite.html
index 809a7f86..e1241e05 100644
--- a/example-apps/dashnote/public/dashnote-lite.html
+++ b/example-apps/dashnote/public/dashnote-lite.html
@@ -129,7 +129,7 @@ Get note by ID
// package and serves it as a browser-native ES module. Pinned to the same
// version the React app at ../package.json depends on so both UIs behave
// identically against the same testnet contract.
- import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@4.0.0';
+ import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@4.1.0';
// The "note" data contract is already published on testnet by the React app.
// Anyone querying with the same contract id hits the same documents.
diff --git a/example-apps/dashnote/src/dash/createNote.ts b/example-apps/dashnote/src/dash/createNote.ts
index 64d74051..2c557b6d 100644
--- a/example-apps/dashnote/src/dash/createNote.ts
+++ b/example-apps/dashnote/src/dash/createNote.ts
@@ -4,7 +4,6 @@
* SDK method: sdk.documents.create({ document, identityKey, signer })
*/
import type { Logger } from "../lib/logger";
-import { PLATFORM_VERSION_OVERRIDE } from "../../../../platformVersion.mjs";
import { loadSdkModule } from "./sdkModule";
import type { DashKeyManager, DashSdk } from "./types";
@@ -47,7 +46,7 @@ export async function createNote({
const json =
typeof document.toJSON === "function"
- ? (document.toJSON(PLATFORM_VERSION_OVERRIDE) as Record)
+ ? (document.toJSON(sdk.version()) as Record)
: {};
const noteId = String(json.$id ?? json.id ?? "");
if (!noteId) {
diff --git a/example-apps/dashnote/src/dash/types.ts b/example-apps/dashnote/src/dash/types.ts
index 42df3e1e..1f77a0ec 100644
--- a/example-apps/dashnote/src/dash/types.ts
+++ b/example-apps/dashnote/src/dash/types.ts
@@ -22,6 +22,8 @@ export interface DashDocumentLike {
}
export interface DashSdk {
+ /** Platform protocol version negotiated with the network. */
+ version(): number;
contracts: {
fetch(contractId: string): Promise<{
toJSON?: () => Record;
diff --git a/example-apps/dashnote/test/dash.test.ts b/example-apps/dashnote/test/dash.test.ts
index 0a3c1b90..9c2ebe62 100644
--- a/example-apps/dashnote/test/dash.test.ts
+++ b/example-apps/dashnote/test/dash.test.ts
@@ -33,6 +33,7 @@ function makeKeyManager() {
describe("createNote", () => {
it("creates a note with a trimmed title", async () => {
const sdk = {
+ version: () => 13,
documents: {
create: vi.fn().mockResolvedValue(undefined),
},
@@ -61,6 +62,7 @@ describe("createNote", () => {
it("omits a blank title for body-only notes", async () => {
mockDocumentConstructor.mockReset();
const sdk = {
+ version: () => 13,
documents: {
create: vi.fn().mockResolvedValue(undefined),
},
diff --git a/example-apps/dashproof-lab/public/dashproof-lite.html b/example-apps/dashproof-lab/public/dashproof-lite.html
index dfa6ae06..8898daed 100644
--- a/example-apps/dashproof-lab/public/dashproof-lite.html
+++ b/example-apps/dashproof-lab/public/dashproof-lite.html
@@ -120,7 +120,7 @@ History by chainId
// package and serves it as a browser-native ES module. Pinned to the same
// version the React app at ../package.json depends on so both UIs behave
// identically against the same testnet contract.
- import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@4.0.0';
+ import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@4.1.0';
// The "anchor" data contract is already published on testnet by the React app.
// Anyone querying with the same contract id hits the same documents.
diff --git a/example-apps/dashrate/public/dashrate-lite.html b/example-apps/dashrate/public/dashrate-lite.html
index fdfe4552..fd82bc71 100644
--- a/example-apps/dashrate/public/dashrate-lite.html
+++ b/example-apps/dashrate/public/dashrate-lite.html
@@ -138,7 +138,7 @@ Recent reviews
// The rest of the file (DOM wiring, render helpers) is plumbing.
// ============================================================================
- import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@4.0.0';
+ import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@4.1.0';
const CONTRACT_ID = 'BdgTqaTAPYMyhp1WdeWdcvYSgoD7AuJ7tVCaCSXyQgyP';
const DOC_TYPE = 'review';
diff --git a/example-apps/dashrate/src/dash/review.ts b/example-apps/dashrate/src/dash/review.ts
index 49657f72..c1705d04 100644
--- a/example-apps/dashrate/src/dash/review.ts
+++ b/example-apps/dashrate/src/dash/review.ts
@@ -7,7 +7,6 @@
* sdk.documents.get(...)
* sdk.documents.replace(...)
*/
-import { PLATFORM_VERSION_OVERRIDE } from "../../../../platformVersion.mjs";
import type { Logger } from "../lib/logger";
import { loadSdkModule } from "./sdkModule";
import { findMyReviewForResource } from "./queries";
@@ -67,10 +66,7 @@ export async function saveReview({
await sdk.documents.create({ document, identityKey, signer });
const json =
typeof document.toJSON === "function"
- ? (document.toJSON(PLATFORM_VERSION_OVERRIDE) as Record<
- string,
- unknown
- >)
+ ? (document.toJSON(sdk.version()) as Record)
: {};
const reviewId = String(json.$id ?? json.id ?? "");
if (!reviewId) throw new Error("Created review returned no ID.");
diff --git a/example-apps/dashrate/src/dash/types.ts b/example-apps/dashrate/src/dash/types.ts
index 9e3ec752..ddf561e6 100644
--- a/example-apps/dashrate/src/dash/types.ts
+++ b/example-apps/dashrate/src/dash/types.ts
@@ -27,6 +27,8 @@ export type DashReviewQueryResults =
| Record;
export interface DashSdk {
+ /** Platform protocol version negotiated with the network. */
+ version(): number;
contracts: {
fetch(contractId: string): Promise<{
toJSON?: () => Record;
diff --git a/example-apps/dashrate/test/review.test.ts b/example-apps/dashrate/test/review.test.ts
index afab00b7..95787caa 100644
--- a/example-apps/dashrate/test/review.test.ts
+++ b/example-apps/dashrate/test/review.test.ts
@@ -43,6 +43,7 @@ function makeKeyManager(): DashKeyManager {
function makeSdk(overrides: Partial = {}): DashSdk {
return {
+ version: () => 13,
documents: {
create: vi.fn().mockResolvedValue(undefined),
replace: vi.fn().mockResolvedValue(undefined),
diff --git a/package.json b/package.json
index 26561820..1d38fa25 100644
--- a/package.json
+++ b/package.json
@@ -6,10 +6,10 @@
"scripts": {
"fmt": "prettier --write '**/*.{js,mjs}'",
"lint": "tsc",
- "test": "node --test --test-timeout=120000 test/read-only.test.mjs",
+ "test": "node --test --test-timeout=120000 test/read-only.test.mjs test/platform-version.test.mjs test/platform-version-config.test.mjs test/lite-sdk-versions.test.mjs",
"test:read-only": "node --test --test-timeout=120000 test/read-only.test.mjs",
"test:read-write": "node --test --test-timeout=300000 --test-concurrency=1 test/read-write.test.mjs",
- "test:all": "node --test --test-timeout=300000 --test-concurrency=1 test/read-only.test.mjs test/read-write.test.mjs",
+ "test:all": "node --test --test-timeout=300000 --test-concurrency=1 test/read-only.test.mjs test/platform-version.test.mjs test/platform-version-config.test.mjs test/lite-sdk-versions.test.mjs test/read-write.test.mjs",
"test:setup": "mocha --slow 500 test/setupDashClient.test.mjs --exit",
"walkthrough:dashmint-lab": "node scripts/walkthroughs/record.mjs dashmint-lab",
"walkthrough:dashnote": "node scripts/walkthroughs/record.mjs dashnote",
diff --git a/platformVersion.d.mts b/platformVersion.d.mts
deleted file mode 100644
index 78cdb7d4..00000000
--- a/platformVersion.d.mts
+++ /dev/null
@@ -1 +0,0 @@
-export declare const PLATFORM_VERSION_OVERRIDE: number;
diff --git a/platformVersion.mjs b/platformVersion.mjs
deleted file mode 100644
index f04ad4c9..00000000
--- a/platformVersion.mjs
+++ /dev/null
@@ -1,4 +0,0 @@
-// TODO: Remove this pin once dashpay/platform#3809 lands in the consumed
-// @dashevo/evo-sdk release; unpinned clients should then auto-detect the
-// network protocol version safely.
-export const PLATFORM_VERSION_OVERRIDE = 12;
diff --git a/setupDashClient-core.d.mts b/setupDashClient-core.d.mts
index ec832a32..d2ae2f06 100644
--- a/setupDashClient-core.d.mts
+++ b/setupDashClient-core.d.mts
@@ -200,8 +200,6 @@ export declare class AddressKeyManager {
export declare const KEY_SPECS: readonly unknown[];
-export declare const PLATFORM_VERSION_OVERRIDE: number;
-
export declare function dip13KeyPath(
network: string,
identityIndex: number,
diff --git a/setupDashClient-core.mjs b/setupDashClient-core.mjs
index 3b9b3103..1ee6c254 100644
--- a/setupDashClient-core.mjs
+++ b/setupDashClient-core.mjs
@@ -25,7 +25,6 @@ import {
SecurityLevel,
wallet,
} from '@dashevo/evo-sdk';
-import { PLATFORM_VERSION_OVERRIDE } from './platformVersion.mjs';
/** @typedef {import('@dashevo/evo-sdk').Identity} Identity */
/** @typedef {import('@dashevo/evo-sdk').IdentityPublicKey} IdentityPublicKey */
@@ -103,21 +102,25 @@ export async function dip13KeyPath(network, identityIndex, keyIndex) {
// SDK client helpers
// ---------------------------------------------------------------------------
-export { PLATFORM_VERSION_OVERRIDE };
-
/**
* Create and connect an EvoSDK client for the selected network.
*
+ * The platform protocol version is deliberately left unset so the SDK
+ * negotiates it with the network: it starts from a conservative version and
+ * ratchets up to whatever the network reports, capped at the newest version
+ * the SDK itself understands. Pinning it via the `version` option disables
+ * that negotiation outright, so a hardcoded value silently goes stale the
+ * next time the network upgrades. Use `sdk.version()` when you need the
+ * negotiated value.
+ *
* @param {string} [network='testnet']
* @returns {Promise}
*/
export async function createClient(network = 'testnet') {
const factories = /** @type {Record EvoSDK>} */ ({
- testnet: () =>
- EvoSDK.testnetTrusted({ version: PLATFORM_VERSION_OVERRIDE }),
- mainnet: () =>
- EvoSDK.mainnetTrusted({ version: PLATFORM_VERSION_OVERRIDE }),
- local: () => EvoSDK.localTrusted({ version: PLATFORM_VERSION_OVERRIDE }),
+ testnet: () => EvoSDK.testnetTrusted(),
+ mainnet: () => EvoSDK.mainnetTrusted(),
+ local: () => EvoSDK.localTrusted(),
});
const factory = factories[network];
diff --git a/test/lite-sdk-versions.test.mjs b/test/lite-sdk-versions.test.mjs
new file mode 100644
index 00000000..79f347ed
--- /dev/null
+++ b/test/lite-sdk-versions.test.mjs
@@ -0,0 +1,42 @@
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import { describe, it } from 'node:test';
+
+const litePages = [
+ ['dashmint-lab', 'dashmint-lite.html'],
+ ['dashnote', 'dashnote-lite.html'],
+ ['dashrate', 'dashrate-lite.html'],
+ ['dashproof-lab', 'dashproof-lite.html'],
+];
+
+describe('Standalone lite SDK versions', () => {
+ for (const [appName, pageName] of litePages) {
+ it(`${appName} matches its companion app`, () => {
+ const appDirectory = new URL(
+ `../example-apps/${appName}/`,
+ import.meta.url,
+ );
+ const packageJson = JSON.parse(
+ readFileSync(new URL('package.json', appDirectory), 'utf8'),
+ );
+ const page = readFileSync(
+ new URL(`public/${pageName}`, appDirectory),
+ 'utf8',
+ );
+ const expectedImport =
+ `https://esm.sh/@dashevo/evo-sdk@` +
+ packageJson.dependencies['@dashevo/evo-sdk'];
+ const sdkImports = [
+ ...page.matchAll(
+ /from\s+['"](https:\/\/esm\.sh\/@dashevo\/evo-sdk@[^'"]+)['"]/g,
+ ),
+ ].map((match) => match[1]);
+
+ assert.deepEqual(
+ sdkImports,
+ [expectedImport],
+ `${pageName} must import exactly the SDK version from ${appName}/package.json`,
+ );
+ });
+ }
+});
diff --git a/test/platform-version-config.test.mjs b/test/platform-version-config.test.mjs
new file mode 100644
index 00000000..55df27ce
--- /dev/null
+++ b/test/platform-version-config.test.mjs
@@ -0,0 +1,24 @@
+import assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import { EvoSDK } from '@dashevo/evo-sdk';
+import { createClient } from '../setupDashClient-core.mjs';
+
+const trustedFactories = [
+ ['testnet', 'testnetTrusted'],
+ ['mainnet', 'mainnetTrusted'],
+ ['local', 'localTrusted'],
+];
+
+describe('Platform protocol version configuration', () => {
+ for (const [network, factoryName] of trustedFactories) {
+ it(`${network} leaves the SDK version unpinned`, async (t) => {
+ const sdk = { connect: t.mock.fn(async () => {}) };
+ const factory = t.mock.method(EvoSDK, factoryName, () => sdk);
+
+ assert.equal(await createClient(network), sdk);
+ assert.equal(factory.mock.callCount(), 1);
+ assert.deepEqual(factory.mock.calls[0].arguments, []);
+ assert.equal(sdk.connect.mock.callCount(), 1);
+ });
+ }
+});
diff --git a/test/platform-version.test.mjs b/test/platform-version.test.mjs
new file mode 100644
index 00000000..0f88013e
--- /dev/null
+++ b/test/platform-version.test.mjs
@@ -0,0 +1,41 @@
+import assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import { EvoSDK } from '@dashevo/evo-sdk';
+import { createClient } from '../setupDashClient-core.mjs';
+
+// DPNS — a system contract, so it exists on every network and needs no fixture.
+const DPNS_CONTRACT_ID = 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec';
+
+describe('Platform protocol version', () => {
+ // Regression guard for #112: createClient() must leave the protocol version
+ // unpinned so the SDK negotiates it. A hardcoded pin disables negotiation
+ // silently — no error, no warning — and the client stays on the old version
+ // forever once the network upgrades.
+ for (const network of ['testnet', 'mainnet']) {
+ it(`${network} negotiates the version instead of using a hardcoded pin`, async () => {
+ const sdk = await createClient(network);
+
+ // status() hands back a WASM handle — the fields are only reachable
+ // through toJSON().
+ const status = (await sdk.system.status()).toJSON();
+ const networkVersion = Number(status.version.protocol.drive.current);
+ const sdkNewestKnown = await EvoSDK.getLatestVersionNumber();
+
+ // A proof-bearing read is what carries the network's version back to the
+ // client; the negotiated value is only settled after the first one.
+ await sdk.contracts.fetch(DPNS_CONTRACT_ID);
+
+ // The client should land on the network's active version, except when the
+ // network has moved past what this SDK release understands — then it stays
+ // at its own ceiling. Asserting against the min() of the two keeps this
+ // test honest without turning every network upgrade into a red build.
+ assert.equal(
+ sdk.version(),
+ Math.min(networkVersion, sdkNewestKnown),
+ `client settled on protocol version ${sdk.version()}, but ${network} ` +
+ `is on ${networkVersion} and this SDK understands up to ` +
+ `${sdkNewestKnown} — check for a reintroduced version pin`,
+ );
+ });
+ }
+});