Skip to content

[ANCHOR-1296]: Fix SEP-6 withdraw refund memo parameter names and add validation - #2032

Merged
ceciliaromao merged 7 commits into
developfrom
fix/anchor-1296-sep6-withdraw-refund-memo-param
Sep 30, 2026
Merged

ceciliaromao merged 7 commits into
developfrom
fix/anchor-1296-sep6-withdraw-refund-memo-param

Conversation

@ceciliaromao

Copy link
Copy Markdown
Collaborator

Description

  • Fixes GET /sep6/withdraw: it read the refund memo from refundMemo/refundMemoType query params, but SEP-6 names them refund_memo/refund_memo_type — the same names /withdraw-exchange already used correctly. A spec-compliant wallet's refund memo was silently dropped. The endpoint now reads the spec names, and keeps the old camelCase names working as a deprecated compatibility alias: each field resolves independently, the spec name winning whenever it's non-empty.
  • Adds refund-memo validation to both /withdraw and /withdraw-exchange, mirroring SEP-31's existing validation exactly: refund_memo and refund_memo_type must be specified together or both omitted, and the pair's type/value combination is validated, with every failure mapped to a 400 instead of an unhandled 500.

Context

  • Found while specifying the last SEP-6 coverage-audit branch (test/anchor-1296-sep6-coverage) — fixing only the parameter name would have opened a new, unvalidated input path, so both changes land together here.
  • Production change, so this PR targets develop directly rather than the coverage-audit branch.

Testing

  • ./gradlew test
  • ./gradlew spotlessCheck :core:test :platform:test
  • ./gradlew :essential-tests:test --tests "*Sep6Tests*" — run twice in a row (51/51 passing)
  • Independent Verifier pass (spec-anchored outcome check across all 16 acceptance criteria + a discrimination-sensor mutation test, 2/2 mutations killed) and a 6-dimension pre-push review (security / quality / architecture / performance / requirements / regression) — the one finding raised (an architecture style note) was checked against an independent verifier and refuted: no documented convention requires the alternative it proposed

Documentation

  • GET /sep6/withdraw now accepts refund_memo/refund_memo_type per the SEP-6 spec. The previous refundMemo/refundMemoType names still work, as a deprecated, non-spec compatibility alias — clients should migrate to the spec names.
  • Behavior change: /withdraw-exchange now rejects a malformed refund memo with HTTP 400 instead of accepting it unvalidated.

Known limitations

  • The camelCase alias has no removal timeline yet — tracked as a follow-up decision, not part of this fix.

@ceciliaromao
ceciliaromao merged commit de51b82 into develop Sep 30, 2026
14 of 16 checks passed
@ceciliaromao
ceciliaromao deleted the fix/anchor-1296-sep6-withdraw-refund-memo-param branch September 30, 2026 18:08
ceciliaromao added a commit that referenced this pull request Oct 1, 2026
#2034)

### Description

Partial release of the ANCHOR-1279 SEP-compliance coverage audit: brings
everything accumulated on `chore/anchor-1279-sep-coverage-audit` into
`develop` for the first time. Net diff is 14 files (+1791/−89) — most of
the audit branch's 50 raw commits are re-merges of `develop` back into
itself and carry no new content.

Two pieces of work land here, each already reviewed and merged into the
audit branch on its own PR, neither of which had reached `develop`
before:

- **SEP-31 coverage** (#2012) — closes the remaining SEP-31 gaps from
the audit's spec cross-check: `fee_details.details` breakdown on both
the quote and no-quote paths (plus a precision desync fix on the
no-quote path), a `GET /info` `fields.transaction` gap that silently
dropped configured field metadata, an HTTP 400-vs-500 distinction added
to the shared `SepClient` base class, `quotes_required: true` coverage
(previously never exercised by any asset), and the remaining
`stellar-anchor-tests` → AP-suite gaps (table below).
- **Last SEP-6 coverage-audit gaps** (#2031) — closes the audit's final
five SEP-6 assertions: full transaction-schema validation on `GET
/transaction`, `stellar.toml`'s `TRANSFER_SERVER` validity, `/info`'s
per-asset field metadata, `/deposit-exchange`/`/withdraw-exchange` JWT
and required-param coverage, the Amount Formula verified independently
against the SEP-38 quote, and the six SEP-6 statuses no test had reached
(`on_hold`, `pending_customer_info_update`,
`pending_transaction_info_update`, `too_small`, `too_large`,
`no_market`).

### Context

- Both PRs targeted `chore/anchor-1279-sep-coverage-audit` as their
base, per the project's routing rule for test-only/coverage branches —
so this is the first time either reaches `develop`.
- Going forward, every new ANCHOR-1279 branch targets `develop`
directly; the audit branch is retired as a base after this merge.
- Full audit trail: `docs/audits/ANCHOR-1279-sep-coverage-audit.md`
(moved out of the tracked repo into the git-excluded `.specs/` in an
earlier PR on this branch).

### Testing

- `./gradlew test`
- Verified with `git merge-tree` that this merges into current `develop`
with no conflicts.
- Both constituent PRs (#2012, #2031) passed CI and were reviewed
independently before merging into the audit branch; no new changes are
introduced by this merge itself.

### Documentation

- `GET /info` (SEP-31) can now include a `fields` object per asset —
additive, omitted when not configured.
- `SepClient.handleResponse` now maps HTTP 400 to
`SepValidationException` for every SEP client built on it (previously
only 403/404 were distinguished).
- No other public API or user-facing behavior changes.

### Known limitations

- `expired` status / quote-expiry-driven auto-expiration for SEP-31
(identified during #2012, confirmed unimplemented) — real feature work,
not filed as a ticket yet.
- SEP-6 audit assertions #10/#11 ("SEP-9 fields match config") and
#3/#4/#35/#36 (`authentication_required: false`) describe behavior that
doesn't exist in this codebase — see the SEP-6 table's notes for what's
verified instead.
- Remaining ANCHOR-1296 (SEP-6) work —
`fix/anchor-1296-sep6-withdraw-refund-memo-param` (#2032, open) and
`feat/anchor-1296-sep6-patch-quote-claimable` (in progress) — ships in
separate, later PRs directly against `develop`.

### `stellar-anchor-tests` → this repo's test suite (SEP-31)

Mapping of all 14 SEP-31 assertions in `stellar-anchor-tests` to their
equivalent in AP's own suite:

| `stellar-anchor-tests` assertion | AP test |
|---|---|
| GET /info matches expected schema | ✅ `Sep31Tests.kt:56` |
| Has expected asset enabled | ✅ `Sep31Tests.kt:56` |
| Check optional transaction 'fields' | ✅ `Sep31ServiceTest.kt` (`test
INFO response advertises fields...`) |
| Has DIRECT_PAYMENT_SERVER attribute | ✅ `Sep31Tests.kt` (`test
DIRECT_PAYMENT_SERVER has expected format`) |
| Requires a SEP-10 JWT | ✅ `Sep31Tests.kt` (`test requires a SEP-10
JWT`) |
| Can create a transaction | ✅ `Sep31Tests.kt:60` |
| Returns 400 when no amount is given | ✅ `Sep31Tests.kt` (`test returns
400 when no amount is given`) |
| Returns 400 when no asset_code is given | ✅ `Sep31Tests.kt`
(`testBadAsset` + `test returns 400 when no asset_code is given`) |
| Can fetch a created transaction | ✅ `Sep31Tests.kt:68` |
| GET tx response complies with protocol schema | ✅ `Sep31Tests.kt`
(`assertCompliesWithProtocolSchema`) |
| Returns 404 for a non-existent transaction | ✅ `Sep31Tests.kt` (`test
returns 404 for a non-existent transaction`) |
| [quotes_required] can create a transaction | ✅ `Sep31Tests.kt` (`test
quotes_required can create and fetch a transaction with a quote`) |
| [quotes_required] can fetch a created transaction | ✅ same test |
| [quotes_required] response complies with protocol schema | ✅ same test
|

**14 Verified, 0 partial, 0 gap.**

### `stellar-anchor-tests` → this repo's test suite (SEP-6)

Mapping of all 38 SEP-6 assertions in `stellar-anchor-tests` to their
equivalent in AP's own suite, closed across #2009, #2017, #2018, #2022,
#2025 and #2031:

| `stellar-anchor-tests` assertion | AP test |
|---|---|
| Deposit requires JWT if auth required | ✅ `Sep6Tests.kt` (`test sep6
deposit rejects request without JWT`) |
| Deposit requires 'asset_code' | ✅ `Sep6Tests.kt` (`test sep6 deposit
rejects request without asset_code`) |
| Deposit requires 'account' if auth not required | N/A —
`authentication_required` is hardcoded `true`; this branch never exists
|
| Deposit rejects invalid 'account' if auth not required | N/A — same as
above |
| Deposit rejects unsupported asset_code | ✅ `Sep6Tests.kt` (`test sep6
deposit rejects unsupported asset_code`) |
| Deposit success response | ✅ `Sep6Tests.kt` (`test sep6 deposit`) |
| GET /info matches expected schema | ✅ `Sep6Tests.kt` (`test Sep6 info
endpoint`) — STRICT JSONAssert |
| Asset enabled for deposit in /info | ✅ `Sep6Tests.kt` (`test Sep6 info
endpoint`) |
| Asset enabled for withdraw in /info | ✅ `Sep6Tests.kt` (`test Sep6
info endpoint`) |
| SEP-9 fields for deposit match config | See Known limitations — ✅
verifies the actual field derivation instead |
| SEP-9 fields for withdraw match config | See Known limitations — ✅
verifies the actual field derivation instead |
| TOML has valid transfer server URL | ✅ `Sep6Tests.kt` (`test sep6 TOML
TRANSFER_SERVER is a well-formed URL`) |
| /transaction requires JWT | ✅ `Sep6Tests.kt` (`test sep6 GET
transaction rejects request without JWT`) |
| Record on /transaction after deposit | ✅ `Sep6Tests.kt` (`test sep6
deposit`) |
| Record on /transaction after withdraw | ✅ `Sep6Tests.kt` (`test sep6
withdraw`) |
| Deposit transaction schema on /transaction | ✅ `Sep6Tests.kt` (`test
sep6 GET transaction returns a schema-complete deposit record`) |
| Withdraw transaction schema on /transaction | ✅ `Sep6Tests.kt` (`test
sep6 GET transaction returns a schema-complete withdrawal record`) |
| 404 for nonexistent transaction ID | ✅ `Sep6Tests.kt` (`test sep6 GET
transaction returns 404 for an unknown id`) |
| 404 for nonexistent external transaction ID | ✅ `Sep6Tests.kt` (`test
sep6 GET transaction returns 404 for an unknown
external_transaction_id`) |
| 404 for nonexistent Stellar transaction ID | ✅ `Sep6Tests.kt` (`test
sep6 GET transaction returns 404 for an unknown stellar_transaction_id`)
|
| /transactions requires JWT | ✅ `Sep6Tests.kt` (`test sep6 GET
transactions rejects request without JWT`) |
| Record on /transactions after deposit | ✅ `Sep6Tests.kt` (`test sep6
deposit appears in transactions listing with valid schema`) |
| Record on /transactions after withdraw | ✅ `Sep6Tests.kt` (`test sep6
withdrawal appears in transactions listing with valid schema`) |
| Deposit schema on /transactions | ✅ same test |
| Withdraw schema on /transactions | ✅ same test |
| Empty list for accounts w/o transactions | ✅ `Sep6Tests.kt` (`test
sep6 GET transactions returns empty list for account with no history`) |
| Proper count for 'limit' param | ✅ `Sep6Tests.kt` (`test sep6 GET
transactions honors limit parameter exactly`) |
| Descending creation order | ✅ `Sep6Tests.kt` (`test sep6 GET
transactions are ordered by started_at descending`) |
| 'no_older_than' filtering | ✅ `Sep6Tests.kt` (`test sep6 GET
transactions no_older_than filters strictly newer records`) |
| kind=withdrawal filter | ✅ `Sep6Tests.kt` (`test sep6 GET transactions
kind=withdrawal returns only the withdrawal`) |
| kind=deposit filter | ✅ `Sep6Tests.kt` (`test sep6 GET transactions
kind=deposit returns only the deposit`) |
| Rejects bad asset_code on /transactions | ✅ `Sep6Tests.kt` (`test sep6
GET transactions rejects unsupported asset_code`) |
| Withdraw requires JWT if auth required | ✅ `Sep6Tests.kt` (`test sep6
withdraw rejects request without JWT`) |
| Withdraw requires asset_code | ✅ `Sep6Tests.kt` (`test sep6 withdraw
rejects request without asset_code`) |
| Withdraw requires 'account' if auth not required | N/A — same as
deposit's equivalent |
| Withdraw rejects invalid 'account' if auth not required | N/A — same
as deposit's equivalent |
| Withdraw rejects unsupported asset_code | ✅ `Sep6Tests.kt` (`test sep6
withdraw rejects unsupported asset_code`) |
| Withdraw success response | ✅ `Sep6Tests.kt` (`test sep6 withdraw`) |

**34 Verified, 0 partial, 4 N/A (unreachable — `authentication_required`
is always `true`), 0 gap.**

---------

Co-authored-by: Amanda Gonsalves <64379712+amandagonsalves@users.noreply.github.com>
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.

2 participants