Skip to content

Integrate escrow app - #261

Closed
Kukks wants to merge 34 commits into
masterfrom
integrate-escrow-app
Closed

Kukks wants to merge 34 commits into
masterfrom
integrate-escrow-app

Conversation

@Kukks

@Kukks Kukks commented Dec 10, 2025 •

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Introduced Escrow app with embedded iframe, wallet-backed signing/funding flows, and deep-link navigation
    • Added Escrow app icon and an Escrow entry in the Apps list
    • Added a new "Sign" option in Advanced settings
  • Documentation

    • Updated README and environment variable docs (including new escrow URL variable)
  • Tests

    • Added comprehensive RPC handler tests for escrow interactions

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Dec 10, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds an Escrow app: UI icon and screen, an iframe host with keep-alive and origin validation, an RPC message handler for wallet operations (signing, balance, funding, keep-alive), navigation/deeplink wiring, tests, and updated docs/type for a new environment variable.

Changes

Cohort / File(s) Summary
Documentation & Types
README.md, src/vite-env.d.ts
Reworked Environment Variables table and added VITE_ARK_ESCROW_URL declaration in Vite types.
Icon Component
src/icons/Escrow.tsx
New EscrowIcon component rendering a circular escrow image.
Navigation & Options
src/providers/navigation.tsx, src/providers/options.tsx, src/providers/wallet.tsx, src/screens/Apps/Index.tsx
Added AppEscrow page enum and tab mapping; imported/wired Escrow screen into navigation and Apps list; added Sign option near Logs; added deep-link case routing for appId: "escrow".
Escrow Screen & Iframe Host
src/screens/Apps/Escrow/Index.tsx, src/screens/Apps/Escrow/ArkadeIframeHost.tsx
New AppEscrow screen that resolves network-specific escrow URL, integrates wallet helpers (address, balance, signing, funding), and embeds ArkadeIframeHost; ArkadeIframeHost manages iframe lifecycle, origin validation, message dispatching, and keep-alive polling.
RPC Layer & Tests
src/screens/Apps/Escrow/RpcHandler.ts, src/screens/Apps/Escrow/RpcHandler.test.ts
Added typed message handler factory makeMessageHandler (methods: keep-alive, get-x-public-key, sign-login-challenge, get-ark-wallet-address, get-ark-wallet-balance, sign-transaction, fund-address) and a comprehensive test suite covering success and failure paths.

Sequence Diagram(s)

sequenceDiagram
    participant Iframe as Escrow Iframe
    participant Host as Host App (ArkadeIframeHost)
    participant Handler as RPC Handler (makeMessageHandler)
    participant Wallet as Wallet Service

    Iframe->>Host: postMessage(RPC request)
    Host->>Host: validate origin & source
    Host->>Handler: handle(InboundMessage)
    Handler->>Wallet: invoke method (e.g., sign / getBalance / fund)
    Wallet-->>Handler: result (signed PSBT / balance / txId)
    Handler-->>Host: OutboundMessage (response)
    Host-->>Iframe: postMessage(response)

    alt Keep-alive flow
        Host->>Iframe: postMessage(ARKADE_KEEP_ALIVE) every 2s until alive, then every 5s
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Areas needing careful review:
    • src/screens/Apps/Escrow/RpcHandler.ts — async dispatch, error wrapping, payload normalization, types.
    • src/screens/Apps/Escrow/ArkadeIframeHost.tsx — origin checks, message source validation, polling timing and cleanup.
    • src/screens/Apps/Escrow/Index.tsx — wallet integration for PSBT signing (main + checkpoints), hex handling, and navigation side-effects.
    • Tests: ensure RpcHandler.test.ts mocks align with real wallet APIs and edge cases.

Possibly related PRs

  • Feat/lendaswap #190 — modifies navigation/page rendering patterns for iframe-based apps; likely overlaps with src/providers/navigation.tsx changes.
  • Support app deep-link via URL hash #222 — changes deep-link handling in src/providers/wallet.tsx; related to the added escrow deep-link case.
  • update to ts-sdk 0.2.0 #121 — refactors wallet initialization/types in src/providers/wallet.tsx; may intersect with wallet integration and deep-link routing.

Suggested reviewers

  • louisinger
  • bordalix

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 'Integrate escrow app' clearly and directly describes the main objective of the pull request, which adds escrow app functionality throughout the codebase.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch integrate-escrow-app

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
src/screens/Apps/Index.tsx (1)

95-140: Clean up unused deepLinkInfo and consider network-based Escrow gating

  • deepLinkInfo from FlowContext is currently unused (isEscrowEnabled is hard-coded to true), which is triggering the static-analysis warning and adds noise.
  • Also, Escrow is rendered in the list on all networks, but AppEscrow only shows an iframe when BASE_URLS[aspInfo.network] is non-null; on unsupported networks, clicking Escrow leads to a header-only screen.

Consider either:

  • Dropping FlowContext / deepLinkInfo here and keeping Escrow always visible, or
  • Using a real condition (e.g., a feature flag or a network check mirroring BASE_URLS) to control isEscrowEnabled so the tile is hidden when the Escrow app is not actually available.

Example if you just want it always on and to silence the warning:

-import { FlowContext } from '../../providers/flow'
+// import { FlowContext } from '../../providers/flow'
...
-export default function Apps() {
-  const { deepLinkInfo } = useContext(FlowContext)
-  const isEscrowEnabled = true // deepLinkInfo?.appId === 'escrow'
+export default function Apps() {
+  const isEscrowEnabled = true
🧹 Nitpick comments (5)
src/providers/options.tsx (1)

53-57: Consider using a distinct icon for the Sign option.

The Sign option reuses LogsIcon, which is already used for the Logs option (lines 48-52). This may confuse users when navigating settings. Consider creating or using a dedicated icon (e.g., a pen/signature icon) for better visual distinction.

src/screens/Apps/Escrow/RpcHandler.ts (2)

208-211: Unreachable code after exhaustive switch.

Line 211 is unreachable because the switch (message.kind) covers both possible cases (ARKADE_KEEP_ALIVE and ARKADE_RPC_REQUEST), and the inner switch (method) is also exhaustive. TypeScript's control flow analysis should flag this. Consider removing the unreachable return or adding an explicit default case with never type assertion for compile-time exhaustiveness checking.

         case 'fund-address': {
           // ... existing code ...
         }
+        default: {
+          const _exhaustiveCheck: never = method
+          return { tag: 'failure', error: new Error(`Unknown method: ${_exhaustiveCheck}`) }
+        }
       }
     }
+    default: {
+      const _exhaustiveCheck: never = message
+      return { tag: 'failure', error: new Error('Unknown message kind') }
+    }
   }
-  return { tag: 'failure', error: new Error('Unknown message kind') }
 }

124-160: Consider consistent error handling for getter methods.

The get-x-public-key, get-ark-wallet-address, and get-ark-wallet-balance methods don't have try-catch wrappers, unlike sign-login-challenge, sign-transaction, and fund-address. If any of these getter functions throw, the error will bubble up as an unhandled promise rejection rather than returning a structured { tag: 'failure', error } result.

src/screens/Apps/Escrow/Index.tsx (1)

17-26: Centralize the crypto hash configuration and align Escrow availability with network support

  • hashes.sha256 = sha256 mutates global crypto-library configuration inside this screen. That’s easy to miss and couples the global signing behavior to whether this component module has been loaded.
  • Separately, escrowAppUrl is only set when BASE_URLS[aspInfo.network] is non-null, so on networks without an entry (e.g., where it’s null), AppEscrow renders just the header and no iframe.

Consider:

  • Moving the hashes.sha256 = sha256 assignment to a single, shared initialization point (e.g., wherever the wallet / signing stack is first configured) so that all flows using the identity share the same, explicit hash configuration.
  • Optionally tying the Escrow tile visibility (in Apps/Index.tsx) to the same BASE_URLS/network logic so users don’t navigate into a header-only Escrow page on unsupported networks.

Also applies to: 36-41, 72-81

src/screens/Apps/Escrow/ArkadeIframeHost.tsx (1)

10-27: Tighten the origin check to use equality instead of startsWith

The message handling pipeline (origin check + event.source === iframeRef.current?.contentWindow + shape validation) looks solid.

Given that allowedChildOrigins are already bare origins (as built in AppEscrow), you can simplify and harden the origin check:

-      if (!allowedChildOrigins.some((_) => _.startsWith(event.origin))) {
+      if (!allowedChildOrigins.includes(event.origin)) {
         console.error(`[wallet]: ignoring message from ${event.origin}`)
         return
       }

This keeps the intent obvious and avoids any accidental prefix matches if allowedChildOrigins ever started to include paths.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7703715 and ecf94b7.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • public/escrow.png is excluded by !**/*.png
📒 Files selected for processing (11)
  • README.md (1 hunks)
  • src/icons/Escrow.tsx (1 hunks)
  • src/providers/navigation.tsx (4 hunks)
  • src/providers/options.tsx (1 hunks)
  • src/providers/wallet.tsx (1 hunks)
  • src/screens/Apps/Escrow/ArkadeIframeHost.tsx (1 hunks)
  • src/screens/Apps/Escrow/Index.tsx (1 hunks)
  • src/screens/Apps/Escrow/RpcHandler.test.ts (1 hunks)
  • src/screens/Apps/Escrow/RpcHandler.ts (1 hunks)
  • src/screens/Apps/Index.tsx (3 hunks)
  • src/vite-env.d.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-30T18:33:29.839Z
Learnt from: bordalix
Repo: arkade-os/wallet PR: 114
File: src/lib/asp.ts:0-0
Timestamp: 2025-06-30T18:33:29.839Z
Learning: In src/lib/asp.ts, only the collaborativeExit function should accept both IWallet and ServiceWorkerWallet types. Other wallet functions (getBalance, getTxHistory, getReceivingAddresses, redeemNotes, sendOnChain, settleVtxos) should maintain their original IWallet-only signatures and not be updated for consistency.

Applied to files:

  • src/providers/wallet.tsx
  • src/screens/Apps/Escrow/RpcHandler.ts
🧬 Code graph analysis (6)
src/providers/options.tsx (1)
src/icons/Logs.tsx (1)
  • LogsIcon (1-10)
src/screens/Apps/Index.tsx (3)
src/providers/flow.tsx (1)
  • FlowContext (89-104)
src/App.tsx (1)
  • App (36-164)
src/icons/Escrow.tsx (1)
  • EscrowIcon (1-3)
src/screens/Apps/Escrow/RpcHandler.test.ts (1)
src/screens/Apps/Escrow/RpcHandler.ts (1)
  • makeMessageHandler (116-213)
src/screens/Apps/Escrow/Index.tsx (6)
src/providers/navigation.tsx (1)
  • NavigationContext (174-178)
src/providers/wallet.tsx (1)
  • WalletContext (42-56)
src/providers/flow.tsx (1)
  • FlowContext (89-104)
src/providers/asp.tsx (1)
  • AspContext (15-20)
src/screens/Apps/Escrow/RpcHandler.ts (1)
  • makeMessageHandler (116-213)
src/screens/Apps/Escrow/ArkadeIframeHost.tsx (1)
  • ArkadeIframeHost (10-83)
src/screens/Apps/Escrow/RpcHandler.ts (1)
src/test/setup.mjs (2)
  • address (208-208)
  • txid (96-96)
src/screens/Apps/Escrow/ArkadeIframeHost.tsx (1)
src/screens/Apps/Escrow/RpcHandler.ts (1)
  • MessageHandler (105-105)
🪛 GitHub Check: test
src/screens/Apps/Index.tsx

[warning] 96-96:
'deepLinkInfo' is assigned a value but never used


[warning] 96-96:
'deepLinkInfo' is assigned a value but never used

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test
  • GitHub Check: Cloudflare Pages: wallet-mutinynet
  • GitHub Check: Cloudflare Pages: wallet-bitcoin
🔇 Additional comments (6)
src/vite-env.d.ts (1)

9-9: LGTM!

The environment variable type declaration follows the established pattern and correctly marks the variable as optional.

README.md (1)

37-37: LGTM!

The new VITE_ARK_ESCROW_URL environment variable is properly documented.

src/screens/Apps/Escrow/RpcHandler.ts (1)

101-116: Clean API design with tagged union Result type.

The use of discriminated unions for message types and the tagged Result type for success/failure handling is a solid pattern that enables type-safe message processing.

src/icons/Escrow.tsx (1)

1-3: LGTM, but note the architectural inconsistency.

The /escrow.png asset exists in the public directory. However, this component differs from other icon components in the codebase—all others use inline SVGs (e.g., AddIcon, BackIcon, HomeIcon), while this uses an external PNG image. This approach may impact consistency and performance, as inline SVGs are bundled with the application while the PNG requires a separate network request.

src/providers/wallet.tsx (1)

131-146: Escrow deeplink routing is consistent with existing apps

The new 'escrow' case in the deep-link switch cleanly routes to Pages.AppEscrow and matches the pattern used for other apps (Boltz/Lenda*). No issues spotted.

src/providers/navigation.tsx (1)

27-76: Navigation wiring for AppEscrow looks good; just confirm enum renumbering is safe

  • Import, Pages.AppEscrow enum entry, pageTab mapping, and pageComponent switch case all match the existing pattern for other Apps-tab screens.
  • Since Pages.AppEscrow was inserted in the middle of the enum, the numeric values of following Pages members have shifted. This is harmless as long as you don’t serialize or share the raw numeric enum values outside this module (e.g., in storage or URLs).

If you do persist Pages somewhere, it’s worth double-checking that you’re storing something stable (like names/strings) rather than the numeric enum value.

Also applies to: 103-113

Comment thread README.md Resolved
Comment on lines +150 to +166
it('handles fund-address', async () => {
;(props.fundAddress as any).mockResolvedValue(undefined)
const handler = makeMessageHandler(props)
const res = await handler({
kind: 'ARKADE_RPC_REQUEST',
id,
method: 'fund-address',
payload: { address: 'ark1qq', amount: 123 },
})
expect(props.fundAddress).toHaveBeenCalledWith('ark1qq', 123)
expect(res.tag).toBe('success')
if (res.tag === 'success') {
const out: any = res.result
expect(out.method).toBe('fund-address')
expect(out.payload).toEqual({})
}
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Align fund-address test with RpcHandler implementation (payload currently mismatched)

RpcHandler.ts returns a payload containing { txid } for method: 'fund-address', but the test here:

;(props.fundAddress as any).mockResolvedValue(undefined)
// ...
expect(out.payload).toEqual({})

expects an empty payload. These semantics don’t match and will either break the test or obscure regressions.

If you want to keep the { txid } contract (which is more useful to callers), adjust the test like this:

-  it('handles fund-address', async () => {
-    ;(props.fundAddress as any).mockResolvedValue(undefined)
+  it('handles fund-address', async () => {
+    ;(props.fundAddress as any).mockResolvedValue('txid-123')
...
-    expect(res.tag).toBe('success')
-    if (res.tag === 'success') {
-      const out: any = res.result
-      expect(out.method).toBe('fund-address')
-      expect(out.payload).toEqual({})
-    }
+    expect(res.tag).toBe('success')
+    if (res.tag === 'success') {
+      const out: any = res.result
+      expect(out.method).toBe('fund-address')
+      expect(out.payload).toEqual({ txid: 'txid-123' })
+    }

If instead you intend fund-address to be fire-and-forget with an empty payload, then RpcHandler.ts should be changed to return { payload: {} } for that method.

🤖 Prompt for AI Agents
In src/screens/Apps/Escrow/RpcHandler.test.ts around lines 150 to 166, the test
for 'fund-address' currently mocks props.fundAddress to resolve to undefined and
asserts an empty payload, but RpcHandler returns a payload { txid } for that
method; update the test to mock props.fundAddress to resolve to a txid string
(e.g. 'txid123') and assert that the response payload equals { txid: 'txid123' }
and that fundAddress was called with the expected args, so the test matches the
RpcHandler contract (alternatively, if you prefer fire-and-forget semantics
change RpcHandler to return { payload: {} } but keep tests consistent).

Comment on lines +76 to +79
type RpcFundAddressResponse = {
method: 'fund-address'
payload: {}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Type mismatch: RpcFundAddressResponse payload does not include txid.

The response type declares an empty payload object, but the implementation at lines 197-200 returns { txid }. This causes a type inconsistency.

 type RpcFundAddressResponse = {
   method: 'fund-address'
-  payload: {}
+  payload: {
+    txid: string
+  }
 }
📝 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
type RpcFundAddressResponse = {
method: 'fund-address'
payload: {}
}
type RpcFundAddressResponse = {
method: 'fund-address'
payload: {
txid: string
}
}
🤖 Prompt for AI Agents
In src/screens/Apps/Escrow/RpcHandler.ts around lines 76 to 79, the
RpcFundAddressResponse type currently defines payload as an empty object but the
implementation returns { txid }; update the type so payload includes txid (e.g.
payload: { txid: string }) to match the returned object, and adjust any callers
or tests that consume RpcFundAddressResponse if they expect a different shape.

import { base64 } from '@scure/base'
import makeMessageHandler from './RpcHandler'

// Needed to sign the message, perhaps to be lifted up int App structure?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we fixed this? @bordalix

Copilot AI and others added 7 commits December 12, 2025 14:42
…s in restricted environments (#192)

* Initial plan

* Add JavaScript/JIT capability detection with informative error messages

Co-authored-by: tiero <3596602+tiero@users.noreply.github.com>

* Format code with prettier

Co-authored-by: tiero <3596602+tiero@users.noreply.github.com>

* rebuild service worker

* refactor how blocking errors are shown

* fix tests

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tiero <3596602+tiero@users.noreply.github.com>
Co-authored-by: João Bordalo <bordalix@users.noreply.github.com>
@pietro909
pietro909 force-pushed the integrate-escrow-app branch from ecf94b7 to 27b5b11 Compare December 12, 2025 13:43
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Dec 12, 2025 •

Copy link
Copy Markdown

Deploying wallet-mutinynet with  Cloudflare Pages  Cloudflare Pages

Latest commit: 27b5b11
Status: ✅  Deploy successful!
Preview URL: https://4f01356c.arkade-wallet.pages.dev
Branch Preview URL: https://integrate-escrow-app.arkade-wallet.pages.dev

View logs

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying wallet-bitcoin with  Cloudflare Pages  Cloudflare Pages

Latest commit: 27b5b11
Status: ✅  Deploy successful!
Preview URL: https://6212c1d8.wallet-bitcoin.pages.dev
Branch Preview URL: https://integrate-escrow-app.wallet-bitcoin.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
README.md (1)

29-37: Remove stray periods after backticked env var names (docs rendering).
This was previously flagged; the periods are still present.

-| `VITE_UTXO_MAX_AMOUNT`.     | Overwrite the server's utxoMaxAmount                            | `VITE_UTXO_MAX_AMOUNT=-1`                         |
-| `VITE_UTXO_MIN_AMOUNT`.     | Overwrite the server's utxoMinAmount                            | `VITE_UTXO_MIN_AMOUNT=330`                        |
-| `VITE_VTXO_MAX_AMOUNT`.     | Overwrite the server's vtxoMaxAmount                            | `VITE_VTXO_MAX_AMOUNT=-1`                         |
-| `VITE_VTXO_MIN_AMOUNT`.     | Overwrite the server's vtxoMinAmount                            | `VITE_VTXO_MIN_AMOUNT=330`                        |
+| `VITE_UTXO_MAX_AMOUNT`      | Overwrite the server's utxoMaxAmount                            | `VITE_UTXO_MAX_AMOUNT=-1`                         |
+| `VITE_UTXO_MIN_AMOUNT`      | Overwrite the server's utxoMinAmount                            | `VITE_UTXO_MIN_AMOUNT=330`                        |
+| `VITE_VTXO_MAX_AMOUNT`      | Overwrite the server's vtxoMaxAmount                            | `VITE_VTXO_MAX_AMOUNT=-1`                         |
+| `VITE_VTXO_MIN_AMOUNT`      | Overwrite the server's vtxoMinAmount                            | `VITE_VTXO_MIN_AMOUNT=330`                        |
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ecf94b7 and 27b5b11.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • public/escrow.png is excluded by !**/*.png
📒 Files selected for processing (11)
  • README.md (1 hunks)
  • src/icons/Escrow.tsx (1 hunks)
  • src/providers/navigation.tsx (4 hunks)
  • src/providers/options.tsx (1 hunks)
  • src/providers/wallet.tsx (1 hunks)
  • src/screens/Apps/Escrow/ArkadeIframeHost.tsx (1 hunks)
  • src/screens/Apps/Escrow/Index.tsx (1 hunks)
  • src/screens/Apps/Escrow/RpcHandler.test.ts (1 hunks)
  • src/screens/Apps/Escrow/RpcHandler.ts (1 hunks)
  • src/screens/Apps/Index.tsx (3 hunks)
  • src/vite-env.d.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/providers/navigation.tsx
  • src/screens/Apps/Escrow/RpcHandler.test.ts
  • src/vite-env.d.ts
  • src/icons/Escrow.tsx
  • src/screens/Apps/Escrow/RpcHandler.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-30T18:33:29.839Z
Learnt from: bordalix
Repo: arkade-os/wallet PR: 114
File: src/lib/asp.ts:0-0
Timestamp: 2025-06-30T18:33:29.839Z
Learning: In src/lib/asp.ts, only the collaborativeExit function should accept both IWallet and ServiceWorkerWallet types. Other wallet functions (getBalance, getTxHistory, getReceivingAddresses, redeemNotes, sendOnChain, settleVtxos) should maintain their original IWallet-only signatures and not be updated for consistency.

Applied to files:

  • src/providers/wallet.tsx
🧬 Code graph analysis (4)
src/providers/options.tsx (1)
src/icons/Logs.tsx (1)
  • LogsIcon (1-10)
src/screens/Apps/Escrow/Index.tsx (4)
src/providers/navigation.tsx (1)
  • NavigationContext (174-178)
src/providers/wallet.tsx (1)
  • WalletContext (42-56)
src/screens/Apps/Escrow/RpcHandler.ts (1)
  • makeMessageHandler (116-213)
src/screens/Apps/Escrow/ArkadeIframeHost.tsx (1)
  • ArkadeIframeHost (10-83)
src/screens/Apps/Escrow/ArkadeIframeHost.tsx (2)
src/screens/Apps/Escrow/RpcHandler.ts (1)
  • MessageHandler (105-105)
src/test/setup.mjs (1)
  • result (14-14)
src/screens/Apps/Index.tsx (3)
src/providers/flow.tsx (1)
  • FlowContext (89-104)
src/App.tsx (1)
  • App (36-164)
src/icons/Escrow.tsx (1)
  • EscrowIcon (1-3)
🪛 GitHub Check: test
src/screens/Apps/Index.tsx

[warning] 96-96:
'deepLinkInfo' is assigned a value but never used


[warning] 96-96:
'deepLinkInfo' is assigned a value but never used

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: Cloudflare Pages: wallet-mutinynet
🔇 Additional comments (3)
src/providers/options.tsx (1)

53-57: Ensure SettingsOptions.Sign is fully wired + consider a dedicated icon (not LogsIcon).
If SettingsOptions.Sign is new, confirm enum + settings routing/rendering exists; otherwise tapping this entry will dead-end. Also, reusing LogsIcon for signing is likely confusing—worth adding a SignIcon (or similar) for clarity.

src/providers/wallet.tsx (1)

131-146: Deep-link routing for appId: 'escrow' looks consistent.
Matches existing boltz/lendasat/lendaswap handling and keeps wallet fallback behavior intact.

src/screens/Apps/Escrow/Index.tsx (1)

2-5: Verify @noble/secp256k1 hash wiring + PSBT signing assumptions.
hashes.sha256 = sha256 and the PSBT signing flow (Transaction.fromPSBT(..., { allowUnknown: true }), plus signing checkpoints with [0]) are dependency/API-sensitive—worth confirming with the exact library versions in this repo that this is the intended/required setup and that checkpoint PSBTs always have the expected input structure.

Also applies to: 17-19, 56-76

Comment on lines +15 to +21
const poll = useCallback(() => {
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage({ kind: 'ARKADE_KEEP_ALIVE', timestamp: Date.now() }, childOrigin)
setTimeout(() => poll(), isAlive ? 5000 : 2000)
}
}, [isAlive, iframeRef, childOrigin])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix timer/listener leaks and make origin validation strict (===).
As written, polling can continue after unmount, and the load listener can be registered multiple times. Also, origin checking via startsWith is brittle—prefer exact origin matching.

 export const ArkadeIframeHost: React.FC<Props> = ({ src, allowedChildOrigins, messageHandler }) => {
   const iframeRef = useRef<HTMLIFrameElement | null>(null)
   const childOrigin = useMemo(() => new URL(src).origin, [src])
   const [isAlive, setIsAlive] = useState(false)
+  const timeoutRef = useRef<number | null>(null)
+  const isAliveRef = useRef(false)
+
+  useEffect(() => {
+    isAliveRef.current = isAlive
+  }, [isAlive])
 
   const poll = useCallback(() => {
     if (iframeRef.current?.contentWindow) {
       iframeRef.current.contentWindow.postMessage({ kind: 'ARKADE_KEEP_ALIVE', timestamp: Date.now() }, childOrigin)
-      setTimeout(() => poll(), isAlive ? 5000 : 2000)
+      if (timeoutRef.current) window.clearTimeout(timeoutRef.current)
+      timeoutRef.current = window.setTimeout(() => poll(), isAliveRef.current ? 5000 : 2000)
     }
-  }, [isAlive, iframeRef, childOrigin])
+  }, [iframeRef, childOrigin])
 
   useEffect(() => {
     const onMessage = async (event: MessageEvent) => {
-      if (!allowedChildOrigins.some((_) => _.startsWith(event.origin))) {
+      if (!allowedChildOrigins.some((allowed) => allowed === event.origin)) {
         console.error(`[wallet]: ignoring message from ${event.origin}`)
         return
       }
@@
     window.addEventListener('message', onMessage)
 
     return () => {
       window.removeEventListener('message', onMessage)
+      if (timeoutRef.current) window.clearTimeout(timeoutRef.current)
     }
   }, [messageHandler, isAlive, allowedChildOrigins])
 
-  useEffect(() => {
-    iframeRef.current?.addEventListener('load', () => {
-      poll()
-    })
-  }, [poll])
+  const onLoad = useCallback(() => poll(), [poll])
@@
     <iframe
       ref={iframeRef}
       title='Ark Escrow'
       src={src}
+      onLoad={onLoad}
       style={{ width: '100%', height: `100%`, border: 0, display: 'flex' }}
       sandbox='allow-scripts allow-forms allow-same-origin'
       allow='clipboard-read; clipboard-write'
     />

Also applies to: 24-27, 67-71

🤖 Prompt for AI Agents
In src/screens/Apps/Escrow/ArkadeIframeHost.tsx around lines 15-21 (also apply
to 24-27 and 67-71): the poll timer and load event listener leak because
timeouts keep running after unmount and the load handler can be attached
multiple times; change origin checks from startsWith to strict ===, store the
timeout id and clear it on unmount/when polling stops, ensure poll is wrapped to
avoid re-registering (or use a ref to track mounted state) so poll stops when
unmounted, add the load listener only once (register in useEffect with an empty
dependency array or proper deps) and remove that listener in the effect cleanup,
and validate event.origin with === before processing postMessage.

Comment on lines +17 to +27
// Needed to sign the message, perhaps to be lifted up int App structure?
hashes.sha256 = sha256

const BASE_URLS: Record<Network, string | null> = {
bitcoin: import.meta.env.VITE_ARK_ESCROW_URL ?? 'https://escrow.arkade.sh/client/',
mutinynet: import.meta.env.VITE_ARK_ESCROW_URL ?? 'https://api.escrow.mutinynet.arkade.sh/client/',
signet: null,
regtest: import.meta.env.VITE_ARK_ESCROW_URL ?? 'http://localhost:3002/client',
testnet: null,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid stale escrowAppUrl + narrow allowedOrigins to the active iframe origin.
Right now, switching to a network with BASE_URLS[network] === null won’t clear escrowAppUrl, so the iframe may remain mounted with the previous URL. Also, consider only allowing the selected escrow origin rather than all known origins.

   useEffect(() => {
-    if (!aspInfo.network || !svcWallet) return
-    const baseUrl = BASE_URLS[aspInfo.network as Network]
-    if (!baseUrl) return // No escrow app for this network
-    setEscrowAppUrl(baseUrl)
-  }, [aspInfo, svcWallet])
+    if (!aspInfo.network || !svcWallet) {
+      setEscrowAppUrl(null)
+      return
+    }
+    const baseUrl = BASE_URLS[aspInfo.network as Network]
+    // No escrow app for this network
+    setEscrowAppUrl(baseUrl ?? null)
+  }, [aspInfo.network, svcWallet])
@@
-  const allowedOrigins = Object.values(BASE_URLS)
-    .filter((_) => _ !== null)
-    .map((url) => new URL(url).origin)
+  const allowedOrigins = escrowAppUrl ? [new URL(escrowAppUrl).origin] : []
@@
-      {escrowAppUrl !== null && (
+      {escrowAppUrl !== null && (
         <ArkadeIframeHost src={escrowAppUrl} allowedChildOrigins={allowedOrigins} messageHandler={handlers} />
       )}

Also applies to: 36-42, 96-105

🤖 Prompt for AI Agents
In src/screens/Apps/Escrow/Index.tsx around lines 17-27 (also apply same changes
at 36-42 and 96-105): the component currently leaves escrowAppUrl set when
switching to a network whose BASE_URLS[network] is null and uses a broad
allowedOrigins list; update the logic so that when BASE_URLS[network] is null
you explicitly clear escrowAppUrl (e.g., set it to null/empty) to unmount the
iframe and prevent using a stale URL, and compute allowedOrigins dynamically
from the single active origin (only include the origin corresponding to the
selected network) instead of allowing all known origins so postMessage listeners
only accept messages from the mounted iframe origin.

Comment on lines +12 to 16
import EscrowIcon from '../../icons/Escrow'
import LendasatIcon from './Lendasat/LendasatIcon'
import LendaswapIcon from './Lendaswap/LendaswapIcon'
import { FlowContext } from '../../providers/flow'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix unused deepLinkInfo + remove hardcoded enablement (or drop the import).
Right now deepLinkInfo is read but never used, and Escrow is always enabled. Either use the intended gating or remove the dead code. Also prefer omitting link instead of link=''.

 export default function Apps() {
-  const { deepLinkInfo } = useContext(FlowContext)
-  const isEscrowEnabled = true // deepLinkInfo?.appId === 'escrow'
+  const { deepLinkInfo } = useContext(FlowContext)
+  const isEscrowEnabled = deepLinkInfo?.appId === 'escrow'
@@
             {isEscrowEnabled ? (
               <App
                 name='Escrow'
                 icon={<EscrowIcon />}
                 desc='Escrow system on Ark'
-                link=''
+                link={undefined}
                 page={Pages.AppEscrow}
                 live
               />
             ) : null}

Also applies to: 95-140

@arkana-ai-bot arkana-ai-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PROTOCOL-CRITICAL: human review required.

The signing and fund-transfer surfaces introduced here accept instructions from an iframe with insufficient validation. Several issues can result in silent fund loss.


[CRITICAL] All network origins are trusted simultaneously in every build

src/screens/Apps/Escrow/Index.tsx:91-94

const allowedOrigins = Object.values(BASE_URLS)
  .filter((_) => _ !== null)
  .map((url) => new URL(url).origin)

BASE_URLS always produces three non-null entries: https://escrow.arkade.sh, https://api.escrow.mutinynet.arkade.sh, and http://localhost:3002. All three end up in allowedChildOrigins regardless of the active network. On a mainnet wallet, any page served from http://localhost:3002 can send RPC messages that are accepted without question—triggering signArkTransaction or fundAddress silently.

Fix: pass only the origin corresponding to aspInfo.network, not all of them.


[CRITICAL] Origin comparison is directionally wrong

src/screens/Apps/Escrow/ArkadeIframeHost.tsx:30

if (!allowedChildOrigins.some((_) => _.startsWith(event.origin))) {

This checks whether an allowed origin starts with the event origin, not the other way around. An attacker who controls a domain that is a string-prefix of an allowed domain (e.g. https://escrow.arkad for allowed https://escrow.arkade.sh) would pass the check. Use exact equality:

if (!allowedChildOrigins.includes(event.origin)) {

[CRITICAL] signArkTransaction and fundAddress have zero user confirmation

src/screens/Apps/Escrow/Index.tsx:55-78

Both operations move money:

  • signArkTransaction signs arbitrary PSBTs (main tx + checkpoints) delivered by the iframe.
  • fundAddress calls svcWallet.sendBitcoin({ address, amount }) with iframe-supplied values.

Neither shows the user a confirmation dialog. A compromised or malicious escrow page (or any page that passes the broken origin check above) can drain the wallet silently. The Lendasat integration has the same gap but it relies on an external SDK with its own origin management. Escrow is rolling its own transport and must gate both operations behind a user-visible approval step before funds move.


[HIGH] Checkpoint always signed at index 0

src/screens/Apps/Escrow/Index.tsx:66

const signed = await svcWallet.identity.sign(
  Transaction.fromPSBT(cp, { allowUnknown: true }), [0]
)

Every checkpoint PSBT is signed at input index [0] unconditionally. If a checkpoint has wallet inputs at other indices they will be skipped, producing an incomplete signature that silently breaks the escrow protocol. The escrow app should specify which input indices need signing, or the wallet must inspect the PSBT to find its own inputs.


[HIGH] Main transaction signed without index constraint

src/screens/Apps/Escrow/Index.tsx:63

const signedTx = await svcWallet.identity.sign(tx)

No input-index whitelist is passed. In a multi-party escrow PSBT this will sign every input that matches the wallet key, including counterparty inputs if the PSBT was crafted to include them.


[MEDIUM] RpcFundAddressResponse type does not include txid

src/screens/Apps/Escrow/RpcHandler.ts:73 (type), line ~187 (impl)

The type declares payload: {} but the implementation returns { kind: 'ARKADE_RPC_RESPONSE', id, method, payload: { txid } }. The escrow app cannot know a txid was returned because it is not typed. The test also asserts expect(out.payload).toEqual({}) using a mock that returns undefined, so the test silently passes with { txid: undefined }. Either surface txid in the type or remove it from the response; don't leave it untyped.


[MEDIUM] Polling creates multiple concurrent poll loops

src/screens/Apps/Escrow/ArkadeIframeHost.tsx:17-22

poll is a useCallback that calls setTimeout(() => poll(), ...). Each time isAlive changes, poll is recreated, but the previous setTimeout callback still holds a reference to the old poll closure and will continue firing, spawning a parallel poll chain. After a few isAlive transitions the page has multiple overlapping timers.

Fix: use a useRef to hold the timeout ID and cancel it on each re-render:

const timerRef = useRef<ReturnType<typeof setTimeout>>()
const poll = useCallback(() => {
  clearTimeout(timerRef.current)
  if (iframeRef.current?.contentWindow) {
    iframeRef.current.contentWindow.postMessage(...)
    timerRef.current = setTimeout(poll, isAlive ? 5000 : 2000)
  }
}, [isAlive, childOrigin])

[MEDIUM] load event listener accumulates without cleanup

src/screens/Apps/Escrow/ArkadeIframeHost.tsx:65-67

useEffect(() => {
  iframeRef.current?.addEventListener('load', () => { poll() })
}, [poll])

Every time poll changes a new load listener is added with no removal. After several isAlive flips the iframe has multiple load listeners, each starting an independent poll chain on the next navigation.


[LOW] Escrow is unconditionally visible in the Apps list

src/screens/Apps/Index.tsx:109

const isEscrowEnabled = true // deepLinkInfo?.appId === 'escrow'

The commented-out gating logic suggests this was supposed to be behind a deep-link flag. As-is, every user on every network sees the Escrow entry. If the escrow service is not ready for all networks this will confuse users trying to use it on signet/testnet where BASE_URLS returns null.


[LOW] sandbox="allow-scripts allow-same-origin" on an env-overridable URL

src/screens/Apps/Escrow/ArkadeIframeHost.tsx:80

allow-same-origin + allow-scripts together in a sandbox removes the sandbox for same-origin frames (browsers treat it as "no sandbox"). src is overridable via VITE_ARK_ESCROW_URL. If a developer sets that to the same origin as the wallet (e.g. http://localhost:5173), the iframe has full DOM access. At minimum document this danger and consider whether allow-same-origin is actually required.


[LOW] package-lock.json added to a pnpm project

package.json declares "pnpm": ">=8" as the required package manager. Adding package-lock.json (npm format) alongside it creates two competing lockfiles. Danger already flagged this. This should be deleted or .gitignored; it was likely generated by accident.


[LOW] useMemo dependency list is stale / incorrect

src/screens/Apps/Escrow/Index.tsx:79-87

const handlers = useMemo(
  () => makeMessageHandler({ getXOnlyPublicKey, ... }),
  [svcWallet, navigate, setSendInfo],
)

setSendInfo is imported from FlowContext but not used by any of the handler functions in this file. The actual closures (getXOnlyPublicKey, signArkTransaction, etc.) are re-created on every render and then captured by useMemo only when svcWallet changes—but since the functions are inline and not useCallback-wrapped, ESLint's exhaustive-deps rule would flag all of them as missing. The memo works coincidentally because svcWallet is the only meaningful dependency, but this is fragile. Use useCallback for each handler or move them outside the component.


Test gaps

  • RpcHandler.test.ts: no test for fund-address returning the txid in the payload.
  • No tests for ArkadeIframeHost origin validation (the most critical security control).
  • sign-transaction test does not verify that signArkTransaction is called with the decoded payload rather than raw base64.
  • No test for the case where getXOnlyPublicKey returns null.

@Kukks Kukks closed this Oct 19, 2026
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.

7 participants