feat(demo): real testnet tx links + interactive buy-files flow
- Tx/explorer links open mempool.space/testnet/tx/<id>; 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) <noreply@anthropic.com>
This commit is contained in:
parent
2cffa79d9d
commit
4d0c2d6717
@ -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(`
|
||||
╔════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
|
||||
@ -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
|
||||
|
||||
@ -78,9 +78,12 @@ export const IFRAME_BLOCKED_APPS = new Set<string>([])
|
||||
/** 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/<hash> 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user