Skip to content

feat: add devnet support#25

Merged
PastaPastaPasta merged 3 commits into
mainfrom
claude/recursing-kalam-06be62
May 22, 2026
Merged

feat: add devnet support#25
PastaPastaPasta merged 3 commits into
mainfrom
claude/recursing-kalam-06be62

Conversation

@PastaPastaPasta
Copy link
Copy Markdown
Member

@PastaPastaPasta PastaPastaPasta commented May 17, 2026

Summary

Adds devnet support so the bridge can be used on networks other than testnet/mainnet. Users can pick the hardcoded devnet-tadi preset or enter a custom devnet config (DAPI addresses, Insight URL, optional RPC URL, optional faucet) which is persisted to localStorage. Custom devnets can also be loaded via ?network=<name> URL param.

Platform connection: For devnets, uses new EvoSDK({ addresses, network: 'testnet', trusted: true, ... }) to point at custom HP masternodes.

IS lock retrieval: Dual-stack approach.

  • Mainnet/testnet: races JSON-RPC (digitalcash.dev) against DAPI subscription via Promise.any — first success wins.
  • Devnets: DAPI subscription only (no JSON-RPC proxy exists). Uses bloom filter + subscribeToTransactionsWithProofs, ported from the WIP branch feat/remove-insight-wip.

Dependencies added: @dashevo/dapi-client, @dashevo/dashcore-lib, vite-plugin-node-polyfills. These are Node-heavy packages that significantly increase bundle size (~22MB). Upstream cleanup of these packages for browser use is being investigated separately; this PR accepts the polyfill cost for now to unblock devnet usage.

Trade-offs: trusted: true for custom devnets means responses aren't proof-verified — acceptable since users control which devnet they connect to.

Test plan

  • npm run build passes (TypeScript + Vite production build)
  • Default testnet/mainnet selection still works
  • ?network=devnet-tadi URL param loads devnet correctly (purple DEVNET-TADI badge)
  • Devnet dropdown opens and shows preset + Custom devnet... option
  • Custom devnet modal renders all 5 fields with placeholders
  • No console errors on page load (devnet or testnet)
  • End-to-end deposit + IS lock flow on a live devnet (manual test required)
  • Custom devnet form save → localStorage round-trip (manual test required)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added support for custom devnets with a new configuration modal allowing users to connect to custom networks beyond testnet and mainnet
    • Improved network selection UI with dropdown menu for available devnets
    • Enhanced InstantSend lock waiting with dual backend support and improved fallback mechanisms

Review Change Stack

Bridge now supports devnets (e.g. devnet-tadi) alongside testnet/mainnet. Users can pick a preset devnet or enter custom config (DAPI addresses, Insight URL, optional RPC URL) which is persisted to localStorage.

Platform connections to devnets use EvoSDK with custom masternode addresses and trusted mode. IS lock retrieval uses a dual-stack approach: JSON-RPC and DAPI subscription race in parallel for mainnet/testnet (first wins), DAPI subscription only for devnets which have no JSON-RPC proxy.
- Collapse 4 duplicated waitForInstantSendLock call sites in main.ts

- Move hash160 derivation inside DAPISubscriptionClient

- Flatten Promise/IIFE nesting; remove resolved flag

- Eagerly construct dapi-client in constructor

- Set modal input values via DOM properties (XSS-safe)

- Warn on unknown network fallback to testnet
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Warning

Rate limit exceeded

@PastaPastaPasta has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 27 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 924c255b-df03-400a-8710-21020bffebc8

📥 Commits

Reviewing files that changed from the base of the PR and between 1ec459b and abd1fff.

📒 Files selected for processing (2)
  • index.html
  • src/main.ts
📝 Walkthrough

Walkthrough

This PR introduces custom devnet support and replaces direct JSON-RPC InstantSend lock polling with a dual-backend service that races JSON-RPC and DAPI subscription requests. It systematically widens network type parameters from literal unions to strings, enabling support for arbitrary network names persisted via localStorage, and integrates the new infrastructure through client initialization, bridge operations, and UI components.

Changes

Devnet and InstantSend infrastructure

Layer / File(s) Summary
Network configuration and registry
src/config.ts
NetworkConfig expanded with type discriminator and optional dapiAddresses/rpcUrl; new DEVNET_TADI constant; registry-backed storage system for custom devnets with localStorage persistence; helper functions to create, save, remove, initialize, and query devnets; getNetwork now accepts generic string and falls back to TESTNET when not found.
InstantSend lock service with dual backends
src/api/dapi-subscription.ts, src/api/islock.ts, src/api/dapi.ts, src/types/external.d.ts
New DAPISubscriptionClient wraps DAPI subscriptions: builds Bloom filters, subscribes to transactions-with-proofs, scans for matching InstantLock txid with timeout enforcement. New IslockService races JSON-RPC polling (when configured) against subscription via Promise.any. DAPIClient generalized to accept any network string and optional rpcUrl override. Ambient type declarations added for @dashevo/dapi-client (DAPIClient class, BloomFilter/Subscription APIs) and @dashevo/dashcore-lib (BloomFilter and InstantLock interfaces).
Network type signature widening
src/crypto/hd.ts, src/crypto/keys.ts, src/platform/identity.ts, src/platform/dpns.ts, src/platform/contract.ts, src/types.ts, src/ui/state.ts, src/main.ts
All exported functions with network: 'testnet' | 'mainnet' parameter changed to network: string. Affects HD wallet derivation paths, identity key generation, platform identity/DPNS operations, contract publishing, state management, and address validation. Runtime behavior unchanged; only compile-time constraints relaxed.
Client initialization, network switching, and InstantSend integration
src/main.ts
New initClients(network) and switchNetwork(network) orchestrate InsightClient and IslockService reinitialization. New showCustomDevnetModal collects devnet parameters, persists via createCustomDevnetConfig/saveCustomDevnet, disconnects platform SDK, and switches networks. Network selector updated to handle data-network attributes and __custom__ option triggering modal. Bridge flows (startTopUp, startSendToAddress, startBridge, recheckDeposit) replace dapiClient.waitForInstantSendLock(txid, timeout) with islockService.waitForInstantSendLock(txid, assetLockPublicKey, utxo).
Platform SDK creation and UI components
src/platform/client.ts, src/ui/components.ts
PlatformNetwork type broadened to generic string. createPlatformSdk consults getNetwork(network) and branches on config.type: mainnet uses EvoSDK.mainnetTrusted, devnet with dapiAddresses builds trusted SDK with those addresses. Explorer URL builder returns undefined for unsupported networks. Network badge class derived from getNetwork(state.network).type with escaped display. Init network selector expands from Testnet/Mainnet buttons to devnet dropdown from getAvailableNetworks() with Custom devnet option. escapeHtml exported for reuse.
Build configuration and dependencies
package.json, vite.config.ts, tsconfig.json, index.html
package.json adds @dashevo/dapi-client and vite-plugin-node-polyfills dependencies. Vite config applies Node polyfills (Buffer, global, process) and pre-bundles @dashevo/dapi-client/@dashevo/dashcore-lib; adds commonjsOptions.transformMixedEsModules: true. TypeScript lib updated from ES2020 to ES2021. HTML adds CSS for devnet button states, dropdown menu/options, devnet badge, and custom devnet modal (overlay, container sizing, form styling, action row).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • dashpay/dash-bridge#21: Modifies the same platform identity functions (registerIdentity, topUpIdentity, updateIdentity, sendToPlatformAddress) in src/platform/identity.ts to add hardened SDK settings, overlapping with this PR's type signature changes.
  • dashpay/dash-bridge#15: Impacts the send_to_address flow that this PR refactors by switching from direct dapiClient.waitForInstantSendLock to the new dual-backend islockService.waitForInstantSendLock pattern.
  • dashpay/dash-bridge#23: Modifies src/platform/client.ts to change Platform SDK networking model (network type widening vs. centralized connect helpers), with overlapping code-level changes.

Poem

🐰 Hops through networks both old and new,
Devnets bloom in configs true,
Two paths race for locks so bright—
JSON-RPC meets subscription's flight!
Type constraints gently slip away,
Strings flow free in every way. 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat: add devnet support' directly and clearly summarizes the main change: adding devnet (development network) support to the bridge application.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/recursing-kalam-06be62

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/api/islock.ts`:
- Around line 39-45: The two backend calls (jsonRpcPromise and dapiPromise from
jsonRpcClient.waitForInstantSendLock and
subscriptionClient.waitForInstantSendLock) keep running after Promise.any
resolves; add cancellation so the loser is stopped. Create separate
AbortController (or similar cancel token) per call, pass their signals into
waitForInstantSendLock (or extend those methods to accept a signal), await
Promise.any, then in the then/catch cleanup call abort() on the controller(s)
for any promise that did not win (or call the clients' cancel/unsubscribe
methods if available) so the polling/subscription is terminated immediately.
Ensure waitForInstantSendLock implementations honor the signal or add a cancel
API if needed.

In `@src/config.ts`:
- Around line 93-98: Prevent custom devnets from overriding built-in networks by
rejecting any save or delete that uses a reserved name; in saveCustomDevnet (and
the corresponding custom-devnet remove/update functions around the 106-126 area)
add a guard against reserved names (e.g., "mainnet" and "testnet" or a
RESERVED_NETWORK_NAMES Set) before mutating localStorage or NETWORK_REGISTRY,
and return/throw an error or no-op when the name is reserved; use
loadCustomDevnets, CUSTOM_DEVNETS_KEY and NETWORK_REGISTRY to determine existing
custom entries but never allow replacing entries whose name is in the reserved
set.
- Around line 72-87: Update loadCustomDevnets to more strictly validate
deserialized objects before returning them: in the filter/type-guard for
NetworkConfig (function loadCustomDevnets) add checks that c.minFee and
c.dustThreshold are numbers, that c.dapiAddresses is an array and
c.dapiAddresses.every(a => typeof a === 'string'), and keep the existing checks
(name, type==='devnet', insightApiUrl, addressPrefix, wifPrefix, platformHrp).
Also ensure JSON.parse is wrapped so failures fall through to returning []
(preserve the try/catch behavior) so malformed localStorage entries never get
inserted into the registry.

In `@src/main.ts`:
- Around line 317-320: The devnet toggle handler uses devnetMenu.style.display
=== 'none' which fails when the initial display is an empty string set by CSS;
update the click handler for devnetToggle to check the actual computed
visibility (use getComputedStyle(devnetMenu).display === 'none') or better:
toggle a CSS class (e.g., 'hidden') on devnetMenu via
devnetMenu.classList.toggle('hidden') so the first click reliably opens the
menu; modify the listener attached to devnetToggle accordingly (refer to
devnetToggle.addEventListener and devnetMenu).
- Around line 132-135: switchNetwork currently replaces clients without tearing
down the old IslockService, leaking subscriptions; before calling
initClients(network) retrieve the existing IslockService instance from global
state (or wherever it’s stored), call its shutdown/close/disconnect/unsubscribe
method (the actual API on IslockService) to stop subscriptions, clear that
reference, then proceed to updateState(setNetwork(...)) and call
initClients(network) to create and store the new instance; reference the
switchNetwork function, initClients function, and the IslockService instance
name used in your state to locate where to add the teardown.

In `@src/platform/client.ts`:
- Around line 41-50: The code silently falls back to EvoSDK.testnetTrusted()
when config.type === 'devnet' but config.dapiAddresses is missing/empty; change
this to fail fast by validating the devnet config before returning a client: in
the branch that checks config.type === 'devnet', verify that
config.dapiAddresses is a non-empty array and if not throw an Error (or reject)
with a clear message (e.g., "devnet selected but dapiAddresses missing"),
otherwise construct and return new EvoSDK({...}) as before; update any callers
expecting a client to handle the thrown error.

In `@src/ui/components.ts`:
- Around line 277-281: The devnetOptions map is using escapeHtml(d.name)
(text-safe) for both the button label and the data-network attribute, which can
break the quoted attribute if the devnet name contains a quote; update the
mapping in the devnetOptions block so the button text still uses
escapeHtml(d.name) but the data-network attribute uses an attribute-safe escape
(e.g., an escapeAttr or encodeAttribute function that escapes quotes and other
attribute-special characters) when producing data-network="${...}"; adjust the
same change for the other similar block (lines referenced in the comment) and
keep references to escapeHtml, devnetOptions, state.network and devnets when
making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f44c1aec-321d-4163-9f66-736e778bb8cb

📥 Commits

Reviewing files that changed from the base of the PR and between e1a28e4 and 1ec459b.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (19)
  • index.html
  • package.json
  • src/api/dapi-subscription.ts
  • src/api/dapi.ts
  • src/api/islock.ts
  • src/config.ts
  • src/crypto/hd.ts
  • src/crypto/keys.ts
  • src/main.ts
  • src/platform/client.ts
  • src/platform/contract.ts
  • src/platform/dpns.ts
  • src/platform/identity.ts
  • src/types.ts
  • src/types/external.d.ts
  • src/ui/components.ts
  • src/ui/state.ts
  • tsconfig.json
  • vite.config.ts

Comment thread src/api/islock.ts
Comment on lines +39 to +45
// Race JSON-RPC polling against DAPI subscription — first success wins
const jsonRpcPromise = this.jsonRpcClient.waitForInstantSendLock(txid, timeoutMs, onRetry);
const dapiPromise = this.subscriptionClient.waitForInstantSendLock(txid, publicKey, utxo, timeoutMs);

try {
return await Promise.any([jsonRpcPromise, dapiPromise]);
} catch (error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Cancel the losing backend after Promise.any resolves.

The losing request keeps running (polling or subscription stream) until its own timeout. This adds unnecessary network load and leaves transient resources alive longer than needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/islock.ts` around lines 39 - 45, The two backend calls
(jsonRpcPromise and dapiPromise from jsonRpcClient.waitForInstantSendLock and
subscriptionClient.waitForInstantSendLock) keep running after Promise.any
resolves; add cancellation so the loser is stopped. Create separate
AbortController (or similar cancel token) per call, pass their signals into
waitForInstantSendLock (or extend those methods to accept a signal), await
Promise.any, then in the then/catch cleanup call abort() on the controller(s)
for any promise that did not win (or call the clients' cancel/unsubscribe
methods if available) so the polling/subscription is terminated immediately.
Ensure waitForInstantSendLock implementations honor the signal or add a cancel
API if needed.

Comment thread src/config.ts
Comment on lines +72 to +87
function loadCustomDevnets(): NetworkConfig[] {
try {
const stored = localStorage.getItem(CUSTOM_DEVNETS_KEY);
if (!stored) return [];
const parsed = JSON.parse(stored);
if (!Array.isArray(parsed)) return [];
return parsed.filter(
(c): c is NetworkConfig =>
c &&
typeof c.name === 'string' &&
c.type === 'devnet' &&
typeof c.insightApiUrl === 'string' &&
typeof c.addressPrefix === 'number' &&
typeof c.wifPrefix === 'number' &&
typeof c.platformHrp === 'string'
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden custom-devnet deserialization validation before registry load.

loadCustomDevnets() currently accepts records without validating required numeric fields used downstream (minFee, dustThreshold) and without validating dapiAddresses. A malformed localStorage entry can load into the registry and later fail at runtime.

Suggested fix
     return parsed.filter(
       (c): c is NetworkConfig =>
         c &&
         typeof c.name === 'string' &&
         c.type === 'devnet' &&
         typeof c.insightApiUrl === 'string' &&
         typeof c.addressPrefix === 'number' &&
         typeof c.wifPrefix === 'number' &&
-        typeof c.platformHrp === 'string'
+        typeof c.minFee === 'number' &&
+        typeof c.dustThreshold === 'number' &&
+        typeof c.platformHrp === 'string' &&
+        Array.isArray(c.dapiAddresses) &&
+        c.dapiAddresses.every((a: unknown) => typeof a === 'string')
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/config.ts` around lines 72 - 87, Update loadCustomDevnets to more
strictly validate deserialized objects before returning them: in the
filter/type-guard for NetworkConfig (function loadCustomDevnets) add checks that
c.minFee and c.dustThreshold are numbers, that c.dapiAddresses is an array and
c.dapiAddresses.every(a => typeof a === 'string'), and keep the existing checks
(name, type==='devnet', insightApiUrl, addressPrefix, wifPrefix, platformHrp).
Also ensure JSON.parse is wrapped so failures fall through to returning []
(preserve the try/catch behavior) so malformed localStorage entries never get
inserted into the registry.

Comment thread src/config.ts
Comment on lines +93 to +98
export function saveCustomDevnet(config: NetworkConfig): void {
const customs = loadCustomDevnets().filter((c) => c.name !== config.name);
customs.push(config);
localStorage.setItem(CUSTOM_DEVNETS_KEY, JSON.stringify(customs));
NETWORK_REGISTRY.set(config.name, config);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Block overriding built-in network names in custom devnet saves.

Line 93 allows saving any name, so a custom entry can replace testnet/mainnet in NETWORK_REGISTRY. That can silently reroute core flows under a trusted devnet config.

Suggested fix
+const RESERVED_NETWORK_NAMES = new Set(['testnet', 'mainnet', 'devnet-tadi']);
+
 export function saveCustomDevnet(config: NetworkConfig): void {
+  if (config.type !== 'devnet') {
+    throw new Error('Only devnet configs can be saved as custom networks');
+  }
+  if (RESERVED_NETWORK_NAMES.has(config.name)) {
+    throw new Error(`"${config.name}" is reserved and cannot be overridden`);
+  }
   const customs = loadCustomDevnets().filter((c) => c.name !== config.name);
   customs.push(config);
   localStorage.setItem(CUSTOM_DEVNETS_KEY, JSON.stringify(customs));
   NETWORK_REGISTRY.set(config.name, config);
 }

As per coding guidelines, "Centralize network configuration (testnet/mainnet) in config.ts, with network selection determined by URL parameter ?network=mainnet (defaults to testnet)".

Also applies to: 106-126

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/config.ts` around lines 93 - 98, Prevent custom devnets from overriding
built-in networks by rejecting any save or delete that uses a reserved name; in
saveCustomDevnet (and the corresponding custom-devnet remove/update functions
around the 106-126 area) add a guard against reserved names (e.g., "mainnet" and
"testnet" or a RESERVED_NETWORK_NAMES Set) before mutating localStorage or
NETWORK_REGISTRY, and return/throw an error or no-op when the name is reserved;
use loadCustomDevnets, CUSTOM_DEVNETS_KEY and NETWORK_REGISTRY to determine
existing custom entries but never allow replacing entries whose name is in the
reserved set.

Comment thread src/main.ts
Comment on lines +132 to +135
function switchNetwork(network: string): void {
updateState(setNetwork(state, network));
initClients(network);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Disconnect previous IslockService instance during network switch.

Line 134 replaces the client without cleaning up the previous subscription client. Repeated switches can leak open connections until GC/timeouts.

Suggested fix
-function switchNetwork(network: string): void {
+async function switchNetwork(network: string): Promise<void> {
+  await islockService?.disconnect().catch(() => {});
   updateState(setNetwork(state, network));
   initClients(network);
 }
-        switchNetwork(network);
+        void switchNetwork(network);

As per coding guidelines, "Use main.ts as the entry point for UI orchestration and global state management, initializing network configuration and coordinating the bridge workflow".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.ts` around lines 132 - 135, switchNetwork currently replaces clients
without tearing down the old IslockService, leaking subscriptions; before
calling initClients(network) retrieve the existing IslockService instance from
global state (or wherever it’s stored), call its
shutdown/close/disconnect/unsubscribe method (the actual API on IslockService)
to stop subscriptions, clear that reference, then proceed to
updateState(setNetwork(...)) and call initClients(network) to create and store
the new instance; reference the switchNetwork function, initClients function,
and the IslockService instance name used in your state to locate where to add
the teardown.

Comment thread src/main.ts
Comment on lines +317 to +320
devnetToggle.addEventListener('click', (e) => {
e.stopPropagation();
devnetMenu.style.display = devnetMenu.style.display === 'none' ? 'flex' : 'none';
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Devnet menu toggle can require an extra click on first open.

The toggle checks devnetMenu.style.display === 'none', but initial value is often '' when hidden via CSS class. First click may set 'none' again instead of opening.

Suggested fix
     devnetToggle.addEventListener('click', (e) => {
       e.stopPropagation();
-      devnetMenu.style.display = devnetMenu.style.display === 'none' ? 'flex' : 'none';
+      const isHidden = getComputedStyle(devnetMenu).display === 'none';
+      devnetMenu.style.display = isHidden ? 'flex' : 'none';
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
devnetToggle.addEventListener('click', (e) => {
e.stopPropagation();
devnetMenu.style.display = devnetMenu.style.display === 'none' ? 'flex' : 'none';
});
devnetToggle.addEventListener('click', (e) => {
e.stopPropagation();
const isHidden = getComputedStyle(devnetMenu).display === 'none';
devnetMenu.style.display = isHidden ? 'flex' : 'none';
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.ts` around lines 317 - 320, The devnet toggle handler uses
devnetMenu.style.display === 'none' which fails when the initial display is an
empty string set by CSS; update the click handler for devnetToggle to check the
actual computed visibility (use getComputedStyle(devnetMenu).display === 'none')
or better: toggle a CSS class (e.g., 'hidden') on devnetMenu via
devnetMenu.classList.toggle('hidden') so the first click reliably opens the
menu; modify the listener attached to devnetToggle accordingly (refer to
devnetToggle.addEventListener and devnetMenu).

Comment thread src/platform/client.ts
Comment on lines +41 to 50
if (config.type === 'devnet' && config.dapiAddresses?.length) {
return new EvoSDK({
addresses: config.dapiAddresses,
network: 'testnet',
trusted: true,
...options,
});
}

return EvoSDK.testnetTrusted(options);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast on invalid devnet config instead of silently using testnet.

When config.type is devnet but dapiAddresses is empty/missing, this currently falls back to EvoSDK.testnetTrusted(). That can route platform operations to the wrong network under a devnet selection.

Suggested fix
 function createPlatformSdk(network: PlatformNetwork): EvoSDK {
   const options = { settings: PLATFORM_REQUEST_SETTINGS };
   const config = getNetwork(network);

   if (config.type === 'mainnet') {
     return EvoSDK.mainnetTrusted(options);
   }

-  if (config.type === 'devnet' && config.dapiAddresses?.length) {
-    return new EvoSDK({
-      addresses: config.dapiAddresses,
-      network: 'testnet',
-      trusted: true,
-      ...options,
-    });
+  if (config.type === 'devnet') {
+    if (!config.dapiAddresses?.length) {
+      throw new Error(`Devnet "${network}" is missing DAPI addresses`);
+    }
+    return new EvoSDK({
+      addresses: config.dapiAddresses,
+      network: 'testnet',
+      trusted: true,
+      ...options,
+    });
   }

   return EvoSDK.testnetTrusted(options);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/client.ts` around lines 41 - 50, The code silently falls back to
EvoSDK.testnetTrusted() when config.type === 'devnet' but config.dapiAddresses
is missing/empty; change this to fail fast by validating the devnet config
before returning a client: in the branch that checks config.type === 'devnet',
verify that config.dapiAddresses is a non-empty array and if not throw an Error
(or reject) with a clear message (e.g., "devnet selected but dapiAddresses
missing"), otherwise construct and return new EvoSDK({...}) as before; update
any callers expecting a client to handle the thrown error.

Comment thread src/ui/components.ts
Comment on lines +277 to +281
const devnetOptions = devnets
.map((d) => {
const safe = escapeHtml(d.name);
return `<button class="devnet-option ${state.network === d.name ? 'active' : ''}" data-network="${safe}">${safe}</button>`;
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Escape data-network for attribute context (not text context).

escapeHtml() is text-safe, but this value is also injected into a quoted attribute. A devnet name containing " can break data-network and inject markup/script.

🔧 Proposed fix
+function escapeHtmlAttribute(str: string): string {
+  return str
+    .replace(/&/g, '&amp;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&`#39`;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;');
+}
+
   const devnetOptions = devnets
     .map((d) => {
-      const safe = escapeHtml(d.name);
-      return `<button class="devnet-option ${state.network === d.name ? 'active' : ''}" data-network="${safe}">${safe}</button>`;
+      const safeText = escapeHtml(d.name);
+      const safeAttr = escapeHtmlAttribute(d.name);
+      return `<button class="devnet-option ${state.network === d.name ? 'active' : ''}" data-network="${safeAttr}">${safeText}</button>`;
     })
     .join('');

Also applies to: 283-294

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/components.ts` around lines 277 - 281, The devnetOptions map is using
escapeHtml(d.name) (text-safe) for both the button label and the data-network
attribute, which can break the quoted attribute if the devnet name contains a
quote; update the mapping in the devnetOptions block so the button text still
uses escapeHtml(d.name) but the data-network attribute uses an attribute-safe
escape (e.g., an escapeAttr or encodeAttribute function that escapes quotes and
other attribute-special characters) when producing data-network="${...}"; adjust
the same change for the other similar block (lines referenced in the comment)
and keep references to escapeHtml, devnetOptions, state.network and devnets when
making the change.

@PastaPastaPasta PastaPastaPasta merged commit f8545eb into main May 22, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant