Compare commits

...
2 Commits
Author SHA1 Message Date
nobswebdev cde7fab90c move monero wallet creation into container entrypoint 2026-09-03 23:03:49 +02:00
nobswebdev 9241474d48 add electrum config 2026-09-03 13:30:58 +02:00
13 changed files with 128 additions and 256 deletions
+12
View File
@@ -106,6 +106,18 @@ MONERO_MIN_INCOMING_ATOMIC=10000000 # 0.00001 XMR (~half a USD cent at that mome
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)
ELECTRUM_VERSION=4.8.1
ELECTRUM_NETWORK=testnet
ELECTRUM_SERVER=electrum.blockstream.info:60002:s
ELECTRUM_DAEMON_HOST=electrum-daemon
ELECTRUM_DAEMON_PORT=7777
ELECTRUM_DAEMON_RPC_USER=electrum
ELECTRUM_DAEMON_RPC_PASSWORD=change-me
ELECTRUM_DAEMON_RPC_TIMEOUT_MS=10000
ELECTRUM_WALLET_DIR=./electrum-daemon/wallet
ELECTRUM_WALLET_NAME=shop
ELECTRUM_WALLET_PASSWORD=change-me
SIMPLEX_CHAT_VERSION=v6.5.6
SIMPLEX_WS_URL=ws://simplex-cli:5225
SIMPLEX_BOT_DISPLAY_NAME=NullCartBot
+1 -4
View File
@@ -1,6 +1,6 @@
# NullCart
Self-hosted Monero shop with clearnet (HTTPS) and Tor onion hosting.
Self-hosted crypto shop with clearnet (HTTPS) and Tor onion hosting.
For production deployment, see the [deployment guide](deploy/DEPLOYMENT_GUIDE.md).
@@ -12,9 +12,6 @@ For production deployment, see the [deployment guide](deploy/DEPLOYMENT_GUIDE.md
# Create and edit .env.dev as needed
cp .env.example .env.dev
# Create monero wallet used by shop
./monero-wallet-rpc/setup-monero-wallet.sh --env-file .env.dev
# Build and start docker containers
docker compose --env-file .env.dev -f docker-compose.dev.yml build --no-cache
docker compose --env-file .env.dev -f docker-compose.dev.yml up --force-recreate
+12
View File
@@ -10,6 +10,8 @@ import {
PostgresConfig,
ShopSettingsConfig
} from '../types/Config';
import { ElectrumNetwork } from '../types/ElectrumNetwork';
import { ElectrumWalletConfig } from '../types/ElectrumWalletConfig';
import { MoneroNetwork } from '../types/MoneroNetwork';
import { MoneroWalletConfig } from '../types/MoneroWalletConfig';
import { ConfirmationTier } from '../types/ConfirmationTier';
@@ -210,6 +212,15 @@ export const getMoneroWalletConfig = (): MoneroWalletConfig => ({
rpcTimeoutMs: envInt('MONERO_WALLET_RPC_TIMEOUT_MS')
});
export const getElectrumWalletConfig = (): ElectrumWalletConfig => ({
network: env('ELECTRUM_NETWORK') as ElectrumNetwork,
server: env('ELECTRUM_SERVER'),
rpcUrl: `http://${env('ELECTRUM_DAEMON_HOST')}:${envInt('ELECTRUM_DAEMON_PORT')}`,
username: env('ELECTRUM_DAEMON_RPC_USER'),
password: env('ELECTRUM_DAEMON_RPC_PASSWORD'),
rpcTimeoutMs: envInt('ELECTRUM_DAEMON_RPC_TIMEOUT_MS')
});
export const getSimplexConfig = (): SimplexConfig => ({
wsUrl: env('SIMPLEX_WS_URL'),
botDisplayName: env('SIMPLEX_BOT_DISPLAY_NAME')
@@ -226,5 +237,6 @@ export default () => ({
order: getOrderConfig(),
invoice: getInvoiceConfig(),
moneroWallet: getMoneroWalletConfig(),
electrumWallet: getElectrumWalletConfig(),
simplex: getSimplexConfig()
});
+32
View File
@@ -5,6 +5,7 @@ import { ShopFiatCurrency } from '../types/ShopFiatCurrency';
import { IsBase64 } from '../validation/decorators/isBase64';
import { IsConfirmationTiers } from '../validation/decorators/isConfirmationTiers';
import { IsEnabledPaymentMethods } from '../validation/decorators/isEnabledPaymentMethods';
import { ElectrumNetwork } from '../types/ElectrumNetwork';
import { MoneroNetwork } from '../types/MoneroNetwork';
class EnvironmentVariables {
@@ -342,6 +343,37 @@ class EnvironmentVariables {
@Min(1000)
MONERO_WALLET_RPC_TIMEOUT_MS: number;
@IsNotEmpty()
@IsEnum(ElectrumNetwork)
ELECTRUM_NETWORK: ElectrumNetwork;
@IsNotEmpty()
@IsString()
ELECTRUM_SERVER: string;
@IsNotEmpty()
@IsString()
ELECTRUM_DAEMON_HOST: string;
@IsNotEmpty()
@IsNumber()
@Min(1)
@Max(65535)
ELECTRUM_DAEMON_PORT: number;
@IsNotEmpty()
@IsString()
ELECTRUM_DAEMON_RPC_USER: string;
@IsNotEmpty()
@IsString()
ELECTRUM_DAEMON_RPC_PASSWORD: string;
@IsNotEmpty()
@IsNumber()
@Min(1000)
ELECTRUM_DAEMON_RPC_TIMEOUT_MS: number;
@IsNotEmpty()
@IsString()
SIMPLEX_WS_URL: string;
+2
View File
@@ -1,4 +1,5 @@
import { ConfirmationTier } from './ConfirmationTier';
import { ElectrumWalletConfig } from './ElectrumWalletConfig';
import { MoneroWalletConfig } from './MoneroWalletConfig';
import { NodeEnv } from './NodeEnv';
import { PaymentMethod } from '../modules/payment/types/PaymentMethod';
@@ -122,5 +123,6 @@ export interface Config {
order: OrderConfig;
invoice: InvoiceConfig;
moneroWallet: MoneroWalletConfig;
electrumWallet: ElectrumWalletConfig;
simplex: SimplexConfig;
}
+4
View File
@@ -0,0 +1,4 @@
export enum ElectrumNetwork {
Mainnet = 'mainnet',
Testnet = 'testnet'
}
+10
View File
@@ -0,0 +1,10 @@
import { ElectrumNetwork } from './ElectrumNetwork';
export interface ElectrumWalletConfig {
network: ElectrumNetwork;
server: string;
rpcUrl: string;
username: string;
password: string;
rpcTimeoutMs: number;
}
+7 -13
View File
@@ -74,13 +74,7 @@ Example (default in `.env.example`):
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.
## 5. Create the Monero wallet
```bash
./monero-wallet-rpc/setup-monero-wallet.sh --env-file .env.prod
```
## 6. Bootstrap TLS certificates
## 5. Bootstrap TLS certificates
Nginx needs certificate files before it can start on port 443. For the **first** deploy, create a temporary self-signed pair (replaced after Let's Encrypt):
@@ -88,9 +82,9 @@ Nginx needs certificate files before it can start on port 443. For the **first**
./deploy/scripts/bootstrap-certs.sh
```
After the stack is running, obtain real certificates (step 8).
After the stack is running, obtain real certificates (step 7).
## 7. Start the stack
## 6. Start the stack
```bash
./deploy/scripts/deploy.sh
@@ -102,7 +96,7 @@ Wait until `backend` and `nginx` are healthy:
docker compose --env-file .env.prod -f docker-compose.prod.yml ps
```
## 8. Issue Let's Encrypt certificates
## 7. Issue Let's Encrypt certificates
Remove the temporary bootstrap certificates under `deploy/certs/live/` (Certbot cannot issue into the layout created by `bootstrap-certs.sh`):
@@ -142,20 +136,20 @@ Save and exit the editor. Optional — run once manually to verify:
/root/nullcart/deploy/scripts/renew-certs.sh
```
## 9. Tor onion address
## 8. Tor onion address
```bash
./deploy/scripts/show-onion.sh
```
## 10. Complete shop setup
## 9. Complete shop setup
1. Open the CMS on clearnet or onion (`/cms`).
2. Log in with `CMS_PASSWORD` from `.env.prod`.
3. Finish the setup checklist in settings.
4. Connect SimpleX notifications in shop settings.
## 11. Updates
## 10. Updates
```bash
./deploy/scripts/update.sh
+2 -2
View File
@@ -50,8 +50,8 @@ services:
]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s
retries: 10
start_period: 300s
simplex-cli:
build:
+2 -2
View File
@@ -47,8 +47,8 @@ services:
]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s
retries: 10
start_period: 300s
simplex-cli:
build:
+7 -13
View File
@@ -13,6 +13,7 @@ RUN apt-get update \
&& curl -fsSL "https://downloads.getmonero.org/cli/monero-linux-${monero_arch}-v${MONERO_VERSION}.tar.bz2" \
| tar -xj -C /tmp \
&& install -m 755 "$(find /tmp -type f -name monero-wallet-rpc | head -n 1)" /monero-wallet-rpc \
&& install -m 755 "$(find /tmp -type f -name monero-wallet-cli | head -n 1)" /monero-wallet-cli \
&& rm -rf /var/lib/apt/lists/*
FROM debian:bookworm-slim
@@ -22,17 +23,10 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /monero-wallet-rpc /usr/local/bin/monero-wallet-rpc
COPY --from=build /monero-wallet-cli /usr/local/bin/monero-wallet-cli
CMD ["/bin/sh", "-ec", "\
exec monero-wallet-rpc \
\"--${MONERO_NETWORK}\" \
--daemon-address=\"${MONERO_DAEMON_ADDRESS}\" \
--trusted-daemon \
--no-initial-sync \
--rpc-bind-ip=0.0.0.0 \
--rpc-bind-port=${MONERO_WALLET_RPC_PORT} \
--confirm-external-bind \
--rpc-login=\"${MONERO_WALLET_RPC_USERNAME}:${MONERO_WALLET_RPC_PASSWORD}\" \
--wallet-file=\"/monero/wallet/${MONERO_WALLET_NAME}\" \
--password=\"${MONERO_WALLET_PASSWORD}\" \
"]
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
WALLET_DIR="/monero/wallet"
WALLET_PATH="${WALLET_DIR}/${MONERO_WALLET_NAME}"
NETWORK_FLAG="--${MONERO_NETWORK}"
mkdir -p "${WALLET_DIR}"
chmod 700 "${WALLET_DIR}"
if [[ ! -f "${WALLET_PATH}" && ! -f "${WALLET_PATH}.keys" ]]; then
echo "Creating ${MONERO_NETWORK} wallet at ${WALLET_PATH}..."
monero-wallet-cli "${NETWORK_FLAG}" \
--offline \
--log-file /dev/null \
--generate-new-wallet "${WALLET_PATH}" \
--password "${MONERO_WALLET_PASSWORD}" \
--mnemonic-language English \
--command save
chmod 600 "${WALLET_PATH}" "${WALLET_PATH}.keys" 2>/dev/null || true
echo "Wallet created at ${WALLET_PATH}"
fi
exec monero-wallet-rpc \
"${NETWORK_FLAG}" \
--daemon-address="${MONERO_DAEMON_ADDRESS}" \
--trusted-daemon \
--no-initial-sync \
--rpc-bind-ip=0.0.0.0 \
--rpc-bind-port="${MONERO_WALLET_RPC_PORT}" \
--confirm-external-bind \
--rpc-login="${MONERO_WALLET_RPC_USERNAME}:${MONERO_WALLET_RPC_PASSWORD}" \
--wallet-file="${WALLET_PATH}" \
--password="${MONERO_WALLET_PASSWORD}"
-222
View File
@@ -1,222 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE=""
MONERO_CLI_INSTALL_PATH="/usr/local/bin/monero-wallet-cli"
usage() {
cat <<EOF
Usage: $(basename "$0") --env-file PATH
Installs official monero-wallet-cli system-wide (if missing) and creates the shop wallet
in monero-wallet-rpc/wallet on the host. monero-wallet-rpc mounts the same directory via compose.
Ubuntu/Debian only (uses apt-get for curl, tar, and bzip2 if missing).
Run after filling in .env (especially MONERO_WALLET_PASSWORD), before docker compose up.
Options:
--env-file PATH Env file to load (required)
-h, --help Show this help
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--env-file)
ENV_FILE="$2"
shift 2
;;
-h | --help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$ENV_FILE" ]]; then
echo "--env-file is required." >&2
usage >&2
exit 1
fi
if [[ "$ENV_FILE" != /* ]]; then
ENV_FILE="${ROOT_DIR}/${ENV_FILE#./}"
fi
if [[ ! -f "$ENV_FILE" ]]; then
echo "Env file not found: $ENV_FILE" >&2
exit 1
fi
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
main() {
validate_env
ensure_dependencies
setup_wallet_config
create_wallet_if_missing
}
validate_env() {
local missing=()
for var in MONERO_VERSION MONERO_WALLET_DIR MONERO_WALLET_NAME MONERO_NETWORK MONERO_WALLET_PASSWORD; do
if [[ -z "${!var:-}" ]]; then
missing+=("$var")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo "Missing required env vars in ${ENV_FILE}: ${missing[*]}" >&2
exit 1
fi
case "$MONERO_NETWORK" in
mainnet | stagenet | testnet) ;;
*)
echo "MONERO_NETWORK must be mainnet, stagenet, or testnet (got: ${MONERO_NETWORK})." >&2
exit 1
;;
esac
}
ensure_dependencies() {
if ! command -v apt-get >/dev/null 2>&1; then
echo "apt-get is required. This script supports Ubuntu/Debian only." >&2
exit 1
fi
local missing=()
for cmd in curl tar bzip2; do
if command -v "$cmd" >/dev/null 2>&1; then
log_skip "$cmd"
else
log_install "$cmd"
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
run_privileged apt-get update -qq
run_privileged apt-get install -y -qq "${missing[@]}"
fi
install_monero_wallet_cli_if_missing
}
install_monero_wallet_cli_if_missing() {
local arch archive_url tmp_dir extracted_cli
if command -v monero-wallet-cli >/dev/null 2>&1; then
log_skip "monero-wallet-cli"
return
fi
log_install "monero-wallet-cli"
arch="$(uname -m)"
case "$arch" in
x86_64 | amd64)
archive_url="https://downloads.getmonero.org/cli/monero-linux-x64-v${MONERO_VERSION}.tar.bz2"
;;
aarch64 | arm64)
archive_url="https://downloads.getmonero.org/cli/monero-linux-armv8-v${MONERO_VERSION}.tar.bz2"
;;
*)
echo "Unsupported CPU architecture: ${arch}" >&2
echo "Install monero-wallet-cli from https://www.getmonero.org/downloads/ and re-run." >&2
exit 1
;;
esac
tmp_dir="$(mktemp -d)"
trap "rm -rf '${tmp_dir}'" RETURN
curl -fsSL "$archive_url" | tar -xj -C "$tmp_dir"
extracted_cli="$(find "$tmp_dir" -type f -name monero-wallet-cli | head -n 1)"
if [[ -z "$extracted_cli" ]]; then
echo "Could not find monero-wallet-cli in the downloaded archive." >&2
exit 1
fi
run_privileged install -m 755 "$extracted_cli" "$MONERO_CLI_INSTALL_PATH"
}
setup_wallet_config() {
WALLET_DIR="$(resolve_path "$MONERO_WALLET_DIR")"
WALLET_PATH="${WALLET_DIR}/${MONERO_WALLET_NAME}"
NETWORK="$MONERO_NETWORK"
}
resolve_path() {
local path="$1"
if [[ "$path" != /* ]]; then
path="${ROOT_DIR}/${path#./}"
fi
printf '%s' "$path"
}
create_wallet_if_missing() {
mkdir -p "$WALLET_DIR"
chmod 700 "$WALLET_DIR"
if [[ -f "$WALLET_PATH" || -f "${WALLET_PATH}.keys" ]]; then
echo "Wallet already exists at ${WALLET_PATH} — skipping creation."
return
fi
echo "Creating ${NETWORK} wallet at ${WALLET_PATH}..."
monero-wallet-cli "--${NETWORK}" \
--offline \
--log-file /dev/null \
--generate-new-wallet "$WALLET_PATH" \
--password "$MONERO_WALLET_PASSWORD" \
--mnemonic-language English \
--command save
chmod 600 "${WALLET_PATH}" "${WALLET_PATH}.keys" 2>/dev/null || true
cat <<EOF
Wallet created.
Wallet directory: ${WALLET_DIR}
EOF
}
run_privileged() {
if [[ "${EUID}" -eq 0 ]]; then
"$@"
elif command -v sudo >/dev/null 2>&1; then
sudo "$@"
else
echo "Root or sudo is required to install missing packages." >&2
exit 1
fi
}
log_skip() {
echo "Package ${1} already installed — skipping."
}
log_install() {
echo "Package ${1} is not installed — installing..."
}
main