Skip to content

feat: add send to platform address mode#15

Merged
PastaPastaPasta merged 3 commits into
PastaPastaPasta:mainfrom
thephez:feat/send-to-platform-address-v2
Mar 3, 2026
Merged

feat: add send to platform address mode#15
PastaPastaPasta merged 3 commits into
PastaPastaPasta:mainfrom
thephez:feat/send-to-platform-address-v2

Conversation

@thephez
Copy link
Copy Markdown
Contributor

@thephez thephez commented Feb 19, 2026

Based on #14

Add bridge mode for sending L1 DASH to any bech32m platform address
via asset lock, without requiring the recipient's private key.

  • Add 'send_to_address' BridgeMode with recipient address entry UI
  • Add sendToPlatformAddress() using sdk.addresses.fundFromAssetLock()
  • Add platformHrp to NetworkConfig for bech32m address validation
  • Use pure JS bech32m.decode() from @scure/base (avoids WASM init issues)
  • Add mode-aware recheckDeposit branching
  • XSS-escape user-supplied addresses in innerHTML

Tested locally. All operations worked.
Preview: https://thephez.github.io/dash-bridge/

Co-authored-by: PastaClaw pastaclaw@users.noreply.github.com
Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

Summary by CodeRabbit

  • New Features

    • Added "Send to Platform Address" mode with recipient entry, bech32m validation, end-to-end transfer, and a "Send complete" final step.
    • UI flows, processing screens, and backups/download exports now include recipient address info and adjusted filenames.
    • Network configs include platform address prefixes for proper address validation.
  • Chores

    • Improved address validation, user-facing error messaging, and logging.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 99a977b and 489f91b.

📒 Files selected for processing (2)
  • src/main.ts
  • src/platform/identity.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main.ts
  • src/platform/identity.ts

📝 Walkthrough

Walkthrough

Adds a “send to platform address” flow: network HRP config, bech32m platform-address validation, new UI steps/mode/state, a platform API sendToPlatformAddress that funds a recipient from an asset lock, and orchestration wiring through the main deposit/finalization flow.

Changes

Cohort / File(s) Summary
Configuration & Types
src/config.ts, src/types.ts
Added platformHrp to NetworkConfig (TESTNET: tdash, MAINNET: dash); added send_to_address mode, enter_recipient_address & sending_to_address steps, and recipientPlatformAddress?: string in BridgeState.
Platform Module
src/platform/identity.ts, src/platform/index.ts
Added sendToPlatformAddress(...) which builds AssetLockProof, derives PrivateKey from WIF, uses PlatformAddressSigner and calls sdk.addresses.fundFromAssetLock; re-exported in platform index.
Main Flow & Orchestration
src/main.ts
Introduced UI event handlers, bech32m validatePlatformAddress, recipient address state, new mode/button, branching in deposit/finalization to support top-up and send_to_address, and instrumentation around InstantSend and send flow.
UI Components & Rendering
src/ui/components.ts
Added renderEnterRecipientAddressStep, wired enter_recipient_address rendering, updated Deposit/Processing/Complete steps and backups/downloads to reflect send_to_address flows and include recipient address in outputs.
State Management & UI API
src/ui/state.ts, src/ui/index.ts
Added setRecipientPlatformAddress and setSendToAddressComplete, clearModeSensitiveFields, updated setMode to initialize send_to_address flow, and extended step descriptions/progress; re-exported new setters.
Package
package.json
Added dependency @scure/base@2.0.0 for bech32m/base usage.

Sequence Diagram

sequenceDiagram
    participant User as User/UI
    participant Main as Main Flow
    participant Validator as Validator
    participant State as State Manager
    participant Platform as Platform Module
    participant SDK as evo-sdk

    User->>Main: Select "Send to Platform Address"
    Main->>State: setMode('send_to_address')
    State-->>Main: enter_recipient_address

    User->>Main: Enter recipient address + continue
    Main->>Validator: validatePlatformAddress(addr, platformHrp)
    Validator-->>Main: valid
    Main->>State: setRecipientPlatformAddress(addr)

    Main->>Main: Generate deposit keys, request deposit
    User->>Main: Fund deposit
    Main->>Main: Wait for deposit + InstantSend lock
    Main->>Main: Build AssetLockProof
    Main->>State: setStep('sending_to_address')

    Main->>Platform: sendToPlatformAddress(addr, proofData, wif, network)
    Platform->>SDK: sdk.addresses.fundFromAssetLock(proof, outputs)
    SDK-->>Platform: tx confirmed

    Platform-->>Main: { success: true, recipientAddress }
    Main->>State: setSendToAddressComplete()
    Main-->>User: Display completion
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I hopped across the bridge with glee,
Bech32m bright, the HRP for me.
A lock, a proof, a parcel sent —
Recipient smiles, the funds are spent! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main feature added: a new 'send to platform address' mode for the Dash bridge application.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 5

🧹 Nitpick comments (4)
src/ui/state.ts (1)

126-131: clearModeSensitiveFields preserves recipientPlatformAddress for send_to_address, but the send_to_address branch immediately overrides it with undefined — the preservation is dead code.

The conditional mode === 'send_to_address' ? state.recipientPlatformAddress : undefined in clearModeSensitiveFields is immediately negated by the explicit recipientPlatformAddress: undefined on line 93. Either:

  • Remove the conditional and always set recipientPlatformAddress: undefined in the helper (the send_to_address branch handles its own init), or
  • Preserve the address on re-entry and remove the explicit undefined from the branch (if that UX is desired).
♻️ Simplified version (always clear, let the branch handle init)
-function clearModeSensitiveFields(state: BridgeState, mode: BridgeMode): BridgeState {
+function clearModeSensitiveFields(state: BridgeState): BridgeState {
   return {
     ...state,
-    recipientPlatformAddress: mode === 'send_to_address' ? state.recipientPlatformAddress : undefined,
+    recipientPlatformAddress: undefined,
   };
 }

And update all three call sites:

-  const clearedState = clearModeSensitiveFields(state, mode);
+  const clearedState = clearModeSensitiveFields(state);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/state.ts` around lines 126 - 131, clearModeSensitiveFields currently
returns recipientPlatformAddress conditionally but that value is immediately
clobbered by the send_to_address branch which sets recipientPlatformAddress =
undefined; to fix, always clear recipientPlatformAddress inside
clearModeSensitiveFields (replace the ternary with recipientPlatformAddress:
undefined) and let the send_to_address branch initialize it if needed, then
update all three call sites that use clearModeSensitiveFields to match this
behavior; refer to the clearModeSensitiveFields function, the
recipientPlatformAddress field, and the branch that handles 'send_to_address'
when making the change.
src/main.ts (1)

1094-1194: startSendToAddress nearly duplicates startTopUp — consider extracting common asset-lock flow.

Steps 1–6 (key generation → deposit wait → tx build → sign → broadcast → wait for IS lock → build proof) are identical in startTopUp, startSendToAddress, and partially in startBridge. Only Step 7 diverges. This triplication makes it easy for a bug fix to be applied to one path but missed in others.

A shared helper like executeAssetLockFlow(state, insightClient, dapiClient) returning the proof + WIF could eliminate ~80 duplicated lines across the three functions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.ts` around lines 1094 - 1194, startSendToAddress duplicates the
asset-lock steps present in startTopUp and startBridge; extract Steps 1–6 (key
generation, deposit wait, tx build, sign, broadcast, wait for IS lock, build
proof) into a shared helper (e.g., executeAssetLockFlow) that takes the
necessary inputs (state, insightClient, dapiClient, network) and returns the
assetLockProof and assetLockPrivateKeyWif (and any metadata like txid/utxo if
needed); replace the duplicated blocks in startSendToAddress, startTopUp, and
startBridge with calls to executeAssetLockFlow, ensure updateState calls remain
in the appropriate caller (or have the helper return intermediate results so
callers can update state), and update error handling to propagate exceptions
consistently.
src/ui/components.ts (1)

868-879: Backup note is misleading for send_to_address mode.

The else branch covers both topup and send_to_address, but the note on line 877 reads "One-time key for top-up. Use this WIF to recover funds if top-up fails.". For send_to_address mode, this is inaccurate. Also, backup.targetIdentityId will be undefined for send_to_address, which is harmless but noisy in the JSON.

♻️ Differentiate the backup content by mode
-  } else {
-    // For top-up mode: include target identity and one-time key
-    backup.targetIdentityId = state.targetIdentityId;
-    backup.assetLockKey = state.assetLockKeyPair
-      ? {
-          wif: privateKeyToWif(state.assetLockKeyPair.privateKey, network),
-          publicKeyHex: bytesToHex(state.assetLockKeyPair.publicKey),
-          note: 'One-time key for top-up. Use this WIF to recover funds if top-up fails.',
-        }
-      : null;
+  } else {
+    // For top-up / send_to_address mode: include one-time key
+    if (isTopUp) {
+      backup.targetIdentityId = state.targetIdentityId;
+    }
+    const modeLabel = isSendToAddress ? 'send-to-address' : 'top-up';
+    backup.assetLockKey = state.assetLockKeyPair
+      ? {
+          wif: privateKeyToWif(state.assetLockKeyPair.privateKey, network),
+          publicKeyHex: bytesToHex(state.assetLockKeyPair.publicKey),
+          note: `One-time key for ${modeLabel}. Use this WIF to recover funds if the operation fails.`,
+        }
+      : null;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/components.ts` around lines 868 - 879, The backup branch conflates
top-up and send_to_address modes: update the else block to inspect the operation
mode (e.g., state.mode or equivalent) and produce mode-specific backup fields —
for topup set backup.targetIdentityId = state.targetIdentityId only when
state.targetIdentityId is defined and set assetLockKey.note to the top-up
wording, and for send_to_address omit targetIdentityId and use an accurate note
for assetLockKey (or null) based on state.assetLockKeyPair; adjust the code
around backup.targetIdentityId, backup.assetLockKey, state.targetIdentityId and
state.assetLockKeyPair to avoid emitting undefined/noisy fields and to use the
correct note per mode.
src/platform/identity.ts (1)

440-449: Document the WASM serde constraint for fundFromAssetLock parameters instead of using as any casts.

The outputs and feeStrategy parameters are cast to any because the SDK's WASM serde deserializer requires plain objects rather than typed instances. While this workaround is already explained in an inline comment, defining local interfaces that document the expected shape would improve clarity and catch unintended API shape changes at review time. Since @dashevo/evo-sdk@3.1.0-dev.1 is a pre-release version, this would provide better future-proofing before the stable release.

For example:

interface FundFromAssetLockOutput {
  address: string;
}

interface FeeStrategy {
  type: 'reduceOutput';
  index: number;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/platform/identity.ts` around lines 440 - 449, The outputs and feeStrategy
are currently cast to any to satisfy the WASM serde requirement; define local
interfaces (e.g. FundFromAssetLockOutput { address: string } and FeeStrategy {
type: 'reduceOutput'; index: number }) near the call and use them to type the
arrays passed into sdk.addresses.fundFromAssetLock (the call inside withRetry)
instead of using as any, so the shapes are documented and checked while still
providing plain object values for the WASM deserializer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Line 13: The package.json currently pins a pre-release "@dashevo/evo-sdk" to
"3.1.0-dev.1", which is unstable; update the dependency version for
"@dashevo/evo-sdk" to the stable 3.1.x release when it becomes available (for
example change "3.1.0-dev.1" to a stable semver like "^3.1.0" or "3.1.x") so the
project uses the released API instead of the dev pre-release.
- Line 13: The project directly imports bech32m from `@scure/base` in src/main.ts
but `@scure/base` is not declared in package.json; add `@scure/base` as an explicit
dependency in package.json (pin a compatible version — e.g., match versions used
by `@scure/bip32/`@scure/bip39 or the latest stable) so the direct import in
src/main.ts (bech32m) will resolve reliably even if transitive deps change.

In `@src/platform/dpns.ts`:
- Around line 84-85: The current disabled check in src/platform/dpns.ts uses
strict undefined comparison for key.disabledAt; change the isDisabled
computation (the isDisabled variable that reads key.disabledAt) to a null-safe
check (e.g., treat both null and undefined as "not set") by using a
null-coalescing or != null style test so keys with disabledAt: null are not
considered disabled.

In `@src/platform/identity.ts`:
- Around line 452-470: The current validation in the fundFromAssetLock response
path (variable result and the maybeResult block) treats any presence of
maybeResult.message as an error and can false-positive on successful responses;
change the check to only treat explicit failure signals as errors (e.g.,
maybeResult.success === false or maybeResult.error !== undefined) and do not
treat maybeResult.message !== undefined as an error condition, or alternatively
validate against the SDK's documented response shape instead of checking for
message; update the error construction thrown from fundFromAssetLock to use only
those explicit failure conditions so successful responses with a message field
are accepted.
- Around line 23-42: The local hexToBytes in src/platform/identity.ts duplicates
the implementation in src/utils/hex.ts but adds an empty/null check; remove the
duplication by either (A) updating the hexToBytes in src/utils/hex.ts to include
the same !hex/empty-string validation and then import and use that single
hexToBytes from src/platform/identity.ts, or (B) if the stricter validation is
intentionally specific to identity operations, keep the local hexToBytes and add
a comment explaining the divergence and ensure callers of src/utils/hex.ts are
not relying on the empty-string check; locate the functions by name hexToBytes
and base64ToBytes when making the change.

---

Nitpick comments:
In `@src/main.ts`:
- Around line 1094-1194: startSendToAddress duplicates the asset-lock steps
present in startTopUp and startBridge; extract Steps 1–6 (key generation,
deposit wait, tx build, sign, broadcast, wait for IS lock, build proof) into a
shared helper (e.g., executeAssetLockFlow) that takes the necessary inputs
(state, insightClient, dapiClient, network) and returns the assetLockProof and
assetLockPrivateKeyWif (and any metadata like txid/utxo if needed); replace the
duplicated blocks in startSendToAddress, startTopUp, and startBridge with calls
to executeAssetLockFlow, ensure updateState calls remain in the appropriate
caller (or have the helper return intermediate results so callers can update
state), and update error handling to propagate exceptions consistently.

In `@src/platform/identity.ts`:
- Around line 440-449: The outputs and feeStrategy are currently cast to any to
satisfy the WASM serde requirement; define local interfaces (e.g.
FundFromAssetLockOutput { address: string } and FeeStrategy { type:
'reduceOutput'; index: number }) near the call and use them to type the arrays
passed into sdk.addresses.fundFromAssetLock (the call inside withRetry) instead
of using as any, so the shapes are documented and checked while still providing
plain object values for the WASM deserializer.

In `@src/ui/components.ts`:
- Around line 868-879: The backup branch conflates top-up and send_to_address
modes: update the else block to inspect the operation mode (e.g., state.mode or
equivalent) and produce mode-specific backup fields — for topup set
backup.targetIdentityId = state.targetIdentityId only when
state.targetIdentityId is defined and set assetLockKey.note to the top-up
wording, and for send_to_address omit targetIdentityId and use an accurate note
for assetLockKey (or null) based on state.assetLockKeyPair; adjust the code
around backup.targetIdentityId, backup.assetLockKey, state.targetIdentityId and
state.assetLockKeyPair to avoid emitting undefined/noisy fields and to use the
correct note per mode.

In `@src/ui/state.ts`:
- Around line 126-131: clearModeSensitiveFields currently returns
recipientPlatformAddress conditionally but that value is immediately clobbered
by the send_to_address branch which sets recipientPlatformAddress = undefined;
to fix, always clear recipientPlatformAddress inside clearModeSensitiveFields
(replace the ternary with recipientPlatformAddress: undefined) and let the
send_to_address branch initialize it if needed, then update all three call sites
that use clearModeSensitiveFields to match this behavior; refer to the
clearModeSensitiveFields function, the recipientPlatformAddress field, and the
branch that handles 'send_to_address' when making the change.

Comment thread package.json
Comment thread src/platform/dpns.ts
Comment thread src/platform/identity.ts
Comment thread src/platform/identity.ts
@thephez
Copy link
Copy Markdown
Contributor Author

thephez commented Feb 19, 2026

@coderabbitai only the last commit of this branch matters. The other commits are covered in #14 which this branch is based on. Do not comment on any of those commits

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 19, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@thephez: Understood! I'll focus only on the last commit in this PR and ignore the commits from PR #14. Let me identify the last commit and its changes.


🏁 Script executed:

#!/bin/bash
# Get the list of commits in PR `#15`
echo "=== Commits in PR `#15` ==="
gh pr view 15 --json commits --jq '.commits[] | "\(.oid[0:7]) - \(.messageHeadline) - \(.committedDate)"'

echo -e "\n=== Last commit details ==="
gh pr view 15 --json commits --jq '.commits[-1] | "SHA: \(.oid)\nMessage: \(.messageHeadline)\nDate: \(.committedDate)\nAuthor: \(.author.login)"'

echo -e "\n=== Files changed in the last commit ==="
LAST_COMMIT=$(gh pr view 15 --json commits --jq '.commits[-1].oid')
git show --name-status --pretty=format:"" $LAST_COMMIT

Length of output: 660


@thephez: Perfect! I can see the last commit in this PR is 7b32426 - "Add workflow_dispatch trigger to deploy workflow", which only modifies .github/workflows/deploy.yml.

Let me examine what changed in that commit:


🏁 Script executed:

#!/bin/bash
# Show the actual diff for the last commit
LAST_COMMIT=$(gh pr view 15 --json commits --jq '.commits[-1].oid')
echo "=== Changes in commit $LAST_COMMIT ==="
git show $LAST_COMMIT

Length of output: 569


@thephez: Got it! I'll focus exclusively on commit 7b32426 - "Add workflow_dispatch trigger to deploy workflow".

Review of Last Commit

The change adds a workflow_dispatch: trigger to the GitHub Pages deployment workflow. This is a clean, straightforward addition that:

Enables manual deployment triggers - The workflow can now be run manually from the GitHub Actions UI in addition to automatic deployment on pushes to main

Preserves existing behavior - The automatic deployment on push to main remains unchanged

Follows best practices - Adding workflow_dispatch to deployment workflows is a common pattern that provides useful flexibility for maintainers

The change looks good with no issues! This gives you the ability to manually redeploy if needed without requiring a push to main.


🧠 Learnings used
Learnt from: CR
Repo: PastaPastaPasta/dash-bridge PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-23T20:00:00.425Z
Learning: Supported bridge modes are: create (for identity creation via HD wallet) and topup (for adding credits to existing identity using random one-time keypair)

@thephez thephez force-pushed the feat/send-to-platform-address-v2 branch from 7b32426 to 3581eaf Compare February 19, 2026 21:59
@thephez
Copy link
Copy Markdown
Contributor Author

thephez commented Feb 19, 2026

@coderabbitai review latest commit

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 19, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the latest commit in PR `#15`
echo "=== Latest commit in PR `#15` ==="
gh pr view 15 --json commits --jq '.commits[-1] | "SHA: \(.oid)\nShort SHA: \(.oid[0:7])\nMessage: \(.messageHeadline)\nDate: \(.committedDate)\nAuthor: \(.author.login)"'

echo -e "\n=== Files changed in the latest commit ==="
LAST_COMMIT=$(gh pr view 15 --json commits --jq -r '.commits[-1].oid')
git show --name-status --pretty=format:"" $LAST_COMMIT

echo -e "\n=== Full diff of the latest commit ==="
git show $LAST_COMMIT

Length of output: 32527


@thephez: I'll review commit 3581eaf - "feat: add send to platform address mode".

Review of Latest Commit

This commit adds a comprehensive send-to-platform-address feature. Here's my analysis:

Security - Well HandledGood! XSS danger comes from injecting user and third-party markup into the DOM, and proper sanitization is essential. The code correctly uses escapeHtml() for user addresses.

1. XSS Protection

  • User-supplied recipientPlatformAddress is properly escaped using escapeHtml() before being inserted via innerHTML
  • This prevents malicious script injection attempts

2. Address Validation

  • Uses bech32m.decode() from @scure/base for cryptographic validation
  • Checks that the decoded prefix matches the network's platformHrp (tdash/dash)
  • Good defensive programming with try-catch

⚠️ Type Safety Concerns

The sendToPlatformAddress() function in src/platform/identity.ts has multiple as any casts:

outputs: [{ address: recipientAddress }] as any,
// ...
feeStrategy: [{ type: 'reduceOutput', index: 0 }] as any,

Issues:

  • Defeats TypeScript's type checking
  • Comment mentions "WASM serde deserializer expects plain objects" - this suggests a type definition mismatch
  • Makes future refactoring risky

Recommendation: Consider:

  1. Defining proper TypeScript interfaces that match the WASM expectations
  2. Filing an issue with the SDK to export proper types for these parameters
  3. At minimum, add a detailed code comment explaining why the casts are necessary and under what conditions they might break

Architecture - Well Designed

Mode-aware branching in recheckDeposit():

if (state.mode === 'topup') {
  // topup flow
} else if (state.mode === 'send_to_address') {
  // send to address flow
} else {
  // create mode - register identity
}

This is clean and maintainable. The separation keeps each mode's logic isolated.

UX - Good Progressive Disclosure

  • New enter_recipient_address step with validation
  • Clear error messages: "Please enter a valid bech32m platform address (starts with {prefix})"
  • Mode-aware headlines and descriptions throughout the UI
  • Proper handling of truncated addresses for display

State Management

  • New fields properly typed in BridgeState
  • clearModeSensitiveFields() helper prevents state leakage between modes
  • recipientPlatformAddress only persists when in send_to_address mode

🔍 Minor Observations

  1. Backup filename generation includes recipient address hash - good for user recognition
  2. Continue button is properly disabled when no address is entered
  3. Key backup includes recipientPlatformAddress for send_to_address mode - helpful for recovery/records

Summary

The implementation is solid with good security practices. The main concern is the type safety issue with as any casts, which should be addressed either through proper typing or at minimum with detailed documentation explaining the SDK's constraints.


🧠 Learnings used
Learnt from: CR
Repo: PastaPastaPasta/dash-bridge PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-23T20:00:00.425Z
Learning: Supported bridge modes are: create (for identity creation via HD wallet) and topup (for adding credits to existing identity using random one-time keypair)

Add bridge mode for sending L1 DASH to any bech32m platform address
via asset lock, without requiring the recipient's private key.

- Add 'send_to_address' BridgeMode with recipient address entry UI
- Add sendToPlatformAddress() using sdk.addresses.fundFromAssetLock()
- Add platformHrp to NetworkConfig for bech32m address validation
- Use pure JS bech32m.decode() from @scure/base (avoids WASM init issues)
- Add mode-aware recheckDeposit branching
- XSS-escape user-supplied addresses in innerHTML

Co-authored-by: PastaClaw <pastaclaw@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thephez thephez force-pushed the feat/send-to-platform-address-v2 branch from 3581eaf to b2f755a Compare February 20, 2026 23:12
@thephez
Copy link
Copy Markdown
Contributor Author

thephez commented Feb 20, 2026

Rebased to main

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/ui/components.ts (1)

870-879: ⚠️ Potential issue | 🟡 Minor

Backup note is misleading for send_to_address mode.

The note field is hard-coded to 'One-time key for top-up. Use this WIF to recover funds if top-up fails.', but this else branch now covers send_to_address mode as well. Additionally, backup.targetIdentityId is written as state.targetIdentityId (which will be undefined in send_to_address mode), producing a spurious undefined field in the recovery JSON.

🐛 Proposed fix
   } else {
-    // For top-up mode: include target identity and one-time key
-    backup.targetIdentityId = state.targetIdentityId;
+    // For top-up/send_to_address mode: include one-time asset-lock key
+    if (isTopUp) {
+      backup.targetIdentityId = state.targetIdentityId;
+    }
     backup.assetLockKey = state.assetLockKeyPair
       ? {
           wif: privateKeyToWif(state.assetLockKeyPair.privateKey, network),
           publicKeyHex: bytesToHex(state.assetLockKeyPair.publicKey),
-          note: 'One-time key for top-up. Use this WIF to recover funds if top-up fails.',
+          note: isSendToAddress
+            ? 'One-time key for send-to-address. Use this WIF to recover funds if the send fails.'
+            : 'One-time key for top-up. Use this WIF to recover funds if top-up fails.',
         }
       : null;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/components.ts` around lines 870 - 879, The backup generation currently
always sets backup.targetIdentityId = state.targetIdentityId (which is undefined
in send_to_address mode) and unconditionally uses a top-up specific note; update
the logic in the component that builds backup (the block that assigns
backup.targetIdentityId and backup.assetLockKey) to: 1) only set
backup.targetIdentityId when state.mode indicates top-up (e.g., state.mode ===
'top_up' or whatever mode flag your code uses), 2) set the assetLockKey.note
dynamically based on the mode (use the existing top-up message for top-up mode
and a send-to-address appropriate message for send_to_address mode), and 3)
ensure you set backup.targetIdentityId to null or omit the property for
send_to_address so no undefined is emitted (adjust the branch around
backup.targetIdentityId and assetLockKey assignment in the same function/block
to implement these conditional assignments).
🧹 Nitpick comments (2)
src/platform/identity.ts (1)

444-446: Consider replacing as any casts with typed interfaces or a documented workaround comment.

Both outputs and feeStrategy casts bypass TypeScript's type checking entirely. While the existing comment explains the WASM serde motivation, these casts will silently break if the SDK's deserializer expectation changes. A minimal improvement is to define local structural interfaces matching what the WASM layer actually expects:

♻️ Proposed refactor
+// These plain-object shapes match what the WASM serde deserializer expects.
+// See: https://github.com/<org>/<repo>/issues/<N> for tracking proper SDK types.
+interface PlatformAddressOutput { address: string }
+interface FeeStrategyEntry { type: string; index: number }
+
   const result = await withRetry(
     () => sdk.addresses.fundFromAssetLock({
       assetLockProof,
       assetLockPrivateKey,
-      outputs: [{ address: recipientAddress }] as any,
+      outputs: [{ address: recipientAddress }] as unknown as PlatformAddressOutput[],
       signer,
-      feeStrategy: [{ type: 'reduceOutput', index: 0 }] as any,
+      feeStrategy: [{ type: 'reduceOutput', index: 0 }] as unknown as FeeStrategyEntry[],
     }),
     retryOptions
   );

This narrows the cast surface so that structural mismatches (e.g., a renamed field) produce a TypeScript error rather than a silent runtime failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/platform/identity.ts` around lines 444 - 446, The code is using unsafe
casts to any for the transaction fields outputs and feeStrategy (the object
built with outputs: [{ address: recipientAddress }], signer, feeStrategy: [{
type: 'reduceOutput', index: 0 }]) which hides mismatches from the WASM
deserializer; replace these casts by declaring small local structural interfaces
(e.g., OutputShape { address: string }, FeeStrategyShape { type: string; index?:
number } or more precise shapes matching the WASM expectations) and use them for
outputs and feeStrategy instead of as any, or if a deliberate mismatch is
unavoidable add a short comment linking to the WASM serde contract and mark the
cast with a specific TODO comment; update the object definitions that reference
outputs, feeStrategy, recipientAddress and signer to use the new interfaces so
TypeScript will surface structural errors rather than allowing silent runtime
failures.
src/main.ts (1)

1097-1194: startSendToAddress duplicates all six steps of startTopUp verbatim.

Steps 1–6 (key generation, deposit wait, tx build, tx sign, broadcast, InstantSend) are copy-pasted from startTopUp. Only step 7 differs. Consider extracting the shared asset-lock flow into a helper:

♻️ Suggested refactor sketch
+/**
+ * Shared steps 1-6: generate key pair, wait for deposit, build/sign/broadcast tx,
+ * wait for InstantSend lock, and return the proof + WIF for step 7.
+ */
+async function buildAssetLockProof(initialState: BridgeState): Promise<{
+  assetLockProof: AssetLockProofData;
+  assetLockPrivateKeyWif: string;
+  stateWithKeys: BridgeState;
+}> {
+  const network = getNetwork(initialState.network);
+  updateState(setStep(initialState, 'generating_keys'));
+  const assetLockKeyPair = generateKeyPair();
+  const depositAddress = publicKeyToAddress(assetLockKeyPair.publicKey, network);
+  const stateWithKeys = setOneTimeKeyPair(initialState, assetLockKeyPair, depositAddress);
+  updateState(stateWithKeys);
+  downloadKeyBackup(stateWithKeys);
+  updateState(setStep(stateWithKeys, 'detecting_deposit'));
+  // ... (steps 2-6, identical to both functions)
+  return { assetLockProof, assetLockPrivateKeyWif, stateWithKeys };
+}

Both startTopUp and startSendToAddress would then call buildAssetLockProof and only implement their specific step 7 logic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.ts` around lines 1097 - 1194, startSendToAddress duplicates steps
1–6 from startTopUp; extract the shared asset-lock flow into a helper (e.g.,
buildAssetLockProof or extractAssetLockFlow) that performs key generation,
deposit waiting, TX creation, signing, broadcasting and InstantSend wait and
returns the necessary outputs (assetLockProof, assetLockPrivateKeyWif,
signedTxHex/txid, utxo, etc.) and any state updates; then replace the duplicated
code in startSendToAddress and startTopUp with a call to that helper and keep
only the platform-specific Step 7 (sendToPlatformAddress in startSendToAddress
and top-up logic in startTopUp), preserving existing updateState calls and error
handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main.ts`:
- Line 9: The project imports bech32m from '@scure/base' (import { bech32m }
from '@scure/base') but that package is not declared in package.json; add
'@scure/base' to the dependencies in package.json (matching the appropriate
version used by `@scure/bip32/`@scure/bip39 or a compatible semver) and run
npm/yarn install to ensure clean installs succeed.

---

Outside diff comments:
In `@src/ui/components.ts`:
- Around line 870-879: The backup generation currently always sets
backup.targetIdentityId = state.targetIdentityId (which is undefined in
send_to_address mode) and unconditionally uses a top-up specific note; update
the logic in the component that builds backup (the block that assigns
backup.targetIdentityId and backup.assetLockKey) to: 1) only set
backup.targetIdentityId when state.mode indicates top-up (e.g., state.mode ===
'top_up' or whatever mode flag your code uses), 2) set the assetLockKey.note
dynamically based on the mode (use the existing top-up message for top-up mode
and a send-to-address appropriate message for send_to_address mode), and 3)
ensure you set backup.targetIdentityId to null or omit the property for
send_to_address so no undefined is emitted (adjust the branch around
backup.targetIdentityId and assetLockKey assignment in the same function/block
to implement these conditional assignments).

---

Duplicate comments:
In `@src/platform/identity.ts`:
- Around line 462-470: The current failure check incorrectly treats any presence
of maybeResult.message as an error; update the conditional in the block handling
maybeResult so that a message field on a successful response does not trigger an
exception—only treat the result as failure when maybeResult.success === false or
maybeResult.error is defined (keep using maybeResult.error ??
maybeResult.message ?? 'unknown error' for the thrown details but only when
actually failing). Locate the conditional referencing maybeResult.success,
maybeResult.error, and maybeResult.message and remove the message presence check
(or guard message usage behind a failing success flag) so successful responses
with a message no longer false-positive.

---

Nitpick comments:
In `@src/main.ts`:
- Around line 1097-1194: startSendToAddress duplicates steps 1–6 from
startTopUp; extract the shared asset-lock flow into a helper (e.g.,
buildAssetLockProof or extractAssetLockFlow) that performs key generation,
deposit waiting, TX creation, signing, broadcasting and InstantSend wait and
returns the necessary outputs (assetLockProof, assetLockPrivateKeyWif,
signedTxHex/txid, utxo, etc.) and any state updates; then replace the duplicated
code in startSendToAddress and startTopUp with a call to that helper and keep
only the platform-specific Step 7 (sendToPlatformAddress in startSendToAddress
and top-up logic in startTopUp), preserving existing updateState calls and error
handling.

In `@src/platform/identity.ts`:
- Around line 444-446: The code is using unsafe casts to any for the transaction
fields outputs and feeStrategy (the object built with outputs: [{ address:
recipientAddress }], signer, feeStrategy: [{ type: 'reduceOutput', index: 0 }])
which hides mismatches from the WASM deserializer; replace these casts by
declaring small local structural interfaces (e.g., OutputShape { address: string
}, FeeStrategyShape { type: string; index?: number } or more precise shapes
matching the WASM expectations) and use them for outputs and feeStrategy instead
of as any, or if a deliberate mismatch is unavoidable add a short comment
linking to the WASM serde contract and mark the cast with a specific TODO
comment; update the object definitions that reference outputs, feeStrategy,
recipientAddress and signer to use the new interfaces so TypeScript will surface
structural errors rather than allowing silent runtime failures.

Comment thread src/main.ts
Previously imported as a transitive dependency via @scure/bip32 and
@scure/bip39, which could break on dependency updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thephez
Copy link
Copy Markdown
Contributor Author

thephez commented Mar 2, 2026

@PastaPastaPasta addressed coderabbit comment. Can we get this one merged this week?

- Remove speculative `message` check from fundFromAssetLock error
  handling that could false-positive on successful results
- Make recheckDeposit() create-mode check explicit and throw on
  unexpected modes instead of silently defaulting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@PastaPastaPasta PastaPastaPasta merged commit f1642aa into PastaPastaPasta:main Mar 3, 2026
3 checks passed
@thephez thephez deleted the feat/send-to-platform-address-v2 branch March 3, 2026 15:48
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.

2 participants