Skip to content

About

a payjoin browser app

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Browser Payjoin

A browser payjoin app in Vue + TypeScript

This is the browser sibling of node-payjoin.

This enables sending and receiving Draft BIP 77 Async Payjoin (v2) transactions against bitcoind. Unlike node-payjoin, there is no v1 mode here, it is v2 only.

DO NOT USE WITH REAL FUNDS

This code does only minimal validation, and the test coverage is thin. Use it on regtest only.

Full disclosure: this app was entirely vibe coded and hasn't been thoroughly reviewed. Treat every line as unaudited.

How it works

Both sender and receiver live in the same app, each session kept separate so you can drive the whole handshake from one browser. A few things worth knowing:

  • It talks to Bitcoin Core over JSON-RPC through the Vite dev server, which adds the Basic auth header and forwards /rpc/<wallet> to the per-wallet endpoint <url>/wallet/<name>.
  • Sessions and settings persist to localStorage, so they survive a refresh. Reopening a session from the list resumes its polling.
  • The relays and directory reject cross-origin browser requests, so OHTTP traffic goes through the dev server too, as /pj?target=<url>. Both routes only exist under npm run dev; npm run preview serves the built app but reaches nothing.

Demo

Payjoin v2 lets the sender and receiver complete a transaction without being online at the same time. Learn more about how that works here.

Payjoin needs the sender and receiver to each have spendable UTXOs, so we'll create two wallets and fund each.

Start a regtest node

Shortcut: ./scripts/start-regtest.sh runs every step below and waits until the node is ready.

This keeps the chain data inside the repo so it's easy to delete later.

mkdir -p .bitcoin-regtest

cat > .bitcoin-regtest/bitcoin.conf <<'EOF'
regtest=1
server=1
fallbackfee=0.00001000
rpcuser=admin1
rpcpassword=123

[regtest]
rpcport=18443
EOF

bitcoind -regtest -datadir="$PWD/.bitcoin-regtest" -daemon

Set a shell helper for the rest of the commands (must be run in bash):

BITCOIN_CLI="bitcoin-cli -regtest -datadir=$PWD/.bitcoin-regtest"

$BITCOIN_CLI getblockchaininfo

The RPC user and password above are the proxy's defaults. If you change them, set BITCOIN_RPC_USER and BITCOIN_RPC_PASS (and BITCOIN_RPC_URL for a non-default host) when you start the dev server.

Create and fund wallets

Shortcut: ./scripts/fund-regtest.sh creates and funds both wallets, and is safe to re-run to top up balances.

The wallets must be named sender and receiver, since that's what the app's RPC calls target:

$BITCOIN_CLI createwallet sender
$BITCOIN_CLI createwallet receiver

# We need 101 blocks for the UTXOs to be spendable due to the coinbase maturity requirement.
$BITCOIN_CLI -rpcwallet=sender generatetoaddress 101 "$($BITCOIN_CLI -rpcwallet=sender getnewaddress)"
$BITCOIN_CLI -rpcwallet=receiver generatetoaddress 101 "$($BITCOIN_CLI -rpcwallet=receiver getnewaddress)"

# Check the balances before doing a Payjoin transaction.
$BITCOIN_CLI -rpcwallet=sender getbalance
$BITCOIN_CLI -rpcwallet=receiver getbalance

Great! Our wallets are set up.

Install dependencies and run

npm install
npm run dev

Open the printed URL. From here the flow is the CLI's just receive / just send / resume steps, done with buttons instead.

Generate a receive address

On the home screen, tap Receive, enter an amount in sats along with the expiry and payjoin directory (the directory defaults to https://payjo.in), and tap Receive. The app validates the directory by fetching its OHTTP keys, then shows a bitcoin URI as text and a QR code. This is the same URI node-payjoin prints, carrying the receiver's address, amount, payjoin directory, and session info:

bitcoin:tb1qfttmt4z68cfyn2z25t3dusp03rq6gxrucfxs5a?amount=0.0001&pj=HTTPS://PAYJO.IN/EUQKYLU92GC6U%23RK1QFWVXS2LQ2VD4T6DUMQ0F4RZQ5NL9GM0EFWVHJZ9L796L20Z7SL3J+OH1QYP87E2AVMDKXDTU6R25WCPQ5ZUF02XHNPA65JMD8ZA2W4YRQN6UUWG+EX10T57UE

The session is now open and polling the directory. Copy the URI to hand to the sender. You can close the app or go back home; the session keeps its place in localStorage, and reopening it from the session list resumes polling. Being offline is fine, the sender can send against it meanwhile.

Send a payjoin

Payjoin is a multistep asynchronous handshake. Both sides live in the same app here, each session kept separate in localStorage by role and persister id, so you can drive the whole thing from one browser.

Tap Send, paste the receiver's URI, and hit Enter to see the parsed details. Tap Send to sign and post the original PSBT to the directory. This is the fallback transaction: a normal payment to the receiver's address that the receiver could broadcast if the payjoin coordination fails. The sender session then polls for the receiver's proposal.

Resume a receive

A receiver session polls the directory for the sender's PSBT, so it needs to be running for the payjoin to progress. Once the original PSBT arrives, the receiver validates it, contributes one of its own inputs, builds the payjoin proposal PSBT, signs it, and posts it back to the directory for the sender.

Tap the receiver row in the session list, and tap Resume in the header to start polling again. Once the log says Sent payjoin proposal to sender the receiver flow has been completed and you can close out of the receive session.

Resume a send

The sender session polls for the receiver's proposal after it posts the original PSBT. Once the proposal arrives, the sender checks it against the original PSBT, signs the inputs it owns, finalizes the transaction, and hands it to Bitcoin Core to broadcast.

Tap the sender row, and tap Resume. It picks up wherever it left off, waiting on the proposal or finishing one that already arrived. Once the log says Broadcast payjoin transaction <txid> the payjoin is broadcasted and you can close out of the send session.

Congratulations! You've completed a v2 payjoin, and since it's v2, the two sides never had to be online at once. A session that has finished or aborted drops off the home list; find it under Menu > Inactive sessions.

Stop the regtest node

Shortcut: ./scripts/stop-regtest.sh stops the node and waits for it to shut down.

When you're done, stop the node:

$BITCOIN_CLI stop

Since the chain data lives in .bitcoin-regtest, deleting that directory wipes the regtest state entirely for a clean start next time.

About

a payjoin browser app

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages