|
6 | 6 | import React, { useState, useEffect } from 'https://esm.sh/react@18.3.1' |
7 | 7 | import { createRoot } from 'https://esm.sh/react-dom@18.3.1/client' |
8 | 8 | import htm from 'https://esm.sh/htm@3.1.1' |
| 9 | +import { Connection, PublicKey, Transaction, SystemProgram, LAMPORTS_PER_SOL } from 'https://esm.sh/@solana/web3.js@1.98.4' |
9 | 10 |
|
10 | 11 | const html = htm.bind(React.createElement) |
11 | 12 | const PROXY = window.TXODDS_PROXY ?? 'http://localhost:8801' |
@@ -101,6 +102,18 @@ const ESCROW_PROGRAM = 'R5NWNg9eRLWWQU81Xbzz5Du1k7jTDeeT92Ty6qCeXet' |
101 | 102 | const SETTLE_SOL = 0.001 |
102 | 103 | const shortAddr = (a) => (a ? `${String(a).slice(0, 4)}…${String(a).slice(-4)}` : '') |
103 | 104 | const addrLink = (a) => `https://explorer.solana.com/address/${a}?cluster=devnet` |
| 105 | +const txLink = (s) => `https://explorer.solana.com/tx/${s}?cluster=devnet` |
| 106 | +const DEVNET_RPC = 'https://api.devnet.solana.com' |
| 107 | + |
| 108 | +// Detect an injected browser wallet (Phantom / Solflare) — no wallet-adapter needed for a no-build app. |
| 109 | +function getWallet() { |
| 110 | + const w = window |
| 111 | + const phantom = w.phantom?.solana ?? (w.solana?.isPhantom ? w.solana : null) |
| 112 | + const solflare = w.solflare?.isSolflare ? w.solflare : null |
| 113 | + if (phantom) return { name: 'Phantom', provider: phantom } |
| 114 | + if (solflare) return { name: 'Solflare', provider: solflare } |
| 115 | + return null |
| 116 | +} |
104 | 117 |
|
105 | 118 | // ── odds board ────────────────────────────────────────────────────────────── |
106 | 119 | // LIVE TxODDS markets are messy: Pct values arrive as strings ("41.946"), some priced "NA", |
@@ -268,6 +281,60 @@ function SettleResult({ r }) { |
268 | 281 | <a href=${addrLink(ESCROW_PROGRAM)} target="_blank" rel="noreferrer">escrow program ↗</a></div>` |
269 | 282 | } |
270 | 283 |
|
| 284 | +// Pay for the read yourself with Phantom / Solflare — a real Solana Pay reference-tagged transfer to |
| 285 | +// the seller, verified on-chain by the proxy. The wallet signs; we submit to devnet so the cluster is |
| 286 | +// guaranteed regardless of the wallet's setting. (Needs a Devnet-funded wallet.) |
| 287 | +function PayButton({ fixture }) { |
| 288 | + const [st, setSt] = useState({ status: 'idle', msg: '' }) |
| 289 | + const wallet = getWallet() |
| 290 | + |
| 291 | + const pay = async () => { |
| 292 | + if (!wallet) { setSt({ status: 'error', msg: 'No Phantom/Solflare detected — install one and switch it to Devnet' }); return } |
| 293 | + try { |
| 294 | + setSt({ status: 'busy', msg: 'connecting wallet…' }) |
| 295 | + const { provider } = wallet |
| 296 | + const conn = await provider.connect() |
| 297 | + const payer = new PublicKey((conn?.publicKey ?? provider.publicKey).toString()) |
| 298 | + |
| 299 | + setSt({ status: 'busy', msg: 'building payment…' }) |
| 300 | + const intent = await (await fetch(`${PROXY}/api/pay-intent?fixtureId=${fixture.FixtureId}&amount=${SETTLE_SOL}`)).json() |
| 301 | + const connection = new Connection(DEVNET_RPC, 'confirmed') |
| 302 | + const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed') |
| 303 | + const ix = SystemProgram.transfer({ |
| 304 | + fromPubkey: payer, toPubkey: new PublicKey(intent.recipient), |
| 305 | + lamports: Math.round(intent.amountSol * LAMPORTS_PER_SOL), |
| 306 | + }) |
| 307 | + ix.keys.push({ pubkey: new PublicKey(intent.reference), isSigner: false, isWritable: false }) // Solana Pay reference |
| 308 | + const tx = new Transaction({ feePayer: payer, blockhash, lastValidBlockHeight }).add(ix) |
| 309 | + |
| 310 | + setSt({ status: 'busy', msg: `approve in ${wallet.name}…` }) |
| 311 | + const signed = await provider.signTransaction(tx) |
| 312 | + const sig = await connection.sendRawTransaction(signed.serialize()) |
| 313 | + setSt({ status: 'busy', msg: 'confirming on devnet…' }) |
| 314 | + await connection.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, 'confirmed') |
| 315 | + |
| 316 | + const v = await (await fetch(`${PROXY}/api/pay-verify?sig=${sig}&reference=${intent.reference}&amount=${intent.amountSol}&recipient=${intent.recipient}`)).json() |
| 317 | + setSt({ status: v.ok ? 'ok' : 'error', msg: v.ok ? '' : 'paid, but verification failed', explorer: v.explorer ?? txLink(sig), amountSol: intent.amountSol }) |
| 318 | + } catch (e) { |
| 319 | + setSt({ status: 'error', msg: String(e?.message ?? e).slice(0, 100) }) |
| 320 | + } |
| 321 | + } |
| 322 | + |
| 323 | + if (st.status === 'ok') return html` |
| 324 | + <div class="settled ok"><span class="bind-tag">paid</span> you paid <b>${st.amountSol} SOL</b> with ${wallet?.name} — |
| 325 | + <a href=${st.explorer} target="_blank" rel="noreferrer">tx ↗</a> · verified by its Solana Pay reference</div>` |
| 326 | + |
| 327 | + return html` |
| 328 | + <div class="pay-self"> |
| 329 | + <button class="pay-btn" disabled=${st.status === 'busy'} onClick=${pay}> |
| 330 | + ${st.status === 'busy' |
| 331 | + ? html`<span class="spin"></span> ${st.msg}` |
| 332 | + : `Pay it yourself with Phantom / Solflare · ${SETTLE_SOL} SOL`} |
| 333 | + </button> |
| 334 | + ${st.status === 'error' && html`<span class="pay-err">${st.msg}</span>`} |
| 335 | + </div>` |
| 336 | +} |
| 337 | + |
271 | 338 | function App() { |
272 | 339 | const [fixtures, setFixtures] = useState(null) |
273 | 340 | const [source, setSource] = useState(null) // 'live' | 'demo' |
@@ -376,6 +443,7 @@ function App() { |
376 | 443 | <span class="spin"></span> agent delivered — arbiter settling ${SETTLE_SOL} SOL in escrow on devnet… |
377 | 444 | </div>`} |
378 | 445 | ${settleRes && html`<${SettleResult} r=${settleRes} />`} |
| 446 | + ${selected && html`<${PayButton} fixture=${selected} />`} |
379 | 447 | </div> |
380 | 448 | </div> |
381 | 449 | </section>`} |
|
0 commit comments