Compare commits
3
Commits
6c894d58ba
...
2d8a508898
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d8a508898 | ||
|
|
de52534271 | ||
|
|
626f2ea4f0 |
@@ -53,6 +53,7 @@ VALIDATION_DIGITAL_STOCK_ATTACHMENTS_MAX=5
|
||||
VALIDATION_SHIPPING_NOTE_MIN_LENGTH=20
|
||||
VALIDATION_SHIPPING_NOTE_MAX_LENGTH=4000
|
||||
VALIDATION_ORDER_MESSAGE_MAX_LENGTH=2000
|
||||
VALIDATION_BITCOIN_WITHDRAW_MAX_FEE_RATE_SAT_VBYTE=500
|
||||
|
||||
SIGNED_COOKIE_JWT_SECRET=change-me-in-production
|
||||
|
||||
@@ -146,4 +147,5 @@ VITE_VALIDATION_DIGITAL_STOCK_ATTACHMENTS_MAX=5
|
||||
VITE_VALIDATION_SHIPPING_NOTE_MIN_LENGTH=20
|
||||
VITE_VALIDATION_SHIPPING_NOTE_MAX_LENGTH=4000
|
||||
VITE_VALIDATION_ORDER_MESSAGE_MAX_LENGTH=2000
|
||||
VITE_VALIDATION_BITCOIN_WITHDRAW_MAX_FEE_RATE_SAT_VBYTE=500
|
||||
VITE_ORDERS_DETAIL_POLL_INTERVAL_MS=5000
|
||||
|
||||
@@ -103,7 +103,8 @@ export const getAppConfig = (): AppConfig => {
|
||||
digitalStockAttachmentsMax: envInt('VALIDATION_DIGITAL_STOCK_ATTACHMENTS_MAX'),
|
||||
shippingNoteMinLength: envInt('VALIDATION_SHIPPING_NOTE_MIN_LENGTH'),
|
||||
shippingNoteMaxLength: envInt('VALIDATION_SHIPPING_NOTE_MAX_LENGTH'),
|
||||
orderMessageMaxLength: envInt('VALIDATION_ORDER_MESSAGE_MAX_LENGTH')
|
||||
orderMessageMaxLength: envInt('VALIDATION_ORDER_MESSAGE_MAX_LENGTH'),
|
||||
bitcoinWithdrawMaxFeeRateSatVbyte: envInt('VALIDATION_BITCOIN_WITHDRAW_MAX_FEE_RATE_SAT_VBYTE')
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -153,6 +153,12 @@ class EnvironmentVariables {
|
||||
@Max(10000)
|
||||
VALIDATION_ORDER_MESSAGE_MAX_LENGTH: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(1000)
|
||||
VALIDATION_BITCOIN_WITHDRAW_MAX_FEE_RATE_SAT_VBYTE: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(4)
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
|
||||
import { getAppConfig } from '../../../config';
|
||||
import { IsBitcoinAddress } from '../../../validation/decorators/isBitcoinAddress';
|
||||
|
||||
const {
|
||||
validation: { bitcoinWithdrawMaxFeeRateSatVbyte }
|
||||
} = getAppConfig();
|
||||
|
||||
export class BitcoinWalletWithdrawDto {
|
||||
@Transform(({ value }: { value: unknown }) => (typeof value === 'string' ? value.trim() : value))
|
||||
@IsString()
|
||||
@@ -11,7 +16,7 @@ export class BitcoinWalletWithdrawDto {
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
@Max(bitcoinWithdrawMaxFeeRateSatVbyte)
|
||||
feeRateSatVbyte: number;
|
||||
|
||||
@IsString()
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface AppConfig {
|
||||
shippingNoteMinLength: number;
|
||||
shippingNoteMaxLength: number;
|
||||
orderMessageMaxLength: number;
|
||||
bitcoinWithdrawMaxFeeRateSatVbyte: number;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<el-alert type="warning" :closable="false" show-icon title="Wallet is syncing. Please wait." />
|
||||
</div>
|
||||
|
||||
<el-card shadow="never">
|
||||
<el-card class="mb-24" shadow="never">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-16">
|
||||
<span>Status</span>
|
||||
@@ -35,29 +35,105 @@
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mb-24" shadow="never">
|
||||
<template #header>
|
||||
<span>Withdraw all</span>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
ref="withdrawFormRef"
|
||||
label-position="top"
|
||||
:model="withdrawForm"
|
||||
:rules="withdrawFormRules"
|
||||
@submit.prevent="onWithdrawSubmit"
|
||||
>
|
||||
<el-form-item label="Destination address" prop="destinationAddress">
|
||||
<el-input
|
||||
v-model="withdrawForm.destinationAddress"
|
||||
class="withdraw-address-input"
|
||||
autocomplete="off"
|
||||
:placeholder="`${walletStatus.network} Bitcoin address`"
|
||||
:disabled="withdrawing"
|
||||
@input="withdrawFormRef?.clearValidate('destinationAddress')"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Fee rate (sat/vbyte)" prop="feeRateSatVbyte">
|
||||
<el-input-number
|
||||
v-model="withdrawForm.feeRateSatVbyte"
|
||||
class="fee-rate-input"
|
||||
:min="1"
|
||||
:max="maxBitcoinWithdrawFeeRateSatVbyte"
|
||||
:step="1"
|
||||
:disabled="withdrawing"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-button type="primary" native-type="submit" :loading="withdrawing">
|
||||
Withdraw all confirmed funds
|
||||
</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<span>Recovery seed</span>
|
||||
</template>
|
||||
|
||||
<p class="m-0 mb-16 secondary-text">
|
||||
This shop is non-custodial. You control the wallet seed. Anyone with the seed can spend all funds.
|
||||
Store it offline and never share it.
|
||||
</p>
|
||||
|
||||
<el-button type="danger" plain :loading="revealingSeed" @click="onRevealSeedClick">
|
||||
Reveal seed
|
||||
</el-button>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="seedDialogVisible" title="Recovery seed" width="560px" destroy-on-close @closed="clearSeed">
|
||||
<el-alert type="error" :closable="false" show-icon title="Store this offline. Do not share it." />
|
||||
|
||||
<el-input v-model="revealedMnemonic" class="mt-16" type="textarea" :rows="4" readonly autocomplete="off" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { onBeforeMount, ref } from 'vue';
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { computed, onBeforeMount, reactive, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { config } from '@/config';
|
||||
import { WalletSyncStatus } from '@/types/wallet/WalletSyncStatus';
|
||||
import { useBitcoinWalletStore } from '@/stores/bitcoinWallet';
|
||||
import { isBitcoinAddress } from '@/utils/bitcoin/isBitcoinAddress';
|
||||
import { resolveAxiosErrorMessage } from '@/utils/resolveAxiosErrorMessage';
|
||||
import { resolveWalletSyncStatusLabel } from '@/utils/wallet/resolveWalletSyncStatusLabel';
|
||||
import { resolveWalletSyncStatusTagType } from '@/utils/wallet/resolveWalletSyncStatusTagType';
|
||||
|
||||
const bitcoinWalletStore = useBitcoinWalletStore();
|
||||
|
||||
const {
|
||||
validation: { bitcoinWithdrawMaxFeeRateSatVbyte: maxBitcoinWithdrawFeeRateSatVbyte }
|
||||
} = config;
|
||||
|
||||
const { status: walletStatus } = storeToRefs(bitcoinWalletStore);
|
||||
|
||||
const { fetchStatus } = bitcoinWalletStore;
|
||||
const { fetchStatus, withdrawAll, revealSeed } = bitcoinWalletStore;
|
||||
|
||||
const loading = ref(true);
|
||||
const loadError = ref(false);
|
||||
const refreshing = ref(false);
|
||||
const withdrawing = ref(false);
|
||||
const revealingSeed = ref(false);
|
||||
const withdrawFormRef = ref<FormInstance>();
|
||||
const withdrawForm = reactive({
|
||||
destinationAddress: '',
|
||||
feeRateSatVbyte: 3
|
||||
});
|
||||
const seedDialogVisible = ref(false);
|
||||
const revealedMnemonic = ref('');
|
||||
|
||||
onBeforeMount(async () => {
|
||||
loading.value = true;
|
||||
@@ -67,6 +143,53 @@ onBeforeMount(async () => {
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const withdrawFormRules = computed<FormRules>(() => ({
|
||||
destinationAddress: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (walletStatus.value?.syncStatus !== WalletSyncStatus.Synced) {
|
||||
callback(new Error('Wait until the wallet finishes syncing before withdrawing'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
callback(new Error('Enter a destination address'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const network = walletStatus.value?.network;
|
||||
|
||||
if (!network || !isBitcoinAddress(value, network)) {
|
||||
callback(new Error(`Enter a valid ${network ?? 'Bitcoin'} address.`));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
feeRateSatVbyte: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (!Number.isInteger(value) || value < 1 || value > maxBitcoinWithdrawFeeRateSatVbyte) {
|
||||
callback(
|
||||
new Error(`Enter a fee rate between 1 and ${maxBitcoinWithdrawFeeRateSatVbyte} sat/vbyte`)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
const loadWalletStatus = async (): Promise<void> => {
|
||||
loadError.value = false;
|
||||
|
||||
@@ -90,4 +213,131 @@ const refreshStatus = async (): Promise<void> => {
|
||||
refreshing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const promptForPassword = async (title: string): Promise<string | null> => {
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt('Enter your CMS password to continue.', title, {
|
||||
confirmButtonText: 'Continue',
|
||||
cancelButtonText: 'Cancel',
|
||||
inputType: 'password',
|
||||
inputValidator: value => (value.trim().length > 0 ? true : 'Password is required')
|
||||
});
|
||||
|
||||
return value.trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const onWithdrawSubmit = async (): Promise<void> => {
|
||||
const formEl = withdrawFormRef.value;
|
||||
|
||||
if (!formEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await formEl.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedAddress = withdrawForm.destinationAddress.trim();
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`Withdraw all confirmed funds at ${withdrawForm.feeRateSatVbyte} sat/vbyte to:\n${trimmedAddress}`,
|
||||
'Confirm withdrawal',
|
||||
{
|
||||
confirmButtonText: 'Continue',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = await promptForPassword('Confirm withdrawal');
|
||||
|
||||
if (!password) {
|
||||
return;
|
||||
}
|
||||
|
||||
withdrawing.value = true;
|
||||
|
||||
try {
|
||||
const result = await withdrawAll({
|
||||
destinationAddress: trimmedAddress,
|
||||
feeRateSatVbyte: withdrawForm.feeRateSatVbyte,
|
||||
password
|
||||
});
|
||||
|
||||
ElMessage.success(`Withdrew ${result.amountBtc} BTC`);
|
||||
|
||||
await ElMessageBox.alert(result.txHash, 'Transaction hash', {
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
|
||||
withdrawFormRef.value?.resetFields();
|
||||
|
||||
await loadWalletStatus();
|
||||
} catch (error) {
|
||||
ElMessage.error({ message: resolveAxiosErrorMessage(error, 'Withdrawal failed'), duration: 5000 });
|
||||
} finally {
|
||||
withdrawing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onRevealSeedClick = async (): Promise<void> => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'The recovery seed grants full control over this shop wallet. Store it offline. Never share it with anyone.',
|
||||
'Reveal recovery seed?',
|
||||
{
|
||||
confirmButtonText: 'I understand',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = await promptForPassword('Reveal recovery seed');
|
||||
|
||||
if (!password) {
|
||||
return;
|
||||
}
|
||||
|
||||
revealingSeed.value = true;
|
||||
|
||||
try {
|
||||
const result = await revealSeed({ password });
|
||||
|
||||
revealedMnemonic.value = result.mnemonic;
|
||||
seedDialogVisible.value = true;
|
||||
} catch (error) {
|
||||
const errorMessage = resolveAxiosErrorMessage(error, 'Could not reveal seed');
|
||||
|
||||
ElMessage.error(errorMessage);
|
||||
} finally {
|
||||
revealingSeed.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const clearSeed = (): void => {
|
||||
revealedMnemonic.value = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.withdraw-address-input {
|
||||
width: 100%;
|
||||
max-width: min(560px, 100%);
|
||||
}
|
||||
|
||||
.fee-rate-input {
|
||||
width: 120px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -263,8 +263,7 @@ const onWithdrawSubmit = async (): Promise<void> => {
|
||||
});
|
||||
}
|
||||
|
||||
withdrawForm.destinationAddress = '';
|
||||
withdrawFormRef.value?.clearValidate();
|
||||
withdrawFormRef.value?.resetFields();
|
||||
|
||||
await loadWalletStatus();
|
||||
} catch (error) {
|
||||
@@ -277,7 +276,7 @@ const onWithdrawSubmit = async (): Promise<void> => {
|
||||
const onRevealSeedClick = async (): Promise<void> => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'The recovery seed grants full control over this shop wallet. Store it offline. Never share it or enter it on untrusted sites.',
|
||||
'The recovery seed grants full control over this shop wallet. Store it offline. Never share it with anyone.',
|
||||
'Reveal recovery seed?',
|
||||
{
|
||||
confirmButtonText: 'I understand',
|
||||
|
||||
@@ -42,7 +42,8 @@ export const config = {
|
||||
digitalStockAttachmentsMax: parseInt(env('VITE_VALIDATION_DIGITAL_STOCK_ATTACHMENTS_MAX'), 10),
|
||||
shippingNoteMinLength: parseInt(env('VITE_VALIDATION_SHIPPING_NOTE_MIN_LENGTH'), 10),
|
||||
shippingNoteMaxLength: parseInt(env('VITE_VALIDATION_SHIPPING_NOTE_MAX_LENGTH'), 10),
|
||||
orderMessageMaxLength: parseInt(env('VITE_VALIDATION_ORDER_MESSAGE_MAX_LENGTH'), 10)
|
||||
orderMessageMaxLength: parseInt(env('VITE_VALIDATION_ORDER_MESSAGE_MAX_LENGTH'), 10),
|
||||
bitcoinWithdrawMaxFeeRateSatVbyte: parseInt(env('VITE_VALIDATION_BITCOIN_WITHDRAW_MAX_FEE_RATE_SAT_VBYTE'), 10)
|
||||
},
|
||||
|
||||
orders: {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { api } from '@/plugins/axios';
|
||||
import type { BitcoinWalletRevealSeedPayload } from '@/types/bitcoinWallet/BitcoinWalletRevealSeedPayload';
|
||||
import type { BitcoinWalletRevealSeedResult } from '@/types/bitcoinWallet/BitcoinWalletRevealSeedResult';
|
||||
import type { BitcoinWalletStatus } from '@/types/bitcoinWallet/BitcoinWalletStatus';
|
||||
import type { BitcoinWalletWithdrawPayload } from '@/types/bitcoinWallet/BitcoinWalletWithdrawPayload';
|
||||
import type { BitcoinWalletWithdrawResult } from '@/types/bitcoinWallet/BitcoinWalletWithdrawResult';
|
||||
|
||||
export const useBitcoinWalletStore = defineStore('bitcoinWallet', () => {
|
||||
const status = ref<BitcoinWalletStatus | null>(null);
|
||||
@@ -14,8 +18,22 @@ export const useBitcoinWalletStore = defineStore('bitcoinWallet', () => {
|
||||
return data;
|
||||
};
|
||||
|
||||
const withdrawAll = async (payload: BitcoinWalletWithdrawPayload): Promise<BitcoinWalletWithdrawResult> => {
|
||||
const { data } = await api.post<BitcoinWalletWithdrawResult>('/bitcoin-wallet/withdraw', payload);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const revealSeed = async (payload: BitcoinWalletRevealSeedPayload): Promise<BitcoinWalletRevealSeedResult> => {
|
||||
const { data } = await api.post<BitcoinWalletRevealSeedResult>('/bitcoin-wallet/reveal-seed', payload);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
return {
|
||||
status,
|
||||
fetchStatus
|
||||
fetchStatus,
|
||||
withdrawAll,
|
||||
revealSeed
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface BitcoinWalletRevealSeedPayload {
|
||||
password: string;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface BitcoinWalletRevealSeedResult {
|
||||
mnemonic: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface BitcoinWalletWithdrawPayload {
|
||||
destinationAddress: string;
|
||||
feeRateSatVbyte: number;
|
||||
password: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface BitcoinWalletWithdrawResult {
|
||||
txHash: string;
|
||||
amountBtc: string;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { BitcoinNetwork } from '@/types/bitcoinWallet/BitcoinNetwork';
|
||||
|
||||
// Soft validation only: prefix/length checks to reject obvious garbage and wrong-network
|
||||
// addresses early. Checksums and spendability are validated by Electrum on payto.
|
||||
const BASE58 = '[1-9A-HJ-NP-Za-km-z]';
|
||||
|
||||
const NETWORK_ADDRESS_PATTERNS: Record<BitcoinNetwork, RegExp[]> = {
|
||||
mainnet: [
|
||||
new RegExp(`^1${BASE58}{25,34}$`),
|
||||
new RegExp(`^3${BASE58}{25,34}$`),
|
||||
/^bc1[a-z0-9]{25,87}$/
|
||||
],
|
||||
testnet4: [
|
||||
new RegExp(`^[mn]${BASE58}{25,34}$`),
|
||||
new RegExp(`^2${BASE58}{25,34}$`),
|
||||
/^(?:tb1|bcrt1)[a-z0-9]{25,87}$/
|
||||
]
|
||||
};
|
||||
|
||||
export const isBitcoinAddress = (value: unknown, network: BitcoinNetwork): boolean => {
|
||||
if (typeof value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
|
||||
return NETWORK_ADDRESS_PATTERNS[network].some(pattern => pattern.test(trimmed));
|
||||
};
|
||||
Vendored
+1
@@ -29,6 +29,7 @@ interface ImportMetaEnv {
|
||||
readonly VITE_VALIDATION_SHIPPING_NOTE_MIN_LENGTH?: string;
|
||||
readonly VITE_VALIDATION_SHIPPING_NOTE_MAX_LENGTH?: string;
|
||||
readonly VITE_VALIDATION_ORDER_MESSAGE_MAX_LENGTH?: string;
|
||||
readonly VITE_VALIDATION_BITCOIN_WITHDRAW_MAX_FEE_RATE_SAT_VBYTE?: string;
|
||||
readonly VITE_SHOP_FIAT_CURRENCY?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ services:
|
||||
VITE_VALIDATION_SHIPPING_NOTE_MIN_LENGTH: ${VITE_VALIDATION_SHIPPING_NOTE_MIN_LENGTH}
|
||||
VITE_VALIDATION_SHIPPING_NOTE_MAX_LENGTH: ${VITE_VALIDATION_SHIPPING_NOTE_MAX_LENGTH}
|
||||
VITE_VALIDATION_ORDER_MESSAGE_MAX_LENGTH: ${VITE_VALIDATION_ORDER_MESSAGE_MAX_LENGTH}
|
||||
VITE_VALIDATION_BITCOIN_WITHDRAW_MAX_FEE_RATE_SAT_VBYTE: ${VITE_VALIDATION_BITCOIN_WITHDRAW_MAX_FEE_RATE_SAT_VBYTE}
|
||||
VITE_ORDERS_DETAIL_POLL_INTERVAL_MS: ${VITE_ORDERS_DETAIL_POLL_INTERVAL_MS}
|
||||
container_name: ${COMPOSE_PROJECT_NAME}_nginx
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -25,6 +25,7 @@ ARG VITE_VALIDATION_DIGITAL_STOCK_ATTACHMENTS_MAX
|
||||
ARG VITE_VALIDATION_SHIPPING_NOTE_MIN_LENGTH
|
||||
ARG VITE_VALIDATION_SHIPPING_NOTE_MAX_LENGTH
|
||||
ARG VITE_VALIDATION_ORDER_MESSAGE_MAX_LENGTH
|
||||
ARG VITE_VALIDATION_BITCOIN_WITHDRAW_MAX_FEE_RATE_SAT_VBYTE
|
||||
ARG VITE_ORDERS_DETAIL_POLL_INTERVAL_MS
|
||||
|
||||
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL \
|
||||
@@ -45,6 +46,7 @@ ENV VITE_API_BASE_URL=$VITE_API_BASE_URL \
|
||||
VITE_VALIDATION_SHIPPING_NOTE_MIN_LENGTH=$VITE_VALIDATION_SHIPPING_NOTE_MIN_LENGTH \
|
||||
VITE_VALIDATION_SHIPPING_NOTE_MAX_LENGTH=$VITE_VALIDATION_SHIPPING_NOTE_MAX_LENGTH \
|
||||
VITE_VALIDATION_ORDER_MESSAGE_MAX_LENGTH=$VITE_VALIDATION_ORDER_MESSAGE_MAX_LENGTH \
|
||||
VITE_VALIDATION_BITCOIN_WITHDRAW_MAX_FEE_RATE_SAT_VBYTE=$VITE_VALIDATION_BITCOIN_WITHDRAW_MAX_FEE_RATE_SAT_VBYTE \
|
||||
VITE_ORDERS_DETAIL_POLL_INTERVAL_MS=$VITE_ORDERS_DETAIL_POLL_INTERVAL_MS
|
||||
|
||||
RUN npm run build
|
||||
|
||||
Reference in New Issue
Block a user