11import { Hono } from 'hono' ;
22import { db , type Database } from '../db/client.js' ;
33import { tasks , taskBids , escrowTransactions } from '../db/schema.js' ;
4- import { eq , and } from 'drizzle-orm' ;
4+ import { eq , and , sql } from 'drizzle-orm' ;
55import { authMiddleware , requireScope , type AuthContext } from '../middleware/auth.js' ;
66import { BidCreateSchema , TASK_STATUS , BID_STATUS , ESCROW_STATUS } from '@swarmdock/shared' ;
77import { eventBus } from '../lib/events.js' ;
@@ -13,7 +13,7 @@ type BidRouteDeps = {
1313 db : Pick < Database , 'select' | 'insert' | 'update' | 'transaction' > ;
1414 authMiddleware : typeof authMiddleware ;
1515 requireScope : typeof requireScope ;
16- eventBus : Pick < typeof eventBus , 'emit' > ;
16+ eventBus : Pick < typeof eventBus , 'emit' | 'broadcast' > ;
1717 createTxHash : ( ) => string ;
1818 requirePayment : typeof requireX402Payment ;
1919} ;
@@ -82,7 +82,7 @@ export function createBidsApp(overrides: Partial<BidRouteDeps> = {}) {
8282 await database . update ( tasks ) . set ( { status : TASK_STATUS . BIDDING , updatedAt : new Date ( ) } ) . where ( eq ( tasks . id , taskId ) ) ;
8383 }
8484
85- eventBus . broadcast ( {
85+ events . broadcast ( {
8686 type : 'task.updated' ,
8787 data : { taskId, status : TASK_STATUS . BIDDING } ,
8888 } ) ;
@@ -110,23 +110,19 @@ export function createBidsApp(overrides: Partial<BidRouteDeps> = {}) {
110110 const [ task ] = await database . select ( ) . from ( tasks ) . where ( eq ( tasks . id , taskId ) ) . limit ( 1 ) ;
111111 if ( ! task ) return c . json ( { error : 'Task not found' } , 404 ) ;
112112 if ( task . requesterId !== agent . agent_id ) return c . json ( { error : 'Not task owner' } , 403 ) ;
113- if ( ! [ TASK_STATUS . OPEN , TASK_STATUS . BIDDING ] . includes ( task . status as 'open' | 'bidding' ) ) {
114- return c . json ( { error : 'Task not accepting bids' } , 400 ) ;
115- }
116113
117- const [ bid ] = await database
114+ const [ preflightBid ] = await database
118115 . select ( )
119116 . from ( taskBids )
120117 . where ( and ( eq ( taskBids . id , bidId ) , eq ( taskBids . taskId , taskId ) ) )
121118 . limit ( 1 ) ;
122-
123- if ( ! bid ) return c . json ( { error : 'Bid not found' } , 404 ) ;
124- if ( bid . status !== BID_STATUS . PENDING ) return c . json ( { error : 'Bid no longer pending' } , 400 ) ;
119+ if ( ! preflightBid ) return c . json ( { error : 'Bid not found' } , 404 ) ;
120+ if ( preflightBid . status !== BID_STATUS . PENDING ) return c . json ( { error : 'Bid no longer pending' } , 400 ) ;
125121
126122 const paymentGate = await requirePayment ( c , {
127123 accepts : {
128124 scheme : 'exact' ,
129- price : microUsdcToUsdPrice ( bid . proposedPrice ) ,
125+ price : microUsdcToUsdPrice ( preflightBid . proposedPrice ) ,
130126 network : getX402Network ( ) ,
131127 payTo : process . env . PLATFORM_WALLET_ADDRESS ?? '0x0000000000000000000000000000000000000000' ,
132128 } ,
@@ -138,7 +134,7 @@ export function createBidsApp(overrides: Partial<BidRouteDeps> = {}) {
138134 error : 'Payment required to fund escrow' ,
139135 taskId,
140136 bidId,
141- amount : bid . proposedPrice . toString ( ) ,
137+ amount : preflightBid . proposedPrice . toString ( ) ,
142138 } ,
143139 } ) ,
144140 } ) ;
@@ -150,7 +146,38 @@ export function createBidsApp(overrides: Partial<BidRouteDeps> = {}) {
150146 const pendingEscrowTxHash = paymentGate . pendingSettlement ? null : createTxHash ( ) ;
151147
152148 // Accept this bid, assign the task, and record pending/funded escrow in one transaction.
153- const { updatedTask, escrow } = await database . transaction ( async ( tx ) => {
149+ const transactionResult = await database . transaction ( async ( tx ) => {
150+ await tx . execute ( sql `SELECT id FROM tasks WHERE id = ${ taskId } FOR UPDATE` ) ;
151+
152+ const [ lockedTask ] = await tx
153+ . select ( )
154+ . from ( tasks )
155+ . where ( eq ( tasks . id , taskId ) )
156+ . limit ( 1 ) ;
157+
158+ if ( ! lockedTask ) {
159+ return { ok : false , status : 404 , body : { error : 'Task not found' } } as const ;
160+ }
161+ if ( lockedTask . requesterId !== agent . agent_id ) {
162+ return { ok : false , status : 403 , body : { error : 'Not task owner' } } as const ;
163+ }
164+ if ( ! [ TASK_STATUS . OPEN , TASK_STATUS . BIDDING ] . includes ( lockedTask . status as 'open' | 'bidding' ) ) {
165+ return { ok : false , status : 400 , body : { error : 'Task not accepting bids' } } as const ;
166+ }
167+
168+ const [ bid ] = await tx
169+ . select ( )
170+ . from ( taskBids )
171+ . where ( and ( eq ( taskBids . id , bidId ) , eq ( taskBids . taskId , taskId ) ) )
172+ . limit ( 1 ) ;
173+
174+ if ( ! bid ) {
175+ return { ok : false , status : 404 , body : { error : 'Bid not found' } } as const ;
176+ }
177+ if ( bid . status !== BID_STATUS . PENDING ) {
178+ return { ok : false , status : 400 , body : { error : 'Bid no longer pending' } } as const ;
179+ }
180+
154181 await tx . update ( taskBids ) . set ( { status : BID_STATUS . ACCEPTED } ) . where ( eq ( taskBids . id , bidId ) ) ;
155182 await tx . update ( taskBids ) . set ( { status : BID_STATUS . REJECTED } )
156183 . where ( and ( eq ( taskBids . taskId , taskId ) , eq ( taskBids . status , BID_STATUS . PENDING ) ) ) ;
@@ -172,9 +199,14 @@ export function createBidsApp(overrides: Partial<BidRouteDeps> = {}) {
172199 network : process . env . X402_NETWORK ?? 'base-sepolia' ,
173200 } ) . returning ( ) ;
174201
175- return { updatedTask, escrow } ;
202+ return { ok : true , updatedTask, escrow, bid } as const ;
176203 } ) ;
177204
205+ if ( ! transactionResult . ok ) {
206+ return c . json ( transactionResult . body , transactionResult . status ) ;
207+ }
208+
209+ const { updatedTask, escrow, bid } = transactionResult ;
178210 let settledEscrow = escrow ;
179211 let settlementHeaders : Record < string , string > = { } ;
180212
@@ -222,7 +254,7 @@ export function createBidsApp(overrides: Partial<BidRouteDeps> = {}) {
222254 type : 'payment.escrowed' ,
223255 data : { taskId, amount : bid . proposedPrice . toString ( ) , txHash : settledEscrow . escrowTxHash } ,
224256 } ) ;
225- eventBus . broadcast ( {
257+ events . broadcast ( {
226258 type : 'task.updated' ,
227259 data : { taskId, status : TASK_STATUS . ASSIGNED , assigneeId : bid . bidderId } ,
228260 } ) ;
0 commit comments