wire bitcoin invoice creation and payment polling
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Logger, ServiceUnavailableException } from '@nestjs/common';
|
||||
import type { ConfigService } from '@nestjs/config';
|
||||
import type { Repository } from 'typeorm';
|
||||
import type { ElectrumWalletRpcClient } from '../../bitcoinWallet/services/ElectrumWalletRpcClient';
|
||||
import type { MoneroWalletRpcClient } from '../../moneroWallet/services/MoneroWalletRpcClient';
|
||||
import type { ExchangeRateService } from '../../exchangeRate/services/ExchangeRateService';
|
||||
import { Invoice } from '../entities/Invoice';
|
||||
@@ -19,7 +20,10 @@ describe('InvoiceService', () => {
|
||||
let configService: {
|
||||
get: jest.Mock;
|
||||
};
|
||||
let walletRpcClient: {
|
||||
let moneroWalletRpcClient: {
|
||||
createAddress: jest.Mock;
|
||||
};
|
||||
let bitcoinWalletRpcClient: {
|
||||
createAddress: jest.Mock;
|
||||
};
|
||||
let exchangeRateService: {
|
||||
@@ -45,6 +49,10 @@ describe('InvoiceService', () => {
|
||||
return { confirmationTiers };
|
||||
}
|
||||
|
||||
if (key === 'shopSettings.bitcoin') {
|
||||
return { confirmationTiers };
|
||||
}
|
||||
|
||||
if (key === 'order') {
|
||||
return { checkoutValidityMs: 3_600_000, shippingPaymentValidityMs: 7_200_000 };
|
||||
}
|
||||
@@ -53,13 +61,17 @@ describe('InvoiceService', () => {
|
||||
})
|
||||
};
|
||||
|
||||
walletRpcClient = {
|
||||
moneroWalletRpcClient = {
|
||||
createAddress: jest.fn().mockResolvedValue({
|
||||
address: '4MoneroPaymentAddressExample',
|
||||
address_index: 12
|
||||
})
|
||||
};
|
||||
|
||||
bitcoinWalletRpcClient = {
|
||||
createAddress: jest.fn().mockResolvedValue('bc1qtestpaymentaddress')
|
||||
};
|
||||
|
||||
exchangeRateService = {
|
||||
getLiveFiatPerCrypto: jest.fn().mockReturnValue(150)
|
||||
};
|
||||
@@ -67,7 +79,8 @@ describe('InvoiceService', () => {
|
||||
service = new InvoiceService(
|
||||
invoiceRepo as unknown as Repository<Invoice>,
|
||||
configService as unknown as ConfigService,
|
||||
walletRpcClient as unknown as MoneroWalletRpcClient,
|
||||
moneroWalletRpcClient as unknown as MoneroWalletRpcClient,
|
||||
bitcoinWalletRpcClient as unknown as ElectrumWalletRpcClient,
|
||||
exchangeRateService as unknown as ExchangeRateService
|
||||
);
|
||||
});
|
||||
@@ -76,9 +89,9 @@ describe('InvoiceService', () => {
|
||||
errorLogSpy.mockRestore();
|
||||
});
|
||||
|
||||
const issueCheckoutInvoice = () =>
|
||||
const issueCheckoutInvoice = (paymentMethod: PaymentMethod = PaymentMethod.Xmr) =>
|
||||
service.issueInvoice({
|
||||
paymentMethod: PaymentMethod.Xmr,
|
||||
paymentMethod,
|
||||
reason: InvoiceReason.Checkout,
|
||||
contextId: 'session-uuid',
|
||||
amountFiat: 15
|
||||
@@ -91,11 +104,11 @@ describe('InvoiceService', () => {
|
||||
new ServiceUnavailableException("We can't show a price right now. Please try again in a few minutes.")
|
||||
);
|
||||
|
||||
expect(walletRpcClient.createAddress).not.toHaveBeenCalled();
|
||||
expect(moneroWalletRpcClient.createAddress).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws and logs when wallet address allocation fails for checkout invoices', async () => {
|
||||
walletRpcClient.createAddress.mockRejectedValue(new Error('rpc down'));
|
||||
moneroWalletRpcClient.createAddress.mockRejectedValue(new Error('rpc down'));
|
||||
|
||||
await expect(issueCheckoutInvoice()).rejects.toThrow(
|
||||
new ServiceUnavailableException("We can't take payments right now. Please try again in a few minutes.")
|
||||
@@ -108,7 +121,7 @@ describe('InvoiceService', () => {
|
||||
it('creates a checkout invoice with converted totals and monero details', async () => {
|
||||
const invoice = await issueCheckoutInvoice();
|
||||
|
||||
expect(walletRpcClient.createAddress).toHaveBeenCalledWith('checkout - session-uuid');
|
||||
expect(moneroWalletRpcClient.createAddress).toHaveBeenCalledWith('checkout - session-uuid');
|
||||
expect(invoiceRepo.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
reason: InvoiceReason.Checkout,
|
||||
@@ -145,6 +158,10 @@ describe('InvoiceService', () => {
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'shopSettings.bitcoin') {
|
||||
return { confirmationTiers };
|
||||
}
|
||||
|
||||
if (key === 'order') {
|
||||
return { checkoutValidityMs: 3_600_000, shippingPaymentValidityMs: 7_200_000 };
|
||||
}
|
||||
@@ -185,7 +202,7 @@ describe('InvoiceService', () => {
|
||||
);
|
||||
|
||||
exchangeRateService.getLiveFiatPerCrypto.mockReturnValue(150);
|
||||
walletRpcClient.createAddress.mockRejectedValue(new Error('rpc down'));
|
||||
moneroWalletRpcClient.createAddress.mockRejectedValue(new Error('rpc down'));
|
||||
|
||||
await expect(
|
||||
service.issueInvoice({
|
||||
@@ -200,7 +217,7 @@ describe('InvoiceService', () => {
|
||||
)
|
||||
);
|
||||
|
||||
walletRpcClient.createAddress.mockResolvedValue({
|
||||
moneroWalletRpcClient.createAddress.mockResolvedValue({
|
||||
address: '4ShippingPaymentAddressExample',
|
||||
address_index: 3
|
||||
});
|
||||
@@ -212,6 +229,76 @@ describe('InvoiceService', () => {
|
||||
amountFiat: 5
|
||||
});
|
||||
|
||||
expect(walletRpcClient.createAddress).toHaveBeenCalledWith('order-shipping - order-1');
|
||||
expect(moneroWalletRpcClient.createAddress).toHaveBeenCalledWith('order-shipping - order-1');
|
||||
});
|
||||
|
||||
it('throws when the live BTC rate is unavailable for checkout invoices', async () => {
|
||||
exchangeRateService.getLiveFiatPerCrypto.mockImplementation(
|
||||
(method: PaymentMethod) => (method === PaymentMethod.Btc ? null : 150)
|
||||
);
|
||||
|
||||
await expect(issueCheckoutInvoice(PaymentMethod.Btc)).rejects.toThrow(
|
||||
new ServiceUnavailableException("We can't show a price right now. Please try again in a few minutes.")
|
||||
);
|
||||
|
||||
expect(bitcoinWalletRpcClient.createAddress).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws and logs when Bitcoin address allocation fails for checkout invoices', async () => {
|
||||
exchangeRateService.getLiveFiatPerCrypto.mockImplementation(
|
||||
(method: PaymentMethod) => (method === PaymentMethod.Btc ? 60_000 : 150)
|
||||
);
|
||||
bitcoinWalletRpcClient.createAddress.mockRejectedValue(new Error('rpc down'));
|
||||
|
||||
await expect(issueCheckoutInvoice(PaymentMethod.Btc)).rejects.toThrow(
|
||||
new ServiceUnavailableException("We can't take payments right now. Please try again in a few minutes.")
|
||||
);
|
||||
|
||||
expect(errorLogSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to allocate Bitcoin payment address'));
|
||||
expect(invoiceRepo.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates a checkout invoice with converted totals and bitcoin details', async () => {
|
||||
exchangeRateService.getLiveFiatPerCrypto.mockImplementation(
|
||||
(method: PaymentMethod) => (method === PaymentMethod.Btc ? 60_000 : 150)
|
||||
);
|
||||
|
||||
const invoice = await issueCheckoutInvoice(PaymentMethod.Btc);
|
||||
|
||||
expect(bitcoinWalletRpcClient.createAddress).toHaveBeenCalledWith('checkout - session-uuid');
|
||||
expect(invoiceRepo.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
reason: InvoiceReason.Checkout,
|
||||
paymentMethod: PaymentMethod.Btc,
|
||||
amountFiat: 15,
|
||||
fiatCurrency: 'USD',
|
||||
paymentAddress: 'bc1qtestpaymentaddress',
|
||||
expectedTotalAtomic: '25000',
|
||||
expiresAt: expect.any(Date),
|
||||
btcDetails: {
|
||||
fiatPerBtcAtCreation: 60_000,
|
||||
requiredConfirmations: 1
|
||||
}
|
||||
})
|
||||
);
|
||||
expect(invoiceRepo.save).toHaveBeenCalled();
|
||||
expect(invoice).toEqual(expect.objectContaining({ id: 'invoice-1', amountFiat: 15 }));
|
||||
});
|
||||
|
||||
it('uses BTC-specific shipping rate messages', async () => {
|
||||
exchangeRateService.getLiveFiatPerCrypto.mockReturnValue(null);
|
||||
|
||||
await expect(
|
||||
service.issueInvoice({
|
||||
paymentMethod: PaymentMethod.Btc,
|
||||
reason: InvoiceReason.Shipping,
|
||||
contextId: 'order-1',
|
||||
amountFiat: 5
|
||||
})
|
||||
).rejects.toThrow(
|
||||
new ServiceUnavailableException(
|
||||
"We can't quote shipping in BTC right now. Please try again in a few minutes."
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user