Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
947dd23dd5 | ||
|
|
11c94c7834 | ||
|
|
a7282a1888 |
+6
-2
@@ -87,6 +87,8 @@ COINPAPRIKA_RATE_FETCH_TIMEOUT_MS=5000
|
|||||||
|
|
||||||
BASE64_ENCRYPTION_KEY="nyRya1KpYSQ+drpO132mkOEMUR+uq6K7tWvpMfppIME=" # Generate with: openssl rand -base64 32
|
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_CONFIRMATION_TIERS='[{"upToTotalFiat":"30","minConfirmations":0},{"upToTotalFiat":"100","minConfirmations":3},{"upToTotalFiat":"300","minConfirmations":5},{"minConfirmations":10}]'
|
||||||
MONERO_VERSION=0.18.3.4
|
MONERO_VERSION=0.18.3.4
|
||||||
MONERO_NETWORK=stagenet
|
MONERO_NETWORK=stagenet
|
||||||
@@ -99,6 +101,10 @@ MONERO_WALLET_RPC_TIMEOUT_MS=10000
|
|||||||
MONERO_WALLET_DIR=./monero-wallet-rpc/wallet
|
MONERO_WALLET_DIR=./monero-wallet-rpc/wallet
|
||||||
MONERO_WALLET_NAME=shop
|
MONERO_WALLET_NAME=shop
|
||||||
MONERO_WALLET_PASSWORD=change-me
|
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_CHAT_VERSION=v6.5.6
|
||||||
SIMPLEX_WS_URL=ws://simplex-cli:5225
|
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_SHIPPING_PAYMENT_VALIDITY_MS=259200000 # 72 hours
|
||||||
ORDER_DATA_RETENTION_DAYS=30
|
ORDER_DATA_RETENTION_DAYS=30
|
||||||
|
|
||||||
MONERO_MIN_INCOMING_ATOMIC=10000000 # 0.00001 XMR
|
|
||||||
|
|
||||||
VITE_API_BASE_URL=http://localhost:3000/api
|
VITE_API_BASE_URL=http://localhost:3000/api
|
||||||
VITE_SHOP_FIAT_CURRENCY=USD
|
VITE_SHOP_FIAT_CURRENCY=USD
|
||||||
VITE_PRODUCT_THUMB_ALLOWED_MIMES=image/jpeg,image/png
|
VITE_PRODUCT_THUMB_ALLOWED_MIMES=image/jpeg,image/png
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ 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';
|
||||||
import { SimplexConfig } from '../types/SimplexConfig';
|
import { SimplexConfig } from '../types/SimplexConfig';
|
||||||
|
import { parseEnabledPaymentMethods } from '../utils/payment/parseEnabledPaymentMethods';
|
||||||
|
|
||||||
const env = (key: string): string => process.env[key] || '';
|
const env = (key: string): string => process.env[key] || '';
|
||||||
|
|
||||||
@@ -177,8 +178,12 @@ export const getEncryptionConfig = (): EncryptionConfig => ({
|
|||||||
export const getShopSettingsConfig = (): ShopSettingsConfig => ({
|
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,
|
||||||
|
enabledPaymentMethods: parseEnabledPaymentMethods(env('PAYMENT_METHODS_ENABLED')),
|
||||||
monero: {
|
monero: {
|
||||||
confirmationTiers: JSON.parse(env('MONERO_CONFIRMATION_TIERS')) as MoneroConfirmationTier[]
|
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 => ({
|
export const getInvoiceConfig = (): InvoiceConfig => ({
|
||||||
minByMethod: {
|
minByMethod: {
|
||||||
[PaymentMethod.Xmr]: String(envInt('MONERO_MIN_INCOMING_ATOMIC')),
|
[PaymentMethod.Xmr]: String(envInt('MONERO_MIN_INCOMING_ATOMIC')),
|
||||||
[PaymentMethod.Btc]: String(envInt('BTC_MIN_INCOMING_ATOMIC'))
|
[PaymentMethod.Btc]: String(envInt('BITCOIN_MIN_INCOMING_ATOMIC'))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ 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 { IsEnabledPaymentMethods } from '../validation/decorators/isEnabledPaymentMethods';
|
||||||
import { MoneroNetwork } from '../types/MoneroNetwork';
|
import { MoneroNetwork } from '../types/MoneroNetwork';
|
||||||
|
|
||||||
class EnvironmentVariables {
|
class EnvironmentVariables {
|
||||||
@@ -265,9 +266,19 @@ class EnvironmentVariables {
|
|||||||
|
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsMoneroConfirmationTiers()
|
@IsConfirmationTiers()
|
||||||
MONERO_CONFIRMATION_TIERS: string;
|
MONERO_CONFIRMATION_TIERS: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@IsConfirmationTiers()
|
||||||
|
BITCOIN_CONFIRMATION_TIERS: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@IsEnabledPaymentMethods()
|
||||||
|
PAYMENT_METHODS_ENABLED: string;
|
||||||
|
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(60000)
|
@Min(60000)
|
||||||
@@ -294,6 +305,12 @@ class EnvironmentVariables {
|
|||||||
@Max(Number.MAX_SAFE_INTEGER)
|
@Max(Number.MAX_SAFE_INTEGER)
|
||||||
MONERO_MIN_INCOMING_ATOMIC: number;
|
MONERO_MIN_INCOMING_ATOMIC: number;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
@Max(Number.MAX_SAFE_INTEGER)
|
||||||
|
BITCOIN_MIN_INCOMING_ATOMIC: number;
|
||||||
|
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
MONERO_DAEMON_ADDRESS: string;
|
MONERO_DAEMON_ADDRESS: string;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddBtcInvoiceDetails1784800000000 implements MigrationInterface {
|
||||||
|
name = 'AddBtcInvoiceDetails1784800000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TYPE "public"."invoices_paymentmethod_enum" ADD VALUE 'btc'`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "invoice_btc_details" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "fiatPerBtcAtCreation" numeric(12,2) NOT NULL, "requiredConfirmations" integer NOT NULL, "invoiceId" uuid, CONSTRAINT "UQ_invoice_btc_details_invoice_id" UNIQUE ("invoiceId"), CONSTRAINT "PK_invoice_btc_details" PRIMARY KEY ("id"))`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "invoice_btc_details" ADD CONSTRAINT "FK_d307f6e0ecc770f47c0bc320d3d" FOREIGN KEY ("invoiceId") REFERENCES "invoices"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "invoice_btc_details" DROP CONSTRAINT "FK_d307f6e0ecc770f47c0bc320d3d"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "invoice_btc_details"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { MoneroWalletModule } from '../moneroWallet/MoneroWalletModule';
|
import { MoneroWalletModule } from '../moneroWallet/MoneroWalletModule';
|
||||||
import { ExchangeRateModule } from '../exchangeRate/ExchangeRateModule';
|
import { ExchangeRateModule } from '../exchangeRate/ExchangeRateModule';
|
||||||
import { Invoice } from './entities/Invoice';
|
import { Invoice } from './entities/Invoice';
|
||||||
|
import { InvoiceBtcDetails } from './entities/InvoiceBtcDetails';
|
||||||
import { InvoiceMoneroDetails } from './entities/InvoiceMoneroDetails';
|
import { InvoiceMoneroDetails } from './entities/InvoiceMoneroDetails';
|
||||||
import { InvoicePayment } from './entities/InvoicePayment';
|
import { InvoicePayment } from './entities/InvoicePayment';
|
||||||
import { InvoicePaymentService } from './services/InvoicePaymentService';
|
import { InvoicePaymentService } from './services/InvoicePaymentService';
|
||||||
@@ -10,7 +11,7 @@ import { InvoiceService } from './services/InvoiceService';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Invoice, InvoicePayment, InvoiceMoneroDetails]),
|
TypeOrmModule.forFeature([Invoice, InvoicePayment, InvoiceMoneroDetails, InvoiceBtcDetails]),
|
||||||
MoneroWalletModule,
|
MoneroWalletModule,
|
||||||
ExchangeRateModule
|
ExchangeRateModule
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ColumnBigIntTransformer } from '../../../utils/ColumnBigIntTransformer'
|
|||||||
import { ColumnNumericTransformer } from '../../../utils/ColumnNumericTransformer';
|
import { ColumnNumericTransformer } from '../../../utils/ColumnNumericTransformer';
|
||||||
import { PaymentMethod } from '../types/PaymentMethod';
|
import { PaymentMethod } from '../types/PaymentMethod';
|
||||||
import { InvoiceReason } from '../types/InvoiceReason';
|
import { InvoiceReason } from '../types/InvoiceReason';
|
||||||
|
import { InvoiceBtcDetails } from './InvoiceBtcDetails';
|
||||||
import { InvoiceMoneroDetails } from './InvoiceMoneroDetails';
|
import { InvoiceMoneroDetails } from './InvoiceMoneroDetails';
|
||||||
import { InvoicePayment } from './InvoicePayment';
|
import { InvoicePayment } from './InvoicePayment';
|
||||||
|
|
||||||
@@ -43,6 +44,9 @@ export class Invoice {
|
|||||||
@OneToOne(() => InvoiceMoneroDetails, moneroDetails => moneroDetails.invoice, { cascade: true })
|
@OneToOne(() => InvoiceMoneroDetails, moneroDetails => moneroDetails.invoice, { cascade: true })
|
||||||
moneroDetails: InvoiceMoneroDetails | null;
|
moneroDetails: InvoiceMoneroDetails | null;
|
||||||
|
|
||||||
|
@OneToOne(() => InvoiceBtcDetails, btcDetails => btcDetails.invoice, { cascade: true })
|
||||||
|
btcDetails: InvoiceBtcDetails | null;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { ColumnNumericTransformer } from '../../../utils/ColumnNumericTransformer';
|
||||||
|
import { Invoice } from './Invoice';
|
||||||
|
|
||||||
|
@Entity('invoice_btc_details')
|
||||||
|
export class InvoiceBtcDetails {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@OneToOne(() => Invoice, invoice => invoice.btcDetails, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn()
|
||||||
|
invoice: Invoice;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'numeric',
|
||||||
|
precision: 12,
|
||||||
|
scale: 2,
|
||||||
|
transformer: new ColumnNumericTransformer()
|
||||||
|
})
|
||||||
|
fiatPerBtcAtCreation: number;
|
||||||
|
|
||||||
|
@Column({ type: 'int' })
|
||||||
|
requiredConfirmations: number;
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -196,7 +196,9 @@ export class ShopSettingsService {
|
|||||||
}: ShopSettings): ShopSettingsView {
|
}: ShopSettings): ShopSettingsView {
|
||||||
const setupChecklist = this.buildSetupChecklist({ logoStorageKey, faviconStorageKey, simplexLink, shippingNote });
|
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 logoUrl = logoStorageKey ? getShopBrandingPublicUrl(logoStorageKey) : null;
|
||||||
const faviconUrl = faviconStorageKey ? getShopBrandingPublicUrl(faviconStorageKey) : null;
|
const faviconUrl = faviconStorageKey ? getShopBrandingPublicUrl(faviconStorageKey) : null;
|
||||||
@@ -208,6 +210,7 @@ export class ShopSettingsService {
|
|||||||
shopName,
|
shopName,
|
||||||
shopFiatCurrency,
|
shopFiatCurrency,
|
||||||
monero,
|
monero,
|
||||||
|
bitcoin,
|
||||||
logoUrl,
|
logoUrl,
|
||||||
faviconUrl,
|
faviconUrl,
|
||||||
simplexLink,
|
simplexLink,
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ConfirmationTier } from '../../../types/ConfirmationTier';
|
||||||
|
|
||||||
|
export interface ShopSettingsBitcoinView {
|
||||||
|
confirmationTiers: ConfirmationTier[];
|
||||||
|
}
|
||||||
@@ -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,12 +1,14 @@
|
|||||||
import { ShopFiatCurrency } from '../../../types/ShopFiatCurrency';
|
import { ShopFiatCurrency } from '../../../types/ShopFiatCurrency';
|
||||||
import { SetupChecklist } from './SetupChecklist';
|
import { SetupChecklist } from './SetupChecklist';
|
||||||
import { ShopSettingsMoneroView } from './ShopSettingsMoneroView';
|
import { ShopSettingsMoneroView } from './ShopSettingsMoneroView';
|
||||||
|
import { ShopSettingsBitcoinView } from './ShopSettingsBitcoinView';
|
||||||
|
|
||||||
export interface ShopSettingsView {
|
export interface ShopSettingsView {
|
||||||
id: string | null;
|
id: string | null;
|
||||||
shopName: string;
|
shopName: string;
|
||||||
shopFiatCurrency: ShopFiatCurrency;
|
shopFiatCurrency: ShopFiatCurrency;
|
||||||
monero: ShopSettingsMoneroView;
|
monero: ShopSettingsMoneroView;
|
||||||
|
bitcoin: ShopSettingsBitcoinView;
|
||||||
logoUrl: string | null;
|
logoUrl: string | null;
|
||||||
faviconUrl: string | null;
|
faviconUrl: string | null;
|
||||||
simplexLink: string | null;
|
simplexLink: string | null;
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -54,8 +54,12 @@ export interface AppConfig {
|
|||||||
export interface ShopSettingsConfig {
|
export interface ShopSettingsConfig {
|
||||||
shopName: string;
|
shopName: string;
|
||||||
shopFiatCurrency: ShopFiatCurrency;
|
shopFiatCurrency: ShopFiatCurrency;
|
||||||
|
enabledPaymentMethods: PaymentMethod[];
|
||||||
monero: {
|
monero: {
|
||||||
confirmationTiers: MoneroConfirmationTier[];
|
confirmationTiers: ConfirmationTier[];
|
||||||
|
};
|
||||||
|
bitcoin: {
|
||||||
|
confirmationTiers: ConfirmationTier[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface MoneroConfirmationTier {
|
export interface ConfirmationTier {
|
||||||
upToTotalFiat?: string;
|
upToTotalFiat?: string;
|
||||||
minConfirmations: number;
|
minConfirmations: number;
|
||||||
}
|
}
|
||||||
+2
-2
@@ -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;
|
||||||
@@ -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)];
|
||||||
|
};
|
||||||
+5
-5
@@ -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);
|
||||||
});
|
});
|
||||||
+10
-10
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { InvoiceBtcDetails } from './InvoiceBtcDetails';
|
||||||
import type { InvoiceMoneroDetails } from './InvoiceMoneroDetails';
|
import type { InvoiceMoneroDetails } from './InvoiceMoneroDetails';
|
||||||
import type { InvoicePayment } from './InvoicePayment';
|
import type { InvoicePayment } from './InvoicePayment';
|
||||||
import type { InvoiceReason } from './InvoiceReason';
|
import type { InvoiceReason } from './InvoiceReason';
|
||||||
@@ -14,5 +15,6 @@ export type Invoice = {
|
|||||||
expectedTotalAtomic: string;
|
expectedTotalAtomic: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
moneroDetails?: InvoiceMoneroDetails | null;
|
moneroDetails?: InvoiceMoneroDetails | null;
|
||||||
|
btcDetails?: InvoiceBtcDetails | null;
|
||||||
payments?: InvoicePayment[];
|
payments?: InvoicePayment[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type InvoiceBtcDetails = {
|
||||||
|
id: string;
|
||||||
|
fiatPerBtcAtCreation: number;
|
||||||
|
requiredConfirmations: number;
|
||||||
|
};
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
export enum PaymentMethod {
|
export enum PaymentMethod {
|
||||||
Xmr = 'xmr'
|
Xmr = 'xmr',
|
||||||
|
Btc = 'btc'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const paymentMethodCryptoCurrency: Record<PaymentMethod, string> = {
|
export const paymentMethodCryptoCurrency: Record<PaymentMethod, string> = {
|
||||||
[PaymentMethod.Xmr]: 'XMR'
|
[PaymentMethod.Xmr]: 'XMR',
|
||||||
|
[PaymentMethod.Btc]: 'BTC'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ShopSettingsBitcoin } from './ShopSettingsBitcoin';
|
||||||
import type { ShopSettingsMonero } from './ShopSettingsMonero';
|
import type { ShopSettingsMonero } from './ShopSettingsMonero';
|
||||||
import type { SetupChecklist } from './SetupChecklist';
|
import type { SetupChecklist } from './SetupChecklist';
|
||||||
|
|
||||||
@@ -6,6 +7,7 @@ export interface ShopSettings {
|
|||||||
shopName: string;
|
shopName: string;
|
||||||
shopFiatCurrency: string;
|
shopFiatCurrency: string;
|
||||||
monero: ShopSettingsMonero;
|
monero: ShopSettingsMonero;
|
||||||
|
bitcoin: ShopSettingsBitcoin;
|
||||||
logoUrl: string | null;
|
logoUrl: string | null;
|
||||||
faviconUrl: string | null;
|
faviconUrl: string | null;
|
||||||
simplexLink: 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