Skip to content

Support refunds - #64

Open
Ergologica wants to merge 9 commits into
BetterMoneyLabs:masterfrom
Ergologica:support-refunds
Open

Ergologica wants to merge 9 commits into
BetterMoneyLabs:masterfrom
Ergologica:support-refunds

Conversation

@Ergologica

Copy link
Copy Markdown

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_transaction and complete_refund_transaction in transactions/reserves.rs, built the same way as top_up_reserve_transaction.

Two details worth calling out:

  • Cancelling has to drop R6 as well as R5. The contract requires !(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.
  • Completion withdraws the announced amount capped at what the reserve still holds. R6 is only an upper bound for the contract (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: ReserveBoxSpec read R5 as a Long, but the contract declares it Int (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 as i32 and R6 is picked up as refund_amount. There is a regression test for it.

Storage (chaincash_store)

New refunds table 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 onto reserves, 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_refund on TransactionService, and four routes:

POST /v1/reserves/refund announce
POST /v1/reserves/refund/cancel call off
POST /v1/reserves/refund/complete withdraw now (still subject to the timelock)
GET /v1/reserves/refunds past and current refunds, ?reserve_id= to filter

The 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/clippy clean (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_PERIOD off by one each make the relevant test fail. test_complete_refund_is_locked_until_the_period_is_over also 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 contracts submodule is pinned to (ac99a4f). Upstream chaincash later removed the refund actions from onchain/reserve.es (1d9ae26, "removing refund related actions and registers") and reworked them in basis-token.es with 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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support refunds

1 participant