Release: develop -> main#4323
Merged
Merged
Conversation
* feat(gs): allow ledger_account and ledger_leg on /gs/debug Add both double-entry-ledger tables to the DebugAllowedColumns allowlist so they are queryable through the structured /gs/debug admin endpoint (Debug role). Non-sensitive columns only (ids, FK ids, timestamps, enum/status, numeric amounts, booleans, deterministic account name). ledger_account.name can embed a counterparty institution name for untracked-bank suspense accounts, identical to the value already exposed via bank_tx.bankName. * docs(gs): name both counterparty-name sources in ledger_account allowlist comment The counterparty institution name embedded in untracked-bank SUSPENSE account names comes from bank.name (matched untracked bank) or bank_tx.bankName (no match) — both already on the /gs/debug allowlist. The comment previously named only bank_tx.bankName.
github-actions
Bot
requested review from
TaprootFreak and
davidleomay
as code owners
July 22, 2026 19:33
) * feat(realunit): add swap-only and OCP pay-flow workflow endpoints Add RealUnit Phase 2 pay flow: swap REALU->ZCHF keeping the proceeds in the user wallet, then pay that ZCHF to an Open CryptoPay recipient via the public lnurlp payment-link flow. - swap-only: PUT /realunit/swap/:id/unsigned-transaction builds the REALU transferAndCall swap tx without the deposit sweep (extracted shared buildSwapUnsignedTransaction / reconstructSignedTransaction helpers from the sell flow) - OCP pay: PUT /realunit/pay/unsigned-transaction resolves recipient and amount from the payment-link quote and builds the unsigned ZCHF ERC-20 transfer; PUT /realunit/pay/submit reconstructs the signed hex and submits it into the existing lnurlp tx settlement path; GET /realunit/pay/:id/status exposes the payment status - reuse RealUnitBlockchainService, EvmUtil, payment-link/quote services and the sell flow guard set; no entity/column changes * fix(realunit): harden OCP pay flow (pending nonce, fail-fast on unsupported method, URI validation) - derive the OCP pay-tx nonce from the pending block tag so a still-pending swap tx is counted and the two txs do not collide on the same nonce; add an optional block-tag param to EvmClient.getTransactionCount - fail fast with a typed BadRequestException on both OCP endpoints when the resolved payment method is unsupported by the payment-link engine (Sepolia on DEV/LOC) instead of letting a deep `Invalid method` error bubble up; add the PaymentLinkEvmHexBlockchains constant mirroring the engine's supported set - cross-check the URI token contract against the ZCHF asset and validate the recipient/amount in parseEvmPaymentRequest, throwing a typed error on mismatch or malformed input - document the JWT access-gate (not ownership) semantics on the OCP endpoints - add Swagger error responses and a nonce-ordering note to the OCP endpoints - fix import ordering and extend specs for the new error paths * feat(realunit): add IBAN-free REALU -> ZCHF swap quote and broadcast endpoints The OCP pay flow's PUT /swap/:id/unsigned-transaction consumes a SWAP-type TransactionRequest, but the only way to obtain one was PUT /sell, which requires a fiat IBAN and creates a Sell route + payout. Requiring a sell bank account to do a pure REALU -> ZCHF swap whose proceeds stay in the user wallet (to pay at an OCP/SPAR POS) is semantically wrong and blocked the swap-only path end to end. Add the missing entry points: - PUT /v1/realunit/swap: IBAN-free swap quote. Creates a TransactionRequestType.SWAP request via SwapService.createSwapPaymentInfo (REALU -> ZCHF), with no fiat IBAN, Sell route or payout. Same registration + KYC Level 30 gating as sell, and KYC trading limits stay enforced: a quote over the limit surfaces QuoteError.LIMIT_EXCEEDED, translated into the same KYC-Level-50 requirement the sell path throws. The ZCHF estimate is anchored to the live on-chain brokerbot sell price. Input mirrors the sell DTO's amount XOR targetAmount pattern but drops iban/currency (target is always ZCHF). - PUT /v1/realunit/swap/:id/broadcast: dedicated swap broadcast for clean OCP-flow semantics, reusing a shared private reconstruct/broadcast helper extracted from broadcastSellTransaction (no duplication). The on-chain execution stays the already-built user-signed brokerbot mechanism; this only adds the IBAN-free quote/request-creation and broadcast entry points. No entity or column changes, so no migration is needed (reuses TransactionRequest + Swap route). Update the swap unsigned-transaction and OCP pay ApiOperations to reference the new swap quote (step 0) and swap broadcast, and document the swap flow in CONTRIBUTING.md. Add jest specs: swap quote happy path, IBAN not required, limit-exceeded surfaces a typed error, swap broadcast returns txHash. * fix(realunit): make REALU -> ZCHF swap quote honest about limit-exemption The swap quote's QuoteError.LIMIT_EXCEEDED -> KYC-Level-50 mapping was dead code: TransactionHelper.getLimits returns Number.MAX_VALUE for any RealUnit transaction that is not selling-REALU-for-fiat, so the limit error can never fire for this crypto -> crypto pair. Remove the misleading limit-to-KYC-level throw and document why the swap is limit-exempt by design: KYC trading limits are enforced at the fiat boundary (buy/sell), whereas a REALU -> ZCHF swap is a self-custody, on-chain brokerbot action. Keep the registration and KYC Level 30 access gates (those are enforced) and keep surfacing genuine quote errors (e.g. min/max volume) via the DTO isValid/error fields. Update the Swagger error docs, CONTRIBUTING swap row and the swap spec to reflect the limit-exempt behavior instead of implying limit enforcement. * Fix stale swap comments: drop limit-enforced wording and use swap broadcast endpoint * test(realunit): cover OCP swap/pay branches and evm getTransactionCount to 100% Add diff-coverage for every changed executable line and branch in the OCP pay flow: - getSwapPaymentInfo: asset-not-found, shares<=0, brokerbot-query rejection, missing-id and brokerbot-null estimate fallbacks - broadcast: broadcast-error and no-tx-hash failure paths - createSwapUnsignedTransaction: missing REALU contract and decimals fallback to 18 - createOcpPayUnsignedTransaction: missing ZCHF contract and insufficient-gas paths - EvmClient.getTransactionCount: default latest tag and pending tag - thin controller delegations for the six OCP endpoints * feat(payment-link): support Open CryptoPay on Sepolia for non-PRD testing Add Blockchain.SEPOLIA to the EVM groups of the three payment-link switch statements that previously fell through to the default throw (getDepositAddress, executeHexPayment, PaymentRequestMapper) and to the PaymentLinkEvmHexBlockchains set so the RealUnit OCP guard recognizes it. This makes the RealUnit OCP pay flow testable end-to-end on the Sepolia testnet on non-PRD (DEV/LOC). It stays PRD-safe: on PRD, TestBlockchains includes Sepolia, so PaymentLinkBlockchains filters it out and no PRD payment-link can offer Sepolia, leaving the new EVM cases unreachable there. The RealUnit assertPaymentLinkSupportsMethod guard is kept (still fails fast for genuinely-unsupported methods) and now passes for Sepolia. Update the now-stale mainnet-only comments and flip the RealUnit Sepolia fail-fast specs to assert the flow now proceeds on the testnet. Add a payment-link engine spec covering the new Sepolia EVM routes. * fix(payment-link): add Sepolia to payment-link fee calculation for non-PRD OCP The Sepolia OCP enablement (64685ea) added case Blockchain.SEPOLIA to the getDepositAddress, executeHexPayment and PaymentRequestMapper switches, but missed two further EVM-mainnet switches the OCP hex-payment flow traverses: - PaymentLinkFeeService.calculateFee: the EVM group fell through to undefined for Sepolia, so updateFees cached no fee, getMinFee(SEPOLIA) returned undefined and createTransferAmount dropped the Sepolia transfer-amount. getTransferAmountFor(SEPOLIA, ZCHF) then threw 'Invalid method or asset' at OCP quote activation. - PaymentActivationService.createBlockchainRequest: the EVM/deposit-address group omitted Sepolia, so activation would hit the default invalid-method throw even once the fee was available. Add case Blockchain.SEPOLIA to both EVM-mainnet groups (same minimal additive pattern; getEvmClient(SEPOLIA) and getDepositAddress already support Sepolia). PRD-safe: Sepolia is filtered out of PaymentLinkBlockchains on PRD, so these cases stay unreachable there. Extend payment-link-sepolia.spec to cover both new cases (calculateFee/getMinFee return a real gas price; createBlockchainRequest routes Sepolia to the EVM deposit-address branch). * fix(realunit): await registration gate on swap quote hasRegistrationForWallet is async; without await the Promise is always truthy and the registration gate never rejects. Align the swap path with sell/buy and update the swap-quote specs to the current gate exceptions. * style(evm): format evm-client.spec.ts after getTransactionCount tests * fix(realunit): harden OCP pay flow (status endpoint, ZCHF coverage, swap gate, fail-closed price) - getOcpPayStatus reads the payment via a status-independent lookup (getMostRecentPayment), so a completed OCP payment returns its terminal status instead of a 404 once it is no longer PENDING. - the OCP pay tx now requires the sender's on-chain ZCHF to cover the amount before it is built and again before submit, so the merchant payment can no longer be marked complete while the funding REALU->ZCHF swap is still unmined. - createSwapUnsignedTransaction/broadcastSwapTransaction enforce type===SWAP, the REALU->ZCHF asset binding and the RealUnit registration + KYC30 gate on every step, closing the swap-tx gate bypass and BUY/SELL type confusion. - the swap quote fails closed with PriceSourceUnavailableException (via withPriceSourceGuard) instead of silently reusing the off-chain estimate when the brokerbot price source is down. - build the swap-tx nonce from the pending block tag (consistent with the pay tx), reject swaps that round down to zero shares, add a gas buffer to the pay tx, and parse the swap :id via ParseIntPipe. * style(realunit): apply prettier to the swap/pay test spec * fix(realunit): tighten OCP pay coverage checks and swap broadcast - compare the ZCHF balance wei-exact via getTokenBalanceWei instead of a lossy decimal float - gate swap broadcast: validate the signed payload (to/chainId/value/data) against the request and complete it so the replay guard takes effect - check ETH coverage against the buffered maxFeePerGas rather than the raw gas price - reject malformed unsigned transactions with 400 in reconstructSignedTransaction * fix(realunit): reject a swap broadcast whose signer is not the request user Validate parsedTx.from against the request wallet in broadcastSwapTransaction, so a KYC'd user cannot relay a transaction signed by a different wallet to complete their own swap request. * fix(realunit): apply conformity review fixes to the OCP-pay flow - return 503 (ServiceUnavailableException) instead of 400 when the server-configured blockchain is unsupported - fail loud on missing REALU/ZCHF assets, a missing ZCHF contract address, and missing EIP-1559 fee fields (drop the silent zero-fee fallback) - require @isnotempty on the XOR swap-amount fields and type them optional - align the OCP status Swagger doc with the status-independent lookup; drop an unreachable error-code example - name the includeTx flag, fix section-comment style and the sepolia spec imports * fix(realunit): guard missing ZCHF asset in createOcpPayUnsignedTransaction and sharpen the fee-field test - add the !zchfAsset NotFoundException guard in createOcpPayUnsignedTransaction, matching the sibling build/submit paths (a missing asset returned 500 instead of 404) - assert the legacy type-0 unsigned tx is rejected via the specific reconstruct failure; proven to change behavior if the EIP-1559 fee-field guard is removed * fix(realunit): apply fresh-review fixes to the OCP-pay flow - return 400 instead of an uncaught 500 when a broadcast swap tx carries an invalid signature - make the swap quote consistent with the tx build: report the floored share amount and mark the quote invalid when it rounds below one share - fix two dto import orderings and the pay-dto section-comment style * chore(realunit): drop two unused kyc imports flagged by the code-quality bot * chore(realunit): order the pay dto import and drop an unused test variable * fix(realunit): claim the swap request atomically before broadcasting Two concurrent broadcasts carrying validly signed transactions with different nonces but the same server-derived payload could both pass the isComplete check and payloadMatches, running the REALU to ZCHF swap twice. Claim the request with a conditional update right before sending, and release the claim on every broadcast failure path so a retry stays possible. * Revert "fix(realunit): claim the swap request atomically before broadcasting" This reverts commit 05f042e.
The debug allowlist blocked the entire setting.value column, so non-secret operational settings (e.g. minTotalBalanceChf) could not be read. Genuine secrets live in the Vault, not the setting table. Add value to the setting allowlist (columns + jsonbColumns) and update the rationale comment. No overlap with GsRestrictedColumns, so the module-load invariant still holds.
…ght bucket (#4316) * fix(log): retire arrived Scrypt EUR deposits from the toScrypt in-flight bucket getUnmatchedSenders retired a bank sender only when a receiver shared its reference id (txId/remittanceInfo). Arrived Scrypt EUR deposits (status ok) carry an empty txId, so they never entered receiverRefs and could never retire their bank_tx sender, leaving it in the toScrypt in-flight pending bucket for up to 7 days and double-counting funds already at Scrypt (inflated balancesTotal.totalBalanceChf). Add a deterministic amount+date fallback pass for receivers without a usable reference: match on currency + amount (1% / min 1.0 tolerance) + date (7-day window), consuming each receiver once, selected by smallest amount diff then date diff then id so the result is independent of DB row order. Use externalCreated as the canonical ExchangeTx date and guard non-finite amounts. Refs #4311 * chore(log): apply prettier formatting to new getUnmatchedSenders tests * fix(log): assign arrived deposits to their exact-amount sender globally The pass-2 fallback matched each sender to its own best receiver in date order, so an earlier near-match sender could consume a receiver that a later sender matched exactly, mis-attributing which transfer arrived (bucket off by up to the tolerance). Replace the per-sender greedy with a global candidate-pair assignment: build all in-tolerance (sender, receiver) pairs, sort by amount diff then date diff then ids, and consume front-first so exact matches win regardless of input order. * fix(log): use maximum-cardinality matching so no arrived deposit is stranded The cost-sorted greedy assignment was not maximum-cardinality: a sender's only in-tolerance receiver could be claimed by a competitor that had an alternative, leaving a genuinely-arrived sender counted (residual over-count). Replace it with Kuhn's augmenting-path bipartite matching, with per-sender cost-ordered candidates and cost-ordered sender processing, so it retires the maximum set of arrived senders while still preferring exact matches.
TaprootFreak
approved these changes
Jul 22, 2026
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.
Automatic Release PR
This PR was automatically created after changes were pushed to develop.
Commits: 1 new commit(s)
Checklist