Skip to content

feat(issuer-gateway): idempotency keys for lien-freeze and redeem (#46) - #1104

Merged
DevTobis merged 2 commits into
DevTobis:mainfrom
Hussman256:fix/issuer-gateway-idempotency-keys
Oct 23, 2026
Merged

DevTobis merged 2 commits into
DevTobis:mainfrom
Hussman256:fix/issuer-gateway-idempotency-keys

Conversation

@Hussman256

Copy link
Copy Markdown
Contributor

Closes #46

lien-freeze and redeem create lasting obligations at the issuer (a lien, a redemption payout). Until now, a caller that timed out and retried would create a second one.

Behaviour with an Idempotency-Key header

  • First request: claims (issuer code, route, key) in the new issuer_idempotency table (migration 0010_issuer_idempotency, primary key on all three columns) before calling the issuer, then stores the issuer's result as jsonb.
  • Same key, same parameters: returns the stored result with Idempotent-Replayed: true. The issuer is not called and no audit row is written. The parameters are fingerprinted with sha256. units is compared as a number, and the issuer code is lowercased, matching getAdapter.
  • Same key, different parameters: 422. The same key on another issuer or route is independent.
  • First request still running: 409.
  • Issuer call fails: the claim is deleted, so the same key can be retried.
  • Forwarded: the key is passed to the issuer as an Idempotency-Key header through a new optional IssuerRequestOptions argument on requestLienFreeze and requestRedemption. That covers the case the gateway can't see: a timeout after the issuer did process the request.
  • Malformed key: 400. A valid key is 1–255 printable ASCII characters with no spaces. Keyed audit rows record idempotencyKey.

Without the header, behaviour is unchanged: every request reaches the issuer, and nothing is stored or forwarded. This is documented in the README.

Database failures

  • If storing the result fails after the issuer succeeded, the issuer's answer is still returned. The key stays in_flight and a retry gets 409, not a second obligation. This is logged at error level.
  • If releasing the key fails after the issuer failed, the issuer's error is still what the caller sees, and the release failure is logged.
  • Both cases are listed under Known limits: someone has to remove the row by hand.

Tests (68 passing, up from 57)

src/idempotency.test.ts drives both routes through the real adapters, with fetch stubbed and an in-memory issuer_idempotency table that honours the primary key and the eq/and filters. It covers:

  • Replay: returns the first result with a single issuer call, the stored row, one audit row, and the key forwarded.
  • Different parameters: 422 for changed units, holder, asset or destination; equal units written differently count as the same request.
  • Scoping: by issuer and route, with the issuer code case-insensitive.
  • No key: behaviour unchanged.
  • Failures: the key is released after an issuer failure and the retry succeeds; a request in flight gets 409; a store failure returns the result, logs and makes the retry 409; a release failure still surfaces the issuer's error.
  • Malformed keys: 400.

I checked the tests can fail: removing the different-parameters check makes one fail. typecheck and build pass.

Notes:

…vTobis#46)

Both routes create lasting obligations at the issuer (a lien, a
redemption payout), so a caller's timeout-and-retry duplicated them.

An Idempotency-Key header now claims (issuer code, route, key) in the new
issuer_idempotency table (migration 0010) before the issuer is called and
stores its result. A replay with the same parameters returns the stored
result with Idempotent-Replayed: true and does not call the issuer;
other parameters under the key are 422; a key still in flight is 409; a
failed issuer call releases the key. The key is forwarded to the issuer
as an Idempotency-Key header. Requests without the header are unchanged.
@Hussman256
Hussman256 requested a review from DevTobis as a code owner October 21, 2026 18:10
@drips-wave

drips-wave Bot commented Oct 21, 2026

Copy link
Copy Markdown

@Hussman256 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@DevTobis
DevTobis merged commit ee4e3bd into DevTobis:main Oct 23, 2026
4 checks passed
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.

issuer-gateway: add idempotency keys for lien-freeze and redeem

2 participants