Prune stale unconfirmed invoice payments during wallet polling.
Refactor processInvoice into upsert and prune helpers so absent 0-conf payments are removed when the wallet no longer reports them.
This commit is contained in:
@@ -87,6 +87,7 @@ describe('InvoicePaymentService', () => {
|
|||||||
};
|
};
|
||||||
let paymentRepo: {
|
let paymentRepo: {
|
||||||
update: jest.Mock;
|
update: jest.Mock;
|
||||||
|
delete: jest.Mock;
|
||||||
createQueryBuilder: jest.Mock;
|
createQueryBuilder: jest.Mock;
|
||||||
};
|
};
|
||||||
let insertQueryBuilder: {
|
let insertQueryBuilder: {
|
||||||
@@ -133,6 +134,7 @@ describe('InvoicePaymentService', () => {
|
|||||||
|
|
||||||
paymentRepo = {
|
paymentRepo = {
|
||||||
update: jest.fn().mockResolvedValue(undefined),
|
update: jest.fn().mockResolvedValue(undefined),
|
||||||
|
delete: jest.fn().mockResolvedValue(undefined),
|
||||||
createQueryBuilder: jest.fn().mockReturnValue(insertQueryBuilder)
|
createQueryBuilder: jest.fn().mockReturnValue(insertQueryBuilder)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -384,13 +386,35 @@ describe('InvoicePaymentService', () => {
|
|||||||
expect(paymentRepo.update).not.toHaveBeenCalled();
|
expect(paymentRepo.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does nothing when there are no transfers to process', async () => {
|
it('does not mutate payments when there are no transfers and no existing payments', async () => {
|
||||||
transactionalInvoiceQueryBuilder.getOne.mockResolvedValue(buildXmrInvoice());
|
transactionalInvoiceQueryBuilder.getOne.mockResolvedValue(buildXmrInvoice());
|
||||||
|
|
||||||
await service.processInvoice('invoice-1', []);
|
await service.processInvoice('invoice-1', []);
|
||||||
|
|
||||||
expect(paymentRepo.createQueryBuilder).not.toHaveBeenCalled();
|
expect(paymentRepo.createQueryBuilder).not.toHaveBeenCalled();
|
||||||
expect(paymentRepo.update).not.toHaveBeenCalled();
|
expect(paymentRepo.update).not.toHaveBeenCalled();
|
||||||
|
expect(paymentRepo.delete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes unconfirmed payments that are no longer reported when transfers are empty', async () => {
|
||||||
|
transactionalInvoiceQueryBuilder.getOne.mockResolvedValue(
|
||||||
|
buildBtcInvoice({
|
||||||
|
payments: [
|
||||||
|
{
|
||||||
|
id: 'payment-ghost',
|
||||||
|
txHash: 'ghost',
|
||||||
|
amountAtomic: '50000',
|
||||||
|
confirmations: 0
|
||||||
|
} as InvoicePayment
|
||||||
|
]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await service.processInvoice('invoice-btc-1', []);
|
||||||
|
|
||||||
|
expect(paymentRepo.delete).toHaveBeenCalledWith('payment-ghost');
|
||||||
|
expect(paymentRepo.createQueryBuilder).not.toHaveBeenCalled();
|
||||||
|
expect(paymentRepo.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('skips transfers below the configured minimum', async () => {
|
it('skips transfers below the configured minimum', async () => {
|
||||||
@@ -528,5 +552,34 @@ describe('InvoicePaymentService', () => {
|
|||||||
confirmations: 1
|
confirmations: 1
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('removes unconfirmed payments that are no longer reported', async () => {
|
||||||
|
transactionalInvoiceQueryBuilder.getOne.mockResolvedValue(
|
||||||
|
buildBtcInvoice({
|
||||||
|
payments: [
|
||||||
|
{
|
||||||
|
id: 'payment-original',
|
||||||
|
txHash: 'original',
|
||||||
|
amountAtomic: '50000',
|
||||||
|
confirmations: 0
|
||||||
|
} as InvoicePayment,
|
||||||
|
{
|
||||||
|
id: 'payment-replacement',
|
||||||
|
txHash: 'replacement',
|
||||||
|
amountAtomic: '50000',
|
||||||
|
confirmations: 3
|
||||||
|
} as InvoicePayment
|
||||||
|
]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await service.processInvoice('invoice-btc-1', [
|
||||||
|
buildBtcTransfer({ txHash: 'replacement', amountAtomic: '50000', confirmations: 3 })
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(paymentRepo.delete).toHaveBeenCalledWith('payment-original');
|
||||||
|
expect(paymentRepo.update).not.toHaveBeenCalled();
|
||||||
|
expect(paymentRepo.createQueryBuilder).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -135,8 +135,6 @@ export class InvoicePaymentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async processInvoice(invoiceId: string, transfers: InvoiceIncomingTransfer[]): Promise<void> {
|
private async processInvoice(invoiceId: string, transfers: InvoiceIncomingTransfer[]): Promise<void> {
|
||||||
const { minByMethod } = this.configService.get('invoice') as Config['invoice'];
|
|
||||||
|
|
||||||
await this.dataSource.transaction(async manager => {
|
await this.dataSource.transaction(async manager => {
|
||||||
const invoiceRepo = manager.getRepository(Invoice);
|
const invoiceRepo = manager.getRepository(Invoice);
|
||||||
const paymentRepo = manager.getRepository(InvoicePayment);
|
const paymentRepo = manager.getRepository(InvoicePayment);
|
||||||
@@ -152,36 +150,62 @@ export class InvoicePaymentService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const minIncomingAtomic = minByMethod[invoice.paymentMethod];
|
await this.upsertIncomingPayments(paymentRepo, invoice, transfers);
|
||||||
const knownByTxHash = new Map((invoice.payments ?? []).map(payment => [payment.txHash, payment]));
|
|
||||||
|
|
||||||
for (const transfer of transfers) {
|
await this.pruneAbsentUnconfirmedPayments(paymentRepo, invoice, transfers);
|
||||||
const existing = knownByTxHash.get(transfer.txHash);
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
if (existing.confirmations !== transfer.confirmations) {
|
|
||||||
await paymentRepo.update(existing.id, { confirmations: transfer.confirmations });
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAtomicGte(transfer.amountAtomic, minIncomingAtomic)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await paymentRepo
|
|
||||||
.createQueryBuilder()
|
|
||||||
.insert()
|
|
||||||
.values({
|
|
||||||
invoice: { id: invoiceId },
|
|
||||||
txHash: transfer.txHash,
|
|
||||||
amountAtomic: transfer.amountAtomic,
|
|
||||||
confirmations: transfer.confirmations
|
|
||||||
})
|
|
||||||
.orIgnore()
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async upsertIncomingPayments(
|
||||||
|
paymentRepo: Repository<InvoicePayment>,
|
||||||
|
invoice: Invoice,
|
||||||
|
transfers: InvoiceIncomingTransfer[]
|
||||||
|
): Promise<void> {
|
||||||
|
const { minByMethod } = this.configService.get('invoice') as Config['invoice'];
|
||||||
|
|
||||||
|
const minIncomingAtomic = minByMethod[invoice.paymentMethod];
|
||||||
|
const knownByTxHash = new Map((invoice.payments ?? []).map(payment => [payment.txHash, payment]));
|
||||||
|
|
||||||
|
for (const transfer of transfers) {
|
||||||
|
const existing = knownByTxHash.get(transfer.txHash);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (existing.confirmations !== transfer.confirmations) {
|
||||||
|
await paymentRepo.update(existing.id, { confirmations: transfer.confirmations });
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAtomicGte(transfer.amountAtomic, minIncomingAtomic)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await paymentRepo
|
||||||
|
.createQueryBuilder()
|
||||||
|
.insert()
|
||||||
|
.values({
|
||||||
|
invoice: { id: invoice.id },
|
||||||
|
txHash: transfer.txHash,
|
||||||
|
amountAtomic: transfer.amountAtomic,
|
||||||
|
confirmations: transfer.confirmations
|
||||||
|
})
|
||||||
|
.orIgnore()
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pruneAbsentUnconfirmedPayments(
|
||||||
|
paymentRepo: Repository<InvoicePayment>,
|
||||||
|
invoice: Invoice,
|
||||||
|
transfers: InvoiceIncomingTransfer[]
|
||||||
|
): Promise<void> {
|
||||||
|
const activeTxHashes = new Set(transfers.map(transfer => transfer.txHash));
|
||||||
|
|
||||||
|
for (const payment of invoice.payments ?? []) {
|
||||||
|
if (payment.confirmations === 0 && !activeTxHashes.has(payment.txHash)) {
|
||||||
|
await paymentRepo.delete(payment.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user