add bitcoin shop config and payment method enablement
This commit is contained in:
+6
-2
@@ -87,6 +87,8 @@ COINPAPRIKA_RATE_FETCH_TIMEOUT_MS=5000
|
||||
|
||||
BASE64_ENCRYPTION_KEY="nyRya1KpYSQ+drpO132mkOEMUR+uq6K7tWvpMfppIME=" # Generate with: openssl rand -base64 32
|
||||
|
||||
PAYMENT_METHODS_ENABLED=xmr,btc
|
||||
|
||||
MONERO_CONFIRMATION_TIERS='[{"upToTotalFiat":"30","minConfirmations":0},{"upToTotalFiat":"100","minConfirmations":3},{"upToTotalFiat":"300","minConfirmations":5},{"minConfirmations":10}]'
|
||||
MONERO_VERSION=0.18.3.4
|
||||
MONERO_NETWORK=stagenet
|
||||
@@ -99,6 +101,10 @@ MONERO_WALLET_RPC_TIMEOUT_MS=10000
|
||||
MONERO_WALLET_DIR=./monero-wallet-rpc/wallet
|
||||
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_MIN_INCOMING_ATOMIC=7 # 0.00000007 BTC (~half a USD cent at that moment)
|
||||
|
||||
SIMPLEX_CHAT_VERSION=v6.5.6
|
||||
SIMPLEX_WS_URL=ws://simplex-cli:5225
|
||||
@@ -110,8 +116,6 @@ ORDER_CHECKOUT_STATUS_REFRESH_SEC=15
|
||||
ORDER_SHIPPING_PAYMENT_VALIDITY_MS=259200000 # 72 hours
|
||||
ORDER_DATA_RETENTION_DAYS=30
|
||||
|
||||
MONERO_MIN_INCOMING_ATOMIC=10000000 # 0.00001 XMR
|
||||
|
||||
VITE_API_BASE_URL=http://localhost:3000/api
|
||||
VITE_SHOP_FIAT_CURRENCY=USD
|
||||
VITE_PRODUCT_THUMB_ALLOWED_MIMES=image/jpeg,image/png
|
||||
|
||||
@@ -17,6 +17,7 @@ import { NodeEnv } from '../types/NodeEnv';
|
||||
import { ShopFiatCurrency } from '../types/ShopFiatCurrency';
|
||||
import { PaymentMethod } from '../modules/payment/types/PaymentMethod';
|
||||
import { SimplexConfig } from '../types/SimplexConfig';
|
||||
import { parseEnabledPaymentMethods } from '../utils/payment/parseEnabledPaymentMethods';
|
||||
|
||||
const env = (key: string): string => process.env[key] || '';
|
||||
|
||||
@@ -177,8 +178,12 @@ export const getEncryptionConfig = (): EncryptionConfig => ({
|
||||
export const getShopSettingsConfig = (): ShopSettingsConfig => ({
|
||||
shopName: env('SHOP_NAME'),
|
||||
shopFiatCurrency: env('SHOP_FIAT_CURRENCY') as ShopFiatCurrency,
|
||||
enabledPaymentMethods: parseEnabledPaymentMethods(env('PAYMENT_METHODS_ENABLED')),
|
||||
monero: {
|
||||
confirmationTiers: JSON.parse(env('MONERO_CONFIRMATION_TIERS')) as ConfirmationTier[]
|
||||
},
|
||||
bitcoin: {
|
||||
confirmationTiers: JSON.parse(env('BITCOIN_CONFIRMATION_TIERS')) as ConfirmationTier[]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -192,7 +197,7 @@ export const getOrderConfig = (): OrderConfig => ({
|
||||
export const getInvoiceConfig = (): InvoiceConfig => ({
|
||||
minByMethod: {
|
||||
[PaymentMethod.Xmr]: String(envInt('MONERO_MIN_INCOMING_ATOMIC')),
|
||||
[PaymentMethod.Btc]: String(envInt('BTC_MIN_INCOMING_ATOMIC'))
|
||||
[PaymentMethod.Btc]: String(envInt('BITCOIN_MIN_INCOMING_ATOMIC'))
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { NodeEnv } from '../types/NodeEnv';
|
||||
import { ShopFiatCurrency } from '../types/ShopFiatCurrency';
|
||||
import { IsBase64 } from '../validation/decorators/isBase64';
|
||||
import { IsConfirmationTiers } from '../validation/decorators/isConfirmationTiers';
|
||||
import { IsEnabledPaymentMethods } from '../validation/decorators/isEnabledPaymentMethods';
|
||||
import { MoneroNetwork } from '../types/MoneroNetwork';
|
||||
|
||||
class EnvironmentVariables {
|
||||
@@ -268,6 +269,16 @@ class EnvironmentVariables {
|
||||
@IsConfirmationTiers()
|
||||
MONERO_CONFIRMATION_TIERS: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@IsConfirmationTiers()
|
||||
BITCOIN_CONFIRMATION_TIERS: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@IsEnabledPaymentMethods()
|
||||
PAYMENT_METHODS_ENABLED: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(60000)
|
||||
@@ -294,6 +305,12 @@ class EnvironmentVariables {
|
||||
@Max(Number.MAX_SAFE_INTEGER)
|
||||
MONERO_MIN_INCOMING_ATOMIC: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(Number.MAX_SAFE_INTEGER)
|
||||
BITCOIN_MIN_INCOMING_ATOMIC: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
MONERO_DAEMON_ADDRESS: string;
|
||||
|
||||
@@ -196,7 +196,9 @@ export class ShopSettingsService {
|
||||
}: ShopSettings): ShopSettingsView {
|
||||
const setupChecklist = this.buildSetupChecklist({ logoStorageKey, faviconStorageKey, simplexLink, shippingNote });
|
||||
|
||||
const { shopName, shopFiatCurrency, monero } = this.configService.get('shopSettings') as Config['shopSettings'];
|
||||
const { shopName, shopFiatCurrency, monero, bitcoin } = this.configService.get(
|
||||
'shopSettings'
|
||||
) as Config['shopSettings'];
|
||||
|
||||
const logoUrl = logoStorageKey ? getShopBrandingPublicUrl(logoStorageKey) : null;
|
||||
const faviconUrl = faviconStorageKey ? getShopBrandingPublicUrl(faviconStorageKey) : null;
|
||||
@@ -208,6 +210,7 @@ export class ShopSettingsService {
|
||||
shopName,
|
||||
shopFiatCurrency,
|
||||
monero,
|
||||
bitcoin,
|
||||
logoUrl,
|
||||
faviconUrl,
|
||||
simplexLink,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ConfirmationTier } from '../../../types/ConfirmationTier';
|
||||
|
||||
export interface ShopSettingsBitcoinView {
|
||||
confirmationTiers: ConfirmationTier[];
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { ShopFiatCurrency } from '../../../types/ShopFiatCurrency';
|
||||
import { SetupChecklist } from './SetupChecklist';
|
||||
import { ShopSettingsMoneroView } from './ShopSettingsMoneroView';
|
||||
import { ShopSettingsBitcoinView } from './ShopSettingsBitcoinView';
|
||||
|
||||
export interface ShopSettingsView {
|
||||
id: string | null;
|
||||
shopName: string;
|
||||
shopFiatCurrency: ShopFiatCurrency;
|
||||
monero: ShopSettingsMoneroView;
|
||||
bitcoin: ShopSettingsBitcoinView;
|
||||
logoUrl: string | null;
|
||||
faviconUrl: string | null;
|
||||
simplexLink: string | null;
|
||||
|
||||
@@ -54,9 +54,13 @@ export interface AppConfig {
|
||||
export interface ShopSettingsConfig {
|
||||
shopName: string;
|
||||
shopFiatCurrency: ShopFiatCurrency;
|
||||
enabledPaymentMethods: PaymentMethod[];
|
||||
monero: {
|
||||
confirmationTiers: ConfirmationTier[];
|
||||
};
|
||||
bitcoin: {
|
||||
confirmationTiers: ConfirmationTier[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface JwtConfig {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { PaymentMethod } from '../../modules/payment/types/PaymentMethod';
|
||||
|
||||
export const isPaymentMethodEnabled = (
|
||||
method: PaymentMethod,
|
||||
enabledMethods: readonly PaymentMethod[]
|
||||
): boolean => enabledMethods.includes(method);
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PaymentMethod } from '../../modules/payment/types/PaymentMethod';
|
||||
|
||||
const PAYMENT_METHOD_VALUES = new Set<string>(Object.values(PaymentMethod));
|
||||
|
||||
export const isPaymentMethodValue = (value: string): value is PaymentMethod => PAYMENT_METHOD_VALUES.has(value);
|
||||
@@ -0,0 +1,25 @@
|
||||
import { PaymentMethod } from '../../modules/payment/types/PaymentMethod';
|
||||
import { parseEnabledPaymentMethods } from './parseEnabledPaymentMethods';
|
||||
|
||||
describe('parseEnabledPaymentMethods', () => {
|
||||
it('parses a single enabled method', () => {
|
||||
expect(parseEnabledPaymentMethods('xmr')).toEqual([PaymentMethod.Xmr]);
|
||||
});
|
||||
|
||||
it('parses multiple enabled methods', () => {
|
||||
expect(parseEnabledPaymentMethods('xmr, btc')).toEqual([PaymentMethod.Xmr, PaymentMethod.Btc]);
|
||||
});
|
||||
|
||||
it('returns an empty array for empty input', () => {
|
||||
expect(parseEnabledPaymentMethods('')).toEqual([]);
|
||||
});
|
||||
|
||||
it('filters out unsupported methods', () => {
|
||||
expect(parseEnabledPaymentMethods('eth')).toEqual([]);
|
||||
expect(parseEnabledPaymentMethods('xmr,eth')).toEqual([PaymentMethod.Xmr]);
|
||||
});
|
||||
|
||||
it('filters out duplicate methods', () => {
|
||||
expect(parseEnabledPaymentMethods('xmr,xmr')).toEqual([PaymentMethod.Xmr]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { PaymentMethod } from '../../modules/payment/types/PaymentMethod';
|
||||
import { isPaymentMethodValue } from './isPaymentMethodValue';
|
||||
|
||||
export const parseEnabledPaymentMethods = (raw: string): PaymentMethod[] => {
|
||||
const methods = raw
|
||||
.split(',')
|
||||
.map(value => value.trim())
|
||||
.filter(Boolean)
|
||||
.filter(isPaymentMethodValue);
|
||||
|
||||
return [...new Set(methods)];
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { validateSync } from 'class-validator';
|
||||
import { IsEnabledPaymentMethods } from './isEnabledPaymentMethods';
|
||||
|
||||
class TestDto {
|
||||
@IsEnabledPaymentMethods()
|
||||
PAYMENT_METHODS_ENABLED: string;
|
||||
}
|
||||
|
||||
const validateMethods = (value: string) => {
|
||||
const dto = Object.assign(new TestDto(), { PAYMENT_METHODS_ENABLED: value });
|
||||
|
||||
return validateSync(dto);
|
||||
};
|
||||
|
||||
describe('IsEnabledPaymentMethods', () => {
|
||||
it('accepts xmr only', () => {
|
||||
expect(validateMethods('xmr')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('accepts xmr and btc', () => {
|
||||
expect(validateMethods('xmr,btc')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('rejects empty string', () => {
|
||||
expect(validateMethods('').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('rejects unsupported methods', () => {
|
||||
expect(validateMethods('eth').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('rejects duplicate methods', () => {
|
||||
expect(validateMethods('xmr,xmr').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Validate, ValidatorConstraint, type ValidatorConstraintInterface } from 'class-validator';
|
||||
import { isPaymentMethodValue } from '../../utils/payment/isPaymentMethodValue';
|
||||
|
||||
@ValidatorConstraint({ name: 'isEnabledPaymentMethods' })
|
||||
class IsEnabledPaymentMethodsConstraint implements ValidatorConstraintInterface {
|
||||
validate(value: unknown): boolean {
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tokens = value
|
||||
.split(',')
|
||||
.map(token => token.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (tokens.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (new Set(tokens).size !== tokens.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return tokens.every(isPaymentMethodValue);
|
||||
}
|
||||
|
||||
defaultMessage(): string {
|
||||
return '$property must be a comma-separated list of supported payment methods without duplicates';
|
||||
}
|
||||
}
|
||||
|
||||
export const IsEnabledPaymentMethods = () => Validate(IsEnabledPaymentMethodsConstraint);
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface BitcoinConfirmationTier {
|
||||
upToTotalFiat?: string;
|
||||
minConfirmations: number;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ShopSettingsBitcoin } from './ShopSettingsBitcoin';
|
||||
import type { ShopSettingsMonero } from './ShopSettingsMonero';
|
||||
import type { SetupChecklist } from './SetupChecklist';
|
||||
|
||||
@@ -6,6 +7,7 @@ export interface ShopSettings {
|
||||
shopName: string;
|
||||
shopFiatCurrency: string;
|
||||
monero: ShopSettingsMonero;
|
||||
bitcoin: ShopSettingsBitcoin;
|
||||
logoUrl: string | null;
|
||||
faviconUrl: string | null;
|
||||
simplexLink: string | null;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { BitcoinConfirmationTier } from './BitcoinConfirmationTier';
|
||||
|
||||
export interface ShopSettingsBitcoin {
|
||||
confirmationTiers: BitcoinConfirmationTier[];
|
||||
}
|
||||
Reference in New Issue
Block a user