fix(bazaar): validar autorización y vigencia en accept antes de bloquear (#8) - #28
Merged
Merged
Conversation
…ear (#8) `POST /api/v1/bazaar/accept` solo comprobaba que el intent existiera y estuviera `active`. Todo lo demas que deberia condicionar una liquidacion faltaba, y cada hueco acababa en un escrow real en cadena. Las puertas nuevas, todas ANTES del lock — rechazar despues de bloquear deja un escrow colgado sin nadie que lo libere, y el bazaar no tiene reembolso: - 403 self_acceptance_forbidden: aceptar tu propio intent cuesta $0.005, no entrega nada, y servia para fabricarse historial. - 409 intent_expired: `expires_at` no lo leia nadie. - 404 quote_not_found / 409 quote_intent_mismatch / 409 quote_expired. Para separar "no existe" de "es de otro intent" hace falta poder mirar la quote sola, asi que se anade `getQuote(id)` en db/bazaar.ts: buscarla dentro de las del intent hace ambos casos indistinguibles. - 409 no_valid_quote cuando no se indica quote_id y no queda ninguna vigente. - 409 quote_below_min_rate: `min_rate` tampoco lo leia nadie. - 400 amount_not_derivable en vez del literal "28.57", que se colaba en el escrow en cuanto el intent no pedia USDC. Ahora el importe sale del lado del intent denominado en USDC, y si no hay ninguno se contesta 400 en vez de inventarselo. Sin quote_id ya no se coge `quotes[0]` —la primera que llego, que no es una eleccion sino un accidente— sino la de mejor rate entre las vigentes. Los rate se comparan con Number(). pg devuelve DECIMAL como string (rate es DECIMAL(10,6), min_rate DECIMAL(5,4)) y entre strings `"9" > "10"` es cierto: la comparacion lexicografica elige justo la peor. Es el mismo fallo que ya costo un arreglo en volume_usdc, y hay un test que lo fija. Cambio de comportamiento: accept sin ninguna quote vigente ahora responde 409 donde antes daba 200 con `quote_id: "auto"` y `market_maker: "market-maker-agent"`. Ese relleno era del mismo material que el 28.57. Se actualizan los tres tests que lo daban por bueno. 19 tests nuevos, uno por criterio del issue, y todos los de rechazo verifican ademas que `lockAtomicSwap` no se llamo. El de "no queda ningun importe cableado" lee la fuente ignorando comentarios, para que el propio comentario que documenta el arreglo no lo haga fallar. Verificado: typecheck limpio, 25 archivos / 182 tests / 1 skip, y test:concurrency 2/2. Closes #8 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J2txyR4THyCdW3x5V9yfzK
This was referenced Aug 31, 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.
Closes #8
Qué había
acceptsolo comprobaba que el intent existiera y estuvieraactive. Todo lo demás que debería condicionar una liquidación faltaba, y cada hueco terminaba en un escrow real en cadena.Las puertas, todas antes del lock
El orden importa: rechazar después de bloquear deja un escrow colgado sin nadie que lo libere, y el bazaar no tiene ruta de reembolso. Por eso cada test de rechazo verifica además que
lockAtomicSwapno llegó a llamarse.self_acceptance_forbiddenintent_expiredquote_idque no existeundefined)quote_not_foundquote_idde otro intentquote_intent_mismatchquote_expiredquote_idy ninguna vigenteno_valid_quotemin_ratequote_below_min_rateamount_not_derivableexpires_at,valid_untilymin_rateestaban los tres en la base y no los leía nadie.Tres cosas que merecen mirada
getQuote(id)es nuevo endb/bazaar.ts. No es adorno:getQuotesForIntent()solo devuelve las del intent, así que una quote de otro intent y una que no existe se ven idénticas (ausentes de la lista). El issue pide 409 y 404 respectivamente, y para separarlas hay que poder mirar la quote sola.Los rate se comparan con
Number(). pg devuelve DECIMAL como string —rateesDECIMAL(10,6),min_rateesDECIMAL(5,4)— y entre strings"9" > "10"es cierto. La comparación lexicográfica elige exactamente la peor quote. Es el mismo fallo que ya costófix(bazaar): coerce pg NUMERIC to numberen este repo, así que hay dos tests que lo fijan, uno para la selección y otro paramin_rate."Mejor" = rate más alto, que es lo más favorable para quien publicó el intent. Está dicho en un comentario, como pedía el issue. Antes se cogía
quotes[0]: la primera que llegó, ordenada por fecha. Eso no es una elección, es un accidente.Cambio de comportamiento (deliberado)
Aceptar sin ninguna quote vigente ahora da 409 donde antes daba 200 con
quote_id: "auto"ymarket_maker: "market-maker-agent". Ese relleno era del mismo material que el28.57, y con la quote ya siempre presente los dos fallbacks quedaban muertos. Se actualizan los tres tests que daban por bueno el camino sin quote.amount_usdcdel body se sigue respetando como override explícito. Contrastarlo contra el importe derivado —y contra lo que de verdad se bloquea— es #14, que toca justo esa parte; no lo invado.Verificación
19 tests nuevos en
bazaar-accept-validation.test.ts, uno por criterio. El de "no queda ningún importe cableado" lee la fuente ignorando comentarios: el comentario que documenta el arreglo nombra el 28.57 a propósito, y eso es documentación, no un importe.Criterios
quote_idde otro intent rechazadoquote_id, se elige la mejor (el test dice cuál)min_raterechazadaCoordinación
Toca
apps/api/src/routes/bazaar.ts, el mismo archivo que #26 (BRIDGE-12), que está pendiente de rebase. Los cambios de aquí están todos dentro del handler deaccept; los de #26 están enensureBazaarInitialized()y las constantes de arriba. No se solapan salvo que #26 vuelva a traer el reformateo de todo el archivo, que es justo lo que se le pidió quitar.