@@ -47,17 +47,34 @@ import {
4747
4848import { sorobanEscrowAdapter } from './blockchain'
4949import { escrowRepository } from './repository'
50+ import {
51+ executeCriticalOperation ,
52+ type CriticalOperationType ,
53+ initializeFailSafe ,
54+ } from '@/lib/security/failSafe'
5055
5156// ---------------------------------------------------------------------------
5257// Service class
5358// ---------------------------------------------------------------------------
5459
5560export class EscrowService {
61+ private failSafeInitialized = false
62+
5663 constructor (
5764 private readonly blockchain : IEscrowBlockchainAdapter = sorobanEscrowAdapter ,
5865 private readonly repo : IEscrowRepository = escrowRepository
5966 ) { }
6067
68+ /**
69+ * Initialize fail-safe system (call this on application startup)
70+ */
71+ async initialize ( ) : Promise < void > {
72+ if ( ! this . failSafeInitialized ) {
73+ await initializeFailSafe ( )
74+ this . failSafeInitialized = true
75+ }
76+ }
77+
6178 // =========================================================================
6279 // createEscrow
6380 // =========================================================================
@@ -169,33 +186,63 @@ export class EscrowService {
169186 throw new EscrowInvalidStateError ( 'Contract has no escrow address — deploy first' )
170187 }
171188
172- // --- Verify on-chain ---
173- const verification = await this . blockchain . verifyFunding ( {
174- contractAddress : contract . escrowAddress ,
175- txHash : input . fundingTxHash ,
176- expectedAmount : input . amount ,
177- currency : contract . currency ,
178- } )
179-
180- if ( ! verification . verified ) {
181- throw new EscrowFundingVerificationError (
182- `Funding verification failed: on-chain amount ${ verification . onChainAmount } does not match expected ${ input . amount } `
183- )
184- }
185-
186- const now = new Date ( ) . toISOString ( )
187- const updated = await this . repo . updateContractEscrowStatus (
188- contract . id ,
189- 'funded' ,
189+ // --- Execute with fail-safe ---
190+ const { operation, result } = await executeCriticalOperation (
190191 {
191- fundedAt : now ,
192- fundingTxHash : input . fundingTxHash ,
193- status : 'active' ,
194- startedAt : now ,
192+ type : 'escrow_fund' as CriticalOperationType ,
193+ userId : parseInt ( contract . clientId , 10 ) ,
194+ walletAddress : input . callerWalletAddress ,
195+ resourceId : contract . id ,
196+ data : {
197+ contractId : input . contractId ,
198+ fundingTxHash : input . fundingTxHash ,
199+ amount : input . amount ,
200+ } ,
201+ amount : Number ( input . amount ) ,
202+ } ,
203+ async ( ) => {
204+ // --- Verify on-chain ---
205+ if ( ! contract . escrowAddress ) {
206+ throw new EscrowInvalidStateError ( 'Contract has no escrow address' )
207+ }
208+
209+ const verification = await this . blockchain . verifyFunding ( {
210+ contractAddress : contract . escrowAddress ,
211+ txHash : input . fundingTxHash ,
212+ expectedAmount : input . amount ,
213+ currency : contract . currency ,
214+ } )
215+
216+ if ( ! verification . verified ) {
217+ throw new EscrowFundingVerificationError (
218+ `Funding verification failed: on-chain amount ${ verification . onChainAmount } does not match expected ${ input . amount } `
219+ )
220+ }
221+
222+ const now = new Date ( ) . toISOString ( )
223+ const updated = await this . repo . updateContractEscrowStatus (
224+ contract . id ,
225+ 'funded' ,
226+ {
227+ fundedAt : now ,
228+ fundingTxHash : input . fundingTxHash ,
229+ status : 'active' ,
230+ startedAt : now ,
231+ }
232+ )
233+
234+ return { contract : updated , fundedAt : now }
195235 }
196236 )
197237
198- return { contract : updated , fundedAt : now }
238+ // If operation requires approval, return pending status
239+ if ( operation . requiresApproval && operation . status === 'pending' ) {
240+ throw new EscrowInvalidStateError (
241+ 'Operation requires admin approval. Please wait for approval before proceeding.'
242+ )
243+ }
244+
245+ return result
199246 }
200247
201248 // =========================================================================
@@ -246,55 +293,85 @@ export class EscrowService {
246293 throw new EscrowInvalidStateError ( 'Contract has no escrow address' )
247294 }
248295
249- // --- Resolve freelancer wallet ---
250- const freelancerWallet = await this . repo . getUserWalletAddress ( contract . freelancerId )
251- if ( ! freelancerWallet ) {
252- throw new EscrowValidationError ( 'Freelancer wallet address not found' )
253- }
254-
255- // --- Trigger on-chain release ---
256- let release : { txHash : string }
257- try {
258- release = await this . blockchain . releaseMilestoneFunds ( {
259- contractAddress : contract . escrowAddress ,
260- milestoneId : milestone . id ,
261- recipientAddress : freelancerWallet ,
262- amount : milestone . amount ,
263- currency : milestone . currency ,
264- } )
265- } catch ( err ) {
266- if ( err instanceof EscrowBlockchainError ) throw err
267- throw new EscrowBlockchainError ( 'Failed to release milestone funds on-chain' , err )
268- }
269-
270- const now = new Date ( ) . toISOString ( )
296+ // --- Execute with fail-safe ---
297+ const { operation, result } = await executeCriticalOperation (
298+ {
299+ type : 'escrow_release' as CriticalOperationType ,
300+ userId : parseInt ( contract . clientId , 10 ) ,
301+ walletAddress : input . callerWalletAddress ,
302+ resourceId : milestone . id ,
303+ data : {
304+ contractId : input . contractId ,
305+ milestoneId : input . milestoneId ,
306+ amount : milestone . amount ,
307+ } ,
308+ amount : Number ( milestone . amount ) ,
309+ } ,
310+ async ( ) => {
311+ // --- Resolve freelancer wallet ---
312+ const freelancerWallet = await this . repo . getUserWalletAddress ( contract . freelancerId )
313+ if ( ! freelancerWallet ) {
314+ throw new EscrowValidationError ( 'Freelancer wallet address not found' )
315+ }
316+
317+ // --- Trigger on-chain release ---
318+ if ( ! contract . escrowAddress ) {
319+ throw new EscrowInvalidStateError ( 'Contract has no escrow address' )
320+ }
321+
322+ let release : { txHash : string }
323+ try {
324+ release = await this . blockchain . releaseMilestoneFunds ( {
325+ contractAddress : contract . escrowAddress ,
326+ milestoneId : milestone . id ,
327+ recipientAddress : freelancerWallet ,
328+ amount : milestone . amount ,
329+ currency : milestone . currency ,
330+ } )
331+ } catch ( err ) {
332+ if ( err instanceof EscrowBlockchainError ) throw err
333+ throw new EscrowBlockchainError ( 'Failed to release milestone funds on-chain' , err )
334+ }
335+
336+ const now = new Date ( ) . toISOString ( )
337+
338+ // --- Update milestone ---
339+ const updatedMilestone = await this . repo . updateMilestoneStatus (
340+ milestone . id ,
341+ 'paid' ,
342+ { releaseTxHash : release . txHash , paidAt : now }
343+ )
271344
272- // --- Update milestone ---
273- const updatedMilestone = await this . repo . updateMilestoneStatus (
274- milestone . id ,
275- 'paid' ,
276- { releaseTxHash : release . txHash , paidAt : now }
277- )
345+ // --- Determine new escrow / contract status ---
346+ const allMilestones = await this . repo . getMilestonesByContractId ( contract . id )
347+ const allPaid = allMilestones . every (
348+ ( m ) => m . id === milestone . id ? true : m . status === 'paid'
349+ )
278350
279- // --- Determine new escrow / contract status ---
280- const allMilestones = await this . repo . getMilestonesByContractId ( contract . id )
281- const allPaid = allMilestones . every (
282- ( m ) => m . id === milestone . id ? true : m . status === 'paid'
283- )
351+ const newEscrowStatus = allPaid ? 'fully_released' : 'partially_released'
352+ const updatedContract = await this . repo . updateContractEscrowStatus (
353+ contract . id ,
354+ newEscrowStatus ,
355+ allPaid ? { status : 'completed' , completedAt : now } : undefined
356+ )
284357
285- const newEscrowStatus = allPaid ? 'fully_released' : 'partially_released'
286- const updatedContract = await this . repo . updateContractEscrowStatus (
287- contract . id ,
288- newEscrowStatus ,
289- allPaid ? { status : 'completed' , completedAt : now } : undefined
358+ return {
359+ milestone : updatedMilestone ,
360+ contract : updatedContract ,
361+ releaseTxHash : release . txHash ,
362+ allMilestonesPaid : allPaid ,
363+ }
364+ }
290365 )
291366
292- return {
293- milestone : updatedMilestone ,
294- contract : updatedContract ,
295- releaseTxHash : release . txHash ,
296- allMilestonesPaid : allPaid ,
367+ // If operation requires approval, return pending status
368+ if ( operation . requiresApproval && operation . status === 'pending' ) {
369+ throw new EscrowInvalidStateError (
370+ 'Operation requires admin approval. Please wait for approval before proceeding.'
371+ )
297372 }
373+
374+ return result
298375 }
299376
300377 // =========================================================================
@@ -337,38 +414,64 @@ export class EscrowService {
337414 throw new EscrowInvalidStateError ( 'Contract has no escrow address' )
338415 }
339416
340- // --- Resolve client wallet ---
341- const clientWallet = await this . repo . getUserWalletAddress ( contract . clientId )
342- if ( ! clientWallet ) {
343- throw new EscrowValidationError ( 'Client wallet address not found' )
344- }
345-
346- // --- Trigger on-chain refund ---
347- let refund : { txHash : string }
348- try {
349- refund = await this . blockchain . refundEscrow ( {
350- contractAddress : contract . escrowAddress ,
351- clientAddress : clientWallet ,
352- amount : contract . totalAmount ,
353- currency : contract . currency ,
354- } )
355- } catch ( err ) {
356- if ( err instanceof EscrowBlockchainError ) throw err
357- throw new EscrowBlockchainError ( 'Failed to refund escrow on-chain' , err )
358- }
359-
360- const now = new Date ( ) . toISOString ( )
361- const updatedContract = await this . repo . updateContractEscrowStatus (
362- contract . id ,
363- 'refunded' ,
417+ // --- Execute with fail-safe ---
418+ const { operation, result } = await executeCriticalOperation (
364419 {
365- status : 'cancelled' ,
366- cancelledAt : now ,
367- cancellationReason : input . reason ,
420+ type : 'escrow_refund' as CriticalOperationType ,
421+ userId : parseInt ( contract . clientId , 10 ) ,
422+ walletAddress : input . callerWalletAddress ,
423+ resourceId : contract . id ,
424+ data : {
425+ contractId : input . contractId ,
426+ reason : input . reason ,
427+ amount : contract . totalAmount ,
428+ } ,
429+ amount : Number ( contract . totalAmount ) ,
430+ } ,
431+ async ( ) => {
432+ // --- Resolve client wallet ---
433+ const clientWallet = await this . repo . getUserWalletAddress ( contract . clientId )
434+ if ( ! clientWallet ) {
435+ throw new EscrowValidationError ( 'Client wallet address not found' )
436+ }
437+
438+ // --- Trigger on-chain refund ---
439+ let refund : { txHash : string }
440+ try {
441+ refund = await this . blockchain . refundEscrow ( {
442+ contractAddress : contract . escrowAddress ,
443+ clientAddress : clientWallet ,
444+ amount : contract . totalAmount ,
445+ currency : contract . currency ,
446+ } )
447+ } catch ( err ) {
448+ if ( err instanceof EscrowBlockchainError ) throw err
449+ throw new EscrowBlockchainError ( 'Failed to refund escrow on-chain' , err )
450+ }
451+
452+ const now = new Date ( ) . toISOString ( )
453+ const updatedContract = await this . repo . updateContractEscrowStatus (
454+ contract . id ,
455+ 'refunded' ,
456+ {
457+ status : 'cancelled' ,
458+ cancelledAt : now ,
459+ cancellationReason : input . reason ,
460+ }
461+ )
462+
463+ return { contract : updatedContract , refundTxHash : refund . txHash }
368464 }
369465 )
370466
371- return { contract : updatedContract , refundTxHash : refund . txHash }
467+ // If operation requires approval, return pending status
468+ if ( operation . requiresApproval && operation . status === 'pending' ) {
469+ throw new EscrowInvalidStateError (
470+ 'Operation requires admin approval. Please wait for approval before proceeding.'
471+ )
472+ }
473+
474+ return result
372475 }
373476
374477 // =========================================================================
0 commit comments