Skip to content

Instantly share code, notes, and snippets.

@0xBEEFCAF3
Last active April 2, 2025 23:29
Show Gist options
  • Save 0xBEEFCAF3/fc6cf842608cca922e90055320abd4e8 to your computer and use it in GitHub Desktop.
Save 0xBEEFCAF3/fc6cf842608cca922e90055320abd4e8 to your computer and use it in GitHub Desktop.
Liana Payjoin Integration

Payjoin Liana Integration

Getting Dev Env Setup

Get regtest setup locally. I use user:pass creds. Cookies work too. For this guide I will assume you are using default ports.

Gui

  1. cd liana-gui
  2. cargo run
  3. For network, be sure to select regtest
  4. You should get a GUI. Create a new wallet and Save your descriptor, you are going to need it in the backend setup.

Lianad

  1. Create a custom config
# Whether to run the process as a UNIX daemon (double fork magic)
daemon = false

# (Optional) Path to the folder where we should store the application data.
# Defaults to `.lianad` in your home folder.
# data_dir = "~/.lianad"

# How verbose logging should be (one of "error", "warn", "info", "debug", "trace")
log_level = "debug"

# The wallet descriptor. It must be a Segwit v0 Pay-To-Witness-Script-Hash (`wsh()`) descriptor
# corresponding to a `or(pk(A),and(pk(B),older(X)))` policy (either public key A can spend immediately
# or public key B can spend after X blocks).
# The public keys must be valid extend keys ("xpubs") ending with a wildcard (i.e. can be derived
# from). The public keys must be multipath expressions with exactly the `0` and `1` derivation indexes,
# that is having a derivation step which is `/<0;1>` before the last step. This is in order to be able
# to derive deposit and change addresses from the same descriptor.
# The extended public keys must be encoded for the network the daemon is to be run (i.e. "xpub"s for the
# main network and "tpub"s for everything else).
#
# YOUR DESCRIPTOR IS UNIQUE AND MUST BE BACKED UP, WITHOUT IT YOU WONT BE ABLE TO RECOVER YOUR FUNDS.
#
# main_descriptor = "wsh(or_d(pk([92162c45]tpubD6NzVbkrYhZ4WzTf9SsD6h7AH7oQEippXK2KP8qvhMMqFoNeN5YFVi7vRyeRSDGtgd2bPyMxUNmHui8t5yCgszxPPxMafu1VVzDpg9aruYW/<0;1>/*),and_v(v:pkh(tpubD6NzVbkrYhZ4Wdgu2yfdmrce5g4fiH1ZLmKhewsnNKupbi4sxjH1ZVAorkBLWSkhsjhg8kiq8C4BrBjMy3SjAKDyDdbuvUa1ToAHbiR98js/<0;1>/*),older(2))))#uact7s3g"
# main_descriptor = "wsh(or_d(pk([f5acc2fd]tpubD6NzVbkrYhZ4YgUx2ZLNt2rLYAMTdYysCRzKoLu2BeSHKvzqPaBDvf17GeBPnExUVPkuBpx4kniP964e2MxyzzazcXLptxLXModSVCVEV1T/<0;1>/*),and_v(v:pkh([8a64f2a9]tpubD6NzVbkrYhZ4WmzFjvQrp7sDa4ECUxTi9oby8K4FZkd3XCBtEdKwUiQyYJaxiJo5y42gyDWEczrFpozEjeLxMPxjf2WtkfcbpUdfvNnozWF/<0;1>/*),older(10))))#d72le4dr"
main_descriptor = "wsh(or_d(pk([4e9cf1dc/48'/1'/0'/2']tpubDEVAGYepj48e9egjeDcwgNpVJgmZba5APhC9Y9eUND1cPfMUtjSgESRgVnsgj4SUB7V5wwSj79GivkSYsQL5RAAmijwKp8RwLfAmZqbDDD6/<0;1>/*),and_v(v:pkh([4e9cf1dc/48'/1'/0'/2']tpubDEVAGYepj48e9egjeDcwgNpVJgmZba5APhC9Y9eUND1cPfMUtjSgESRgVnsgj4SUB7V5wwSj79GivkSYsQL5RAAmijwKp8RwLfAmZqbDDD6/<2;3>/*),older(52596))))#jtkdzxqn"

# This section is the configuration related to the Bitcoin backend.
# On what network shall it operate?
# How often should it poll the Bitcoin backend for updates?
[bitcoin_config]
network = "regtest"
poll_interval_secs = 10	

# This section is specific to the bitcoind implementation of the Bitcoin backend. This is the only
# implementation available for now.
# In order to be able to connect to bitcoind, it needs to know on what port it is listening as well
# as where the authentication cookie is located.
[bitcoind_config]
addr = "127.0.0.1:18443"
auth = "foo:bar"
  1. Replace main_descriptor with your own
  2. cd lianad
  3. RUST_LOG=trace cargo run --bin lianad -- --conf ../my_config.toml

You are ready to hack!

Sender Integration

Briefly, the sender initiates a payment via a BIP21 URI, where the receiver’s directory URL and ephemeral public key are shared as query parameters. Implementing this requires changes across the GUI, lianad, and potentially even a long-running external service.

GUI Changes

  1. The sender must be able to input a BIP21 URI. The frontend application should parse this URI and extract any Payjoin-related query parameters. The existing rust-payjoin crate is well-suited for this task.

  2. The sender should be clearly informed that this is a Payjoin transaction and that their transaction will not be immediately settled. Instead, they will need to return to the PSBT tab to sign the final transaction, which may include external inputs and potentially new outputs. A new 'Send Payjoin' button will appear next to 'sign' in the final send step if a bip21 is a "Payjoin URI".

  3. The sender must be able to sign an updated Payjoin proposal PSBT and broadcast it. The fuctionality for a user to sign and broadcast a PSBT after creatiing already exists.

lianad Changes

  1. Remove any restrictions on external inputs.

  2. Add a new RPC endpoint to send a Payjoin request. This endpoint will function similarly to the existing send RPC, but it will send a PSBT to the BIP77 directory and save a fallback transaction. In the normal BIP77 flow the sender will need to be persisted in long term storage, for a hackathon its fine to keep the sender in memory for the next step.

  3. Implement a non-blocking thread in lianad to long-poll the directory for a response from the receiver. Once a response is received and processed, lianad should update the original PSBT with the Payjoin proposal. The user will then need to sign and broadcast this updated transaction (step 3 in the GUI section above).

  4. As an alternative, we could develop a lightweight external service that performs the long-polling instead of lianad. This service would send an RPC call to update the PSBT upon receiving a response. However, this should be considered a fallback solution, used only if threading within lianad proves thread unsafe or infeasible.

Receiver Changes

  1. When a reciever requests a new address they should have a new option to get a payjoin BIP21. This option should call a new RPC endpoint to create and persist a payjoin Reciever.

  2. A non-blocking thread should then long-poll the server until it recieves a response from the BIP77 directory. Once the reciever processes the response it will need to select and contributes inputs. We can hoopefully lean on the existing listcoins functionality for input selection. After submitting the BIP77 payjoin proposal, the receiver flow is over.

Perhaps it would be easier to have a non-blocking thread running in liana that accepts jobs via some work queue than spawning and cleaning them up on demand. To answer that we would have to do some more exploratory hacking.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment