Payjoin persistence - #242

Merged
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance
Jul 2, 2026
Merged

Payjoin persistence#242
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance

Conversation

@Mshehu5

@Mshehu5Mshehu5 commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Description

#230 needed to be merged for this to go through
Address #149 also follow up to #200
This PR adds persistance to existing async payjoin integration
This introduces neccessary database model and tables also add commad for resume to allow interrupted sessions to be continued also a particular session either send or receive.
A history commad to view payjoin history and status has been added

Notes to the reviewers

Step to review this include making a payjoin transaction

Run a receiver to get a BIP21 URI then pass it to the sender as seen in docs

bdk-cli/README.md

Lines 121 to 141 in b9cf2ac

To start a Payjoin session as the receiver with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet1 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPd2PoUEcGNDHPZmVWgtPYERAwMG6qHheX6LN4oaazp3qZU7mykiaAZga1ZB2SJJR6Mriyq8MocMs7QTe7toaabSwTWu5fRFz/84h/1h/0h/0/*)#8guqp7rn" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet1 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet1 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet1 receive_payjoin --amount 400000 --max_fee_rate 1000 --directory "https://payjo.in" --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com"
```
To send a Payjoin with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet2 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPfBxswkATvZRQ9kDdRbJPtHYZaZCARL2myxcK7DqsqPhRo2G2rRVHFPbowq63BE6S4k2pUMYeF2fUMTT63Q7zhoXtKsM1FaS/84'/1'/0'/0/*)#qf5gnqrf" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet2 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet2 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet2 send_payjoin --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com" --fee_rate 1 --uri "<URI>"
```

To test resumption, interrupt either side with Ctrl+C mid-session then run resume on that side to continue. A few scenarios worth covering: receiver resuming after interrupt and sender resuming after interrupt. Use history after each scenario to confirm the session state was persisted correctly.

docs for this can be seen in 7e4ffd1

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added docs for the new feature

@coveralls

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 22068931916

Details

  • 0 of 763(0.0%) changed or added relevant lines in 5 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-1.9%) to 8.848%

Changes Missing CoverageCovered LinesChanged/Added Lines%
src/error.rs0210.0%
src/utils.rs0220.0%
src/handlers.rs0500.0%
src/payjoin/db.rs02620.0%
src/payjoin/mod.rs04080.0%
Files with Coverage ReductionNew Missed Lines%
src/utils.rs10.0%
src/handlers.rs212.81%
src/payjoin/mod.rs20.0%
TotalsCoverage Status
Change from base Build 21153868360:-1.9%
Covered Lines:268
Relevant Lines:3029

💛 - Coveralls

@notmandatorynotmandatory moved this to In Progress in BDK-CLIFeb 18, 2026
@Mshehu5Mshehu5 changed the title Payjoin persistancePayjoin persistenceFeb 21, 2026
@Mshehu5
Mshehu5 marked this pull request as ready for review February 24, 2026 05:41
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 4 times, most recently from a5e2482 to c79bd95CompareFebruary 24, 2026 12:57
@tvpetertvpeter added this to the CLI 3.0.0 milestone Feb 26, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @Mshehu5 for working on this feature.

Below are some of the observations that I think will make the implementation better:

  • I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.
  • The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.
  • Since the save_event is generated at multiple transitions in a session, I think it will be great if you consider grouping such updates in a db transaction to ensure that entries are saved successfully.

I have also left some comments in the code.

Thank you.

Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/payjoin/mod.rs
@tvpetertvpeter removed this from the CLI 3.0.0 milestone Mar 12, 2026
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 2 times, most recently from 8f4b815 to b6b7cb4CompareMarch 24, 2026 20:32
@codecov

codecovBot commented Mar 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.46154% with 328 lines in your changes missing coverage. Please review.
✅ Project coverage is 28.87%. Comparing base (eea5ba9) to head (2f14a36).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/payjoin/mod.rs26.21%242 Missing ⚠️
src/handlers.rs0.00%33 Missing ⚠️
src/payjoin/db.rs95.00%30 Missing ⚠️
src/payjoin/ohttp.rs73.33%20 Missing ⚠️
src/error.rs0.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #242 +/- ##
===========================================
+ Coverage 11.36% 28.87% +17.51% 
===========================================
Files 8 9 +1 Lines 2807 3626 +819 ===========================================
+ Hits 319 1047 +728 - Misses 2488 2579 +91 
FlagCoverage Δ
rust28.87% <68.46%> (+17.51%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from b6b7cb4 to c012c3aCompareMarch 24, 2026 20:42
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

Thank you for the review!

Just wanted some clarifications

The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent.
Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

@Mshehu5
Mshehu5 requested a review from tvpeterMarch 30, 2026 06:43

@vadim-anfvvadim-anfv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Mshehu5, thanks for this!

Could you update payjoin dependency to 1.0.0-rc.2 and run just pre-push?

I've got some errors from just pre-push like:

error[E0061]: this method takes 1 argument but 2 arguments were supplied
--> src/payjoin/mod.rs:691:30
|
691 | ... .check_payment(
| ^^^^^^^^^^^^^
...
704 | / ... |outpoint| {
705 || ... let utxo = self.wallet.get_utxo(outpoint);
706 || ... match utxo {
707 || ... Some(_) => Ok(false),
... |
710 || ... }
||___________________________- unexpected argument #2 of type `{closure@src/payjoin/mod.rs:704:33: 704:43}`

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 3 times, most recently from 7d7be05 to 6b06cf7CompareApril 13, 2026 11:46
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the review! I bumped Payjoin to 1.0.0-rc.2 and just pre-push passes locally. Mind giving it a try on your end? Would appreciate your feedback

@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@tvpeter Would really appreciate your feedback on this: #242 (comment)

It would help move this PR forward.

@tvpeter

tvpeter commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I think it should apply to all sessions within a specific timeframe, say anything older than 30 days, and if it can be implemented without adding another command, that would be great. This can even go into another PR.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent. Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

That is still fine. It can form another PR, but it's one of the things you can consider adding.

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

The current implementation covering SQLite is good enough (and the most important). Those others can serve as improvements to the persistence feature.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from db0ccec to 9a62b1eCompareApril 14, 2026 15:22
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 10 times, most recently from 06c4ae7 to 54ae896CompareMay 29, 2026 11:21
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thank you for the review. The reqwest issue is fixed in 7ee0a71. The regression was caused by a separate merged PR that updated reqwest without enabling rustls. I have verified the fix locally and it should be ready for you to retest on your end.

@notmandatorynotmandatory left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK bdb7f38

Thanks for the detailed test steps, very helpful for manual testing. I verified canceling receive and separately sender flow works as expected.

Any other feature enhancements such as using a DB transaction for atomic updates, or improved error types I recommend making new issues for. These sort of small improvements would be better made after #278 and in a future milestone. I also don't think we need this to support the redb persistence right now. If someone needs it they can implement it in a new PR.

@tvpeter

Copy link
Copy Markdown
Collaborator

@Mshehu5 pls rebase

Mshehu5 added 9 commits July 2, 2026 12:30
Persist payjoin sender and receiver state in SQLite so interrupted
payjoin sessions can be resumed after the CLI exits. Add dedicated
tables for send and receive sessions, append-only event logs for
state replay, receiver pubkey lookup for sender sessions, and
seen-input tracking for replay protection.
This follows the intended async payjoin design by persisting session
state across interruptions. SQLite keeps the initial persistence
backend simple and builds on existing rusqlite support, at the cost
of a small payjoin-specific schema and serialization layer.
Wire the payjoin persistence layer into the existing send and receive
flows so session state is saved during normal operation and can be
replayed later. Initialize the database from the handlers, replace
the noop persisters with SQLite-backed persisters, resume existing
sessions, and record seen inputs in the receiver flow for replay
protection.
This moves persistence from a standalone storage layer into the
runtime payjoin workflow so interrupted sessions can continue from
saved state instead of starting over. It also simplifies error
handling by relying on `?` once the storage errors map cleanly into
the CLI error type.
Expose persisted payjoin session state through CLI commands so users
can recover interrupted sessions and review prior session progress.
Add `resume` to continue pending sender and receiver sessions,
`history` to list saved sessions, status text helpers for clearer
output, and session ID filtering to target a specific session.
This improves recovery and troubleshooting for long-running async
payjoin flows by making persisted session state available from the
CLI.
Update the README to cover the new `resume` and `history` commands
so users can recover interrupted payjoin sessions and inspect saved
session state. Also add the SQLite dependency needed for the
payjoin persistence workflow.
This keeps the user-facing setup and command documentation aligned
with the new persistence and recovery behavior.
Move to payjoin 0.25.0 to stay closer to the latest release.
Enable rustls for the direct reqwest client because Payjoin
polling uses bdk-cli's own HTTPS client and without a TLS
backend the first relay poll fails.
Refine the skipped monitoring status message for the receiver
flow.
Delete payjoin sessions older than 30 days when the payjoin database is accessed. Remove related event rows in the same cleanup pass.
Cover the new payjoin persistence paths with focused unit tests.
Exercise replay protection, sender and receiver event persistence,
session pruning and history rendering.
These cases were chosen because the PR's new behavior is
concentrated in the persistence layer and session history path
Prevent wallet failures caused by selecting unconfirmed coins for
payjoin inputs.
Without this filter, the wallet can fail with an "unconfirmed
coins" error during payjoin processing.
Payjoin currently keeps using the relay selected during OHTTP key
bootstrapping. If that relay goes offline, the session fails even when
other relay URLs were supplied.
Select from the available relays for each OHTTP request and remember
failures for the rest of the command. Build a fresh request for every
attempt so failover does not reuse linkable OHTTP ciphertext.
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from bdb7f38 to 2f14a36CompareJuly 2, 2026 11:31
@tvpeter
tvpeter merged commit 16dc723 into bitcoindevkit:masterJul 2, 2026
9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 2, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@Mshehu5@coveralls@tvpeter@DanGould@notmandatory@vadim-anfv@evanlinjin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Payjoin persistence - #242

Merged
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance
Jul 2, 2026
Merged

Payjoin persistence#242
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance

Conversation

@Mshehu5

@Mshehu5Mshehu5 commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Description

#230 needed to be merged for this to go through
Address #149 also follow up to #200
This PR adds persistance to existing async payjoin integration
This introduces neccessary database model and tables also add commad for resume to allow interrupted sessions to be continued also a particular session either send or receive.
A history commad to view payjoin history and status has been added

Notes to the reviewers

Step to review this include making a payjoin transaction

Run a receiver to get a BIP21 URI then pass it to the sender as seen in docs

bdk-cli/README.md

Lines 121 to 141 in b9cf2ac

To start a Payjoin session as the receiver with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet1 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPd2PoUEcGNDHPZmVWgtPYERAwMG6qHheX6LN4oaazp3qZU7mykiaAZga1ZB2SJJR6Mriyq8MocMs7QTe7toaabSwTWu5fRFz/84h/1h/0h/0/*)#8guqp7rn" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet1 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet1 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet1 receive_payjoin --amount 400000 --max_fee_rate 1000 --directory "https://payjo.in" --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com"
```
To send a Payjoin with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet2 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPfBxswkATvZRQ9kDdRbJPtHYZaZCARL2myxcK7DqsqPhRo2G2rRVHFPbowq63BE6S4k2pUMYeF2fUMTT63Q7zhoXtKsM1FaS/84'/1'/0'/0/*)#qf5gnqrf" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet2 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet2 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet2 send_payjoin --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com" --fee_rate 1 --uri "<URI>"
```

To test resumption, interrupt either side with Ctrl+C mid-session then run resume on that side to continue. A few scenarios worth covering: receiver resuming after interrupt and sender resuming after interrupt. Use history after each scenario to confirm the session state was persisted correctly.

docs for this can be seen in 7e4ffd1

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added docs for the new feature

@coveralls

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 22068931916

Details

  • 0 of 763(0.0%) changed or added relevant lines in 5 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-1.9%) to 8.848%

Changes Missing CoverageCovered LinesChanged/Added Lines%
src/error.rs0210.0%
src/utils.rs0220.0%
src/handlers.rs0500.0%
src/payjoin/db.rs02620.0%
src/payjoin/mod.rs04080.0%
Files with Coverage ReductionNew Missed Lines%
src/utils.rs10.0%
src/handlers.rs212.81%
src/payjoin/mod.rs20.0%
TotalsCoverage Status
Change from base Build 21153868360:-1.9%
Covered Lines:268
Relevant Lines:3029

💛 - Coveralls

@notmandatorynotmandatory moved this to In Progress in BDK-CLIFeb 18, 2026
@Mshehu5Mshehu5 changed the title Payjoin persistancePayjoin persistenceFeb 21, 2026
@Mshehu5
Mshehu5 marked this pull request as ready for review February 24, 2026 05:41
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 4 times, most recently from a5e2482 to c79bd95CompareFebruary 24, 2026 12:57
@tvpetertvpeter added this to the CLI 3.0.0 milestone Feb 26, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @Mshehu5 for working on this feature.

Below are some of the observations that I think will make the implementation better:

  • I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.
  • The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.
  • Since the save_event is generated at multiple transitions in a session, I think it will be great if you consider grouping such updates in a db transaction to ensure that entries are saved successfully.

I have also left some comments in the code.

Thank you.

Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/payjoin/mod.rs
@tvpetertvpeter removed this from the CLI 3.0.0 milestone Mar 12, 2026
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 2 times, most recently from 8f4b815 to b6b7cb4CompareMarch 24, 2026 20:32
@codecov

codecovBot commented Mar 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.46154% with 328 lines in your changes missing coverage. Please review.
✅ Project coverage is 28.87%. Comparing base (eea5ba9) to head (2f14a36).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/payjoin/mod.rs26.21%242 Missing ⚠️
src/handlers.rs0.00%33 Missing ⚠️
src/payjoin/db.rs95.00%30 Missing ⚠️
src/payjoin/ohttp.rs73.33%20 Missing ⚠️
src/error.rs0.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #242 +/- ##
===========================================
+ Coverage 11.36% 28.87% +17.51% 
===========================================
Files 8 9 +1 Lines 2807 3626 +819 ===========================================
+ Hits 319 1047 +728 - Misses 2488 2579 +91 
FlagCoverage Δ
rust28.87% <68.46%> (+17.51%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from b6b7cb4 to c012c3aCompareMarch 24, 2026 20:42
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

Thank you for the review!

Just wanted some clarifications

The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent.
Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

@Mshehu5
Mshehu5 requested a review from tvpeterMarch 30, 2026 06:43

@vadim-anfvvadim-anfv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Mshehu5, thanks for this!

Could you update payjoin dependency to 1.0.0-rc.2 and run just pre-push?

I've got some errors from just pre-push like:

error[E0061]: this method takes 1 argument but 2 arguments were supplied
--> src/payjoin/mod.rs:691:30
|
691 | ... .check_payment(
| ^^^^^^^^^^^^^
...
704 | / ... |outpoint| {
705 || ... let utxo = self.wallet.get_utxo(outpoint);
706 || ... match utxo {
707 || ... Some(_) => Ok(false),
... |
710 || ... }
||___________________________- unexpected argument #2 of type `{closure@src/payjoin/mod.rs:704:33: 704:43}`

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 3 times, most recently from 7d7be05 to 6b06cf7CompareApril 13, 2026 11:46
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the review! I bumped Payjoin to 1.0.0-rc.2 and just pre-push passes locally. Mind giving it a try on your end? Would appreciate your feedback

@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@tvpeter Would really appreciate your feedback on this: #242 (comment)

It would help move this PR forward.

@tvpeter

tvpeter commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I think it should apply to all sessions within a specific timeframe, say anything older than 30 days, and if it can be implemented without adding another command, that would be great. This can even go into another PR.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent. Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

That is still fine. It can form another PR, but it's one of the things you can consider adding.

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

The current implementation covering SQLite is good enough (and the most important). Those others can serve as improvements to the persistence feature.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from db0ccec to 9a62b1eCompareApril 14, 2026 15:22
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 10 times, most recently from 06c4ae7 to 54ae896CompareMay 29, 2026 11:21
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thank you for the review. The reqwest issue is fixed in 7ee0a71. The regression was caused by a separate merged PR that updated reqwest without enabling rustls. I have verified the fix locally and it should be ready for you to retest on your end.

@notmandatorynotmandatory left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK bdb7f38

Thanks for the detailed test steps, very helpful for manual testing. I verified canceling receive and separately sender flow works as expected.

Any other feature enhancements such as using a DB transaction for atomic updates, or improved error types I recommend making new issues for. These sort of small improvements would be better made after #278 and in a future milestone. I also don't think we need this to support the redb persistence right now. If someone needs it they can implement it in a new PR.

@tvpeter

Copy link
Copy Markdown
Collaborator

@Mshehu5 pls rebase

Mshehu5 added 9 commits July 2, 2026 12:30
Persist payjoin sender and receiver state in SQLite so interrupted
payjoin sessions can be resumed after the CLI exits. Add dedicated
tables for send and receive sessions, append-only event logs for
state replay, receiver pubkey lookup for sender sessions, and
seen-input tracking for replay protection.
This follows the intended async payjoin design by persisting session
state across interruptions. SQLite keeps the initial persistence
backend simple and builds on existing rusqlite support, at the cost
of a small payjoin-specific schema and serialization layer.
Wire the payjoin persistence layer into the existing send and receive
flows so session state is saved during normal operation and can be
replayed later. Initialize the database from the handlers, replace
the noop persisters with SQLite-backed persisters, resume existing
sessions, and record seen inputs in the receiver flow for replay
protection.
This moves persistence from a standalone storage layer into the
runtime payjoin workflow so interrupted sessions can continue from
saved state instead of starting over. It also simplifies error
handling by relying on `?` once the storage errors map cleanly into
the CLI error type.
Expose persisted payjoin session state through CLI commands so users
can recover interrupted sessions and review prior session progress.
Add `resume` to continue pending sender and receiver sessions,
`history` to list saved sessions, status text helpers for clearer
output, and session ID filtering to target a specific session.
This improves recovery and troubleshooting for long-running async
payjoin flows by making persisted session state available from the
CLI.
Update the README to cover the new `resume` and `history` commands
so users can recover interrupted payjoin sessions and inspect saved
session state. Also add the SQLite dependency needed for the
payjoin persistence workflow.
This keeps the user-facing setup and command documentation aligned
with the new persistence and recovery behavior.
Move to payjoin 0.25.0 to stay closer to the latest release.
Enable rustls for the direct reqwest client because Payjoin
polling uses bdk-cli's own HTTPS client and without a TLS
backend the first relay poll fails.
Refine the skipped monitoring status message for the receiver
flow.
Delete payjoin sessions older than 30 days when the payjoin database is accessed. Remove related event rows in the same cleanup pass.
Cover the new payjoin persistence paths with focused unit tests.
Exercise replay protection, sender and receiver event persistence,
session pruning and history rendering.
These cases were chosen because the PR's new behavior is
concentrated in the persistence layer and session history path
Prevent wallet failures caused by selecting unconfirmed coins for
payjoin inputs.
Without this filter, the wallet can fail with an "unconfirmed
coins" error during payjoin processing.
Payjoin currently keeps using the relay selected during OHTTP key
bootstrapping. If that relay goes offline, the session fails even when
other relay URLs were supplied.
Select from the available relays for each OHTTP request and remember
failures for the rest of the command. Build a fresh request for every
attempt so failover does not reuse linkable OHTTP ciphertext.
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from bdb7f38 to 2f14a36CompareJuly 2, 2026 11:31
@tvpeter
tvpeter merged commit 16dc723 into bitcoindevkit:masterJul 2, 2026
9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 2, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@Mshehu5@coveralls@tvpeter@DanGould@notmandatory@vadim-anfv@evanlinjin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Payjoin persistence - #242

Merged
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance
Jul 2, 2026
Merged

Payjoin persistence#242
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance

Conversation

@Mshehu5

@Mshehu5Mshehu5 commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Description

#230 needed to be merged for this to go through
Address #149 also follow up to #200
This PR adds persistance to existing async payjoin integration
This introduces neccessary database model and tables also add commad for resume to allow interrupted sessions to be continued also a particular session either send or receive.
A history commad to view payjoin history and status has been added

Notes to the reviewers

Step to review this include making a payjoin transaction

Run a receiver to get a BIP21 URI then pass it to the sender as seen in docs

bdk-cli/README.md

Lines 121 to 141 in b9cf2ac

To start a Payjoin session as the receiver with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet1 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPd2PoUEcGNDHPZmVWgtPYERAwMG6qHheX6LN4oaazp3qZU7mykiaAZga1ZB2SJJR6Mriyq8MocMs7QTe7toaabSwTWu5fRFz/84h/1h/0h/0/*)#8guqp7rn" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet1 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet1 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet1 receive_payjoin --amount 400000 --max_fee_rate 1000 --directory "https://payjo.in" --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com"
```
To send a Payjoin with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet2 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPfBxswkATvZRQ9kDdRbJPtHYZaZCARL2myxcK7DqsqPhRo2G2rRVHFPbowq63BE6S4k2pUMYeF2fUMTT63Q7zhoXtKsM1FaS/84'/1'/0'/0/*)#qf5gnqrf" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet2 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet2 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet2 send_payjoin --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com" --fee_rate 1 --uri "<URI>"
```

To test resumption, interrupt either side with Ctrl+C mid-session then run resume on that side to continue. A few scenarios worth covering: receiver resuming after interrupt and sender resuming after interrupt. Use history after each scenario to confirm the session state was persisted correctly.

docs for this can be seen in 7e4ffd1

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added docs for the new feature

@coveralls

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 22068931916

Details

  • 0 of 763(0.0%) changed or added relevant lines in 5 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-1.9%) to 8.848%

Changes Missing CoverageCovered LinesChanged/Added Lines%
src/error.rs0210.0%
src/utils.rs0220.0%
src/handlers.rs0500.0%
src/payjoin/db.rs02620.0%
src/payjoin/mod.rs04080.0%
Files with Coverage ReductionNew Missed Lines%
src/utils.rs10.0%
src/handlers.rs212.81%
src/payjoin/mod.rs20.0%
TotalsCoverage Status
Change from base Build 21153868360:-1.9%
Covered Lines:268
Relevant Lines:3029

💛 - Coveralls

@notmandatorynotmandatory moved this to In Progress in BDK-CLIFeb 18, 2026
@Mshehu5Mshehu5 changed the title Payjoin persistancePayjoin persistenceFeb 21, 2026
@Mshehu5
Mshehu5 marked this pull request as ready for review February 24, 2026 05:41
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 4 times, most recently from a5e2482 to c79bd95CompareFebruary 24, 2026 12:57
@tvpetertvpeter added this to the CLI 3.0.0 milestone Feb 26, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @Mshehu5 for working on this feature.

Below are some of the observations that I think will make the implementation better:

  • I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.
  • The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.
  • Since the save_event is generated at multiple transitions in a session, I think it will be great if you consider grouping such updates in a db transaction to ensure that entries are saved successfully.

I have also left some comments in the code.

Thank you.

Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/payjoin/mod.rs
@tvpetertvpeter removed this from the CLI 3.0.0 milestone Mar 12, 2026
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 2 times, most recently from 8f4b815 to b6b7cb4CompareMarch 24, 2026 20:32
@codecov

codecovBot commented Mar 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.46154% with 328 lines in your changes missing coverage. Please review.
✅ Project coverage is 28.87%. Comparing base (eea5ba9) to head (2f14a36).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/payjoin/mod.rs26.21%242 Missing ⚠️
src/handlers.rs0.00%33 Missing ⚠️
src/payjoin/db.rs95.00%30 Missing ⚠️
src/payjoin/ohttp.rs73.33%20 Missing ⚠️
src/error.rs0.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #242 +/- ##
===========================================
+ Coverage 11.36% 28.87% +17.51% 
===========================================
Files 8 9 +1 Lines 2807 3626 +819 ===========================================
+ Hits 319 1047 +728 - Misses 2488 2579 +91 
FlagCoverage Δ
rust28.87% <68.46%> (+17.51%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from b6b7cb4 to c012c3aCompareMarch 24, 2026 20:42
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

Thank you for the review!

Just wanted some clarifications

The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent.
Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

@Mshehu5
Mshehu5 requested a review from tvpeterMarch 30, 2026 06:43

@vadim-anfvvadim-anfv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Mshehu5, thanks for this!

Could you update payjoin dependency to 1.0.0-rc.2 and run just pre-push?

I've got some errors from just pre-push like:

error[E0061]: this method takes 1 argument but 2 arguments were supplied
--> src/payjoin/mod.rs:691:30
|
691 | ... .check_payment(
| ^^^^^^^^^^^^^
...
704 | / ... |outpoint| {
705 || ... let utxo = self.wallet.get_utxo(outpoint);
706 || ... match utxo {
707 || ... Some(_) => Ok(false),
... |
710 || ... }
||___________________________- unexpected argument #2 of type `{closure@src/payjoin/mod.rs:704:33: 704:43}`

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 3 times, most recently from 7d7be05 to 6b06cf7CompareApril 13, 2026 11:46
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the review! I bumped Payjoin to 1.0.0-rc.2 and just pre-push passes locally. Mind giving it a try on your end? Would appreciate your feedback

@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@tvpeter Would really appreciate your feedback on this: #242 (comment)

It would help move this PR forward.

@tvpeter

tvpeter commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I think it should apply to all sessions within a specific timeframe, say anything older than 30 days, and if it can be implemented without adding another command, that would be great. This can even go into another PR.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent. Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

That is still fine. It can form another PR, but it's one of the things you can consider adding.

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

The current implementation covering SQLite is good enough (and the most important). Those others can serve as improvements to the persistence feature.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from db0ccec to 9a62b1eCompareApril 14, 2026 15:22
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 10 times, most recently from 06c4ae7 to 54ae896CompareMay 29, 2026 11:21
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thank you for the review. The reqwest issue is fixed in 7ee0a71. The regression was caused by a separate merged PR that updated reqwest without enabling rustls. I have verified the fix locally and it should be ready for you to retest on your end.

@notmandatorynotmandatory left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK bdb7f38

Thanks for the detailed test steps, very helpful for manual testing. I verified canceling receive and separately sender flow works as expected.

Any other feature enhancements such as using a DB transaction for atomic updates, or improved error types I recommend making new issues for. These sort of small improvements would be better made after #278 and in a future milestone. I also don't think we need this to support the redb persistence right now. If someone needs it they can implement it in a new PR.

@tvpeter

Copy link
Copy Markdown
Collaborator

@Mshehu5 pls rebase

Mshehu5 added 9 commits July 2, 2026 12:30
Persist payjoin sender and receiver state in SQLite so interrupted
payjoin sessions can be resumed after the CLI exits. Add dedicated
tables for send and receive sessions, append-only event logs for
state replay, receiver pubkey lookup for sender sessions, and
seen-input tracking for replay protection.
This follows the intended async payjoin design by persisting session
state across interruptions. SQLite keeps the initial persistence
backend simple and builds on existing rusqlite support, at the cost
of a small payjoin-specific schema and serialization layer.
Wire the payjoin persistence layer into the existing send and receive
flows so session state is saved during normal operation and can be
replayed later. Initialize the database from the handlers, replace
the noop persisters with SQLite-backed persisters, resume existing
sessions, and record seen inputs in the receiver flow for replay
protection.
This moves persistence from a standalone storage layer into the
runtime payjoin workflow so interrupted sessions can continue from
saved state instead of starting over. It also simplifies error
handling by relying on `?` once the storage errors map cleanly into
the CLI error type.
Expose persisted payjoin session state through CLI commands so users
can recover interrupted sessions and review prior session progress.
Add `resume` to continue pending sender and receiver sessions,
`history` to list saved sessions, status text helpers for clearer
output, and session ID filtering to target a specific session.
This improves recovery and troubleshooting for long-running async
payjoin flows by making persisted session state available from the
CLI.
Update the README to cover the new `resume` and `history` commands
so users can recover interrupted payjoin sessions and inspect saved
session state. Also add the SQLite dependency needed for the
payjoin persistence workflow.
This keeps the user-facing setup and command documentation aligned
with the new persistence and recovery behavior.
Move to payjoin 0.25.0 to stay closer to the latest release.
Enable rustls for the direct reqwest client because Payjoin
polling uses bdk-cli's own HTTPS client and without a TLS
backend the first relay poll fails.
Refine the skipped monitoring status message for the receiver
flow.
Delete payjoin sessions older than 30 days when the payjoin database is accessed. Remove related event rows in the same cleanup pass.
Cover the new payjoin persistence paths with focused unit tests.
Exercise replay protection, sender and receiver event persistence,
session pruning and history rendering.
These cases were chosen because the PR's new behavior is
concentrated in the persistence layer and session history path
Prevent wallet failures caused by selecting unconfirmed coins for
payjoin inputs.
Without this filter, the wallet can fail with an "unconfirmed
coins" error during payjoin processing.
Payjoin currently keeps using the relay selected during OHTTP key
bootstrapping. If that relay goes offline, the session fails even when
other relay URLs were supplied.
Select from the available relays for each OHTTP request and remember
failures for the rest of the command. Build a fresh request for every
attempt so failover does not reuse linkable OHTTP ciphertext.
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from bdb7f38 to 2f14a36CompareJuly 2, 2026 11:31
@tvpeter
tvpeter merged commit 16dc723 into bitcoindevkit:masterJul 2, 2026
9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 2, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@Mshehu5@coveralls@tvpeter@DanGould@notmandatory@vadim-anfv@evanlinjin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Payjoin persistence - #242

Merged
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance
Jul 2, 2026
Merged

Payjoin persistence#242
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance

Conversation

@Mshehu5

@Mshehu5Mshehu5 commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Description

#230 needed to be merged for this to go through
Address #149 also follow up to #200
This PR adds persistance to existing async payjoin integration
This introduces neccessary database model and tables also add commad for resume to allow interrupted sessions to be continued also a particular session either send or receive.
A history commad to view payjoin history and status has been added

Notes to the reviewers

Step to review this include making a payjoin transaction

Run a receiver to get a BIP21 URI then pass it to the sender as seen in docs

bdk-cli/README.md

Lines 121 to 141 in b9cf2ac

To start a Payjoin session as the receiver with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet1 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPd2PoUEcGNDHPZmVWgtPYERAwMG6qHheX6LN4oaazp3qZU7mykiaAZga1ZB2SJJR6Mriyq8MocMs7QTe7toaabSwTWu5fRFz/84h/1h/0h/0/*)#8guqp7rn" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet1 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet1 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet1 receive_payjoin --amount 400000 --max_fee_rate 1000 --directory "https://payjo.in" --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com"
```
To send a Payjoin with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet2 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPfBxswkATvZRQ9kDdRbJPtHYZaZCARL2myxcK7DqsqPhRo2G2rRVHFPbowq63BE6S4k2pUMYeF2fUMTT63Q7zhoXtKsM1FaS/84'/1'/0'/0/*)#qf5gnqrf" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet2 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet2 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet2 send_payjoin --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com" --fee_rate 1 --uri "<URI>"
```

To test resumption, interrupt either side with Ctrl+C mid-session then run resume on that side to continue. A few scenarios worth covering: receiver resuming after interrupt and sender resuming after interrupt. Use history after each scenario to confirm the session state was persisted correctly.

docs for this can be seen in 7e4ffd1

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added docs for the new feature

@coveralls

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 22068931916

Details

  • 0 of 763(0.0%) changed or added relevant lines in 5 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-1.9%) to 8.848%

Changes Missing CoverageCovered LinesChanged/Added Lines%
src/error.rs0210.0%
src/utils.rs0220.0%
src/handlers.rs0500.0%
src/payjoin/db.rs02620.0%
src/payjoin/mod.rs04080.0%
Files with Coverage ReductionNew Missed Lines%
src/utils.rs10.0%
src/handlers.rs212.81%
src/payjoin/mod.rs20.0%
TotalsCoverage Status
Change from base Build 21153868360:-1.9%
Covered Lines:268
Relevant Lines:3029

💛 - Coveralls

@notmandatorynotmandatory moved this to In Progress in BDK-CLIFeb 18, 2026
@Mshehu5Mshehu5 changed the title Payjoin persistancePayjoin persistenceFeb 21, 2026
@Mshehu5
Mshehu5 marked this pull request as ready for review February 24, 2026 05:41
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 4 times, most recently from a5e2482 to c79bd95CompareFebruary 24, 2026 12:57
@tvpetertvpeter added this to the CLI 3.0.0 milestone Feb 26, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @Mshehu5 for working on this feature.

Below are some of the observations that I think will make the implementation better:

  • I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.
  • The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.
  • Since the save_event is generated at multiple transitions in a session, I think it will be great if you consider grouping such updates in a db transaction to ensure that entries are saved successfully.

I have also left some comments in the code.

Thank you.

Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/payjoin/mod.rs
@tvpetertvpeter removed this from the CLI 3.0.0 milestone Mar 12, 2026
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 2 times, most recently from 8f4b815 to b6b7cb4CompareMarch 24, 2026 20:32
@codecov

codecovBot commented Mar 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.46154% with 328 lines in your changes missing coverage. Please review.
✅ Project coverage is 28.87%. Comparing base (eea5ba9) to head (2f14a36).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/payjoin/mod.rs26.21%242 Missing ⚠️
src/handlers.rs0.00%33 Missing ⚠️
src/payjoin/db.rs95.00%30 Missing ⚠️
src/payjoin/ohttp.rs73.33%20 Missing ⚠️
src/error.rs0.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #242 +/- ##
===========================================
+ Coverage 11.36% 28.87% +17.51% 
===========================================
Files 8 9 +1 Lines 2807 3626 +819 ===========================================
+ Hits 319 1047 +728 - Misses 2488 2579 +91 
FlagCoverage Δ
rust28.87% <68.46%> (+17.51%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from b6b7cb4 to c012c3aCompareMarch 24, 2026 20:42
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

Thank you for the review!

Just wanted some clarifications

The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent.
Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

@Mshehu5
Mshehu5 requested a review from tvpeterMarch 30, 2026 06:43

@vadim-anfvvadim-anfv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Mshehu5, thanks for this!

Could you update payjoin dependency to 1.0.0-rc.2 and run just pre-push?

I've got some errors from just pre-push like:

error[E0061]: this method takes 1 argument but 2 arguments were supplied
--> src/payjoin/mod.rs:691:30
|
691 | ... .check_payment(
| ^^^^^^^^^^^^^
...
704 | / ... |outpoint| {
705 || ... let utxo = self.wallet.get_utxo(outpoint);
706 || ... match utxo {
707 || ... Some(_) => Ok(false),
... |
710 || ... }
||___________________________- unexpected argument #2 of type `{closure@src/payjoin/mod.rs:704:33: 704:43}`

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 3 times, most recently from 7d7be05 to 6b06cf7CompareApril 13, 2026 11:46
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the review! I bumped Payjoin to 1.0.0-rc.2 and just pre-push passes locally. Mind giving it a try on your end? Would appreciate your feedback

@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@tvpeter Would really appreciate your feedback on this: #242 (comment)

It would help move this PR forward.

@tvpeter

tvpeter commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I think it should apply to all sessions within a specific timeframe, say anything older than 30 days, and if it can be implemented without adding another command, that would be great. This can even go into another PR.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent. Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

That is still fine. It can form another PR, but it's one of the things you can consider adding.

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

The current implementation covering SQLite is good enough (and the most important). Those others can serve as improvements to the persistence feature.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from db0ccec to 9a62b1eCompareApril 14, 2026 15:22
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 10 times, most recently from 06c4ae7 to 54ae896CompareMay 29, 2026 11:21
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thank you for the review. The reqwest issue is fixed in 7ee0a71. The regression was caused by a separate merged PR that updated reqwest without enabling rustls. I have verified the fix locally and it should be ready for you to retest on your end.

@notmandatorynotmandatory left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK bdb7f38

Thanks for the detailed test steps, very helpful for manual testing. I verified canceling receive and separately sender flow works as expected.

Any other feature enhancements such as using a DB transaction for atomic updates, or improved error types I recommend making new issues for. These sort of small improvements would be better made after #278 and in a future milestone. I also don't think we need this to support the redb persistence right now. If someone needs it they can implement it in a new PR.

@tvpeter

Copy link
Copy Markdown
Collaborator

@Mshehu5 pls rebase

Mshehu5 added 9 commits July 2, 2026 12:30
Persist payjoin sender and receiver state in SQLite so interrupted
payjoin sessions can be resumed after the CLI exits. Add dedicated
tables for send and receive sessions, append-only event logs for
state replay, receiver pubkey lookup for sender sessions, and
seen-input tracking for replay protection.
This follows the intended async payjoin design by persisting session
state across interruptions. SQLite keeps the initial persistence
backend simple and builds on existing rusqlite support, at the cost
of a small payjoin-specific schema and serialization layer.
Wire the payjoin persistence layer into the existing send and receive
flows so session state is saved during normal operation and can be
replayed later. Initialize the database from the handlers, replace
the noop persisters with SQLite-backed persisters, resume existing
sessions, and record seen inputs in the receiver flow for replay
protection.
This moves persistence from a standalone storage layer into the
runtime payjoin workflow so interrupted sessions can continue from
saved state instead of starting over. It also simplifies error
handling by relying on `?` once the storage errors map cleanly into
the CLI error type.
Expose persisted payjoin session state through CLI commands so users
can recover interrupted sessions and review prior session progress.
Add `resume` to continue pending sender and receiver sessions,
`history` to list saved sessions, status text helpers for clearer
output, and session ID filtering to target a specific session.
This improves recovery and troubleshooting for long-running async
payjoin flows by making persisted session state available from the
CLI.
Update the README to cover the new `resume` and `history` commands
so users can recover interrupted payjoin sessions and inspect saved
session state. Also add the SQLite dependency needed for the
payjoin persistence workflow.
This keeps the user-facing setup and command documentation aligned
with the new persistence and recovery behavior.
Move to payjoin 0.25.0 to stay closer to the latest release.
Enable rustls for the direct reqwest client because Payjoin
polling uses bdk-cli's own HTTPS client and without a TLS
backend the first relay poll fails.
Refine the skipped monitoring status message for the receiver
flow.
Delete payjoin sessions older than 30 days when the payjoin database is accessed. Remove related event rows in the same cleanup pass.
Cover the new payjoin persistence paths with focused unit tests.
Exercise replay protection, sender and receiver event persistence,
session pruning and history rendering.
These cases were chosen because the PR's new behavior is
concentrated in the persistence layer and session history path
Prevent wallet failures caused by selecting unconfirmed coins for
payjoin inputs.
Without this filter, the wallet can fail with an "unconfirmed
coins" error during payjoin processing.
Payjoin currently keeps using the relay selected during OHTTP key
bootstrapping. If that relay goes offline, the session fails even when
other relay URLs were supplied.
Select from the available relays for each OHTTP request and remember
failures for the rest of the command. Build a fresh request for every
attempt so failover does not reuse linkable OHTTP ciphertext.
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from bdb7f38 to 2f14a36CompareJuly 2, 2026 11:31
@tvpeter
tvpeter merged commit 16dc723 into bitcoindevkit:masterJul 2, 2026
9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 2, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@Mshehu5@coveralls@tvpeter@DanGould@notmandatory@vadim-anfv@evanlinjin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Payjoin persistence - #242

Merged
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance
Jul 2, 2026
Merged

Payjoin persistence#242
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance

Conversation

@Mshehu5

@Mshehu5Mshehu5 commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Description

#230 needed to be merged for this to go through
Address #149 also follow up to #200
This PR adds persistance to existing async payjoin integration
This introduces neccessary database model and tables also add commad for resume to allow interrupted sessions to be continued also a particular session either send or receive.
A history commad to view payjoin history and status has been added

Notes to the reviewers

Step to review this include making a payjoin transaction

Run a receiver to get a BIP21 URI then pass it to the sender as seen in docs

bdk-cli/README.md

Lines 121 to 141 in b9cf2ac

To start a Payjoin session as the receiver with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet1 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPd2PoUEcGNDHPZmVWgtPYERAwMG6qHheX6LN4oaazp3qZU7mykiaAZga1ZB2SJJR6Mriyq8MocMs7QTe7toaabSwTWu5fRFz/84h/1h/0h/0/*)#8guqp7rn" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet1 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet1 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet1 receive_payjoin --amount 400000 --max_fee_rate 1000 --directory "https://payjo.in" --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com"
```
To send a Payjoin with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet2 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPfBxswkATvZRQ9kDdRbJPtHYZaZCARL2myxcK7DqsqPhRo2G2rRVHFPbowq63BE6S4k2pUMYeF2fUMTT63Q7zhoXtKsM1FaS/84'/1'/0'/0/*)#qf5gnqrf" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet2 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet2 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet2 send_payjoin --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com" --fee_rate 1 --uri "<URI>"
```

To test resumption, interrupt either side with Ctrl+C mid-session then run resume on that side to continue. A few scenarios worth covering: receiver resuming after interrupt and sender resuming after interrupt. Use history after each scenario to confirm the session state was persisted correctly.

docs for this can be seen in 7e4ffd1

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added docs for the new feature

@coveralls

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 22068931916

Details

  • 0 of 763(0.0%) changed or added relevant lines in 5 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-1.9%) to 8.848%

Changes Missing CoverageCovered LinesChanged/Added Lines%
src/error.rs0210.0%
src/utils.rs0220.0%
src/handlers.rs0500.0%
src/payjoin/db.rs02620.0%
src/payjoin/mod.rs04080.0%
Files with Coverage ReductionNew Missed Lines%
src/utils.rs10.0%
src/handlers.rs212.81%
src/payjoin/mod.rs20.0%
TotalsCoverage Status
Change from base Build 21153868360:-1.9%
Covered Lines:268
Relevant Lines:3029

💛 - Coveralls

@notmandatorynotmandatory moved this to In Progress in BDK-CLIFeb 18, 2026
@Mshehu5Mshehu5 changed the title Payjoin persistancePayjoin persistenceFeb 21, 2026
@Mshehu5
Mshehu5 marked this pull request as ready for review February 24, 2026 05:41
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 4 times, most recently from a5e2482 to c79bd95CompareFebruary 24, 2026 12:57
@tvpetertvpeter added this to the CLI 3.0.0 milestone Feb 26, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @Mshehu5 for working on this feature.

Below are some of the observations that I think will make the implementation better:

  • I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.
  • The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.
  • Since the save_event is generated at multiple transitions in a session, I think it will be great if you consider grouping such updates in a db transaction to ensure that entries are saved successfully.

I have also left some comments in the code.

Thank you.

Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/payjoin/mod.rs
@tvpetertvpeter removed this from the CLI 3.0.0 milestone Mar 12, 2026
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 2 times, most recently from 8f4b815 to b6b7cb4CompareMarch 24, 2026 20:32
@codecov

codecovBot commented Mar 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.46154% with 328 lines in your changes missing coverage. Please review.
✅ Project coverage is 28.87%. Comparing base (eea5ba9) to head (2f14a36).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/payjoin/mod.rs26.21%242 Missing ⚠️
src/handlers.rs0.00%33 Missing ⚠️
src/payjoin/db.rs95.00%30 Missing ⚠️
src/payjoin/ohttp.rs73.33%20 Missing ⚠️
src/error.rs0.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #242 +/- ##
===========================================
+ Coverage 11.36% 28.87% +17.51% 
===========================================
Files 8 9 +1 Lines 2807 3626 +819 ===========================================
+ Hits 319 1047 +728 - Misses 2488 2579 +91 
FlagCoverage Δ
rust28.87% <68.46%> (+17.51%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from b6b7cb4 to c012c3aCompareMarch 24, 2026 20:42
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

Thank you for the review!

Just wanted some clarifications

The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent.
Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

@Mshehu5
Mshehu5 requested a review from tvpeterMarch 30, 2026 06:43

@vadim-anfvvadim-anfv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Mshehu5, thanks for this!

Could you update payjoin dependency to 1.0.0-rc.2 and run just pre-push?

I've got some errors from just pre-push like:

error[E0061]: this method takes 1 argument but 2 arguments were supplied
--> src/payjoin/mod.rs:691:30
|
691 | ... .check_payment(
| ^^^^^^^^^^^^^
...
704 | / ... |outpoint| {
705 || ... let utxo = self.wallet.get_utxo(outpoint);
706 || ... match utxo {
707 || ... Some(_) => Ok(false),
... |
710 || ... }
||___________________________- unexpected argument #2 of type `{closure@src/payjoin/mod.rs:704:33: 704:43}`

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 3 times, most recently from 7d7be05 to 6b06cf7CompareApril 13, 2026 11:46
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the review! I bumped Payjoin to 1.0.0-rc.2 and just pre-push passes locally. Mind giving it a try on your end? Would appreciate your feedback

@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@tvpeter Would really appreciate your feedback on this: #242 (comment)

It would help move this PR forward.

@tvpeter

tvpeter commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I think it should apply to all sessions within a specific timeframe, say anything older than 30 days, and if it can be implemented without adding another command, that would be great. This can even go into another PR.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent. Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

That is still fine. It can form another PR, but it's one of the things you can consider adding.

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

The current implementation covering SQLite is good enough (and the most important). Those others can serve as improvements to the persistence feature.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from db0ccec to 9a62b1eCompareApril 14, 2026 15:22
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 10 times, most recently from 06c4ae7 to 54ae896CompareMay 29, 2026 11:21
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thank you for the review. The reqwest issue is fixed in 7ee0a71. The regression was caused by a separate merged PR that updated reqwest without enabling rustls. I have verified the fix locally and it should be ready for you to retest on your end.

@notmandatorynotmandatory left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK bdb7f38

Thanks for the detailed test steps, very helpful for manual testing. I verified canceling receive and separately sender flow works as expected.

Any other feature enhancements such as using a DB transaction for atomic updates, or improved error types I recommend making new issues for. These sort of small improvements would be better made after #278 and in a future milestone. I also don't think we need this to support the redb persistence right now. If someone needs it they can implement it in a new PR.

@tvpeter

Copy link
Copy Markdown
Collaborator

@Mshehu5 pls rebase

Mshehu5 added 9 commits July 2, 2026 12:30
Persist payjoin sender and receiver state in SQLite so interrupted
payjoin sessions can be resumed after the CLI exits. Add dedicated
tables for send and receive sessions, append-only event logs for
state replay, receiver pubkey lookup for sender sessions, and
seen-input tracking for replay protection.
This follows the intended async payjoin design by persisting session
state across interruptions. SQLite keeps the initial persistence
backend simple and builds on existing rusqlite support, at the cost
of a small payjoin-specific schema and serialization layer.
Wire the payjoin persistence layer into the existing send and receive
flows so session state is saved during normal operation and can be
replayed later. Initialize the database from the handlers, replace
the noop persisters with SQLite-backed persisters, resume existing
sessions, and record seen inputs in the receiver flow for replay
protection.
This moves persistence from a standalone storage layer into the
runtime payjoin workflow so interrupted sessions can continue from
saved state instead of starting over. It also simplifies error
handling by relying on `?` once the storage errors map cleanly into
the CLI error type.
Expose persisted payjoin session state through CLI commands so users
can recover interrupted sessions and review prior session progress.
Add `resume` to continue pending sender and receiver sessions,
`history` to list saved sessions, status text helpers for clearer
output, and session ID filtering to target a specific session.
This improves recovery and troubleshooting for long-running async
payjoin flows by making persisted session state available from the
CLI.
Update the README to cover the new `resume` and `history` commands
so users can recover interrupted payjoin sessions and inspect saved
session state. Also add the SQLite dependency needed for the
payjoin persistence workflow.
This keeps the user-facing setup and command documentation aligned
with the new persistence and recovery behavior.
Move to payjoin 0.25.0 to stay closer to the latest release.
Enable rustls for the direct reqwest client because Payjoin
polling uses bdk-cli's own HTTPS client and without a TLS
backend the first relay poll fails.
Refine the skipped monitoring status message for the receiver
flow.
Delete payjoin sessions older than 30 days when the payjoin database is accessed. Remove related event rows in the same cleanup pass.
Cover the new payjoin persistence paths with focused unit tests.
Exercise replay protection, sender and receiver event persistence,
session pruning and history rendering.
These cases were chosen because the PR's new behavior is
concentrated in the persistence layer and session history path
Prevent wallet failures caused by selecting unconfirmed coins for
payjoin inputs.
Without this filter, the wallet can fail with an "unconfirmed
coins" error during payjoin processing.
Payjoin currently keeps using the relay selected during OHTTP key
bootstrapping. If that relay goes offline, the session fails even when
other relay URLs were supplied.
Select from the available relays for each OHTTP request and remember
failures for the rest of the command. Build a fresh request for every
attempt so failover does not reuse linkable OHTTP ciphertext.
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from bdb7f38 to 2f14a36CompareJuly 2, 2026 11:31
@tvpeter
tvpeter merged commit 16dc723 into bitcoindevkit:masterJul 2, 2026
9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 2, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@Mshehu5@coveralls@tvpeter@DanGould@notmandatory@vadim-anfv@evanlinjin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Payjoin persistence - #242

Merged
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance
Jul 2, 2026
Merged

Payjoin persistence#242
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance

Conversation

@Mshehu5

@Mshehu5Mshehu5 commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Description

#230 needed to be merged for this to go through
Address #149 also follow up to #200
This PR adds persistance to existing async payjoin integration
This introduces neccessary database model and tables also add commad for resume to allow interrupted sessions to be continued also a particular session either send or receive.
A history commad to view payjoin history and status has been added

Notes to the reviewers

Step to review this include making a payjoin transaction

Run a receiver to get a BIP21 URI then pass it to the sender as seen in docs

bdk-cli/README.md

Lines 121 to 141 in b9cf2ac

To start a Payjoin session as the receiver with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet1 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPd2PoUEcGNDHPZmVWgtPYERAwMG6qHheX6LN4oaazp3qZU7mykiaAZga1ZB2SJJR6Mriyq8MocMs7QTe7toaabSwTWu5fRFz/84h/1h/0h/0/*)#8guqp7rn" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet1 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet1 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet1 receive_payjoin --amount 400000 --max_fee_rate 1000 --directory "https://payjo.in" --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com"
```
To send a Payjoin with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet2 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPfBxswkATvZRQ9kDdRbJPtHYZaZCARL2myxcK7DqsqPhRo2G2rRVHFPbowq63BE6S4k2pUMYeF2fUMTT63Q7zhoXtKsM1FaS/84'/1'/0'/0/*)#qf5gnqrf" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet2 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet2 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet2 send_payjoin --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com" --fee_rate 1 --uri "<URI>"
```

To test resumption, interrupt either side with Ctrl+C mid-session then run resume on that side to continue. A few scenarios worth covering: receiver resuming after interrupt and sender resuming after interrupt. Use history after each scenario to confirm the session state was persisted correctly.

docs for this can be seen in 7e4ffd1

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added docs for the new feature

@coveralls

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 22068931916

Details

  • 0 of 763(0.0%) changed or added relevant lines in 5 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-1.9%) to 8.848%

Changes Missing CoverageCovered LinesChanged/Added Lines%
src/error.rs0210.0%
src/utils.rs0220.0%
src/handlers.rs0500.0%
src/payjoin/db.rs02620.0%
src/payjoin/mod.rs04080.0%
Files with Coverage ReductionNew Missed Lines%
src/utils.rs10.0%
src/handlers.rs212.81%
src/payjoin/mod.rs20.0%
TotalsCoverage Status
Change from base Build 21153868360:-1.9%
Covered Lines:268
Relevant Lines:3029

💛 - Coveralls

@notmandatorynotmandatory moved this to In Progress in BDK-CLIFeb 18, 2026
@Mshehu5Mshehu5 changed the title Payjoin persistancePayjoin persistenceFeb 21, 2026
@Mshehu5
Mshehu5 marked this pull request as ready for review February 24, 2026 05:41
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 4 times, most recently from a5e2482 to c79bd95CompareFebruary 24, 2026 12:57
@tvpetertvpeter added this to the CLI 3.0.0 milestone Feb 26, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @Mshehu5 for working on this feature.

Below are some of the observations that I think will make the implementation better:

  • I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.
  • The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.
  • Since the save_event is generated at multiple transitions in a session, I think it will be great if you consider grouping such updates in a db transaction to ensure that entries are saved successfully.

I have also left some comments in the code.

Thank you.

Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/payjoin/mod.rs
@tvpetertvpeter removed this from the CLI 3.0.0 milestone Mar 12, 2026
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 2 times, most recently from 8f4b815 to b6b7cb4CompareMarch 24, 2026 20:32
@codecov

codecovBot commented Mar 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.46154% with 328 lines in your changes missing coverage. Please review.
✅ Project coverage is 28.87%. Comparing base (eea5ba9) to head (2f14a36).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/payjoin/mod.rs26.21%242 Missing ⚠️
src/handlers.rs0.00%33 Missing ⚠️
src/payjoin/db.rs95.00%30 Missing ⚠️
src/payjoin/ohttp.rs73.33%20 Missing ⚠️
src/error.rs0.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #242 +/- ##
===========================================
+ Coverage 11.36% 28.87% +17.51% 
===========================================
Files 8 9 +1 Lines 2807 3626 +819 ===========================================
+ Hits 319 1047 +728 - Misses 2488 2579 +91 
FlagCoverage Δ
rust28.87% <68.46%> (+17.51%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from b6b7cb4 to c012c3aCompareMarch 24, 2026 20:42
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

Thank you for the review!

Just wanted some clarifications

The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent.
Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

@Mshehu5
Mshehu5 requested a review from tvpeterMarch 30, 2026 06:43

@vadim-anfvvadim-anfv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Mshehu5, thanks for this!

Could you update payjoin dependency to 1.0.0-rc.2 and run just pre-push?

I've got some errors from just pre-push like:

error[E0061]: this method takes 1 argument but 2 arguments were supplied
--> src/payjoin/mod.rs:691:30
|
691 | ... .check_payment(
| ^^^^^^^^^^^^^
...
704 | / ... |outpoint| {
705 || ... let utxo = self.wallet.get_utxo(outpoint);
706 || ... match utxo {
707 || ... Some(_) => Ok(false),
... |
710 || ... }
||___________________________- unexpected argument #2 of type `{closure@src/payjoin/mod.rs:704:33: 704:43}`

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 3 times, most recently from 7d7be05 to 6b06cf7CompareApril 13, 2026 11:46
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the review! I bumped Payjoin to 1.0.0-rc.2 and just pre-push passes locally. Mind giving it a try on your end? Would appreciate your feedback

@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@tvpeter Would really appreciate your feedback on this: #242 (comment)

It would help move this PR forward.

@tvpeter

tvpeter commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I think it should apply to all sessions within a specific timeframe, say anything older than 30 days, and if it can be implemented without adding another command, that would be great. This can even go into another PR.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent. Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

That is still fine. It can form another PR, but it's one of the things you can consider adding.

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

The current implementation covering SQLite is good enough (and the most important). Those others can serve as improvements to the persistence feature.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from db0ccec to 9a62b1eCompareApril 14, 2026 15:22
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 10 times, most recently from 06c4ae7 to 54ae896CompareMay 29, 2026 11:21
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thank you for the review. The reqwest issue is fixed in 7ee0a71. The regression was caused by a separate merged PR that updated reqwest without enabling rustls. I have verified the fix locally and it should be ready for you to retest on your end.

@notmandatorynotmandatory left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK bdb7f38

Thanks for the detailed test steps, very helpful for manual testing. I verified canceling receive and separately sender flow works as expected.

Any other feature enhancements such as using a DB transaction for atomic updates, or improved error types I recommend making new issues for. These sort of small improvements would be better made after #278 and in a future milestone. I also don't think we need this to support the redb persistence right now. If someone needs it they can implement it in a new PR.

@tvpeter

Copy link
Copy Markdown
Collaborator

@Mshehu5 pls rebase

Mshehu5 added 9 commits July 2, 2026 12:30
Persist payjoin sender and receiver state in SQLite so interrupted
payjoin sessions can be resumed after the CLI exits. Add dedicated
tables for send and receive sessions, append-only event logs for
state replay, receiver pubkey lookup for sender sessions, and
seen-input tracking for replay protection.
This follows the intended async payjoin design by persisting session
state across interruptions. SQLite keeps the initial persistence
backend simple and builds on existing rusqlite support, at the cost
of a small payjoin-specific schema and serialization layer.
Wire the payjoin persistence layer into the existing send and receive
flows so session state is saved during normal operation and can be
replayed later. Initialize the database from the handlers, replace
the noop persisters with SQLite-backed persisters, resume existing
sessions, and record seen inputs in the receiver flow for replay
protection.
This moves persistence from a standalone storage layer into the
runtime payjoin workflow so interrupted sessions can continue from
saved state instead of starting over. It also simplifies error
handling by relying on `?` once the storage errors map cleanly into
the CLI error type.
Expose persisted payjoin session state through CLI commands so users
can recover interrupted sessions and review prior session progress.
Add `resume` to continue pending sender and receiver sessions,
`history` to list saved sessions, status text helpers for clearer
output, and session ID filtering to target a specific session.
This improves recovery and troubleshooting for long-running async
payjoin flows by making persisted session state available from the
CLI.
Update the README to cover the new `resume` and `history` commands
so users can recover interrupted payjoin sessions and inspect saved
session state. Also add the SQLite dependency needed for the
payjoin persistence workflow.
This keeps the user-facing setup and command documentation aligned
with the new persistence and recovery behavior.
Move to payjoin 0.25.0 to stay closer to the latest release.
Enable rustls for the direct reqwest client because Payjoin
polling uses bdk-cli's own HTTPS client and without a TLS
backend the first relay poll fails.
Refine the skipped monitoring status message for the receiver
flow.
Delete payjoin sessions older than 30 days when the payjoin database is accessed. Remove related event rows in the same cleanup pass.
Cover the new payjoin persistence paths with focused unit tests.
Exercise replay protection, sender and receiver event persistence,
session pruning and history rendering.
These cases were chosen because the PR's new behavior is
concentrated in the persistence layer and session history path
Prevent wallet failures caused by selecting unconfirmed coins for
payjoin inputs.
Without this filter, the wallet can fail with an "unconfirmed
coins" error during payjoin processing.
Payjoin currently keeps using the relay selected during OHTTP key
bootstrapping. If that relay goes offline, the session fails even when
other relay URLs were supplied.
Select from the available relays for each OHTTP request and remember
failures for the rest of the command. Build a fresh request for every
attempt so failover does not reuse linkable OHTTP ciphertext.
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from bdb7f38 to 2f14a36CompareJuly 2, 2026 11:31
@tvpeter
tvpeter merged commit 16dc723 into bitcoindevkit:masterJul 2, 2026
9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 2, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@Mshehu5@coveralls@tvpeter@DanGould@notmandatory@vadim-anfv@evanlinjin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Payjoin persistence - #242

Merged
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance
Jul 2, 2026
Merged

Payjoin persistence#242
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance

Conversation

@Mshehu5

@Mshehu5Mshehu5 commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Description

#230 needed to be merged for this to go through
Address #149 also follow up to #200
This PR adds persistance to existing async payjoin integration
This introduces neccessary database model and tables also add commad for resume to allow interrupted sessions to be continued also a particular session either send or receive.
A history commad to view payjoin history and status has been added

Notes to the reviewers

Step to review this include making a payjoin transaction

Run a receiver to get a BIP21 URI then pass it to the sender as seen in docs

bdk-cli/README.md

Lines 121 to 141 in b9cf2ac

To start a Payjoin session as the receiver with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet1 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPd2PoUEcGNDHPZmVWgtPYERAwMG6qHheX6LN4oaazp3qZU7mykiaAZga1ZB2SJJR6Mriyq8MocMs7QTe7toaabSwTWu5fRFz/84h/1h/0h/0/*)#8guqp7rn" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet1 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet1 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet1 receive_payjoin --amount 400000 --max_fee_rate 1000 --directory "https://payjo.in" --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com"
```
To send a Payjoin with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet2 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPfBxswkATvZRQ9kDdRbJPtHYZaZCARL2myxcK7DqsqPhRo2G2rRVHFPbowq63BE6S4k2pUMYeF2fUMTT63Q7zhoXtKsM1FaS/84'/1'/0'/0/*)#qf5gnqrf" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet2 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet2 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet2 send_payjoin --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com" --fee_rate 1 --uri "<URI>"
```

To test resumption, interrupt either side with Ctrl+C mid-session then run resume on that side to continue. A few scenarios worth covering: receiver resuming after interrupt and sender resuming after interrupt. Use history after each scenario to confirm the session state was persisted correctly.

docs for this can be seen in 7e4ffd1

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added docs for the new feature

@coveralls

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 22068931916

Details

  • 0 of 763(0.0%) changed or added relevant lines in 5 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-1.9%) to 8.848%

Changes Missing CoverageCovered LinesChanged/Added Lines%
src/error.rs0210.0%
src/utils.rs0220.0%
src/handlers.rs0500.0%
src/payjoin/db.rs02620.0%
src/payjoin/mod.rs04080.0%
Files with Coverage ReductionNew Missed Lines%
src/utils.rs10.0%
src/handlers.rs212.81%
src/payjoin/mod.rs20.0%
TotalsCoverage Status
Change from base Build 21153868360:-1.9%
Covered Lines:268
Relevant Lines:3029

💛 - Coveralls

@notmandatorynotmandatory moved this to In Progress in BDK-CLIFeb 18, 2026
@Mshehu5Mshehu5 changed the title Payjoin persistancePayjoin persistenceFeb 21, 2026
@Mshehu5
Mshehu5 marked this pull request as ready for review February 24, 2026 05:41
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 4 times, most recently from a5e2482 to c79bd95CompareFebruary 24, 2026 12:57
@tvpetertvpeter added this to the CLI 3.0.0 milestone Feb 26, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @Mshehu5 for working on this feature.

Below are some of the observations that I think will make the implementation better:

  • I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.
  • The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.
  • Since the save_event is generated at multiple transitions in a session, I think it will be great if you consider grouping such updates in a db transaction to ensure that entries are saved successfully.

I have also left some comments in the code.

Thank you.

Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/payjoin/mod.rs
@tvpetertvpeter removed this from the CLI 3.0.0 milestone Mar 12, 2026
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 2 times, most recently from 8f4b815 to b6b7cb4CompareMarch 24, 2026 20:32
@codecov

codecovBot commented Mar 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.46154% with 328 lines in your changes missing coverage. Please review.
✅ Project coverage is 28.87%. Comparing base (eea5ba9) to head (2f14a36).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/payjoin/mod.rs26.21%242 Missing ⚠️
src/handlers.rs0.00%33 Missing ⚠️
src/payjoin/db.rs95.00%30 Missing ⚠️
src/payjoin/ohttp.rs73.33%20 Missing ⚠️
src/error.rs0.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #242 +/- ##
===========================================
+ Coverage 11.36% 28.87% +17.51% 
===========================================
Files 8 9 +1 Lines 2807 3626 +819 ===========================================
+ Hits 319 1047 +728 - Misses 2488 2579 +91 
FlagCoverage Δ
rust28.87% <68.46%> (+17.51%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from b6b7cb4 to c012c3aCompareMarch 24, 2026 20:42
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

Thank you for the review!

Just wanted some clarifications

The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent.
Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

@Mshehu5
Mshehu5 requested a review from tvpeterMarch 30, 2026 06:43

@vadim-anfvvadim-anfv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Mshehu5, thanks for this!

Could you update payjoin dependency to 1.0.0-rc.2 and run just pre-push?

I've got some errors from just pre-push like:

error[E0061]: this method takes 1 argument but 2 arguments were supplied
--> src/payjoin/mod.rs:691:30
|
691 | ... .check_payment(
| ^^^^^^^^^^^^^
...
704 | / ... |outpoint| {
705 || ... let utxo = self.wallet.get_utxo(outpoint);
706 || ... match utxo {
707 || ... Some(_) => Ok(false),
... |
710 || ... }
||___________________________- unexpected argument #2 of type `{closure@src/payjoin/mod.rs:704:33: 704:43}`

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 3 times, most recently from 7d7be05 to 6b06cf7CompareApril 13, 2026 11:46
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the review! I bumped Payjoin to 1.0.0-rc.2 and just pre-push passes locally. Mind giving it a try on your end? Would appreciate your feedback

@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@tvpeter Would really appreciate your feedback on this: #242 (comment)

It would help move this PR forward.

@tvpeter

tvpeter commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I think it should apply to all sessions within a specific timeframe, say anything older than 30 days, and if it can be implemented without adding another command, that would be great. This can even go into another PR.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent. Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

That is still fine. It can form another PR, but it's one of the things you can consider adding.

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

The current implementation covering SQLite is good enough (and the most important). Those others can serve as improvements to the persistence feature.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from db0ccec to 9a62b1eCompareApril 14, 2026 15:22
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 10 times, most recently from 06c4ae7 to 54ae896CompareMay 29, 2026 11:21
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thank you for the review. The reqwest issue is fixed in 7ee0a71. The regression was caused by a separate merged PR that updated reqwest without enabling rustls. I have verified the fix locally and it should be ready for you to retest on your end.

@notmandatorynotmandatory left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK bdb7f38

Thanks for the detailed test steps, very helpful for manual testing. I verified canceling receive and separately sender flow works as expected.

Any other feature enhancements such as using a DB transaction for atomic updates, or improved error types I recommend making new issues for. These sort of small improvements would be better made after #278 and in a future milestone. I also don't think we need this to support the redb persistence right now. If someone needs it they can implement it in a new PR.

@tvpeter

Copy link
Copy Markdown
Collaborator

@Mshehu5 pls rebase

Mshehu5 added 9 commits July 2, 2026 12:30
Persist payjoin sender and receiver state in SQLite so interrupted
payjoin sessions can be resumed after the CLI exits. Add dedicated
tables for send and receive sessions, append-only event logs for
state replay, receiver pubkey lookup for sender sessions, and
seen-input tracking for replay protection.
This follows the intended async payjoin design by persisting session
state across interruptions. SQLite keeps the initial persistence
backend simple and builds on existing rusqlite support, at the cost
of a small payjoin-specific schema and serialization layer.
Wire the payjoin persistence layer into the existing send and receive
flows so session state is saved during normal operation and can be
replayed later. Initialize the database from the handlers, replace
the noop persisters with SQLite-backed persisters, resume existing
sessions, and record seen inputs in the receiver flow for replay
protection.
This moves persistence from a standalone storage layer into the
runtime payjoin workflow so interrupted sessions can continue from
saved state instead of starting over. It also simplifies error
handling by relying on `?` once the storage errors map cleanly into
the CLI error type.
Expose persisted payjoin session state through CLI commands so users
can recover interrupted sessions and review prior session progress.
Add `resume` to continue pending sender and receiver sessions,
`history` to list saved sessions, status text helpers for clearer
output, and session ID filtering to target a specific session.
This improves recovery and troubleshooting for long-running async
payjoin flows by making persisted session state available from the
CLI.
Update the README to cover the new `resume` and `history` commands
so users can recover interrupted payjoin sessions and inspect saved
session state. Also add the SQLite dependency needed for the
payjoin persistence workflow.
This keeps the user-facing setup and command documentation aligned
with the new persistence and recovery behavior.
Move to payjoin 0.25.0 to stay closer to the latest release.
Enable rustls for the direct reqwest client because Payjoin
polling uses bdk-cli's own HTTPS client and without a TLS
backend the first relay poll fails.
Refine the skipped monitoring status message for the receiver
flow.
Delete payjoin sessions older than 30 days when the payjoin database is accessed. Remove related event rows in the same cleanup pass.
Cover the new payjoin persistence paths with focused unit tests.
Exercise replay protection, sender and receiver event persistence,
session pruning and history rendering.
These cases were chosen because the PR's new behavior is
concentrated in the persistence layer and session history path
Prevent wallet failures caused by selecting unconfirmed coins for
payjoin inputs.
Without this filter, the wallet can fail with an "unconfirmed
coins" error during payjoin processing.
Payjoin currently keeps using the relay selected during OHTTP key
bootstrapping. If that relay goes offline, the session fails even when
other relay URLs were supplied.
Select from the available relays for each OHTTP request and remember
failures for the rest of the command. Build a fresh request for every
attempt so failover does not reuse linkable OHTTP ciphertext.
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from bdb7f38 to 2f14a36CompareJuly 2, 2026 11:31
@tvpeter
tvpeter merged commit 16dc723 into bitcoindevkit:masterJul 2, 2026
9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 2, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@Mshehu5@coveralls@tvpeter@DanGould@notmandatory@vadim-anfv@evanlinjin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Payjoin persistence - #242

Merged
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance
Jul 2, 2026
Merged

Payjoin persistence#242
tvpeter merged 9 commits into
bitcoindevkit:masterfrom
Mshehu5:payjion_persistance

Conversation

@Mshehu5

@Mshehu5Mshehu5 commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Description

#230 needed to be merged for this to go through
Address #149 also follow up to #200
This PR adds persistance to existing async payjoin integration
This introduces neccessary database model and tables also add commad for resume to allow interrupted sessions to be continued also a particular session either send or receive.
A history commad to view payjoin history and status has been added

Notes to the reviewers

Step to review this include making a payjoin transaction

Run a receiver to get a BIP21 URI then pass it to the sender as seen in docs

bdk-cli/README.md

Lines 121 to 141 in b9cf2ac

To start a Payjoin session as the receiver with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet1 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPd2PoUEcGNDHPZmVWgtPYERAwMG6qHheX6LN4oaazp3qZU7mykiaAZga1ZB2SJJR6Mriyq8MocMs7QTe7toaabSwTWu5fRFz/84h/1h/0h/0/*)#8guqp7rn" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet1 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet1 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet1 receive_payjoin --amount 400000 --max_fee_rate 1000 --directory "https://payjo.in" --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com"
```
To send a Payjoin with regtest RPC and example OHTTP relays:
```
cargo run --features rpc -- --network regtest wallet --wallet payjoin_wallet2 config --ext-descriptor "wpkh(tprv8ZgxMBicQKsPfBxswkATvZRQ9kDdRbJPtHYZaZCARL2myxcK7DqsqPhRo2G2rRVHFPbowq63BE6S4k2pUMYeF2fUMTT63Q7zhoXtKsM1FaS/84'/1'/0'/0/*)#qf5gnqrf" --client-type rpc --database-type sqlite --url "127.0.0.1:18443"
cargo run --features rpc -- wallet --wallet payjoin_wallet2 sync
cargo run --features rpc -- wallet --wallet payjoin_wallet2 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet2 send_payjoin --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com" --fee_rate 1 --uri "<URI>"
```

To test resumption, interrupt either side with Ctrl+C mid-session then run resume on that side to continue. A few scenarios worth covering: receiver resuming after interrupt and sender resuming after interrupt. Use history after each scenario to confirm the session state was persisted correctly.

docs for this can be seen in 7e4ffd1

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added docs for the new feature

@coveralls

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 22068931916

Details

  • 0 of 763(0.0%) changed or added relevant lines in 5 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-1.9%) to 8.848%

Changes Missing CoverageCovered LinesChanged/Added Lines%
src/error.rs0210.0%
src/utils.rs0220.0%
src/handlers.rs0500.0%
src/payjoin/db.rs02620.0%
src/payjoin/mod.rs04080.0%
Files with Coverage ReductionNew Missed Lines%
src/utils.rs10.0%
src/handlers.rs212.81%
src/payjoin/mod.rs20.0%
TotalsCoverage Status
Change from base Build 21153868360:-1.9%
Covered Lines:268
Relevant Lines:3029

💛 - Coveralls

@notmandatorynotmandatory moved this to In Progress in BDK-CLIFeb 18, 2026
@Mshehu5Mshehu5 changed the title Payjoin persistancePayjoin persistenceFeb 21, 2026
@Mshehu5
Mshehu5 marked this pull request as ready for review February 24, 2026 05:41
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 4 times, most recently from a5e2482 to c79bd95CompareFebruary 24, 2026 12:57
@tvpetertvpeter added this to the CLI 3.0.0 milestone Feb 26, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @Mshehu5 for working on this feature.

Below are some of the observations that I think will make the implementation better:

  • I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.
  • The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.
  • Since the save_event is generated at multiple transitions in a session, I think it will be great if you consider grouping such updates in a db transaction to ensure that entries are saved successfully.

I have also left some comments in the code.

Thank you.

Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/payjoin/mod.rs
@tvpetertvpeter removed this from the CLI 3.0.0 milestone Mar 12, 2026
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 2 times, most recently from 8f4b815 to b6b7cb4CompareMarch 24, 2026 20:32
@codecov

codecovBot commented Mar 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.46154% with 328 lines in your changes missing coverage. Please review.
✅ Project coverage is 28.87%. Comparing base (eea5ba9) to head (2f14a36).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/payjoin/mod.rs26.21%242 Missing ⚠️
src/handlers.rs0.00%33 Missing ⚠️
src/payjoin/db.rs95.00%30 Missing ⚠️
src/payjoin/ohttp.rs73.33%20 Missing ⚠️
src/error.rs0.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #242 +/- ##
===========================================
+ Coverage 11.36% 28.87% +17.51% 
===========================================
Files 8 9 +1 Lines 2807 3626 +819 ===========================================
+ Hits 319 1047 +728 - Misses 2488 2579 +91 
FlagCoverage Δ
rust28.87% <68.46%> (+17.51%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from b6b7cb4 to c012c3aCompareMarch 24, 2026 20:42
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

Thank you for the review!

Just wanted some clarifications

The implementation does not provide for pruning/cleanup of data. Given that the db will tend to grow linearly and become sizable over time, I think it will be great to consider adding a pruning subcommand or mechanism that deletes irrelevant data.

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I noticed that the implementation used only the sqlite db, and since sqlite is a db option in the project, I think it will be ideal if you also include implementation for redb db so users are free to choose any to use.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent.
Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

@Mshehu5
Mshehu5 requested a review from tvpeterMarch 30, 2026 06:43

@vadim-anfvvadim-anfv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Mshehu5, thanks for this!

Could you update payjoin dependency to 1.0.0-rc.2 and run just pre-push?

I've got some errors from just pre-push like:

error[E0061]: this method takes 1 argument but 2 arguments were supplied
--> src/payjoin/mod.rs:691:30
|
691 | ... .check_payment(
| ^^^^^^^^^^^^^
...
704 | / ... |outpoint| {
705 || ... let utxo = self.wallet.get_utxo(outpoint);
706 || ... match utxo {
707 || ... Some(_) => Ok(false),
... |
710 || ... }
||___________________________- unexpected argument #2 of type `{closure@src/payjoin/mod.rs:704:33: 704:43}`

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 3 times, most recently from 7d7be05 to 6b06cf7CompareApril 13, 2026 11:46
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the review! I bumped Payjoin to 1.0.0-rc.2 and just pre-push passes locally. Mind giving it a try on your end? Would appreciate your feedback

@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@tvpeter Would really appreciate your feedback on this: #242 (comment)

It would help move this PR forward.

@tvpeter

tvpeter commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

For this feature, how would you like it to be implemented? For example should we allow selecting a specific date or session to start pruning from? Or should it apply to all sessions older than a certain date? Alternatively should it target only expired or unsuccessful sessions?

I think it should apply to all sessions within a specific timeframe, say anything older than 30 days, and if it can be implemented without adding another command, that would be great. This can even go into another PR.

Also for the redb implementation most of the existing integrations have been built around SQLite so it made sense as the initial approach for this implementation. The Redb support may require some additional consideration and it would be good to align with the PayJoin team to ensure the approach is correct and consistent. Given that would you prefer the Redb implementation to be included in this PR or would it be acceptable to handle it in a follow-up?

That is still fine. It can form another PR, but it's one of the things you can consider adding.

It would also be helpful to understand what you consider a hard requirement for this PR versus what can be iterated on in subsequent changes.

The current implementation covering SQLite is good enough (and the most important). Those others can serve as improvements to the persistence feature.

@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from db0ccec to 9a62b1eCompareApril 14, 2026 15:22
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch 10 times, most recently from 06c4ae7 to 54ae896CompareMay 29, 2026 11:21
@Mshehu5

Copy link
Copy Markdown
ContributorAuthor

@va-an Thank you for the review. The reqwest issue is fixed in 7ee0a71. The regression was caused by a separate merged PR that updated reqwest without enabling rustls. I have verified the fix locally and it should be ready for you to retest on your end.

@notmandatorynotmandatory left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK bdb7f38

Thanks for the detailed test steps, very helpful for manual testing. I verified canceling receive and separately sender flow works as expected.

Any other feature enhancements such as using a DB transaction for atomic updates, or improved error types I recommend making new issues for. These sort of small improvements would be better made after #278 and in a future milestone. I also don't think we need this to support the redb persistence right now. If someone needs it they can implement it in a new PR.

@tvpeter

Copy link
Copy Markdown
Collaborator

@Mshehu5 pls rebase

Mshehu5 added 9 commits July 2, 2026 12:30
Persist payjoin sender and receiver state in SQLite so interrupted
payjoin sessions can be resumed after the CLI exits. Add dedicated
tables for send and receive sessions, append-only event logs for
state replay, receiver pubkey lookup for sender sessions, and
seen-input tracking for replay protection.
This follows the intended async payjoin design by persisting session
state across interruptions. SQLite keeps the initial persistence
backend simple and builds on existing rusqlite support, at the cost
of a small payjoin-specific schema and serialization layer.
Wire the payjoin persistence layer into the existing send and receive
flows so session state is saved during normal operation and can be
replayed later. Initialize the database from the handlers, replace
the noop persisters with SQLite-backed persisters, resume existing
sessions, and record seen inputs in the receiver flow for replay
protection.
This moves persistence from a standalone storage layer into the
runtime payjoin workflow so interrupted sessions can continue from
saved state instead of starting over. It also simplifies error
handling by relying on `?` once the storage errors map cleanly into
the CLI error type.
Expose persisted payjoin session state through CLI commands so users
can recover interrupted sessions and review prior session progress.
Add `resume` to continue pending sender and receiver sessions,
`history` to list saved sessions, status text helpers for clearer
output, and session ID filtering to target a specific session.
This improves recovery and troubleshooting for long-running async
payjoin flows by making persisted session state available from the
CLI.
Update the README to cover the new `resume` and `history` commands
so users can recover interrupted payjoin sessions and inspect saved
session state. Also add the SQLite dependency needed for the
payjoin persistence workflow.
This keeps the user-facing setup and command documentation aligned
with the new persistence and recovery behavior.
Move to payjoin 0.25.0 to stay closer to the latest release.
Enable rustls for the direct reqwest client because Payjoin
polling uses bdk-cli's own HTTPS client and without a TLS
backend the first relay poll fails.
Refine the skipped monitoring status message for the receiver
flow.
Delete payjoin sessions older than 30 days when the payjoin database is accessed. Remove related event rows in the same cleanup pass.
Cover the new payjoin persistence paths with focused unit tests.
Exercise replay protection, sender and receiver event persistence,
session pruning and history rendering.
These cases were chosen because the PR's new behavior is
concentrated in the persistence layer and session history path
Prevent wallet failures caused by selecting unconfirmed coins for
payjoin inputs.
Without this filter, the wallet can fail with an "unconfirmed
coins" error during payjoin processing.
Payjoin currently keeps using the relay selected during OHTTP key
bootstrapping. If that relay goes offline, the session fails even when
other relay URLs were supplied.
Select from the available relays for each OHTTP request and remember
failures for the rest of the command. Build a fresh request for every
attempt so failover does not reuse linkable OHTTP ciphertext.
@Mshehu5
Mshehu5force-pushed the payjion_persistance branch from bdb7f38 to 2f14a36CompareJuly 2, 2026 11:31
@tvpeter
tvpeter merged commit 16dc723 into bitcoindevkit:masterJul 2, 2026
9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 2, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants

@Mshehu5@coveralls@tvpeter@DanGould@notmandatory@vadim-anfv@evanlinjin