Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
c99b62b
refactor: rename VmCostPlanIntervalType to IntervalType
v0l Mar 2, 2026
b7fdcdf
feat: schema migration + DB layer for VM subscription link (increment 1)
v0l Mar 2, 2026
7f66382
feat: data migration tool + fix subscription interval SQL (increment 2)
v0l Mar 2, 2026
1634833
feat: migrate VM payments to subscription system (increments 3+4)
v0l Mar 2, 2026
f784ba3
refactor: link vm to subscription_line_item instead of subscription
v0l Mar 2, 2026
d6eb9da
chore: update agents-common submodule
v0l Mar 2, 2026
913298d
fix: derive company_id from template/pricing region in provision()
v0l Mar 2, 2026
e9e0d6f
feat: add is_setup flag to subscription; always compute VM renewal co…
v0l Mar 2, 2026
129e893
feat: pass intervals through renew_subscription
v0l Mar 2, 2026
222eb54
feat: add paginated list_vm_subscription_payments and use in API endp…
v0l Mar 3, 2026
f5cca92
feat: migrate reporting queries from vm_payment to subscription_payment
v0l Mar 3, 2026
3dececd
test: add unit tests for subscription_payment_paid, invoice handler, …
v0l Mar 3, 2026
098ad8d
docs: update changelog and migration notes for vm_payment → subscript…
v0l Mar 3, 2026
27bfcab
fix: add referral FK constraint migration and migration binary improv…
v0l Mar 3, 2026
d016b45
feat: extend migration to include deleted VMs and backfill all vm_pay…
v0l Mar 3, 2026
b1e34fc
fix: copy external_data raw in migration, avoiding encrypt/decrypt ro…
v0l Mar 3, 2026
ee197e3
fix: correct VM renewal payment creation and expiry extension
v0l Mar 3, 2026
2f186f6
fix: add list_pending_vm_subscription_payments and fix expired invoic…
v0l Mar 3, 2026
e3fcf85
fix: remove referral FK rename migration that fails on clean DB
v0l Mar 3, 2026
42f93cd
fix: update subscription line item cost after VM upgrade
v0l Mar 3, 2026
ae0fc20
fix: do not mark subscriptions active for deleted VMs during migration
v0l Mar 3, 2026
2a92da6
fix: return subscriptions in descending order in admin API
v0l Mar 3, 2026
8c3ce03
feat: include subscription details in admin VM info response
v0l Mar 3, 2026
21daf6b
feat: include company_base_currency in admin subscription payment res…
v0l Mar 3, 2026
3c66a1c
docs: update API_CHANGELOG with changes from this session
v0l Mar 3, 2026
7b1134b
fix: return subscription payments in descending order
v0l Mar 3, 2026
867c823
docs: require database-level pagination for all list APIs
v0l Mar 3, 2026
d2e29db
refactor: use database-level pagination for all list APIs
v0l Mar 3, 2026
0c2740a
plan: add subscription lifecycle + generic payment pipeline increment…
v0l Mar 3, 2026
5f25555
feat: generic subscription lifecycle + payment pipeline (increments 1…
v0l Mar 3, 2026
f956ad6
test: lifecycle DB tests for expiry queries and deactivate_subscripti…
v0l Mar 3, 2026
846ae74
feat: PaymentCompletionHandler trait + centralised complete_payment p…
v0l Mar 3, 2026
0caa203
feat: Stripe payment handler + non-VM pipeline tests (increments 18-19)
v0l Mar 3, 2026
c4a42b3
refactor: per-line-item payment completion dispatch
v0l Mar 3, 2026
da38d7d
refactor: SubscriptionLineItemHandler trait unifies payment + lifecycle
v0l Mar 3, 2026
be6bb03
feat: remove vm.expires and vm.auto_renewal_enabled — single source o…
v0l Mar 3, 2026
f243bf1
Add workflow_dispatch with docker label arg
v0l Mar 6, 2026
f104ada
refactor: introduce SubscriptionHandler and rename provisioner types
v0l Mar 10, 2026
115061d
ci: add run-e2e.sh script with per-run DB isolation and non-conflicti…
v0l Mar 10, 2026
67d9ee8
fix: SubscriptionHandler::new returns Result instead of panicking on …
v0l Mar 10, 2026
f135365
fix: correct subscription_type filter in VM lookup queries (IN (3,4) …
v0l Mar 10, 2026
faa10f9
feat: add count_vm_subscription_payments DB method and use it for pag…
v0l Mar 10, 2026
0e1457d
fix: from_subscription_payment returns Result to propagate JSON parse…
v0l Mar 10, 2026
024a777
fix: always send expiry notification when NWC auto-renewal is not active
v0l Mar 10, 2026
ed10a02
test: add VM/subscription lifecycle unit tests (payment activation, e…
v0l Mar 10, 2026
2f11786
ci: fix build.yml to trigger on push/PR events and update build-and-t…
v0l Mar 10, 2026
1ca231b
chore: dba review
v0l Mar 10, 2026
96289fe
fix: increase MariaDB readiness timeout from 30s to 90s in e2e script
v0l Mar 10, 2026
c234bde
chore: increase e2e timeout
v0l Mar 10, 2026
125cb79
chore: increase e2e timeout
v0l Mar 10, 2026
51c2c59
docs: audit and update API docs; add e2e lifecycle tests and LND/work…
v0l Mar 10, 2026
7ebf7bf
Merge branch 'master' of https://github.com/LNVPS/api into feat/vm-pa…
v0l Mar 10, 2026
9633726
feat: implement creating VM state for first-provision UX
v0l Mar 19, 2026
5b03633
fix: add SO_REUSEADDR to API servers and improve cleanup for e2e tests
v0l Mar 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/e2e/admin-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
db: "mysql://root:root@localhost:3376/lnvps"
db: "mysql://root:root@localhost:3377/lnvps"
redis:
url: "redis://localhost:6398"
url: "redis://localhost:6399"
ttl: 30
encryption:
key-file: "/tmp/e2e-encryption.key"
Expand Down
4 changes: 2 additions & 2 deletions .github/e2e/api-config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
db: "mysql://root:root@localhost:3376/lnvps"
db: "mysql://root:root@localhost:3377/lnvps"
lightning:
lnd:
url: "https://localhost:10009"
Expand All @@ -8,7 +8,7 @@ delete-after: 3
public-url: "http://localhost:8000"
read-only: true
redis:
url: "redis://localhost:6398"
url: "redis://localhost:6399"
ttl: 30
nostr:
relays:
Expand Down
112 changes: 90 additions & 22 deletions .github/e2e/wait-for-lnd.sh
Original file line number Diff line number Diff line change
@@ -1,39 +1,107 @@
#!/usr/bin/env bash
set -euo pipefail

# Wait for LND to be fully ready and copy credentials to a known path.
# Wait for both LND nodes to be fully ready, fund them, open a channel from
# lnd-payer → lnd, and copy the lnd-payer credentials to a known host path.
#
# Usage: ./wait-for-lnd.sh [timeout_seconds]

TIMEOUT=${1:-120}
LND_CONTAINER=$(docker compose -f docker-compose.e2e.yaml ps -q lnd)
PAYER_CONTAINER=$(docker compose -f docker-compose.e2e.yaml ps -q lnd-payer)
BITCOIND_CONTAINER=$(docker compose -f docker-compose.e2e.yaml ps -q bitcoind)

echo "Waiting for LND to be ready (timeout: ${TIMEOUT}s)..."
BTC_CLI() {
docker exec "$BITCOIND_CONTAINER" bitcoin-cli -regtest \
-rpcuser=polaruser -rpcpassword=polarpass "$@"
}
LND_CLI() {
docker exec "$LND_CONTAINER" lncli --network=regtest "$@"
}
PAYER_CLI() {
docker exec "$PAYER_CONTAINER" lncli --network=regtest "$@"
}

for i in $(seq 1 "$TIMEOUT"); do
if docker exec "$LND_CONTAINER" lncli --network=regtest getinfo >/dev/null 2>&1; then
echo "LND is ready after ${i}s"
wait_for_node() {
local name="$1"
local cli_fn="$2"
echo "Waiting for ${name} to be ready (timeout: ${TIMEOUT}s)..."
for i in $(seq 1 "$TIMEOUT"); do
if $cli_fn getinfo >/dev/null 2>&1; then
echo "${name} is ready after ${i}s"
return 0
fi
sleep 1
done
echo "ERROR: ${name} did not become ready within ${TIMEOUT}s"
return 1
}

# Copy TLS cert and macaroon to host
mkdir -p /tmp/e2e-lnd/data/chain/bitcoin/regtest
docker cp "$LND_CONTAINER":/root/.lnd/tls.cert /tmp/e2e-lnd/tls.cert
docker cp "$LND_CONTAINER":/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon \
/tmp/e2e-lnd/data/chain/bitcoin/regtest/admin.macaroon
# Wait for both nodes
wait_for_node "lnd" LND_CLI
wait_for_node "lnd-payer" PAYER_CLI

echo "LND credentials copied to /tmp/e2e-lnd/"
# Copy lnd credentials to host (used by the API server)
mkdir -p /tmp/e2e-lnd/data/chain/bitcoin/regtest
docker cp "$LND_CONTAINER":/root/.lnd/tls.cert \
/tmp/e2e-lnd/tls.cert
docker cp "$LND_CONTAINER":/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon \
/tmp/e2e-lnd/data/chain/bitcoin/regtest/admin.macaroon
echo "lnd credentials copied to /tmp/e2e-lnd/"

# Generate a wallet address and mine initial blocks so LND has funds
ADDR=$(docker exec "$LND_CONTAINER" lncli --network=regtest newaddress p2wkh | jq -r .address)
BITCOIND_CONTAINER=$(docker compose -f docker-compose.e2e.yaml ps -q bitcoind)
docker exec "$BITCOIND_CONTAINER" bitcoin-cli -regtest \
-rpcuser=polaruser -rpcpassword=polarpass \
generatetoaddress 101 "$ADDR" >/dev/null
# Copy lnd-payer credentials to host (used by E2E tests to pay invoices)
mkdir -p /tmp/e2e-lnd-payer/data/chain/bitcoin/regtest
docker cp "$PAYER_CONTAINER":/root/.lnd/tls.cert \
/tmp/e2e-lnd-payer/tls.cert
docker cp "$PAYER_CONTAINER":/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon \
/tmp/e2e-lnd-payer/data/chain/bitcoin/regtest/admin.macaroon
echo "lnd-payer credentials copied to /tmp/e2e-lnd-payer/"

echo "Mined 101 blocks to LND address ${ADDR}"
exit 0
# Fund both nodes' on-chain wallets (101 blocks each to activate segwit)
LND_ADDR=$(LND_CLI newaddress p2wkh | jq -r .address)
PAYER_ADDR=$(PAYER_CLI newaddress p2wkh | jq -r .address)
BTC_CLI generatetoaddress 101 "$LND_ADDR" >/dev/null
BTC_CLI generatetoaddress 101 "$PAYER_ADDR" >/dev/null
echo "Funded lnd ($LND_ADDR) and lnd-payer ($PAYER_ADDR) with 101 blocks each"

# Connect lnd-payer to lnd as a peer.
# lnd listens on port 9735 inside the compose network (service hostname "lnd").
# Retry for up to 30 s because the wallet can still be initialising after
# getinfo returns successfully.
LND_PUBKEY=$(LND_CLI getinfo | jq -r .identity_pubkey)
echo "Connecting lnd-payer to lnd (pubkey: ${LND_PUBKEY})..."
for i in $(seq 1 30); do
if PAYER_CLI connect "${LND_PUBKEY}@lnd:9735" 2>/dev/null; then
echo "lnd-payer connected to lnd after ${i}s"
break
fi
if [[ "$i" -eq 30 ]]; then
echo "ERROR: could not connect lnd-payer to lnd within 30s"
exit 1
fi
sleep 1
done

echo "ERROR: LND did not become ready within ${TIMEOUT}s"
docker compose -f docker-compose.e2e.yaml logs lnd | tail -30
exit 1
# Open a 10M sat channel from lnd-payer → lnd
PAYER_CLI openchannel --node_key "$LND_PUBKEY" --local_amt 10000000
echo "Channel open request submitted (10M sats)"

# Mine 6 blocks so the channel is confirmed and active
BTC_CLI generatetoaddress 6 "$LND_ADDR" >/dev/null
echo "Mined 6 confirmation blocks"

# Wait until the channel is active on the payer side
echo "Waiting for channel to become active..."
for i in $(seq 1 60); do
ACTIVE=$(PAYER_CLI listchannels | jq '[.channels[] | select(.active == true)] | length')
if [[ "$ACTIVE" -ge 1 ]]; then
echo "Channel is active after ${i}s"
break
fi
if [[ "$i" -eq 60 ]]; then
echo "ERROR: channel did not become active within 60s"
PAYER_CLI listchannels >&2
exit 1
fi
sleep 1
done
28 changes: 28 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ on:
branches:
- master
pull_request:
workflow_dispatch:
inputs:
docker:
description: 'Docker image to build'
required: false
default: 'all'
type: choice
options:
- all
- lnvps-api
- lnvps-api-admin
- lnvps-operator
- lnvps-nostr
- lnvps-host-info

env:
REGISTRY: registry.v0l.io
Expand All @@ -14,6 +28,10 @@ jobs:
# Build host-info as a multi-arch image first
build-host-info:
runs-on: ubuntu-latest
if: >
github.event_name == 'push' ||
github.event_name == 'pull_request' ||
(github.event_name == 'workflow_dispatch' && (github.event.inputs.docker == 'lnvps-host-info' || github.event.inputs.docker == 'all'))
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down Expand Up @@ -48,6 +66,12 @@ jobs:
build:
runs-on: ubuntu-latest
needs: build-host-info
if: >
always() && (
github.event_name == 'push' ||
github.event_name == 'pull_request' ||
github.event_name == 'workflow_dispatch'
)
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -81,6 +105,10 @@ jobs:
password: ${{ secrets.REGISTRY_TOKEN }}

- name: Build and push ${{ matrix.name }}
if: >
github.event_name == 'push' ||
github.event_name == 'pull_request' ||
(github.event_name == 'workflow_dispatch' && (github.event.inputs.docker == matrix.name || github.event.inputs.docker == 'all'))
uses: docker/build-push-action@v5
with:
context: .
Expand Down
64 changes: 9 additions & 55 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ env:
jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 45

steps:
- name: Checkout code
Expand All @@ -34,63 +34,17 @@ jobs:
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler jq

- name: Start infrastructure (DB, Redis, bitcoind, LND)
run: docker compose -f docker-compose.e2e.yaml up -d

- name: Wait for LND and copy credentials
run: .github/e2e/wait-for-lnd.sh 120

- name: Build API servers
run: |
cargo build -p lnvps_api -p lnvps_api_admin

- name: Start user API
run: |
cargo run -p lnvps_api -- --config .github/e2e/api-config.yaml &
echo $! > /tmp/api.pid
# Wait for user API to be ready
for i in $(seq 1 60); do
if curl -sf http://localhost:8000/ >/dev/null 2>&1; then
echo "User API ready after ${i}s"
break
fi
if [ "$i" -eq 60 ]; then
echo "User API failed to start"
exit 1
fi
sleep 1
done

- name: Start admin API
run: |
cargo run -p lnvps_api_admin --bin lnvps_api_admin -- --config .github/e2e/admin-config.yaml &
echo $! > /tmp/admin-api.pid
for i in $(seq 1 60); do
if curl -sf http://localhost:8001/ >/dev/null 2>&1; then
echo "Admin API ready after ${i}s"
break
fi
if [ "$i" -eq 60 ]; then
echo "Admin API failed to start"
exit 1
fi
sleep 1
done

- name: Run E2E tests
run: cargo test -p lnvps_e2e -- --test-threads=1
env:
LNVPS_E2E_RUN_ID: ${{ github.run_id }}_${{ github.run_attempt }}
run: ./scripts/run-e2e.sh

- name: Dump server logs on failure
if: failure()
run: |
echo "=== User API log ==="
cat /tmp/lnvps-e2e-api.log 2>/dev/null || true
echo "=== Admin API log ==="
cat /tmp/lnvps-e2e-admin-api.log 2>/dev/null || true
echo "=== Docker compose logs ==="
docker compose -f docker-compose.e2e.yaml logs --tail=50
echo "=== User API process ==="
cat /tmp/api.pid 2>/dev/null || true

- name: Cleanup
if: always()
run: |
kill "$(cat /tmp/api.pid 2>/dev/null)" 2>/dev/null || true
kill "$(cat /tmp/admin-api.pid 2>/dev/null)" 2>/dev/null || true
docker compose -f docker-compose.e2e.yaml down -v
docker compose -f docker-compose.e2e.yaml logs --tail=50 2>/dev/null || true
Loading
Loading