From 0cb6b8bef9bde52ba9c45f7b7ecc67889ac3659c Mon Sep 17 00:00:00 2001 From: thephez Date: Mon, 17 Aug 2026 14:10:42 -0400 Subject: [PATCH 1/7] feat(tutorial): add in-browser interactive runner for identity retrieval Add a reusable interactive-tutorial block (JS runner + CSS) that lazily loads the Evo SDK from esm.sh and executes the visible snippet against testnet. Wire it up in conf.py as a classic deferred script and embed a read-only identity fetch example in the retrieve-an-identity tutorial. --- _static/css/pydata-overrides.css | 140 ++++++++++++++++++ _static/js/interactive-tutorial.js | 121 +++++++++++++++ conf.py | 4 + .../retrieve-an-identity.md | 48 ++++++ 4 files changed, 313 insertions(+) create mode 100644 _static/js/interactive-tutorial.js diff --git a/_static/css/pydata-overrides.css b/_static/css/pydata-overrides.css index 2600e17fd..a5875de49 100644 --- a/_static/css/pydata-overrides.css +++ b/_static/css/pydata-overrides.css @@ -192,3 +192,143 @@ sphinx search extension interface. max-height: 60rem; overflow: auto; } +/* Reusable, browser-executed blocks for safe tutorial operations. */ +.interactive-tutorial { + margin: 1rem 0 1.5rem; + padding: 1rem; + border: 1px solid var(--pst-color-border); + border-radius: 0.5rem; + background: var(--pst-color-surface); +} + +.interactive-tutorial__header, +.interactive-tutorial__controls, +.interactive-tutorial__summary { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.interactive-tutorial__header { + justify-content: space-between; + margin-bottom: 1rem; +} + +.interactive-tutorial__help, +.interactive-tutorial__connection, +.interactive-tutorial__empty { + color: var(--pst-color-text-muted); + font-size: 0.85rem; +} + +.interactive-tutorial__connection[data-state="connected"] { + color: #16834a; +} + +.interactive-tutorial__connection[data-state="error"] { + color: var(--pst-color-danger); +} + +.interactive-tutorial__label { + display: block; + margin-bottom: 0.35rem; + font-weight: 600; +} + +.interactive-tutorial__input { + flex: 1 1 28rem; + min-width: 0; + padding: 0.55rem 0.7rem; + border: 1px solid var(--pst-color-border); + border-radius: 0.3rem; + background: var(--pst-color-background); + color: var(--pst-color-text-base); + font-family: var(--pst-font-family-monospace); +} + +.interactive-tutorial__button { + padding: 0.55rem 0.85rem; + border: 1px solid var(--pst-color-primary); + border-radius: 0.3rem; + background: var(--pst-color-primary); + color: #fff; + cursor: pointer; + white-space: nowrap; +} + +.interactive-tutorial__button--secondary { + background: transparent; + color: var(--pst-color-primary); +} + +.interactive-tutorial__button:disabled { + cursor: wait; + opacity: 0.65; +} + +.interactive-tutorial__help { + margin-top: 0.45rem; +} + +.interactive-tutorial__source { + margin-top: 1rem; +} + +.interactive-tutorial__source summary { + cursor: pointer; + font-weight: 600; +} + +.interactive-tutorial__source pre { + margin: 0.5rem 0 0; + padding: 0.85rem; + border: 1px solid var(--pst-color-border); + border-radius: 0.3rem; + background: var(--pst-color-on-background); + overflow: auto; +} + +.interactive-tutorial__result { + margin-top: 1rem; + padding: 0.85rem; + border-radius: 0.3rem; + background: var(--pst-color-on-background); +} + +.interactive-tutorial__summary { + flex-wrap: wrap; + margin-bottom: 0.75rem; +} + +.interactive-tutorial__metric { + padding-right: 1rem; +} + +.interactive-tutorial__metric strong, +.interactive-tutorial__metric span { + display: block; +} + +.interactive-tutorial__metric span { + color: var(--pst-color-text-muted); + font-size: 0.75rem; +} + +.interactive-tutorial__json { + max-height: 28rem; + margin: 0; + overflow: auto; + white-space: pre; + font-size: 0.82rem; +} + +.interactive-tutorial__error { + color: var(--pst-color-danger); +} + +@media (max-width: 767.98px) { + .interactive-tutorial__controls { + align-items: stretch; + flex-direction: column; + } +} diff --git a/_static/js/interactive-tutorial.js b/_static/js/interactive-tutorial.js new file mode 100644 index 000000000..ce39f499a --- /dev/null +++ b/_static/js/interactive-tutorial.js @@ -0,0 +1,121 @@ +const SDK_URL = 'https://esm.sh/@dashevo/evo-sdk@4.1.0'; + +const text = (value) => String(value ?? '—'); + +function metric(label, value) { + const wrapper = document.createElement('div'); + wrapper.className = 'interactive-tutorial__metric'; + const strong = document.createElement('strong'); + strong.textContent = text(value); + const caption = document.createElement('span'); + caption.textContent = label; + wrapper.append(strong, caption); + return wrapper; +} + +function renderIdentity(container, identity) { + const value = typeof identity.toJSON === 'function' ? identity.toJSON() : identity; + const summary = document.createElement('div'); + summary.className = 'interactive-tutorial__summary'; + summary.append( + metric('Identity ID', value.id), + metric('Balance (credits)', Number(value.balance ?? 0).toLocaleString()), + metric('Revision', value.revision), + metric('Public keys', value.publicKeys?.length ?? 0), + ); + + const details = document.createElement('details'); + const label = document.createElement('summary'); + label.textContent = 'Raw response'; + const output = document.createElement('pre'); + output.className = 'interactive-tutorial__json'; + output.textContent = JSON.stringify( + value, + (_key, item) => (typeof item === 'bigint' ? item.toString() : item), + 2, + ); + details.append(label, output); + container.replaceChildren(summary, details); +} + +function renderMessage(container, message, isError = false) { + const messageElement = document.createElement('div'); + messageElement.className = isError + ? 'interactive-tutorial__error' + : 'interactive-tutorial__empty'; + messageElement.textContent = message; + container.replaceChildren(messageElement); +} + +async function initialize(block) { + const defaultValue = block.dataset.defaultValue ?? ''; + const network = block.dataset.network ?? 'testnet'; + const input = block.querySelector('[data-role="input"]'); + const source = block.querySelector('[data-role="source"]')?.textContent?.trim(); + const runButton = block.querySelector('[data-role="run"]'); + const resetButton = block.querySelector('[data-role="reset"]'); + const result = block.querySelector('[data-role="result"]'); + const connection = block.querySelector('[data-role="connection"]'); + + if (!input || !source || !runButton || !resetButton || !result || !connection) return; + input.value = defaultValue; + + // The visible snippet is the executable body. The runner supplies only its + // named inputs and handles UI state/result rendering around it. + const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; + const execute = new AsyncFunction('EvoSDK', 'identityId', source); + + let EvoSDK; + async function loadSdk() { + if (EvoSDK) return EvoSDK; + connection.textContent = 'Loading SDK…'; + connection.dataset.state = 'connecting'; + ({ EvoSDK } = await import(SDK_URL)); + connection.textContent = `SDK loaded · ${network}`; + connection.dataset.state = 'connected'; + return EvoSDK; + } + + async function run() { + const value = input.value.trim(); + if (!value) { + renderMessage(result, 'Enter an identity ID.', true); + input.focus(); + return; + } + + runButton.disabled = true; + input.disabled = true; + renderMessage(result, 'Loading identity…'); + try { + const sdkClass = await loadSdk(); + connection.textContent = `Connecting to ${network}…`; + const identity = await execute(sdkClass, value); + connection.textContent = `Connected to ${network}`; + if (!identity) { + renderMessage(result, 'No identity was found for that ID.', true); + return; + } + renderIdentity(result, identity); + } catch (error) { + connection.textContent = 'Run failed'; + connection.dataset.state = 'error'; + renderMessage(result, `Query failed: ${error?.message ?? error}`, true); + } finally { + runButton.disabled = false; + input.disabled = false; + } + } + + runButton.addEventListener('click', run); + input.addEventListener('keydown', (event) => { + if (event.key === 'Enter') run(); + }); + resetButton.addEventListener('click', () => { + input.value = defaultValue; + renderMessage(result, 'Run the query to inspect the identity.'); + input.focus(); + }); +} + +document.querySelectorAll('.interactive-tutorial').forEach(initialize); diff --git a/conf.py b/conf.py index 96cf214df..ed4a12e0b 100644 --- a/conf.py +++ b/conf.py @@ -142,3 +142,7 @@ def setup(app): app.add_js_file('js/pydata-search-close.js') + # Keep this a classic script so locally built docs also work when opened + # directly via file://. Module scripts loaded from file:// are blocked by + # browser CORS rules; the SDK itself is still loaded lazily over HTTPS. + app.add_js_file('js/interactive-tutorial.js', defer='defer') diff --git a/docs/tutorials/identities-and-names/retrieve-an-identity.md b/docs/tutorials/identities-and-names/retrieve-an-identity.md index da19ae18e..d567e6818 100644 --- a/docs/tutorials/identities-and-names/retrieve-an-identity.md +++ b/docs/tutorials/identities-and-names/retrieve-an-identity.md @@ -38,6 +38,54 @@ try { } ``` +## Try it + +This read-only example connects directly to testnet from your browser. Enter any identity ID, or +use the example ID, and run the same `sdk.identities.fetch()` call used above. No wallet, mnemonic, +or signing key is required. + +```{raw} html +
+
+ Retrieve an identity from testnet + Not connected +
+ +
+ + + +
+
+ Runs sdk.identities.fetch(identityId) against Dash Platform testnet. +
+
+ Code being run +
const sdk = EvoSDK.testnetTrusted();
+await sdk.connect();
+
+const identity = await sdk.identities.fetch(identityId);
+return identity?.toJSON() ?? null;
+
+
+
Run the query to inspect the identity.
+
+
+``` + ## Example Identity The following example response shows a retrieved identity: From e487c9293e65e6e5d616dee54068d24fe3fb7967 Mon Sep 17 00:00:00 2001 From: thephez Date: Mon, 17 Aug 2026 14:31:03 -0400 Subject: [PATCH 2/7] feat(tutorial): generalize interactive runner and add it to more tutorials Rework the runner to support multiple named inputs (data-param) and per-operation result renderers (identity, contract, name, documents, status), materializing form values as declarations in the displayed snippet so the visible code is exactly what executes. Add Try it blocks to the connect, retrieve-a-data-contract, retrieve-documents, and retrieve-a-name tutorials, and migrate the retrieve-an-identity block to the new attributes. --- _static/css/pydata-overrides.css | 23 ++++ _static/js/interactive-tutorial.js | 106 ++++++++++++------ docs/tutorials/connecting-to-testnet.md | 28 +++++ .../retrieve-a-data-contract.md | 33 ++++++ .../retrieve-documents.md | 55 +++++++++ .../identities-and-names/retrieve-a-name.md | 32 ++++++ .../retrieve-an-identity.md | 8 +- 7 files changed, 250 insertions(+), 35 deletions(-) diff --git a/_static/css/pydata-overrides.css b/_static/css/pydata-overrides.css index a5875de49..048b4a41f 100644 --- a/_static/css/pydata-overrides.css +++ b/_static/css/pydata-overrides.css @@ -235,6 +235,29 @@ sphinx search extension interface. font-weight: 600; } +.interactive-tutorial__field { + flex: 1 1 12rem; + min-width: 0; +} + +.interactive-tutorial__field--wide { + flex-basis: 28rem; +} + +.interactive-tutorial__field--small { + flex: 0 1 6rem; +} + +.interactive-tutorial__field .interactive-tutorial__input { + width: 100%; +} + +.interactive-tutorial__actions { + display: flex; + gap: 0.75rem; + margin-top: 0.75rem; +} + .interactive-tutorial__input { flex: 1 1 28rem; min-width: 0; diff --git a/_static/js/interactive-tutorial.js b/_static/js/interactive-tutorial.js index ce39f499a..cb1eb6a42 100644 --- a/_static/js/interactive-tutorial.js +++ b/_static/js/interactive-tutorial.js @@ -13,16 +13,43 @@ function metric(label, value) { return wrapper; } -function renderIdentity(container, identity) { - const value = typeof identity.toJSON === 'function' ? identity.toJSON() : identity; +function normalize(value) { + if (value && typeof value.toJSON === 'function') return normalize(value.toJSON()); + if (value instanceof Map) { + return [...value.entries()].map(([id, item]) => ({ id: text(id), ...normalize(item) })); + } + if (Array.isArray(value)) return value.map(normalize); + if (value && typeof value === 'object') { + return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, normalize(item)])); + } + return value; +} + +function renderResult(container, rawValue, renderer) { + const value = renderer === 'name' ? text(rawValue) : normalize(rawValue); const summary = document.createElement('div'); summary.className = 'interactive-tutorial__summary'; - summary.append( - metric('Identity ID', value.id), - metric('Balance (credits)', Number(value.balance ?? 0).toLocaleString()), - metric('Revision', value.revision), - metric('Public keys', value.publicKeys?.length ?? 0), - ); + if (renderer === 'identity') { + summary.append( + metric('Identity ID', value.id), + metric('Balance (credits)', Number(value.balance ?? 0).toLocaleString()), + metric('Revision', value.revision), + metric('Public keys', value.publicKeys?.length ?? 0), + ); + } else if (renderer === 'contract') { + summary.append( + metric('Contract ID', value.id), + metric('Owner', value.ownerId), + metric('Version', value.version), + metric('Document types', Object.keys(value.documentSchemas ?? {}).length), + ); + } else if (renderer === 'name') { + summary.append(metric('Resolved identity ID', value)); + } else if (renderer === 'documents') { + summary.append(metric('Documents returned', value.length)); + } else if (renderer === 'status') { + summary.append(metric('Connection', 'Successful')); + } const details = document.createElement('details'); const label = document.createElement('summary'); @@ -48,22 +75,32 @@ function renderMessage(container, message, isError = false) { } async function initialize(block) { - const defaultValue = block.dataset.defaultValue ?? ''; const network = block.dataset.network ?? 'testnet'; - const input = block.querySelector('[data-role="input"]'); - const source = block.querySelector('[data-role="source"]')?.textContent?.trim(); + const renderer = block.dataset.renderer ?? 'status'; + const inputs = [...block.querySelectorAll('[data-param]')]; + const sourceElement = block.querySelector('[data-role="source"]'); + const sourceBody = sourceElement?.textContent?.trim(); const runButton = block.querySelector('[data-role="run"]'); const resetButton = block.querySelector('[data-role="reset"]'); const result = block.querySelector('[data-role="result"]'); const connection = block.querySelector('[data-role="connection"]'); - if (!input || !source || !runButton || !resetButton || !result || !connection) return; - input.value = defaultValue; + if (!sourceElement || !sourceBody || !runButton || !resetButton || !result || !connection) return; + inputs.forEach((input) => { input.value = input.dataset.defaultValue ?? ''; }); + + // Materialize current form values as JavaScript declarations above the + // authored snippet. The complete visible snippet is then executed as-is. + function updateDisplayedSource() { + const declarations = inputs.map( + (input) => `const ${input.dataset.param} = ${JSON.stringify(input.value)};`, + ); + sourceElement.textContent = [...declarations, declarations.length ? '' : null, sourceBody] + .filter((line) => line !== null) + .join('\n'); + } + updateDisplayedSource(); - // The visible snippet is the executable body. The runner supplies only its - // named inputs and handles UI state/result rendering around it. const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; - const execute = new AsyncFunction('EvoSDK', 'identityId', source); let EvoSDK; async function loadSdk() { @@ -77,44 +114,49 @@ async function initialize(block) { } async function run() { - const value = input.value.trim(); - if (!value) { - renderMessage(result, 'Enter an identity ID.', true); - input.focus(); + const missing = inputs.find((input) => input.required && !input.value.trim()); + if (missing) { + renderMessage(result, `Enter ${missing.dataset.label ?? 'a value'}.`, true); + missing.focus(); return; } runButton.disabled = true; - input.disabled = true; - renderMessage(result, 'Loading identity…'); + inputs.forEach((input) => { input.disabled = true; }); + renderMessage(result, 'Running query…'); try { const sdkClass = await loadSdk(); connection.textContent = `Connecting to ${network}…`; - const identity = await execute(sdkClass, value); + const execute = new AsyncFunction('EvoSDK', sourceElement.textContent); + const output = await execute(sdkClass); connection.textContent = `Connected to ${network}`; - if (!identity) { - renderMessage(result, 'No identity was found for that ID.', true); + if (output == null) { + renderMessage(result, 'No result was found.', true); return; } - renderIdentity(result, identity); + renderResult(result, output, renderer); } catch (error) { connection.textContent = 'Run failed'; connection.dataset.state = 'error'; renderMessage(result, `Query failed: ${error?.message ?? error}`, true); } finally { runButton.disabled = false; - input.disabled = false; + inputs.forEach((input) => { input.disabled = false; }); } } runButton.addEventListener('click', run); - input.addEventListener('keydown', (event) => { - if (event.key === 'Enter') run(); + inputs.forEach((input) => { + input.addEventListener('input', updateDisplayedSource); + input.addEventListener('keydown', (event) => { + if (event.key === 'Enter') run(); + }); }); resetButton.addEventListener('click', () => { - input.value = defaultValue; - renderMessage(result, 'Run the query to inspect the identity.'); - input.focus(); + inputs.forEach((input) => { input.value = input.dataset.defaultValue ?? ''; }); + updateDisplayedSource(); + renderMessage(result, 'Run the query to inspect the result.'); + inputs[0]?.focus(); }); } diff --git a/docs/tutorials/connecting-to-testnet.md b/docs/tutorials/connecting-to-testnet.md index 4fa78d6ed..908e7d0a2 100644 --- a/docs/tutorials/connecting-to-testnet.md +++ b/docs/tutorials/connecting-to-testnet.md @@ -47,6 +47,34 @@ try { } ``` +### Try it + +Run the connection and status check directly from this page. + +```{raw} html +
+
+ Connect to Dash Platform testnet + Not connected +
+
+ + +
+
+ Code being run +
const sdk = EvoSDK.testnetTrusted();
+await sdk.connect();
+
+const status = await sdk.system.status();
+return status.toJSON();
+
+
+
Connect to inspect the current system status.
+
+
+``` + Once this returns successfully, you're ready to begin developing! See the [Quickstart](../tutorials/introduction.md#quickstart) for recommended next steps. For details on SDK methods, please refer to the [SDK documentation](https://evo-sdk.dash.org/docs.html). ## Connect to a Local Devnet diff --git a/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md b/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md index 422b826f6..adb527609 100644 --- a/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md +++ b/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md @@ -34,6 +34,39 @@ try { } ``` +## Try it + +Retrieve a contract from testnet without configuring a wallet or identity. + +```{raw} html +
+
+ Retrieve a data contract from testnet + Not connected +
+ +
+ + + +
+
+ Code being run +
const sdk = EvoSDK.testnetTrusted();
+await sdk.connect();
+
+const contract = await sdk.contracts.fetch(dataContractId);
+return contract?.toJSON() ?? null;
+
+
+
Run the query to inspect the contract.
+
+
+``` + ## Example Data Contract The following example response shows a retrieved contract: diff --git a/docs/tutorials/contracts-and-documents/retrieve-documents.md b/docs/tutorials/contracts-and-documents/retrieve-documents.md index 21121f09e..43bfd64d1 100644 --- a/docs/tutorials/contracts-and-documents/retrieve-documents.md +++ b/docs/tutorials/contracts-and-documents/retrieve-documents.md @@ -41,6 +41,61 @@ try { } ``` +## Try it + +Query documents on testnet. The limit is converted to a number by the displayed code before it is +passed to the SDK. + +```{raw} html +
+
+ Retrieve documents from testnet + Not connected +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ Code being run +
const sdk = EvoSDK.testnetTrusted();
+await sdk.connect();
+
+const results = await sdk.documents.query({
+  dataContractId,
+  documentTypeName,
+  limit: Number(limit),
+});
+
+return results;
+
+
+
Run the query to inspect matching documents.
+
+
+``` + ### Queries The example code uses a very basic query to return only two results. More extensive querying capabilities are covered in the [query syntax reference](../../reference/query-syntax.md). diff --git a/docs/tutorials/identities-and-names/retrieve-a-name.md b/docs/tutorials/identities-and-names/retrieve-a-name.md index 70586df1e..c842a44e7 100644 --- a/docs/tutorials/identities-and-names/retrieve-a-name.md +++ b/docs/tutorials/identities-and-names/retrieve-a-name.md @@ -121,6 +121,38 @@ Tutorial-Test-000000-backup.dash (ID: 98bruK9TdJki5xP8BYpmNXqdH9ZHzBD9phwDRzhaJs ::: :::: +## Try it + +Resolve a fully-qualified DPNS name to its identity ID. + +```{raw} html +
+
+ Resolve a name on testnet + Not connected +
+ +
+ + + +
+
+ Code being run +
const sdk = EvoSDK.testnetTrusted();
+await sdk.connect();
+
+return sdk.dpns.resolveName(name);
+
+
+
Resolve the name to inspect its identity ID.
+
+
+``` + ## What's Happening After we initialize the Client, we request a name. The [code examples](#code) demonstrate the three ways to request a name: diff --git a/docs/tutorials/identities-and-names/retrieve-an-identity.md b/docs/tutorials/identities-and-names/retrieve-an-identity.md index d567e6818..b5db07bb8 100644 --- a/docs/tutorials/identities-and-names/retrieve-an-identity.md +++ b/docs/tutorials/identities-and-names/retrieve-an-identity.md @@ -47,9 +47,8 @@ or signing key is required. ```{raw} html
Retrieve an identity from testnet @@ -60,8 +59,11 @@ or signing key is required. Date: Mon, 17 Aug 2026 15:26:51 -0400 Subject: [PATCH 3/7] refactor(tutorial): run interactive examples from a predefined operation allow-list Replace AsyncFunction evaluation of DOM-embedded snippet text with an explicit map of read-only operation functions selected via data-operation. The displayed source is derived from the same function that runs, so the markup is no longer executable and the shown code cannot drift from the executed code. Also stack the action buttons on narrow viewports. --- _static/css/pydata-overrides.css | 3 +- _static/js/interactive-tutorial.js | 80 ++++++++++++++++--- docs/tutorials/connecting-to-testnet.md | 8 +- .../retrieve-a-data-contract.md | 8 +- .../retrieve-documents.md | 13 +-- .../identities-and-names/retrieve-a-name.md | 7 +- .../retrieve-an-identity.md | 7 +- 7 files changed, 81 insertions(+), 45 deletions(-) diff --git a/_static/css/pydata-overrides.css b/_static/css/pydata-overrides.css index 048b4a41f..9059e0414 100644 --- a/_static/css/pydata-overrides.css +++ b/_static/css/pydata-overrides.css @@ -350,7 +350,8 @@ sphinx search extension interface. } @media (max-width: 767.98px) { - .interactive-tutorial__controls { + .interactive-tutorial__controls, + .interactive-tutorial__actions { align-items: stretch; flex-direction: column; } diff --git a/_static/js/interactive-tutorial.js b/_static/js/interactive-tutorial.js index cb1eb6a42..5c0f622af 100644 --- a/_static/js/interactive-tutorial.js +++ b/_static/js/interactive-tutorial.js @@ -1,5 +1,52 @@ const SDK_URL = 'https://esm.sh/@dashevo/evo-sdk@4.1.0'; +// Explicit allow-list of safe, read-only tutorial operations. The UI obtains +// its displayed source directly from these functions, preventing code drift. +async function getNetworkStatus({ EvoSDK }) { + const sdk = EvoSDK.testnetTrusted(); + await sdk.connect(); + const status = await sdk.system.status(); + return status.toJSON(); +} + +async function fetchIdentity({ EvoSDK, identityId }) { + const sdk = EvoSDK.testnetTrusted(); + await sdk.connect(); + const identity = await sdk.identities.fetch(identityId); + return identity?.toJSON() ?? null; +} + +async function fetchContract({ EvoSDK, dataContractId }) { + const sdk = EvoSDK.testnetTrusted(); + await sdk.connect(); + const contract = await sdk.contracts.fetch(dataContractId); + return contract?.toJSON() ?? null; +} + +async function queryDocuments({ EvoSDK, dataContractId, documentTypeName, limit }) { + const sdk = EvoSDK.testnetTrusted(); + await sdk.connect(); + return sdk.documents.query({ + dataContractId, + documentTypeName, + limit: Number(limit), + }); +} + +async function resolveName({ EvoSDK, name }) { + const sdk = EvoSDK.testnetTrusted(); + await sdk.connect(); + return sdk.dpns.resolveName(name); +} + +const operations = { + 'network-status': getNetworkStatus, + 'identity-fetch': fetchIdentity, + 'contract-fetch': fetchContract, + 'documents-query': queryDocuments, + 'name-resolve': resolveName, +}; + const text = (value) => String(value ?? '—'); function metric(label, value) { @@ -75,33 +122,42 @@ function renderMessage(container, message, isError = false) { } async function initialize(block) { + const operationName = block.dataset.operation; + const operation = operations[operationName]; const network = block.dataset.network ?? 'testnet'; const renderer = block.dataset.renderer ?? 'status'; const inputs = [...block.querySelectorAll('[data-param]')]; const sourceElement = block.querySelector('[data-role="source"]'); - const sourceBody = sourceElement?.textContent?.trim(); const runButton = block.querySelector('[data-role="run"]'); const resetButton = block.querySelector('[data-role="reset"]'); const result = block.querySelector('[data-role="result"]'); const connection = block.querySelector('[data-role="connection"]'); - if (!sourceElement || !sourceBody || !runButton || !resetButton || !result || !connection) return; + if (!sourceElement || !runButton || !resetButton || !result || !connection) return; + if (!operation) { + renderMessage(result, `Interactive tutorial is not configured correctly: ${operationName ?? 'missing operation'}.`, true); + return; + } inputs.forEach((input) => { input.value = input.dataset.defaultValue ?? ''; }); - // Materialize current form values as JavaScript declarations above the - // authored snippet. The complete visible snippet is then executed as-is. + // Derive the display from the same function object invoked by run(). The DOM + // remains non-executable, while current parameter values make the call clear. function updateDisplayedSource() { const declarations = inputs.map( (input) => `const ${input.dataset.param} = ${JSON.stringify(input.value)};`, ); - sourceElement.textContent = [...declarations, declarations.length ? '' : null, sourceBody] - .filter((line) => line !== null) - .join('\n'); + const argumentNames = inputs.map((input) => input.dataset.param); + const invocationArguments = ['EvoSDK', ...argumentNames].join(', '); + sourceElement.textContent = [ + operation.toString(), + '', + ...declarations, + declarations.length ? '' : null, + `const result = await ${operation.name}({ ${invocationArguments} });`, + ].filter((line) => line !== null).join('\n'); } updateDisplayedSource(); - const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; - let EvoSDK; async function loadSdk() { if (EvoSDK) return EvoSDK; @@ -127,8 +183,10 @@ async function initialize(block) { try { const sdkClass = await loadSdk(); connection.textContent = `Connecting to ${network}…`; - const execute = new AsyncFunction('EvoSDK', sourceElement.textContent); - const output = await execute(sdkClass); + const parameters = Object.fromEntries( + inputs.map((input) => [input.dataset.param, input.value]), + ); + const output = await operation({ ...parameters, EvoSDK: sdkClass }); connection.textContent = `Connected to ${network}`; if (output == null) { renderMessage(result, 'No result was found.', true); diff --git a/docs/tutorials/connecting-to-testnet.md b/docs/tutorials/connecting-to-testnet.md index 908e7d0a2..cd5d5744f 100644 --- a/docs/tutorials/connecting-to-testnet.md +++ b/docs/tutorials/connecting-to-testnet.md @@ -52,7 +52,7 @@ try { Run the connection and status check directly from this page. ```{raw} html -
+
Connect to Dash Platform testnet Not connected @@ -63,11 +63,7 @@ Run the connection and status check directly from this page.
Code being run -
const sdk = EvoSDK.testnetTrusted();
-await sdk.connect();
-
-const status = await sdk.system.status();
-return status.toJSON();
+
Connect to inspect the current system status.
diff --git a/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md b/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md index adb527609..f20597b7c 100644 --- a/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md +++ b/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md @@ -39,7 +39,7 @@ try { Retrieve a contract from testnet without configuring a wallet or identity. ```{raw} html -
+
Retrieve a data contract from testnet Not connected @@ -55,11 +55,7 @@ Retrieve a contract from testnet without configuring a wallet or identity.
Code being run -
const sdk = EvoSDK.testnetTrusted();
-await sdk.connect();
-
-const contract = await sdk.contracts.fetch(dataContractId);
-return contract?.toJSON() ?? null;
+
Run the query to inspect the contract.
diff --git a/docs/tutorials/contracts-and-documents/retrieve-documents.md b/docs/tutorials/contracts-and-documents/retrieve-documents.md index 43bfd64d1..83fc39a7e 100644 --- a/docs/tutorials/contracts-and-documents/retrieve-documents.md +++ b/docs/tutorials/contracts-and-documents/retrieve-documents.md @@ -47,7 +47,7 @@ Query documents on testnet. The limit is converted to a number by the displayed passed to the SDK. ```{raw} html -
+
Retrieve documents from testnet Not connected @@ -79,16 +79,7 @@ passed to the SDK.
Code being run -
const sdk = EvoSDK.testnetTrusted();
-await sdk.connect();
-
-const results = await sdk.documents.query({
-  dataContractId,
-  documentTypeName,
-  limit: Number(limit),
-});
-
-return results;
+
Run the query to inspect matching documents.
diff --git a/docs/tutorials/identities-and-names/retrieve-a-name.md b/docs/tutorials/identities-and-names/retrieve-a-name.md index c842a44e7..08530cfe6 100644 --- a/docs/tutorials/identities-and-names/retrieve-a-name.md +++ b/docs/tutorials/identities-and-names/retrieve-a-name.md @@ -126,7 +126,7 @@ Tutorial-Test-000000-backup.dash (ID: 98bruK9TdJki5xP8BYpmNXqdH9ZHzBD9phwDRzhaJs Resolve a fully-qualified DPNS name to its identity ID. ```{raw} html -
+
Resolve a name on testnet Not connected @@ -142,10 +142,7 @@ Resolve a fully-qualified DPNS name to its identity ID.
Code being run -
const sdk = EvoSDK.testnetTrusted();
-await sdk.connect();
-
-return sdk.dpns.resolveName(name);
+
Resolve the name to inspect its identity ID.
diff --git a/docs/tutorials/identities-and-names/retrieve-an-identity.md b/docs/tutorials/identities-and-names/retrieve-an-identity.md index b5db07bb8..4573b3285 100644 --- a/docs/tutorials/identities-and-names/retrieve-an-identity.md +++ b/docs/tutorials/identities-and-names/retrieve-an-identity.md @@ -47,6 +47,7 @@ or signing key is required. ```{raw} html
@@ -76,11 +77,7 @@ or signing key is required.
Code being run -
const sdk = EvoSDK.testnetTrusted();
-await sdk.connect();
-
-const identity = await sdk.identities.fetch(identityId);
-return identity?.toJSON() ?? null;
+
Run the query to inspect the identity.
From c9b21768b7b88725933b57d849a4590eef702abc Mon Sep 17 00:00:00 2001 From: thephez Date: Mon, 17 Aug 2026 16:09:08 -0400 Subject: [PATCH 4/7] feat(tutorial): integrate interactive runners beneath their code examples Drop the standalone Try It sections and attach each runner directly below the tutorial's code block with a compact toolbar, a text-style reset button, and a View browser code toggle. Expand the status renderer to show network, block height, sync state, peers, and component versions, and improve narrow-viewport layout for inputs, metrics, and the toolbar. --- _static/css/pydata-overrides.css | 74 ++++++++++++++++++- _static/js/interactive-tutorial.js | 16 +++- docs/tutorials/connecting-to-testnet.md | 16 ++-- .../retrieve-a-data-contract.md | 16 ++-- .../retrieve-documents.md | 17 ++--- .../identities-and-names/retrieve-a-name.md | 54 +++++++------- .../retrieve-an-identity.md | 22 ++---- 7 files changed, 137 insertions(+), 78 deletions(-) diff --git a/_static/css/pydata-overrides.css b/_static/css/pydata-overrides.css index 9059e0414..49170096a 100644 --- a/_static/css/pydata-overrides.css +++ b/_static/css/pydata-overrides.css @@ -214,6 +214,24 @@ sphinx search extension interface. margin-bottom: 1rem; } +/* Integrated runner layout used directly beneath a tutorial code example. */ +.interactive-tutorial--integrated { + margin-top: -0.5rem; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.interactive-tutorial__toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin: -1rem -1rem 1rem; + padding: 0.65rem 1rem; + border-bottom: 1px solid var(--pst-color-border); + font-size: 0.9rem; +} + .interactive-tutorial__help, .interactive-tutorial__connection, .interactive-tutorial__empty { @@ -229,6 +247,10 @@ sphinx search extension interface. color: var(--pst-color-danger); } +.interactive-tutorial__connection:empty { + display: none; +} + .interactive-tutorial__label { display: block; margin-bottom: 0.35rem; @@ -270,7 +292,7 @@ sphinx search extension interface. } .interactive-tutorial__button { - padding: 0.55rem 0.85rem; + padding: 0.4rem 0.8rem; border: 1px solid var(--pst-color-primary); border-radius: 0.3rem; background: var(--pst-color-primary); @@ -284,6 +306,18 @@ sphinx search extension interface. color: var(--pst-color-primary); } +.interactive-tutorial__button--text { + padding-right: 0.25rem; + padding-left: 0.25rem; + border-color: transparent; + background: transparent; + color: var(--pst-color-primary); +} + +.interactive-tutorial__button--text:hover { + text-decoration: underline; +} + .interactive-tutorial__button:disabled { cursor: wait; opacity: 0.65; @@ -318,12 +352,23 @@ sphinx search extension interface. background: var(--pst-color-on-background); } +.interactive-tutorial--integrated .interactive-tutorial__result { + padding-top: 0.6rem; + padding-bottom: 0.6rem; +} + +.interactive-tutorial--integrated .interactive-tutorial__summary { + margin-bottom: 0.35rem; +} + .interactive-tutorial__summary { flex-wrap: wrap; margin-bottom: 0.75rem; } .interactive-tutorial__metric { + min-width: 0; + max-width: 100%; padding-right: 1rem; } @@ -332,6 +377,10 @@ sphinx search extension interface. display: block; } +.interactive-tutorial__metric strong { + overflow-wrap: anywhere; +} + .interactive-tutorial__metric span { color: var(--pst-color-text-muted); font-size: 0.75rem; @@ -355,4 +404,27 @@ sphinx search extension interface. align-items: stretch; flex-direction: column; } + + .interactive-tutorial__input, + .interactive-tutorial__field, + .interactive-tutorial__field--wide, + .interactive-tutorial__field--small { + flex: none; + width: 100%; + } + + .interactive-tutorial__summary { + align-items: stretch; + flex-direction: column; + } + + .interactive-tutorial__metric { + padding-right: 0; + } + + .interactive-tutorial__toolbar { + align-items: flex-start; + flex-direction: column; + gap: 0.2rem; + } } diff --git a/_static/js/interactive-tutorial.js b/_static/js/interactive-tutorial.js index 5c0f622af..eac5b3c41 100644 --- a/_static/js/interactive-tutorial.js +++ b/_static/js/interactive-tutorial.js @@ -49,6 +49,12 @@ const operations = { const text = (value) => String(value ?? '—'); +function integer(value) { + if (value == null || value === '') return '—'; + const number = Number(value); + return Number.isFinite(number) ? number.toLocaleString() : text(value); +} + function metric(label, value) { const wrapper = document.createElement('div'); wrapper.className = 'interactive-tutorial__metric'; @@ -95,7 +101,15 @@ function renderResult(container, rawValue, renderer) { } else if (renderer === 'documents') { summary.append(metric('Documents returned', value.length)); } else if (renderer === 'status') { - summary.append(metric('Connection', 'Successful')); + summary.append( + metric('Network', value.network?.chainId), + metric('Latest block', integer(value.chain?.latestBlockHeight)), + metric('Sync status', value.chain?.isCatchingUp ? 'Catching up' : 'Synced'), + metric('Peers', integer(value.network?.peersCount)), + metric('DAPI', value.version?.software?.dapi), + metric('Drive', value.version?.software?.drive), + metric('Tenderdash', value.version?.software?.tenderdash), + ); } const details = document.createElement('details'); diff --git a/docs/tutorials/connecting-to-testnet.md b/docs/tutorials/connecting-to-testnet.md index cd5d5744f..ffe5c2fb9 100644 --- a/docs/tutorials/connecting-to-testnet.md +++ b/docs/tutorials/connecting-to-testnet.md @@ -47,22 +47,18 @@ try { } ``` -### Try it - -Run the connection and status check directly from this page. - ```{raw} html -
-
- Connect to Dash Platform testnet - Not connected +
+
+ Run this example on testnet +
- +
- Code being run + View browser code
diff --git a/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md b/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md index f20597b7c..113905a6c 100644 --- a/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md +++ b/docs/tutorials/contracts-and-documents/retrieve-a-data-contract.md @@ -34,15 +34,11 @@ try { } ``` -## Try it - -Retrieve a contract from testnet without configuring a wallet or identity. - ```{raw} html -
-
- Retrieve a data contract from testnet - Not connected +
+
+ Run this example on testnet +
@@ -51,10 +47,10 @@ Retrieve a contract from testnet without configuring a wallet or identity. data-default-value="FW3DHrQiG24VqzPY4ARenMgjEPpBNuEQTZckV8hbVCG4" type="text" spellcheck="false" autocomplete="off" required> - +
- Code being run + View browser code
diff --git a/docs/tutorials/contracts-and-documents/retrieve-documents.md b/docs/tutorials/contracts-and-documents/retrieve-documents.md index 83fc39a7e..f1bb8d936 100644 --- a/docs/tutorials/contracts-and-documents/retrieve-documents.md +++ b/docs/tutorials/contracts-and-documents/retrieve-documents.md @@ -41,16 +41,11 @@ try { } ``` -## Try it - -Query documents on testnet. The limit is converted to a number by the displayed code before it is -passed to the SDK. - ```{raw} html -
-
- Retrieve documents from testnet - Not connected +
+
+ Run this example on testnet +
@@ -75,10 +70,10 @@ passed to the SDK.
- +
- Code being run + View browser code
diff --git a/docs/tutorials/identities-and-names/retrieve-a-name.md b/docs/tutorials/identities-and-names/retrieve-a-name.md index 08530cfe6..178217e37 100644 --- a/docs/tutorials/identities-and-names/retrieve-a-name.md +++ b/docs/tutorials/identities-and-names/retrieve-a-name.md @@ -34,6 +34,31 @@ try { } ``` +```{raw} html +
+
+ Run this example on testnet + +
+ +
+ + + +
+
+ View browser code +
+
+
+
Resolve the name to inspect its identity ID.
+
+
+``` + **Example Response** ```text @@ -121,35 +146,6 @@ Tutorial-Test-000000-backup.dash (ID: 98bruK9TdJki5xP8BYpmNXqdH9ZHzBD9phwDRzhaJs ::: :::: -## Try it - -Resolve a fully-qualified DPNS name to its identity ID. - -```{raw} html -
-
- Resolve a name on testnet - Not connected -
- -
- - - -
-
- Code being run -
-
-
-
Resolve the name to inspect its identity ID.
-
-
-``` - ## What's Happening After we initialize the Client, we request a name. The [code examples](#code) demonstrate the three ways to request a name: diff --git a/docs/tutorials/identities-and-names/retrieve-an-identity.md b/docs/tutorials/identities-and-names/retrieve-an-identity.md index 4573b3285..298eb88ef 100644 --- a/docs/tutorials/identities-and-names/retrieve-an-identity.md +++ b/docs/tutorials/identities-and-names/retrieve-an-identity.md @@ -38,22 +38,16 @@ try { } ``` -## Try it - -This read-only example connects directly to testnet from your browser. Enter any identity ID, or -use the example ID, and run the same `sdk.identities.fetch()` call used above. No wallet, mnemonic, -or signing key is required. - ```{raw} html
-
- Retrieve an identity from testnet - Not connected +
+ Run this example on testnet +
@@ -67,16 +61,12 @@ or signing key is required. required spellcheck="false" autocomplete="off" - aria-describedby="identity-lookup-help" > - -
-
- Runs sdk.identities.fetch(identityId) against Dash Platform testnet. +
- Code being run + View browser code
From 369ff1157fa6ba56795f6af76813a8f15f710c07 Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 18 Aug 2026 14:29:11 -0400 Subject: [PATCH 5/7] feat(tutorial): add interactive runners for contract history, names, and token info Add allow-listed operations for contract history, identity name lookup, DPNS prefix search, and token info, with matching renderers and runner blocks in the contract history, name retrieval, and token info tutorials. Support per-operation constants (DPNS contract ID) in the displayed source and rename the normalized Map key field to mapKey so the history renderer can read revision timestamps. --- _static/css/pydata-overrides.css | 4 + _static/js/interactive-tutorial.js | 101 +++++++++++++++++- .../retrieve-data-contract-history.md | 25 +++++ .../identities-and-names/retrieve-a-name.md | 50 +++++++++ docs/tutorials/tokens/retrieve-token-info.md | 51 +++++++++ 5 files changed, 230 insertions(+), 1 deletion(-) diff --git a/_static/css/pydata-overrides.css b/_static/css/pydata-overrides.css index 49170096a..619bf8922 100644 --- a/_static/css/pydata-overrides.css +++ b/_static/css/pydata-overrides.css @@ -280,6 +280,10 @@ sphinx search extension interface. margin-top: 0.75rem; } +.interactive-tutorial__controls--secondary { + margin-top: 0.75rem; +} + .interactive-tutorial__input { flex: 1 1 28rem; min-width: 0; diff --git a/_static/js/interactive-tutorial.js b/_static/js/interactive-tutorial.js index eac5b3c41..2d11f3eef 100644 --- a/_static/js/interactive-tutorial.js +++ b/_static/js/interactive-tutorial.js @@ -1,4 +1,5 @@ const SDK_URL = 'https://esm.sh/@dashevo/evo-sdk@4.1.0'; +const DPNS_CONTRACT_ID = 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec'; // Explicit allow-list of safe, read-only tutorial operations. The UI obtains // its displayed source directly from these functions, preventing code drift. @@ -23,6 +24,12 @@ async function fetchContract({ EvoSDK, dataContractId }) { return contract?.toJSON() ?? null; } +async function getContractHistory({ EvoSDK, dataContractId }) { + const sdk = EvoSDK.testnetTrusted(); + await sdk.connect(); + return sdk.contracts.getHistory({ dataContractId }); +} + async function queryDocuments({ EvoSDK, dataContractId, documentTypeName, limit }) { const sdk = EvoSDK.testnetTrusted(); await sdk.connect(); @@ -39,12 +46,62 @@ async function resolveName({ EvoSDK, name }) { return sdk.dpns.resolveName(name); } +async function getIdentityNames({ EvoSDK, identityId }) { + const sdk = EvoSDK.testnetTrusted(); + await sdk.connect(); + return sdk.dpns.usernames({ identityId }); +} + +async function searchNames({ EvoSDK, prefix }) { + const sdk = EvoSDK.testnetTrusted(); + await sdk.connect(); + const normalizedPrefix = await sdk.dpns.convertToHomographSafe(prefix); + return sdk.documents.query({ + dataContractId: DPNS_CONTRACT_ID, + documentTypeName: 'domain', + where: [ + ['normalizedParentDomainName', '==', 'dash'], + ['normalizedLabel', 'startsWith', normalizedPrefix], + ], + orderBy: [['normalizedLabel', 'asc']], + }); +} + +async function getTokenInfo({ EvoSDK, dataContractId, tokenPosition, identityId, recipientId }) { + const sdk = EvoSDK.testnetTrusted(); + await sdk.connect(); + const tokenId = await sdk.tokens.calculateId(dataContractId, Number(tokenPosition)); + const contractInfo = await sdk.tokens.contractInfo(tokenId); + const totalSupply = await sdk.tokens.totalSupply(tokenId); + const statuses = await sdk.tokens.statuses([tokenId]); + const identityBalances = await sdk.tokens.identityBalances(identityId, [tokenId]); + const recipientBalances = await sdk.tokens.identityBalances(recipientId, [tokenId]); + const status = statuses.get(tokenId); + + return { + tokenId: tokenId.toString(), + contractInfo: contractInfo?.toJSON() ?? null, + totalSupply: totalSupply?.totalSupply ?? 0n, + isPaused: status?.isPaused ?? null, + identityBalance: identityBalances.get(tokenId) ?? 0n, + recipientBalance: recipientBalances.get(tokenId) ?? 0n, + }; +} + const operations = { 'network-status': getNetworkStatus, 'identity-fetch': fetchIdentity, 'contract-fetch': fetchContract, + 'contract-history': getContractHistory, 'documents-query': queryDocuments, 'name-resolve': resolveName, + 'identity-names': getIdentityNames, + 'name-search': searchNames, + 'token-info': getTokenInfo, +}; + +const operationConstants = { + 'name-search': { DPNS_CONTRACT_ID }, }; const text = (value) => String(value ?? '—'); @@ -69,7 +126,7 @@ function metric(label, value) { function normalize(value) { if (value && typeof value.toJSON === 'function') return normalize(value.toJSON()); if (value instanceof Map) { - return [...value.entries()].map(([id, item]) => ({ id: text(id), ...normalize(item) })); + return [...value.entries()].map(([key, item]) => ({ mapKey: text(key), ...normalize(item) })); } if (Array.isArray(value)) return value.map(normalize); if (value && typeof value === 'object') { @@ -96,8 +153,45 @@ function renderResult(container, rawValue, renderer) { metric('Version', value.version), metric('Document types', Object.keys(value.documentSchemas ?? {}).length), ); + } else if (renderer === 'history') { + const timestamps = value + .map((entry) => Number(entry.mapKey)) + .filter(Number.isFinite) + .sort((a, b) => a - b); + const versions = value + .map((entry) => Number(entry.version)) + .filter(Number.isFinite); + summary.append( + metric('Revisions', value.length), + metric('Latest version', versions.length ? Math.max(...versions) : '—'), + metric('First revision', timestamps.length ? new Date(timestamps[0]).toLocaleString() : '—'), + metric('Latest revision', timestamps.length ? new Date(timestamps.at(-1)).toLocaleString() : '—'), + ); } else if (renderer === 'name') { summary.append(metric('Resolved identity ID', value)); + } else if (renderer === 'names') { + summary.append( + metric('Names returned', value.length), + metric('Names', value.length ? value.join(', ') : '(none)'), + ); + } else if (renderer === 'name-search') { + const names = value.map((entry) => { + const label = entry.label ?? entry.normalizedLabel; + const parent = entry.parentDomainName ?? entry.normalizedParentDomainName; + return [label, parent].filter(Boolean).join('.'); + }); + summary.append( + metric('Matches', value.length), + metric('Names', names.length ? names.join(', ') : '(none)'), + ); + } else if (renderer === 'token') { + summary.append( + metric('Token ID', value.tokenId), + metric('Total supply', integer(value.totalSupply)), + metric('Status', value.isPaused == null ? 'No status published' : value.isPaused ? 'Paused' : 'Active'), + metric('Identity balance', integer(value.identityBalance)), + metric('Recipient balance', integer(value.recipientBalance)), + ); } else if (renderer === 'documents') { summary.append(metric('Documents returned', value.length)); } else if (renderer === 'status') { @@ -157,12 +251,17 @@ async function initialize(block) { // Derive the display from the same function object invoked by run(). The DOM // remains non-executable, while current parameter values make the call clear. function updateDisplayedSource() { + const constants = Object.entries(operationConstants[operationName] ?? {}).map( + ([name, value]) => `const ${name} = ${JSON.stringify(value)};`, + ); const declarations = inputs.map( (input) => `const ${input.dataset.param} = ${JSON.stringify(input.value)};`, ); const argumentNames = inputs.map((input) => input.dataset.param); const invocationArguments = ['EvoSDK', ...argumentNames].join(', '); sourceElement.textContent = [ + ...constants, + constants.length ? '' : null, operation.toString(), '', ...declarations, diff --git a/docs/tutorials/contracts-and-documents/retrieve-data-contract-history.md b/docs/tutorials/contracts-and-documents/retrieve-data-contract-history.md index c5918c5c2..a36f438e4 100644 --- a/docs/tutorials/contracts-and-documents/retrieve-data-contract-history.md +++ b/docs/tutorials/contracts-and-documents/retrieve-data-contract-history.md @@ -49,6 +49,31 @@ try { } ``` +```{raw} html +
+
+ Run this example on testnet + +
+ +
+ + + +
+
+ View browser code +
+
+
+
Run the query to inspect contract revisions.
+
+
+``` + ## Example data contract history The following example response shows a retrieved contract history: diff --git a/docs/tutorials/identities-and-names/retrieve-a-name.md b/docs/tutorials/identities-and-names/retrieve-a-name.md index 178217e37..8a71f024f 100644 --- a/docs/tutorials/identities-and-names/retrieve-a-name.md +++ b/docs/tutorials/identities-and-names/retrieve-a-name.md @@ -91,6 +91,31 @@ try { } ``` +```{raw} html +
+
+ Run this example on testnet + +
+ +
+ + + +
+
+ View browser code +
+
+
+
Run the query to inspect names owned by this identity.
+
+
+``` + **Example Response** ```text @@ -136,6 +161,31 @@ try { } ``` +```{raw} html +
+
+ Run this example on testnet + +
+ +
+ + + +
+
+ View browser code +
+
+
+
Run the query to inspect matching names.
+
+
+``` + **Example Response** ```text diff --git a/docs/tutorials/tokens/retrieve-token-info.md b/docs/tutorials/tokens/retrieve-token-info.md index 38b3af04e..356a7bba9 100644 --- a/docs/tutorials/tokens/retrieve-token-info.md +++ b/docs/tutorials/tokens/retrieve-token-info.md @@ -72,6 +72,57 @@ try { } ``` +```{raw} html +
+
+ Run this example on testnet + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ View browser code +
+
+
+
Run the query to inspect token information and balances.
+
+
+``` + ## What's Happening After connecting to the client, we derive the token ID from the contract ID and token position with `sdk.tokens.calculateId()`. We then query several pieces of information: From 7ce44404c82b7d173c9c1295f8f3c61e4a50abef Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 18 Aug 2026 16:48:26 -0400 Subject: [PATCH 6/7] build(tutorial): serve Evo SDK from a local bundle instead of esm.sh Interactive tutorial runners now import a self-hosted esbuild bundle (_static/vendor/evo-sdk.js) built from the npm-pinned @dashevo/evo-sdk 4.1.1 rather than fetching esm.sh at runtime. Read the Docs generates the bundle in a pre_build job; locally, make html rebuilds it via the new sdk target after a one-time make sdk-install. Also bump the lite demo apps' esm.sh pin to 4.1.1 to match the bundled version, exclude node_modules from the Sphinx build, and document the setup in CLAUDE.md. --- .gitignore | 2 + .readthedocs.yml | 5 + CLAUDE.md | 8 +- Makefile | 23 +- _static/dashmint-lite.html | 2 +- _static/dashnote-lite.html | 2 +- _static/dashproof-lite.html | 2 +- _static/js/interactive-tutorial.js | 5 +- conf.py | 1 + package-lock.json | 519 +++++++++++++++++++++++++++++ package.json | 13 + scripts/evo-sdk-entry.js | 3 + 12 files changed, 579 insertions(+), 6 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/evo-sdk-entry.js diff --git a/.gitignore b/.gitignore index 4ccc56e36..564bb7782 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ _build **/.DS_Store .local/ .vscode +node_modules/ +_static/vendor/evo-sdk.js diff --git a/.readthedocs.yml b/.readthedocs.yml index a429d8270..6c283608a 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -12,6 +12,11 @@ build: os: "ubuntu-22.04" tools: python: "3.10" + nodejs: "22" + jobs: + pre_build: + - npm ci + - npm run build:sdk python: install: diff --git a/CLAUDE.md b/CLAUDE.md index 9f614f5ce..d91f54a54 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,12 @@ python -m venv venv source venv/bin/activate # or venv\Scripts\activate on Windows pip install -r requirements.txt -# Build documentation +# Install JavaScript build dependencies (once, on a fresh checkout; needed to +# bundle the Evo SDK for the interactive tutorial runners) +make sdk-install + +# Build documentation (also regenerates the gitignored SDK bundle via the +# `sdk` target) make html # Clean build @@ -64,6 +69,7 @@ The site uses a hierarchical structure with: - GitHub integration for edit links and source references - Google Analytics tracking configured - Uses pydata-sphinx-theme with custom CSS overrides +- Interactive tutorial runners (`_static/js/interactive-tutorial.js`) import the Evo SDK from `_static/vendor/evo-sdk.js`, a gitignored bundle produced by `npm run build:sdk` (esbuild, pinned via `package-lock.json`). Read the Docs builds it in a `pre_build` job; locally `make html` rebuilds it automatically (run `make sdk-install` once first). To change the SDK version, update `package.json`/`package-lock.json` and rebuild. ## Editing Guidelines diff --git a/Makefile b/Makefile index d4bb2cbb9..dc089887f 100644 --- a/Makefile +++ b/Makefile @@ -7,12 +7,33 @@ SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build +NPM ?= npm +SDK_BUNDLER = node_modules/.bin/esbuild # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -.PHONY: help Makefile +.PHONY: help sdk-install sdk html Makefile + +# Install the pinned JavaScript build dependencies for local development. +# Read the Docs performs this step separately in .readthedocs.yml. +sdk-install: + $(NPM) ci + +# Generate the browser SDK asset copied by Sphinx with the other static files. +sdk: $(SDK_BUNDLER) + $(NPM) run build:sdk + +# Give a useful setup hint instead of letting npm fail with "esbuild: not found". +$(SDK_BUNDLER): + @echo "Missing JavaScript build dependencies." + @echo "Run 'make sdk-install' once, then retry 'make html'." + @false + +# Keep the generated SDK bundle current for local HTML builds. +html: sdk + @$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). diff --git a/_static/dashmint-lite.html b/_static/dashmint-lite.html index 841166c65..c3be817cf 100644 --- a/_static/dashmint-lite.html +++ b/_static/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.1.0'; + import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@4.1.1'; // 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 diff --git a/_static/dashnote-lite.html b/_static/dashnote-lite.html index e1241e054..0c4438060 100644 --- a/_static/dashnote-lite.html +++ b/_static/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.1.0'; + import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@4.1.1'; // 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/_static/dashproof-lite.html b/_static/dashproof-lite.html index 8898daed1..583e2f3cd 100644 --- a/_static/dashproof-lite.html +++ b/_static/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.1.0'; + import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@4.1.1'; // 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/_static/js/interactive-tutorial.js b/_static/js/interactive-tutorial.js index 2d11f3eef..fd73a75c3 100644 --- a/_static/js/interactive-tutorial.js +++ b/_static/js/interactive-tutorial.js @@ -1,4 +1,7 @@ -const SDK_URL = 'https://esm.sh/@dashevo/evo-sdk@4.1.0'; +// Resolve from this script rather than the current documentation page so the +// URL works at every Sphinx nesting level. The bundle is generated by the +// Read the Docs pre-build job and copied with the other static assets. +const SDK_URL = new URL('../vendor/evo-sdk.js', document.currentScript.src).href; const DPNS_CONTRACT_ID = 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec'; // Explicit allow-list of safe, read-only tutorial operations. The UI obtains diff --git a/conf.py b/conf.py index ed4a12e0b..3123533ea 100644 --- a/conf.py +++ b/conf.py @@ -45,6 +45,7 @@ '.devcontainer', '.codex', '.local', + 'node_modules', 'scripts', 'img/dev/gifs/README.md', 'docs/other', diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..8ac94ae06 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,519 @@ +{ + "name": "dashpay-docs-platform", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dashpay-docs-platform", + "dependencies": { + "@dashevo/evo-sdk": "4.1.1" + }, + "devDependencies": { + "esbuild": "0.25.9" + } + }, + "node_modules/@dashevo/evo-sdk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@dashevo/evo-sdk/-/evo-sdk-4.1.1.tgz", + "integrity": "sha512-DsfHXlLByyhvAerDknozg0HT4KHNsYP6rEy949aj/KGLLydb9bwJBHOWyn5oDsNR016tvr3oeT/jCkjMA4qoCA==", + "dependencies": { + "@dashevo/wasm-sdk": "4.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/@dashevo/wasm-sdk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@dashevo/wasm-sdk/-/wasm-sdk-4.1.1.tgz", + "integrity": "sha512-/be0D7qohZc9fHgYjbtzOg8KOajarzo0NtKCNybv4Z1xeV617Wo0CusnsxCzwbV7C/jDgPJiGel0KtcSTzQcfQ==", + "engines": { + "node": ">=18.18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..b141f70b2 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "dashpay-docs-platform", + "private": true, + "scripts": { + "build:sdk": "esbuild scripts/evo-sdk-entry.js --bundle --format=esm --platform=browser --target=es2022 --minify --outfile=_static/vendor/evo-sdk.js" + }, + "dependencies": { + "@dashevo/evo-sdk": "4.1.1" + }, + "devDependencies": { + "esbuild": "0.25.9" + } +} diff --git a/scripts/evo-sdk-entry.js b/scripts/evo-sdk-entry.js new file mode 100644 index 000000000..cd2b19943 --- /dev/null +++ b/scripts/evo-sdk-entry.js @@ -0,0 +1,3 @@ +// Keep the browser-facing surface deliberately small. esbuild follows and +// bundles the SDK's transitive imports into the generated static asset. +export { EvoSDK } from '@dashevo/evo-sdk'; From c1a8ef5937055931d0c5b8d8e23392eb86522007 Mon Sep 17 00:00:00 2001 From: thephez Date: Wed, 19 Aug 2026 10:44:19 -0400 Subject: [PATCH 7/7] fix(tutorial): harden interactive runner input handling and run lifecycle - Validate number inputs against their min/max bounds before running and cap token position at 65535 - Guard runs with a generation counter so a reset cancels stale results instead of racing the UI - Format large integers via BigInt to avoid precision loss and cache the SDK import with retry after a failed load - Add Node 22 to the devcontainer and run make sdk-install during post-create so the SDK bundle builds - Document serving _build/html over HTTP since browsers block the SDK module import from file:// pages Co-Authored-By: Claude Fable 5 --- .devcontainer/devcontainer.json | 3 + .devcontainer/postCreateCommands.sh | 5 +- CLAUDE.md | 9 ++- _static/js/interactive-tutorial.js | 79 ++++++++++++++++---- conf.py | 6 +- docs/tutorials/tokens/retrieve-token-info.md | 2 +- 6 files changed, 81 insertions(+), 23 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b6d1da49d..d950633bf 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,6 +7,9 @@ // Features to add to the dev container. More info: https://containers.dev/features. "features": { "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/node:1": { + "version": "22" + }, "ghcr.io/devcontainers/features/python:1": {} }, diff --git a/.devcontainer/postCreateCommands.sh b/.devcontainer/postCreateCommands.sh index bc62f4925..2d43cfea2 100755 --- a/.devcontainer/postCreateCommands.sh +++ b/.devcontainer/postCreateCommands.sh @@ -1,4 +1,6 @@ -#/!bin/sh +#!/bin/sh + +set -e # Command to run during postCreateCommand # Done in script because when done via object the commands are run in parallel @@ -7,4 +9,5 @@ # https://containers.dev/implementors/json_reference/#formatting-string-vs-array-properties pip install -r requirements.txt +make sdk-install make html diff --git a/CLAUDE.md b/CLAUDE.md index d91f54a54..6d4be0a09 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,12 @@ python3 scripts/tutorial-sync/sync_tutorial_code.py --source /path/to/platform-t python3 scripts/tutorial-sync/sync_tutorial_code.py --check --source /path/to/platform-tutorials # View built documentation -# Open _build/html/index.html in browser +# Most pages can be opened directly in a browser +# Open _build/html/index.html + +# To use interactive tutorial widgets, serve the build over HTTP because +# browsers block their SDK module import from file:// pages +python -m http.server 8000 -d _build/html ``` ## Architecture @@ -106,4 +111,4 @@ For the full per-release endpoint review process (proto diff, example refresh, d - Configuration: `conf.py`, `requirements.txt` - Templates: `_templates/*.html` - Assets: `_static/**/*` -- Build output: `_build/html/` (excluded from git) \ No newline at end of file +- Build output: `_build/html/` (excluded from git) diff --git a/_static/js/interactive-tutorial.js b/_static/js/interactive-tutorial.js index fd73a75c3..16d6bef39 100644 --- a/_static/js/interactive-tutorial.js +++ b/_static/js/interactive-tutorial.js @@ -111,8 +111,14 @@ const text = (value) => String(value ?? '—'); function integer(value) { if (value == null || value === '') return '—'; - const number = Number(value); - return Number.isFinite(number) ? number.toLocaleString() : text(value); + if (typeof value === 'bigint') return value.toLocaleString(); + if (typeof value === 'number') { + return Number.isSafeInteger(value) ? value.toLocaleString() : text(value); + } + if (typeof value === 'string' && /^[+-]?\d+$/.test(value)) { + return BigInt(value).toLocaleString(); + } + return text(value); } function metric(label, value) { @@ -145,7 +151,7 @@ function renderResult(container, rawValue, renderer) { if (renderer === 'identity') { summary.append( metric('Identity ID', value.id), - metric('Balance (credits)', Number(value.balance ?? 0).toLocaleString()), + metric('Balance (credits)', integer(value.balance ?? 0)), metric('Revision', value.revision), metric('Public keys', value.publicKeys?.length ?? 0), ); @@ -274,17 +280,19 @@ async function initialize(block) { } updateDisplayedSource(); - let EvoSDK; + let sdkPromise; async function loadSdk() { - if (EvoSDK) return EvoSDK; - connection.textContent = 'Loading SDK…'; - connection.dataset.state = 'connecting'; - ({ EvoSDK } = await import(SDK_URL)); - connection.textContent = `SDK loaded · ${network}`; - connection.dataset.state = 'connected'; - return EvoSDK; + sdkPromise ??= import(SDK_URL).then( + ({ EvoSDK }) => EvoSDK, + (error) => { + sdkPromise = undefined; + throw error; + }, + ); + return sdkPromise; } + let runGeneration = 0; async function run() { const missing = inputs.find((input) => input.required && !input.value.trim()); if (missing) { @@ -293,29 +301,63 @@ async function initialize(block) { return; } + const invalidNumber = inputs.find((input) => { + if (input.type !== 'number') return false; + const value = Number(input.value); + const minimum = input.min === '' ? -Infinity : Number(input.min); + const maximum = input.max === '' ? Infinity : Number(input.max); + return !Number.isFinite(value) + || !Number.isInteger(value) + || value < minimum + || value > maximum; + }); + if (invalidNumber) { + const bounds = [ + invalidNumber.min === '' ? null : `at least ${invalidNumber.min}`, + invalidNumber.max === '' ? null : `at most ${invalidNumber.max}`, + ].filter(Boolean).join(' and '); + renderMessage( + result, + `Enter ${invalidNumber.dataset.label ?? 'a value'} as a whole number${bounds ? ` ${bounds}` : ''}.`, + true, + ); + invalidNumber.focus(); + return; + } + + const parameters = Object.fromEntries( + inputs.map((input) => [input.dataset.param, input.value]), + ); + const generation = ++runGeneration; + const isCurrent = () => generation === runGeneration; runButton.disabled = true; inputs.forEach((input) => { input.disabled = true; }); renderMessage(result, 'Running query…'); try { + connection.textContent = 'Loading SDK…'; + connection.dataset.state = 'connecting'; const sdkClass = await loadSdk(); + if (!isCurrent()) return; connection.textContent = `Connecting to ${network}…`; - const parameters = Object.fromEntries( - inputs.map((input) => [input.dataset.param, input.value]), - ); const output = await operation({ ...parameters, EvoSDK: sdkClass }); + if (!isCurrent()) return; connection.textContent = `Connected to ${network}`; + connection.dataset.state = 'connected'; if (output == null) { renderMessage(result, 'No result was found.', true); return; } renderResult(result, output, renderer); } catch (error) { + if (!isCurrent()) return; connection.textContent = 'Run failed'; connection.dataset.state = 'error'; renderMessage(result, `Query failed: ${error?.message ?? error}`, true); } finally { - runButton.disabled = false; - inputs.forEach((input) => { input.disabled = false; }); + if (isCurrent()) { + runButton.disabled = false; + inputs.forEach((input) => { input.disabled = false; }); + } } } @@ -327,7 +369,12 @@ async function initialize(block) { }); }); resetButton.addEventListener('click', () => { + runGeneration += 1; inputs.forEach((input) => { input.value = input.dataset.defaultValue ?? ''; }); + runButton.disabled = false; + inputs.forEach((input) => { input.disabled = false; }); + connection.textContent = ''; + delete connection.dataset.state; updateDisplayedSource(); renderMessage(result, 'Run the query to inspect the result.'); inputs[0]?.focus(); diff --git a/conf.py b/conf.py index 3123533ea..e99b3bacb 100644 --- a/conf.py +++ b/conf.py @@ -143,7 +143,7 @@ def setup(app): app.add_js_file('js/pydata-search-close.js') - # Keep this a classic script so locally built docs also work when opened - # directly via file://. Module scripts loaded from file:// are blocked by - # browser CORS rules; the SDK itself is still loaded lazily over HTTPS. + # Keep this a classic script so it can resolve the generated SDK bundle + # relative to its own URL. Local previews must serve _build/html over HTTP; + # browsers block the bundle's module import from file:// pages. app.add_js_file('js/interactive-tutorial.js', defer='defer') diff --git a/docs/tutorials/tokens/retrieve-token-info.md b/docs/tutorials/tokens/retrieve-token-info.md index 356a7bba9..34d95b59c 100644 --- a/docs/tutorials/tokens/retrieve-token-info.md +++ b/docs/tutorials/tokens/retrieve-token-info.md @@ -90,7 +90,7 @@ try { + type="number" min="0" max="65535" required>