refactor confirmation tiers to shared crypto-agnostic types and validation

This commit is contained in:
2026-09-03 00:28:17 +02:00
parent 5db64ea905
commit a7282a1888
11 changed files with 27 additions and 31 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ import {
} from '../types/Config'; } from '../types/Config';
import { MoneroNetwork } from '../types/MoneroNetwork'; import { MoneroNetwork } from '../types/MoneroNetwork';
import { MoneroWalletConfig } from '../types/MoneroWalletConfig'; import { MoneroWalletConfig } from '../types/MoneroWalletConfig';
import { MoneroConfirmationTier } from '../types/MoneroConfirmationTier'; import { ConfirmationTier } from '../types/ConfirmationTier';
import { NodeEnv } from '../types/NodeEnv'; import { NodeEnv } from '../types/NodeEnv';
import { ShopFiatCurrency } from '../types/ShopFiatCurrency'; import { ShopFiatCurrency } from '../types/ShopFiatCurrency';
import { PaymentMethod } from '../modules/payment/types/PaymentMethod'; import { PaymentMethod } from '../modules/payment/types/PaymentMethod';
@@ -178,7 +178,7 @@ export const getShopSettingsConfig = (): ShopSettingsConfig => ({
shopName: env('SHOP_NAME'), shopName: env('SHOP_NAME'),
shopFiatCurrency: env('SHOP_FIAT_CURRENCY') as ShopFiatCurrency, shopFiatCurrency: env('SHOP_FIAT_CURRENCY') as ShopFiatCurrency,
monero: { monero: {
confirmationTiers: JSON.parse(env('MONERO_CONFIRMATION_TIERS')) as MoneroConfirmationTier[] confirmationTiers: JSON.parse(env('MONERO_CONFIRMATION_TIERS')) as ConfirmationTier[]
} }
}); });
+2 -2
View File
@@ -3,7 +3,7 @@ import { IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsString, Max, Min, validateSy
import { NodeEnv } from '../types/NodeEnv'; import { NodeEnv } from '../types/NodeEnv';
import { ShopFiatCurrency } from '../types/ShopFiatCurrency'; import { ShopFiatCurrency } from '../types/ShopFiatCurrency';
import { IsBase64 } from '../validation/decorators/isBase64'; import { IsBase64 } from '../validation/decorators/isBase64';
import { IsMoneroConfirmationTiers } from '../validation/decorators/isMoneroConfirmationTiers'; import { IsConfirmationTiers } from '../validation/decorators/isConfirmationTiers';
import { MoneroNetwork } from '../types/MoneroNetwork'; import { MoneroNetwork } from '../types/MoneroNetwork';
class EnvironmentVariables { class EnvironmentVariables {
@@ -265,7 +265,7 @@ class EnvironmentVariables {
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
@IsMoneroConfirmationTiers() @IsConfirmationTiers()
MONERO_CONFIRMATION_TIERS: string; MONERO_CONFIRMATION_TIERS: string;
@IsNotEmpty() @IsNotEmpty()
@@ -7,7 +7,7 @@ import type { Config } from '../../../types/Config';
import { getErrorMessage } from '../../../utils/getErrorMessage'; import { getErrorMessage } from '../../../utils/getErrorMessage';
import { convertFiatToXmr } from '../../../utils/monero/convertFiatToXmr'; import { convertFiatToXmr } from '../../../utils/monero/convertFiatToXmr';
import { convertXmrToXmrAtomic } from '../../../utils/monero/convertXmrToXmrAtomic'; import { convertXmrToXmrAtomic } from '../../../utils/monero/convertXmrToXmrAtomic';
import { resolveMinConfirmations } from '../../../utils/monero/resolveMinConfirmations'; import { resolveMinConfirmations } from '../../../utils/confirmation/resolveMinConfirmations';
import { MoneroWalletRpcClient } from '../../moneroWallet/services/MoneroWalletRpcClient'; import { MoneroWalletRpcClient } from '../../moneroWallet/services/MoneroWalletRpcClient';
import { ExchangeRateService } from '../../exchangeRate/services/ExchangeRateService'; import { ExchangeRateService } from '../../exchangeRate/services/ExchangeRateService';
import { Invoice } from '../entities/Invoice'; import { Invoice } from '../entities/Invoice';
@@ -1,5 +1,5 @@
import { MoneroConfirmationTier } from '../../../types/MoneroConfirmationTier'; import { ConfirmationTier } from '../../../types/ConfirmationTier';
export interface ShopSettingsMoneroView { export interface ShopSettingsMoneroView {
confirmationTiers: MoneroConfirmationTier[]; confirmationTiers: ConfirmationTier[];
} }
@@ -1,4 +0,0 @@
export interface BitcoinConfirmationTier {
upToTotalFiat?: string;
minConfirmations: number;
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { MoneroConfirmationTier } from './MoneroConfirmationTier'; import { ConfirmationTier } from './ConfirmationTier';
import { MoneroWalletConfig } from './MoneroWalletConfig'; import { MoneroWalletConfig } from './MoneroWalletConfig';
import { NodeEnv } from './NodeEnv'; import { NodeEnv } from './NodeEnv';
import { PaymentMethod } from '../modules/payment/types/PaymentMethod'; import { PaymentMethod } from '../modules/payment/types/PaymentMethod';
@@ -55,7 +55,7 @@ export interface ShopSettingsConfig {
shopName: string; shopName: string;
shopFiatCurrency: ShopFiatCurrency; shopFiatCurrency: ShopFiatCurrency;
monero: { monero: {
confirmationTiers: MoneroConfirmationTier[]; confirmationTiers: ConfirmationTier[];
}; };
} }
@@ -1,4 +1,4 @@
export interface MoneroConfirmationTier { export interface ConfirmationTier {
upToTotalFiat?: string; upToTotalFiat?: string;
minConfirmations: number; minConfirmations: number;
} }
@@ -1,7 +1,7 @@
import Decimal from 'decimal.js'; import Decimal from 'decimal.js';
import type { MoneroConfirmationTier } from '../../types/MoneroConfirmationTier'; import type { ConfirmationTier } from '../../types/ConfirmationTier';
export const resolveMinConfirmations = (totalFiat: number, tiers: MoneroConfirmationTier[]): number => { export const resolveMinConfirmations = (totalFiat: number, tiers: ConfirmationTier[]): number => {
for (const tier of tiers) { for (const tier of tiers) {
if (tier.upToTotalFiat === undefined) { if (tier.upToTotalFiat === undefined) {
return tier.minConfirmations; return tier.minConfirmations;
@@ -1,13 +1,13 @@
import { validateSync } from 'class-validator'; import { validateSync } from 'class-validator';
import { IsMoneroConfirmationTiers } from './isMoneroConfirmationTiers'; import { IsConfirmationTiers } from './isConfirmationTiers';
class TestDto { class TestDto {
@IsMoneroConfirmationTiers() @IsConfirmationTiers()
MONERO_CONFIRMATION_TIERS: string; CONFIRMATION_TIERS: string;
} }
const validateTiers = (value: string) => { const validateTiers = (value: string) => {
const dto = Object.assign(new TestDto(), { MONERO_CONFIRMATION_TIERS: value }); const dto = Object.assign(new TestDto(), { CONFIRMATION_TIERS: value });
return validateSync(dto); return validateSync(dto);
}; };
@@ -15,7 +15,7 @@ const validateTiers = (value: string) => {
const validTiers = const validTiers =
'[{"upToTotalFiat":"25","minConfirmations":0},{"upToTotalFiat":"250","minConfirmations":5},{"minConfirmations":10}]'; '[{"upToTotalFiat":"25","minConfirmations":0},{"upToTotalFiat":"250","minConfirmations":5},{"minConfirmations":10}]';
describe('IsMoneroConfirmationTiers', () => { describe('IsConfirmationTiers', () => {
it('accepts valid default tiers', () => { it('accepts valid default tiers', () => {
expect(validateTiers(validTiers)).toHaveLength(0); expect(validateTiers(validTiers)).toHaveLength(0);
}); });
@@ -1,5 +1,5 @@
import { Validate, ValidatorConstraint, type ValidatorConstraintInterface } from 'class-validator'; import { Validate, ValidatorConstraint, type ValidatorConstraintInterface } from 'class-validator';
import type { MoneroConfirmationTier } from '../../types/MoneroConfirmationTier'; import type { ConfirmationTier } from '../../types/ConfirmationTier';
const isPositiveDecimalString = (value: string): boolean => { const isPositiveDecimalString = (value: string): boolean => {
const trimmed = value.trim(); const trimmed = value.trim();
@@ -16,12 +16,12 @@ const isPositiveDecimalString = (value: string): boolean => {
const isMinConfirmations = (value: unknown): boolean => const isMinConfirmations = (value: unknown): boolean =>
typeof value === 'number' && Number.isInteger(value) && value >= 0; typeof value === 'number' && Number.isInteger(value) && value >= 0;
const isMoneroConfirmationTier = (value: unknown): value is MoneroConfirmationTier => { const isConfirmationTier = (value: unknown): value is ConfirmationTier => {
if (typeof value !== 'object' || value === null) { if (typeof value !== 'object' || value === null) {
return false; return false;
} }
const tier = value as MoneroConfirmationTier; const tier = value as ConfirmationTier;
if (!isMinConfirmations(tier.minConfirmations)) { if (!isMinConfirmations(tier.minConfirmations)) {
return false; return false;
@@ -34,7 +34,7 @@ const isMoneroConfirmationTier = (value: unknown): value is MoneroConfirmationTi
return typeof tier.upToTotalFiat === 'string' && isPositiveDecimalString(tier.upToTotalFiat); return typeof tier.upToTotalFiat === 'string' && isPositiveDecimalString(tier.upToTotalFiat);
}; };
const isValidMoneroConfirmationTiersJson = (raw: string): boolean => { const isValidConfirmationTiersJson = (raw: string): boolean => {
let parsed: unknown; let parsed: unknown;
try { try {
@@ -43,7 +43,7 @@ const isValidMoneroConfirmationTiersJson = (raw: string): boolean => {
return false; return false;
} }
if (!Array.isArray(parsed) || parsed.length === 0 || !parsed.every(isMoneroConfirmationTier)) { if (!Array.isArray(parsed) || parsed.length === 0 || !parsed.every(isConfirmationTier)) {
return false; return false;
} }
@@ -75,19 +75,19 @@ const isValidMoneroConfirmationTiersJson = (raw: string): boolean => {
return true; return true;
}; };
@ValidatorConstraint({ name: 'isMoneroConfirmationTiers' }) @ValidatorConstraint({ name: 'isConfirmationTiers' })
class IsMoneroConfirmationTiersConstraint implements ValidatorConstraintInterface { class IsConfirmationTiersConstraint implements ValidatorConstraintInterface {
validate(value: unknown): boolean { validate(value: unknown): boolean {
if (typeof value !== 'string' || !value) { if (typeof value !== 'string' || !value) {
return false; return false;
} }
return isValidMoneroConfirmationTiersJson(value); return isValidConfirmationTiersJson(value);
} }
defaultMessage(): string { defaultMessage(): string {
return '$property must be a non-empty JSON array of Monero 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 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';
} }
} }
export const IsMoneroConfirmationTiers = () => Validate(IsMoneroConfirmationTiersConstraint); export const IsConfirmationTiers = () => Validate(IsConfirmationTiersConstraint);