Repository navigation
Support refunds - #64
Open
Ergologica wants to merge 9 commits into
Open
Ergologica wants to merge 9 commits into
Ergologica wants to merge 9 commits into
Conversation
This was referenced Aug 13, 2026
Open
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.
Closes #58.
Refunds, end to end: an API to announce one, automatic completion when the timelock expires, and an API to list past and current refunds with their statuses.
What a refund is here
The reserve contract already has the three actions (
init refund#2,cancel refund#3,complete refund#4) - nothing offchain used them. Announcing a refund writes the initiation height into R5 and the announced amount into R6 and moves no funds;refundNotificationPeriod(14400 blocks, ~20 days) later the owner may spend the box and take out up to R6. Note holders keep being able to redeem for the whole waiting period, which is the point of the delay.Offchain (
chaincash_offchain)init_refund_transaction,cancel_refund_transactionandcomplete_refund_transactionintransactions/reserves.rs, built the same way astop_up_reserve_transaction.Two details worth calling out:
!(selfOut.R5[Int].isDefined), and Ergo forbids gaps between non-mandatory registers - so the output box is rebuilt from scratch rather than cloned from the input. The same applies on completion, so a further refund needs a fresh announcement.SELF.value - selfOut.value <= refundLimit), and a redemption may well have drained the reserve since the announcement. Capping means the automatic completion cannot get stuck; the amount that actually left is reported back.Also a fix while I was in there:
ReserveBoxSpecread R5 as aLong, but the contract declares itInt(selfOut.R5[Int]). Every reserve with a pending refund therefore failed to parse, which silently dropped it from the scanner and from/reserves/wallet. R5 is now read asi32and R6 is picked up asrefund_amount. There is a regression test for it.Storage (
chaincash_store)New
refundstable plus a repository. The reserve box only remembers the pending step - once a refund is settled the registers are cleared and the box is spent - so the history has to live somewhere. It is deliberately not a foreign key ontoreserves, since a reserve row is deleted as soon as its box is spent. A partial unique index mirrors the one-pending-refund-per-reserve rule.Service and server
init_refund/cancel_refund/complete_refundonTransactionService, and four routes:POST /v1/reserves/refundPOST /v1/reserves/refund/cancelPOST /v1/reserves/refund/completeGET /v1/reserves/refunds?reserve_id=to filterThe refund watcher (
chaincash_services::refunds) runs one pass per block and completes every refund of ours that is past its waiting period. It decides what to settle from the reserve boxes rather than from our own records, because the announcement lives on chain: a refund announced before this server existed, or with the database since recreated, is picked up all the same, and the record is backfilled from the registers so the history stays complete. Only reserves the wallet holds the key for are considered. Per-refund failures are logged and retried on the next block rather than holding up the others.Tests
12 new tests,
cargo fmt/clippyclean (no new warnings), full suite green.The offchain ones build each transaction and then sign it against the real reserve contract, so the interpreter validates the context extension, the registers and the timelock rather than my reading of the contract. I checked they bite by mutating the implementation: a wrong action byte, keeping R5 on cancel, and a
REFUND_NOTIFICATION_PERIODoff by one each make the relevant test fail.test_complete_refund_is_locked_until_the_period_is_overalso asserts the negative directly - it builds a completion as if the deadline had passed and has the chain be one block behind, and signing fails.Note on the contract version
This is written against the reserve contract the
contractssubmodule is pinned to (ac99a4f). Upstreamchaincashlater removed the refund actions fromonchain/reserve.es(1d9ae26, "removing refund related actions and registers") and reworked them inbasis-token.eswith a different register layout (R7 for the initiation height, two actions instead of three, full withdrawal only). If the submodule is meant to move to that design, say so and I will rework this against it - the offchain shape (announce, wait, settle automatically, list) carries over, it is the register layout and action numbers that change.