core: keep the preimage the list snapshot drops - #69
Merged
Conversation
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
jamaljsr
reviewed
Aug 3, 2026
jamaljsr
left a comment
Member
There was a problem hiding this comment.
Tested and confirmed it works as advertised. Just found one gap I think we should address.
jamaljsr
force-pushed
the
core/preserve-send-preimage
branch
from
August 5, 2026 18:40
11c2262 to
9b1484a
Compare
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
In this commit, we stop the SDK throwing away proof of payment. The daemon reveals a send's preimage exactly once, on the entry it pushes over the activity stream when the swap settles. Every list read afterwards returns that same entry with the field empty. Confirmed against a regtest send: the entry comes back ENTRY_STATUS_COMPLETE, phase "confirmed", and progress.preimage is "". The SDK was consuming the stream entry for its cursor and discarding the body, then debouncing a list refresh that overwrote the snapshot. So the one frame carrying the preimage was dropped, and the refresh behind it guaranteed nothing could recover it. The field is declared on the public Entry type and was unreachable through any public API. That matters because a preimage is not decoration. It is the only thing a caller can show a third party to demonstrate that an invoice actually settled, since sha256(preimage) equals the payment hash the invoice was minted against and nobody can produce one without having paid. So ActivityStream now records preimages as they go past, keyed by payment hash, and the refresh puts them back on the entries it lists. restorePreimages never invents a value it was not given, leaves an entry that already carries one untouched, returns the input array unchanged when nothing has been seen, and does not mutate what it was handed. The map is cleared on stop so a different wallet cannot inherit the last one's proofs, and holding them only in memory is enough because the stream replays with includeExisting when it reopens.
jamaljsr
force-pushed
the
core/preserve-send-preimage
branch
from
August 5, 2026 18:46
9b1484a to
4d76780
Compare
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
In this PR, we stop the SDK throwing away proof of payment.
The daemon reveals a send's preimage exactly once, on the entry it pushes
over the activity stream when the swap settles. Every list read afterwards
returns that same entry with the field empty. Confirmed against a real
regtest send:
ActivityStream.noteActivitywas consuming that stream entry for itscursor and discarding the body, then debouncing a list refresh that
overwrote the snapshot. So the one frame carrying the preimage was
dropped, and the refresh behind it guaranteed nothing could recover it.
Entry.progress.preimageis declared on the public type and wasunreachable through any public API.
That matters because a preimage is not decoration. It is the only thing a
caller can show a third party to demonstrate that an invoice actually
settled, since
sha256(preimage)equals the payment hash the invoice wasminted against and nobody can produce one without having paid. Verified
end to end on regtest:
So
ActivityStreamnow records preimages as they go past, keyed bypayment hash, and the refresh puts them back on the entries it lists.
restorePreimagesnever invents a value it was not given, leaves an entrythat already carries one untouched, returns the input array unchanged when
nothing has been seen, and does not mutate what it was handed. The map is
cleared on stop so a different wallet cannot inherit the last one's
proofs, and holding them only in memory is enough because the stream
replays with
includeExistingwhen it reopens.Nine tests cover both halves: what the stream remembers and what the
refresh restores. All 204 core tests pass, typecheck is clean.
This was found while building a streaming payments feature on the demo
wallet, where a settled tick has to be provable to a third party. It is
sent on its own because it stands on its own merits and there is no reason
for proof of payment to wait on a UI port.
Note for #58: this touches the refresh path in
engine.ts, so it willwant re-applying by hand rather than merging cleanly if that lands first.
activity.tsshould be conflict free either way.Verified end to end under a real browser send
Since this was first opened, the whole path has been exercised from a browser
wallet on signet rather than a synthetic payer: three consecutive L402 ticks,
each one a real send, each preimage arriving on the settle frame and surviving
the list refresh that follows it. The receiving side checked them
independently:
That is the claim this PR rests on, and it is now something we have watched
rather than something we inferred from regtest. Without the change here the
receiving side has nothing to check, since the preimage is gone by the time
any caller can read the entry.