Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 71 additions & 7 deletions packages/agents-api/src/db/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,18 +198,27 @@ export class AgentsDatabase {
}
}

// Compute timing metrics
// Compute timing metrics (only if timestamps are logically ordered and result is non-negative)
if (escrow.fundedAt && existing?.created_at) {
updates.push('time_to_fund = ?')
values.push(escrow.fundedAt - existing.created_at)
const timeToFund = escrow.fundedAt - existing.created_at
if (timeToFund >= 0) {
updates.push('time_to_fund = ?')
values.push(timeToFund)
}
}
if (escrow.releasedAt && existing?.funded_at) {
updates.push('time_to_release = ?')
values.push(escrow.releasedAt - existing.funded_at)
const timeToRelease = escrow.releasedAt - existing.funded_at
if (timeToRelease >= 0) {
updates.push('time_to_release = ?')
values.push(timeToRelease)
}
}
if (escrow.claimedAt && existing?.released_at) {
updates.push('time_to_claim = ?')
values.push(escrow.claimedAt - existing.released_at)
const timeToClaim = escrow.claimedAt - existing.released_at
if (timeToClaim >= 0) {
updates.push('time_to_claim = ?')
values.push(timeToClaim)
}
}

if (updates.length > 0) {
Expand Down Expand Up @@ -634,6 +643,61 @@ export class AgentsDatabase {
`).get() as { avg_to_fund: number | null; avg_to_release: number | null; avg_to_claim: number | null }
}

/**
* Clean up invalid timing metrics in existing escrows.
* Recalculates time_to_fund, time_to_release, time_to_claim based on actual timestamps.
* Sets metrics to NULL if timestamps are missing or would result in negative values.
* @returns Number of escrows fixed
*/
cleanupInvalidTimingMetrics(): number {
// Fix time_to_fund: should be funded_at - created_at, must be >= 0
const fixFund = this.db.prepare(`
UPDATE escrows SET time_to_fund = CASE
WHEN funded_at IS NOT NULL AND created_at IS NOT NULL AND funded_at >= created_at
THEN funded_at - created_at
ELSE NULL
END
WHERE time_to_fund IS NOT NULL AND (
time_to_fund < 0 OR
funded_at IS NULL OR
created_at IS NULL OR
time_to_fund != (funded_at - created_at)
)
`).run()

// Fix time_to_release: should be released_at - funded_at, must be >= 0
const fixRelease = this.db.prepare(`
UPDATE escrows SET time_to_release = CASE
WHEN released_at IS NOT NULL AND funded_at IS NOT NULL AND released_at >= funded_at
THEN released_at - funded_at
ELSE NULL
END
WHERE time_to_release IS NOT NULL AND (
time_to_release < 0 OR
released_at IS NULL OR
funded_at IS NULL OR
time_to_release != (released_at - funded_at)
)
`).run()

// Fix time_to_claim: should be claimed_at - released_at, must be >= 0
const fixClaim = this.db.prepare(`
UPDATE escrows SET time_to_claim = CASE
WHEN claimed_at IS NOT NULL AND released_at IS NOT NULL AND claimed_at >= released_at
THEN claimed_at - released_at
ELSE NULL
END
WHERE time_to_claim IS NOT NULL AND (
time_to_claim < 0 OR
claimed_at IS NULL OR
released_at IS NULL OR
time_to_claim != (claimed_at - released_at)
)
`).run()

return fixFund.changes + fixRelease.changes + fixClaim.changes
}

close() {
this.db.close()
}
Expand Down
47 changes: 47 additions & 0 deletions packages/agents-api/test/indexer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,51 @@ describe('Database + Reputation integration', () => {
const count = db.countEscrows({ seller: '0xseller' })
expect(count).toBe(bySeller.length)
})

it('prevents negative timing metrics from being stored', () => {
// Create an escrow with a created_at timestamp
db.upsertEscrow({
id: 100, chainId: 8453,
seller: '0xtest', amount: '1000',
state: 'created', createdAt: 2000000000, // Future timestamp
sellerAgentId: 1,
})

// Try to fund it with an earlier timestamp (would cause negative time_to_fund)
db.upsertEscrow({
id: 100, chainId: 8453,
buyer: '0xbuyer', buyerAgentId: 2,
state: 'funded', fundedAt: 1999999000, // Before created_at
})

const escrow = db.getEscrow(100)!
// time_to_fund should NOT be set (or should be null) because it would be negative
expect(escrow.time_to_fund).toBeNull()
})

it('cleanupInvalidTimingMetrics fixes bad data', () => {
// Manually insert an escrow with bad timing via raw SQL
// (simulating legacy data that bypassed validation)
const stmt = (db as any).db.prepare(`
INSERT INTO escrows (id, chain_id, seller, state, created_at, funded_at, time_to_fund, time_to_release)
VALUES (200, 8453, '0xbaddata', 'funded', 1000, 2000, -500, -1000)
`)
stmt.run()

// Verify the bad data exists
let escrow = db.getEscrow(200)!
expect(escrow.time_to_fund).toBe(-500)
expect(escrow.time_to_release).toBe(-1000)

// Run cleanup
const fixed = db.cleanupInvalidTimingMetrics()
expect(fixed).toBeGreaterThan(0)

// Verify timing metrics are now valid
escrow = db.getEscrow(200)!
// time_to_fund should be recalculated to 1000 (funded_at - created_at)
expect(escrow.time_to_fund).toBe(1000)
// time_to_release should be null (no released_at timestamp)
expect(escrow.time_to_release).toBeNull()
})
})