Skip to content

fix(bazaar): validar autorización y vigencia en accept antes de bloquear (#8) - #28

Merged
ericmt-98 merged 1 commit into
mainfrom
fix/bridge-05-accept-validation
Aug 24, 2026
Merged

fix(bazaar): validar autorización y vigencia en accept antes de bloquear (#8)#28
ericmt-98 merged 1 commit into
mainfrom
fix/bridge-05-accept-validation

Conversation

@ericmt-98

Copy link
Copy Markdown
Contributor

Closes #8

Qué había

accept solo comprobaba que el intent existiera y estuviera active. 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 lockAtomicSwap no llegó a llamarse.

Situación Antes Ahora
Aceptas tu propio intent 200 403 self_acceptance_forbidden
Intent vencido 200 409 intent_expired
quote_id que no existe 200 (quote undefined) 404 quote_not_found
quote_id de otro intent 200 409 quote_intent_mismatch
Quote vencida 200 409 quote_expired
Sin quote_id y ninguna vigente 200 409 no_valid_quote
Rate por debajo de min_rate 200 409 quote_below_min_rate
Ninguna pata en USDC 200, escrow de 28.57 400 amount_not_derivable

expires_at, valid_until y min_rate estaban los tres en la base y no los leía nadie.

Tres cosas que merecen mirada

getQuote(id) es nuevo en db/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 — rate es DECIMAL(10,6), min_rate es DECIMAL(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 number en este repo, así que hay dos tests que lo fijan, uno para la selección y otro para min_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" y market_maker: "market-maker-agent". Ese relleno era del mismo material que el 28.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_usdc del 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

npm run typecheck -w @micopay/api     → limpio
npm test -w @micopay/api              → 25 archivos | 182 pasan | 1 skip
npm run test:concurrency -w @micopay/api → 2/2 OK

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

  • Auto-aceptación rechazada
  • Intent vencido rechazado
  • Quote vencida rechazada
  • quote_id de otro intent rechazado
  • Con varias quotes vigentes y sin quote_id, se elige la mejor (el test dice cuál)
  • Quote por debajo de min_rate rechazada
  • No queda ningún importe cableado en el archivo
  • Toda ruta de rechazo cubierta por un test, y ninguna bloquea fondos

Coordinació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 de accept; los de #26 están en ensureBazaarInitialized() 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.

…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
@ericmt-98
ericmt-98 merged commit 493b11a into main Aug 24, 2026
3 checks passed
@ericmt-98
ericmt-98 deleted the fix/bridge-05-accept-validation branch August 24, 2026 17:18
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.

[BRIDGE-05] Bazaar accept performs no authorization or validity checks on intents and quotes

1 participant