From 4d0c2d6717fa591308104cb0c32a6854b4f36e93 Mon Sep 17 00:00:00 2001 From: archipelago Date: Mon, 22 Jun 2026 10:53:05 -0400 Subject: [PATCH] feat(demo): real testnet tx links + interactive buy-files flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tx/explorer links open mempool.space/testnet/tx/; the backend hydrates the wallet's transactions with REAL recent testnet txids at startup (best-effort, falls back to mock hashes offline). Mempool app + demo-external apps open in a new tab; deep-link paths are carried through. - Add the content.* paid-download handlers the buy flow needs (owned-list, preview-peer, download-peer-{paid,invoice,onchain}, request-invoice, invoice-status, request-onchain, onchain-status) — every path resolves to a success state with testnet receive addresses / bolt11 invoices so visitors can walk the full buy → unlock journey. Co-Authored-By: Claude Opus 4.8 (1M context) --- neode-ui/mock-backend.js | 74 ++++++++++++++++++- neode-ui/src/views/AppSession.vue | 7 +- .../src/views/appSession/appSessionConfig.ts | 7 +- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index ff5099dc..0791664d 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -1869,6 +1869,54 @@ app.post('/rpc/v1', (req, res) => { return res.json({ result: { success: true } }) } + // ── Demo paid-content / "buying files" flow ─────────────────────────── + // Every payment path resolves to success so visitors can experience the + // full buy → unlock journey with testnet addresses and invoices. + case 'content.owned-list': { + return res.json({ result: { items: [] } }) + } + case 'content.owned-get': + case 'content.preview-peer': + case 'content.download-peer-paid': + case 'content.download-peer-invoice': + case 'content.download-peer-onchain': { + const filename = params?.filename || 'demo-content' + const body = Buffer.from( + `Archipelago demo — "${filename}"\n\nThis is sample paid content delivered over the ` + + `node-to-node content market. On a real node this would be the actual file you purchased.\n`, + 'utf-8' + ).toString('base64') + return res.json({ result: { data: body, mime_type: 'text/plain', ecash_backend: params?.method || 'cashu' } }) + } + case 'content.request-invoice': { + const price = params?.price_sats || 500 + return res.json({ + result: { + bolt11: 'lntb' + price + '1p' + randomHex(50), + payment_hash: randomHex(32), + price_sats: price, + }, + }) + } + case 'content.invoice-status': { + // Settle after the first poll so the QR flow shows a realistic wait. + return res.json({ result: { paid: true } }) + } + case 'content.request-onchain': { + const price = params?.price_sats || 500 + return res.json({ + result: { + address: 'tb1q' + randomHex(19), + amount_sats: price, + content_id: params?.content_id || '', + expires_at: new Date(Date.now() + 3600000).toISOString(), + }, + }) + } + case 'content.onchain-status': { + return res.json({ result: { paid: true, status: 'confirmed', confirmations: 1 } }) + } + case 'network.diagnostics': { return res.json({ result: { @@ -4087,12 +4135,34 @@ wss.on('connection', (ws, req) => { }) }) +// Best-effort: pull a few REAL recent testnet txids so the wallet's transactions +// deep-link to live mempool.space/testnet pages. Falls back to the mock hashes +// (already set) if offline. Patches the pristine seed so every session inherits them. +async function hydrateRealTestnetTxids() { + if (!DEMO) return + try { + const ctrl = new AbortController() + const t = setTimeout(() => ctrl.abort(), 4000) + const r = await fetch('https://mempool.space/testnet/api/mempool/recent', { signal: ctrl.signal }) + clearTimeout(t) + if (!r.ok) return + const recent = await r.json() + const txids = (Array.isArray(recent) ? recent : []).map(x => x.txid).filter(Boolean) + if (!txids.length) return + SEED_WALLET.transactions.forEach((tx, i) => { if (txids[i]) tx.tx_hash = txids[i] }) + // Also patch the already-built default store (sessions clone from SEED at creation). + defaultStore.walletState.transactions.forEach((tx, i) => { if (txids[i]) tx.tx_hash = txids[i] }) + console.log(`[Demo] Hydrated ${Math.min(txids.length, SEED_WALLET.transactions.length)} real testnet txids`) + } catch { /* offline — keep mock hashes */ } +} + server.listen(PORT, '0.0.0.0', async () => { const runtime = await isContainerRuntimeAvailable() - + // Initialize package data from Docker await initializePackageData() - + await hydrateRealTestnetTxids() + console.log(` ╔════════════════════════════════════════════════════════════╗ ║ ║ diff --git a/neode-ui/src/views/AppSession.vue b/neode-ui/src/views/AppSession.vue index dadfcd3b..33e5cb58 100644 --- a/neode-ui/src/views/AppSession.vue +++ b/neode-ui/src/views/AppSession.vue @@ -109,6 +109,7 @@ import { useAppIdentity } from './appSession/useAppIdentity' import { useNostrBridge } from './appSession/useNostrBridge' import { openExternalUrl } from '@/utils/openExternal' import { useElectrsSync } from '@/composables/useElectrsSync' +import { IS_DEMO, isDemoExternal } from '@/composables/useDemoIntro' const props = defineProps<{ appIdProp?: string @@ -157,7 +158,11 @@ const packageEntry = computed(() => store.data?.['package-data']?.[appId.value] const blockedReason = computed(() => launchBlockedReason(appId.value, packageEntry.value)) const blockedTitle = computed(() => appId.value === 'fedimint' || appId.value === 'fedimintd' ? 'Waiting for Bitcoin sync' : 'App not ready') const isMobile = typeof window !== 'undefined' && window.innerWidth < 768 -const mustOpenNewTab = computed(() => NEW_TAB_APPS.has(appId.value)) +// In the demo, apps backed by a real external site that blocks iframing +// (mempool.space) open in a new tab rather than the in-app session frame. +const mustOpenNewTab = computed(() => + NEW_TAB_APPS.has(appId.value) || (IS_DEMO && isDemoExternal(appId.value)) +) // ElectrumX shows a sync screen before its real UI (the Electrum server only // serves clients once its index is built). Poll /electrs-status while this is diff --git a/neode-ui/src/views/appSession/appSessionConfig.ts b/neode-ui/src/views/appSession/appSessionConfig.ts index 06c87820..5cd5b7fd 100644 --- a/neode-ui/src/views/appSession/appSessionConfig.ts +++ b/neode-ui/src/views/appSession/appSessionConfig.ts @@ -78,9 +78,12 @@ export const IFRAME_BLOCKED_APPS = new Set([]) /** Resolve app URL using direct port mapping (source of truth) */ export function resolveAppUrl(id: string, routeQueryPath?: string, runtimeUrl?: string): string { // Demo: route to the app's mock UI or real external site (mempool.space, - // indee.tx1138.com). Non-demoable apps fall through to a generic notice page. + // indee.tx1138.com). Carry through a deep-link path (e.g. /tx/ for + // mempool). Non-demoable apps fall through to a generic notice page. if (IS_DEMO) { - return demoAppUrl(id) || `/app/${id}/` + const base = demoAppUrl(id) + if (base) return routeQueryPath ? base + routeQueryPath : base + return `/app/${id}/` } // External HTTPS apps