Skip to content

Commit 44bd609

Browse files
authored
Merge pull request #571 from everclearorg/ci/prod-to-staging-merge
ci: sync staging to prod
2 parents 27af3f5 + 072f55b commit 44bd609

File tree

3 files changed

+70
-20
lines changed

3 files changed

+70
-20
lines changed

packages/handler/src/processor/eventProcessor.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -189,17 +189,6 @@ export class EventProcessor {
189189
minAmounts = {
190190
[earmark.designatedPurchaseChain.toString()]: minAmounts[earmark.designatedPurchaseChain.toString()],
191191
};
192-
193-
// Move READY → COMPLETED immediately after applying the chain constraint.
194-
// This gives the earmark one shot at filling with the designated chain.
195-
// If it fails, the earmark is already gone, so the next cycle can re-evaluate fresh.
196-
await database.updateEarmarkStatus(earmark.id, EarmarkStatus.COMPLETED);
197-
logger.info('Earmark marked COMPLETED after applying chain constraint', {
198-
requestId,
199-
invoiceId: event.id,
200-
earmarkId: earmark.id,
201-
designatedPurchaseChain: earmark.designatedPurchaseChain,
202-
});
203192
} else {
204193
logger.warn('Earmarked invoice designated origin not available in minAmounts', {
205194
requestId,
@@ -418,6 +407,26 @@ export class EventProcessor {
418407
throw e;
419408
}
420409
} else {
410+
// Purchase failed — mark earmark COMPLETED so the next cycle can re-evaluate
411+
// fresh (e.g. pick a different chain or create a new earmark).
412+
try {
413+
const earmark = await database.getActiveEarmarkForInvoice(event.id);
414+
if (earmark && earmark.status === EarmarkStatus.READY) {
415+
await database.updateEarmarkStatus(earmark.id, EarmarkStatus.COMPLETED);
416+
logger.info('Earmark marked COMPLETED after failed purchase attempt', {
417+
requestId,
418+
invoiceId: event.id,
419+
earmarkId: earmark.id,
420+
});
421+
}
422+
} catch (error) {
423+
logger.error('Error cleaning up earmark after failed purchase', {
424+
requestId,
425+
invoiceId: event.id,
426+
error: jsonifyError(error),
427+
});
428+
}
429+
421430
logger.info('Method complete with 0 purchases', {
422431
requestId,
423432
invoiceId: event.id,

packages/handler/test/processor/eventProcessor.spec.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ describe('EventProcessor', () => {
7878

7979
mockDatabase = {
8080
getEarmarks: jest.fn(),
81+
getActiveEarmarkForInvoice: jest.fn(),
8182
updateEarmarkStatus: jest.fn(),
8283
} as any;
8384

@@ -618,7 +619,7 @@ describe('EventProcessor', () => {
618619
);
619620
});
620621

621-
it('should move READY earmark to COMPLETED after applying chain constraint', async () => {
622+
it('should keep READY earmark during purchase and mark COMPLETED after failed attempt', async () => {
622623
const event = createInvoiceEvent('invoice-1');
623624
const invoice = createMockInvoice();
624625
const { processPendingEarmark, splitAndSendIntents } = require('#/helpers');
@@ -643,6 +644,12 @@ describe('EventProcessor', () => {
643644
designatedPurchaseChain: 2,
644645
},
645646
]);
647+
mockDatabase.getActiveEarmarkForInvoice.mockResolvedValue({
648+
id: 'earmark-1',
649+
invoiceId: 'invoice-1',
650+
status: EarmarkStatus.READY,
651+
designatedPurchaseChain: 2,
652+
});
646653
mockDatabase.updateEarmarkStatus.mockResolvedValue(undefined);
647654
isValidInvoice.mockReturnValue(null);
648655
isXerc20Supported.mockResolvedValue(false);
@@ -657,18 +664,26 @@ describe('EventProcessor', () => {
657664
});
658665
splitAndSendIntents.mockResolvedValue([]);
659666

660-
await eventProcessor.processInvoiceEnqueued(event);
667+
const result = await eventProcessor.processInvoiceEnqueued(event);
661668

662-
// Earmark should be marked COMPLETED immediately after applying chain constraint
669+
// Earmark stays READY during purchase, then marked COMPLETED after 0 purchases
663670
expect(mockDatabase.updateEarmarkStatus).toHaveBeenCalledWith('earmark-1', EarmarkStatus.COMPLETED);
664671
expect(mockLogger.info).toHaveBeenCalledWith(
665-
'Earmark marked COMPLETED after applying chain constraint',
672+
'Applied earmark chain constraint, earmark stays READY during purchase',
666673
expect.objectContaining({
667674
invoiceId: 'invoice-1',
668675
earmarkId: 'earmark-1',
669676
designatedPurchaseChain: 2,
670677
}),
671678
);
679+
expect(mockLogger.info).toHaveBeenCalledWith(
680+
'Earmark marked COMPLETED after failed purchase attempt',
681+
expect.objectContaining({
682+
invoiceId: 'invoice-1',
683+
earmarkId: 'earmark-1',
684+
}),
685+
);
686+
expect(result.result).toBe(EventProcessingResultType.Failure);
672687
});
673688

674689
it('should move READY earmark to COMPLETED and fail when designated chain not in minAmounts', async () => {

packages/poller/src/invoice/processInvoices.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -667,11 +667,11 @@ export async function processInvoices(context: ProcessingContext, invoices: Invo
667667
// READY earmarks go into the processing map with chain constraint
668668
earmarkedInvoicesMap.set(invoiceId, designatedPurchaseChain);
669669

670-
// Move READY → COMPLETED immediately after capturing the chain constraint.
671-
// This gives the earmark one shot at filling with the designated chain.
672-
// If it fails, the earmark is already gone, so the next cycle can re-evaluate fresh.
673-
await context.database.updateEarmarkStatus(earmark.id, EarmarkStatus.COMPLETED);
674-
logger.info('Earmarked invoice ready for processing, marked COMPLETED', {
670+
// Keep earmark in READY state during purchase processing so that
671+
// getEarmarkedBalance() continues to protect these funds from the
672+
// regular rebalancer. The earmark will be marked COMPLETED after the
673+
// purchase attempt (success or failure).
674+
logger.info('Earmarked invoice ready for processing', {
675675
requestId,
676676
invoiceId,
677677
earmarkId: earmark.id,
@@ -941,6 +941,32 @@ export async function processInvoices(context: ProcessingContext, invoices: Invo
941941
}
942942
}
943943

944+
// Mark COMPLETED earmarks for invoices that were NOT purchased.
945+
// This prevents infinite retry loops when the purchase fails (e.g. insufficient balance)
946+
// while keeping the earmark visible to getEarmarkedBalance() during the purchase window.
947+
const purchasedIds = new Set(allPurchases.map((p) => p.target.intent_id));
948+
for (const [invoiceId] of earmarkedInvoicesMap) {
949+
if (!purchasedIds.has(invoiceId)) {
950+
try {
951+
const earmark = await context.database.getActiveEarmarkForInvoice(invoiceId);
952+
if (earmark && earmark.status === EarmarkStatus.READY) {
953+
await context.database.updateEarmarkStatus(earmark.id, EarmarkStatus.COMPLETED);
954+
logger.info('Earmark marked COMPLETED after failed purchase attempt', {
955+
requestId,
956+
earmarkId: earmark.id,
957+
invoiceId,
958+
});
959+
}
960+
} catch (error) {
961+
logger.error('Error cleaning up earmark after failed purchase', {
962+
requestId,
963+
invoiceId,
964+
error: jsonifyError(error),
965+
});
966+
}
967+
}
968+
}
969+
944970
// Store purchases in cache
945971
if (allPurchases.length > 0) {
946972
try {

0 commit comments

Comments
 (0)