Require min 1 confirmation in payment tier config. #6
+2
-2
@@ -90,7 +90,7 @@ BASE64_ENCRYPTION_KEY="nyRya1KpYSQ+drpO132mkOEMUR+uq6K7tWvpMfppIME=" # Generate
|
||||
|
||||
PAYMENT_METHODS_ENABLED=xmr,btc
|
||||
|
||||
MONERO_CONFIRMATION_TIERS='[{"upToTotalFiat":"30","minConfirmations":0},{"upToTotalFiat":"100","minConfirmations":3},{"upToTotalFiat":"300","minConfirmations":5},{"minConfirmations":10}]'
|
||||
MONERO_CONFIRMATION_TIERS='[{"upToTotalFiat":"30","minConfirmations":1},{"upToTotalFiat":"100","minConfirmations":3},{"upToTotalFiat":"300","minConfirmations":5},{"minConfirmations":10}]'
|
||||
MONERO_VERSION=0.18.3.4
|
||||
MONERO_NETWORK=stagenet
|
||||
MONERO_DAEMON_ADDRESS=xmr-lux.boldsuck.org:38081
|
||||
@@ -104,7 +104,7 @@ MONERO_WALLET_NAME=shop
|
||||
MONERO_WALLET_PASSWORD=change-me
|
||||
MONERO_MIN_INCOMING_ATOMIC=10000000 # 0.00001 XMR (~half a USD cent at that moment)
|
||||
|
||||
BITCOIN_CONFIRMATION_TIERS='[{"upToTotalFiat":"30","minConfirmations":0},{"upToTotalFiat":"100","minConfirmations":1},{"upToTotalFiat":"300","minConfirmations":3},{"minConfirmations":6}]'
|
||||
BITCOIN_CONFIRMATION_TIERS='[{"upToTotalFiat":"30","minConfirmations":1},{"upToTotalFiat":"100","minConfirmations":3},{"upToTotalFiat":"300","minConfirmations":5},{"minConfirmations":6}]'
|
||||
BITCOIN_MIN_INCOMING_ATOMIC=7 # 0.00000007 BTC (~half a USD cent at that moment)
|
||||
|
||||
ELECTRUM_VERSION=4.8.1
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { resolveMinConfirmations } from './resolveMinConfirmations';
|
||||
|
||||
const tiers = [
|
||||
{ upToTotalFiat: '25', minConfirmations: 0 },
|
||||
{ upToTotalFiat: '25', minConfirmations: 1 },
|
||||
{ upToTotalFiat: '250', minConfirmations: 5 },
|
||||
{ minConfirmations: 10 }
|
||||
] as const;
|
||||
|
||||
describe('resolveMinConfirmations', () => {
|
||||
it('returns 0 for small orders (tx-detected tier)', () => {
|
||||
expect(resolveMinConfirmations(10, [...tiers])).toBe(0);
|
||||
it('returns the first tier for small orders', () => {
|
||||
expect(resolveMinConfirmations(10, [...tiers])).toBe(1);
|
||||
});
|
||||
|
||||
it('returns the middle tier for medium orders', () => {
|
||||
|
||||
@@ -35,14 +35,14 @@ describe('formatInvoicePaymentConfirmationStatus', () => {
|
||||
).toBe('2/10');
|
||||
});
|
||||
|
||||
it('treats zero-confirmation tiers as confirmed', () => {
|
||||
it('returns compact progress at zero confirmations', () => {
|
||||
expect(
|
||||
formatInvoicePaymentConfirmationStatus({
|
||||
confirmations: 0,
|
||||
requiredConfirmations: 0,
|
||||
requiredConfirmations: 3,
|
||||
format: 'compact'
|
||||
})
|
||||
).toBe('Confirmed');
|
||||
).toBe('0/3');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { formatRelativeTimeAgo } from '../formatRelativeTimeAgo';
|
||||
import type { InvoicePaymentConfirmationStatusFormat } from './types/InvoicePaymentConfirmationStatusFormat';
|
||||
|
||||
const formatRequiredConfirmationsLabel = (requiredConfirmations: number): string =>
|
||||
requiredConfirmations === 0 ? '0 (tx-detected)' : String(requiredConfirmations);
|
||||
|
||||
export const formatInvoicePaymentConfirmationStatus = ({
|
||||
confirmations,
|
||||
requiredConfirmations,
|
||||
@@ -29,5 +26,5 @@ export const formatInvoicePaymentConfirmationStatus = ({
|
||||
return `${confirmations} / ${requiredConfirmations} confirmations · detected ${detectedAgo}`;
|
||||
}
|
||||
|
||||
return `${confirmations}/${formatRequiredConfirmationsLabel(requiredConfirmations)}`;
|
||||
return `${confirmations}/${requiredConfirmations}`;
|
||||
};
|
||||
|
||||
@@ -463,19 +463,6 @@ describe('toStorefrontInvoiceView', () => {
|
||||
statusVariant: 'confirmed'
|
||||
});
|
||||
});
|
||||
|
||||
it('treats tx-detected invoices as confirmed for payment status display', async () => {
|
||||
const payment = buildPayment({ confirmations: 0 });
|
||||
const invoice = buildInvoice({
|
||||
moneroDetails: buildMoneroDetails({ requiredConfirmations: 0 }),
|
||||
payments: [payment]
|
||||
});
|
||||
|
||||
const view = await toStorefrontInvoiceView(invoice);
|
||||
|
||||
expect(view.showRefresh).toBe(false);
|
||||
expect(view.payments[0].confirmationStatus).toBe('Confirmed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('overpayment', () => {
|
||||
|
||||
@@ -13,20 +13,13 @@ const validateTiers = (value: string) => {
|
||||
};
|
||||
|
||||
const validTiers =
|
||||
'[{"upToTotalFiat":"25","minConfirmations":0},{"upToTotalFiat":"250","minConfirmations":5},{"minConfirmations":10}]';
|
||||
'[{"upToTotalFiat":"25","minConfirmations":1},{"upToTotalFiat":"250","minConfirmations":5},{"minConfirmations":10}]';
|
||||
|
||||
describe('IsConfirmationTiers', () => {
|
||||
it('accepts valid default tiers', () => {
|
||||
expect(validateTiers(validTiers)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('accepts numeric-only tiers without tx-detected (0)', () => {
|
||||
const tiers =
|
||||
'[{"upToTotalFiat":"25","minConfirmations":1},{"upToTotalFiat":"250","minConfirmations":5},{"minConfirmations":10}]';
|
||||
|
||||
expect(validateTiers(tiers)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('rejects empty string', () => {
|
||||
expect(validateTiers('').length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -39,22 +32,16 @@ describe('IsConfirmationTiers', () => {
|
||||
expect(validateTiers('[]').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('rejects tx-detected (0) more than once', () => {
|
||||
it('rejects minConfirmations: 0', () => {
|
||||
const tiers =
|
||||
'[{"upToTotalFiat":"25","minConfirmations":0},{"upToTotalFiat":"250","minConfirmations":0},{"minConfirmations":10}]';
|
||||
'[{"upToTotalFiat":"25","minConfirmations":0},{"upToTotalFiat":"250","minConfirmations":5},{"minConfirmations":10}]';
|
||||
|
||||
expect(validateTiers(tiers).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('rejects tx-detected (0) on catch-all tier', () => {
|
||||
const tiers = '[{"upToTotalFiat":"25","minConfirmations":1},{"minConfirmations":0}]';
|
||||
|
||||
expect(validateTiers(tiers).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('rejects legacy tx-detected string', () => {
|
||||
it('rejects non-numeric minConfirmations', () => {
|
||||
const tiers =
|
||||
'[{"upToTotalFiat":"25","minConfirmations":"tx-detected"},{"upToTotalFiat":"250","minConfirmations":5},{"minConfirmations":10}]';
|
||||
'[{"upToTotalFiat":"25","minConfirmations":"foo"},{"upToTotalFiat":"250","minConfirmations":5},{"minConfirmations":10}]';
|
||||
|
||||
expect(validateTiers(tiers).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ const isPositiveDecimalString = (value: string): boolean => {
|
||||
};
|
||||
|
||||
const isMinConfirmations = (value: unknown): boolean =>
|
||||
typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
||||
typeof value === 'number' && Number.isInteger(value) && value >= 1;
|
||||
|
||||
const isConfirmationTier = (value: unknown): value is ConfirmationTier => {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
@@ -48,22 +48,12 @@ const isValidConfirmationTiersJson = (raw: string): boolean => {
|
||||
}
|
||||
|
||||
const tiers = parsed;
|
||||
const txDetectedTierCount = tiers.filter(tier => tier.minConfirmations === 0).length;
|
||||
|
||||
if (txDetectedTierCount > 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lastTier = tiers[tiers.length - 1];
|
||||
|
||||
if (lastTier.upToTotalFiat !== undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lastTier.minConfirmations === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let index = 0; index < tiers.length - 1; index++) {
|
||||
const tier = tiers[index];
|
||||
|
||||
@@ -86,7 +76,7 @@ class IsConfirmationTiersConstraint implements ValidatorConstraintInterface {
|
||||
}
|
||||
|
||||
defaultMessage(): string {
|
||||
return '$property must be a non-empty JSON array of confirmation tiers; minConfirmations must be 0 (tx-detected) or an integer >= 1, 0 may appear only once and not on the catch-all tier, non-final tiers need a positive upToTotalFiat in shop fiat currency, and the last tier must be a catch-all without upToTotalFiat';
|
||||
return '$property must be a non-empty JSON array of confirmation tiers; minConfirmations must be an integer >= 1, non-final tiers need a positive upToTotalFiat in shop fiat currency, and the last tier must be a catch-all without upToTotalFiat';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -468,7 +468,7 @@ const submitShippingNote = async (): Promise<void> => {
|
||||
};
|
||||
|
||||
const formatConfirmationTier = (tier: ConfirmationTier, currency: string): string => {
|
||||
const requirement = tier.minConfirmations === 0 ? '0 (tx-detected)' : `${tier.minConfirmations} confirmations`;
|
||||
const requirement = `${tier.minConfirmations} confirmations`;
|
||||
|
||||
if (tier.upToTotalFiat === undefined) {
|
||||
return `Above previous tiers → ${requirement}`;
|
||||
|
||||
@@ -61,20 +61,20 @@ Edit `.env.prod`. Mandatory configuration:
|
||||
| `VITE_API_BASE_URL` | `/api` |
|
||||
| `VITE_SHOP_FIAT_CURRENCY` | same as `SHOP_FIAT_CURRENCY` |
|
||||
|
||||
Optional — adjust per-method payment confirmation rules (`MONERO_CONFIRMATION_TIERS`, `BITCOIN_CONFIRMATION_TIERS`). Each is a JSON array with the same shape. For each order, the shop uses `minConfirmations` from the first tier where the order total (in `SHOP_FIAT_CURRENCY`) is `<= upToTotalFiat`. The last tier is a catch-all and must omit `upToTotalFiat`. At most one tier may use `minConfirmations: 0` (accept unconfirmed / mempool); that tier cannot be the catch-all.
|
||||
Optional — adjust per-method payment confirmation rules (`MONERO_CONFIRMATION_TIERS`, `BITCOIN_CONFIRMATION_TIERS`). Each is a JSON array with the same shape. For each order, the shop uses `minConfirmations` from the first tier where the order total (in `SHOP_FIAT_CURRENCY`) is `<= upToTotalFiat`. The last tier is a catch-all and must omit `upToTotalFiat`. Every tier must use `minConfirmations` >= 1.
|
||||
|
||||
Monero example (default in `.env.example`):
|
||||
|
||||
```json
|
||||
[
|
||||
{ "upToTotalFiat": "30", "minConfirmations": 0 },
|
||||
{ "upToTotalFiat": "30", "minConfirmations": 1 },
|
||||
{ "upToTotalFiat": "100", "minConfirmations": 3 },
|
||||
{ "upToTotalFiat": "300", "minConfirmations": 5 },
|
||||
{ "minConfirmations": 10 }
|
||||
]
|
||||
```
|
||||
|
||||
Orders up to 30 → 0 confirmations; up to 100 → 3; up to 300 → 5; above 300 → 10. Tiers are shown read-only in CMS shop settings.
|
||||
Orders up to 30 → 1 confirmation; up to 100 → 3; up to 300 → 5; above 300 → 10. Tiers are shown read-only in CMS shop settings.
|
||||
|
||||
## 5. Bootstrap TLS certificates
|
||||
|
||||
|
||||
Reference in New Issue
Block a user