docs: update functional identifier refs to spirekeeper
Some checks failed
ci.yml / docs: update functional identifier refs to spirekeeper (push) Failing after 0s

Runbook SQL (spirekeeper.dca_*), ext URL paths, code-location paths,
and the DB-schema name in docs/CLAUDE/README move to the new identity.
Rewrites the placeholder description.md with a real one. Historical
aiolabs/satmachineadmin#N issue/repo links stay pointing at the
original repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-13 22:31:21 +02:00
commit 4ac640a499
6 changed files with 142 additions and 143 deletions

View file

@ -213,7 +213,7 @@ commission_amount = 266800 - 258835 = 7,965 sats (to commission wallet)
### Security Considerations
- **Superuser Authentication**: Admin extension requires LNBits superuser login
- **Wallet Admin Keys**: Client extension uses wallet admin keys for user operations
- **Database Access**: Only superusers can write to satoshimachine database
- **Database Access**: Only superusers can write to spirekeeper database
- SSH tunnel encryption for database connectivity
- Read-only database permissions for Lamassu access
- Input sanitization and type validation
@ -230,7 +230,7 @@ on existing installs:
```sql
SELECT a.id, a.username, a.pubkey, m.id, m.machine_npub
FROM accounts a
JOIN ext_satoshimachine.dca_machines m
JOIN ext_spirekeeper.dca_machines m
ON LOWER(a.pubkey) = LOWER(m.machine_npub);
```

View file

@ -188,7 +188,7 @@ The extension creates several tables:
├── config.json # Extension configuration
├── manifest.json # Extension manifest
├── templates/
│ └── satmachineadmin/
│ └── spirekeeper/
│ └── index.html # Main UI template
└── static/
└── js/

View file

@ -1,10 +1,9 @@
SatMachineAdmin can be used as a template for building new extensions, it includes a bunch of functions that can be edited/deleted as you need them.
spirekeeper is the operator-side administration extension for bitSpire — it manages one or more bitSpire ATMs and runs the Dollar Cost Averaging (DCA) distribution that pays registered clients out of their confirmed deposit balances.
This is a longform description that will be used in the advanced description when users click on the "more" button on the extension cards.
It listens for settlement events published by each ATM over Nostr (kind-21000, NIP-44 v2 encrypted), pays the resulting invoices through LNbits, and splits the proceeds across the principal, operator-fee, and commission legs according to the per-machine and super-config fee settings.
Adding some bullets is nice covering:
- **ATM fleet management** — register and configure each bitSpire by its machine npub; publish fee config and cassette state over Nostr.
- **DCA distribution** — proportional ("flow") allocation across clients based on their remaining deposit balances, with a full per-leg audit trail.
- **Operator administration** — deposit confirmation workflow, client balance tracking, settlement history with drill-down, and CSV export for accounting.
- Functionality
- Use cases
...and some other text about just how great this etension is.
Requires LNbits superuser access for administration. The Lightning ATM side is served by the bitSpire device; this extension is the back office.

View file

@ -7,7 +7,7 @@
## 0 · Why this document exists
Today the satoshimachine code lives at `~/dev/shared/extensions/satmachineadmin` on branch `v2-bitspire`. v2 swapped the legacy Lamassu SSH/PostgreSQL polling model for a Nostrnative one: bitSpire publishes invoices over kind21000 NIP44 v2 events, LNbits pays them, and our extension hooks the resulting `Payment` object.
Today the satoshimachine code lives at `~/dev/shared/extensions/spirekeeper` on branch `v2-bitspire`. v2 swapped the legacy Lamassu SSH/PostgreSQL polling model for a Nostrnative one: bitSpire publishes invoices over kind21000 NIP44 v2 events, LNbits pays them, and our extension hooks the resulting `Payment` object.
The hard truth: the *settlement* itself uses Lightning (so it can't be forged once a preimage lands), but everything *around* the settlement — who the ATM is, what operator it belongs to, what the principal/commission split was, and what fiat was dispensed — currently rides on **mutable, unauthenticated metadata** (`Payment.extra`) plus a **stopgap that has the ATM hold the operator's own Nostr private key**. The latter means physical possession of the ATM = total compromise of the operator's LNbits account.
@ -66,7 +66,7 @@ Lamassu's old answer here was TLS cert pinning. We have a richer toolbox — Nos
register_invoice_listener fires
satmachineadmin/tasks.py:_handle_payment
spirekeeper/tasks.py:_handle_payment
┌─────────────────────────────────┴────────────────────────────┐
▼ ▼
@ -144,7 +144,7 @@ T3, T5, T6 are the ones that keep the hardware honest. T3 + T6 are *the* reason
## 4 · Audit findings — current state inventory
Pulled from the two recent codelevel audits of `~/dev/shared/extensions/satmachineadmin` (operatorscoping inventory) and `~/dev/lnbits/nostr-transport` (transport primitives).
Pulled from the two recent codelevel audits of `~/dev/shared/extensions/spirekeeper` (operatorscoping inventory) and `~/dev/lnbits/nostr-transport` (transport primitives).
### 4.1 What's already strong
@ -307,7 +307,7 @@ None of those need to change. The new layers slot in *above* them.
| **S4 — NIP78 permachine config + fleet roster** | Operator publishes `kind:30078` config + `kind:30000` fleet list. Handler crosschecks ATM npub ∈ fleet; reads maxwithdraw/fee policy from config. | G1, G9 | 1 week | Define config schema; backwardscompat path for preNIP78 machines. |
| **S5 — `sender_pubkey` persistence + signed metadata in Payment.extra** | When the dispatcher writes a Payment row, it stamps `Payment.extra.sender_pubkey`, `delegation_root`, and an HMAC over the key fields keyed by the LNbits server's own secret. Mutation postwrite breaks the HMAC. | G2 (DBside), G5, G6 | 35 days | LNbits PR — fairly localised. |
| **S6 — Rate limiting + rostergated autoaccount** | Autoaccountfromnpub only fires if the npub appears in some operator's NIP78 fleet OR if an explicit "open enrollment" flag is set. Relay/handlerlevel rate limit per pubkey. | G8, G9 | 1 week | LNbits PR. |
| **S7 — NIP46 bunker option** | Operator can pair satmachineadmin with a Bunker (Amber, Nunchuk Custody, etc.). Operator's nsec leaves LNbits' DB; LNbits stores only the bunker connection. | G6, partial G5 | 46 weeks | Largest. Defer until S0S5 land. |
| **S7 — NIP46 bunker option** | Operator can pair spirekeeper with a Bunker (Amber, Nunchuk Custody, etc.). Operator's nsec leaves LNbits' DB; LNbits stores only the bunker connection. | G6, partial G5 | 46 weeks | Largest. Defer until S0S5 land. |
| **S8 — Cashin path** | Wire `is_out=True` cashin handling: LNURLwithdraw with expiration matching the kind21000 invoice TTL, attestation receipt on settle, refund queue for stale links. | G10 | 2 weeks | Out of scope for this security doc but tracked here for completeness. |
Recommended sequencing for the *next sprint*: **S0 + S1 + S5**. They give us the biggest security delta with no upstream LNbits dependency for S0/S1 and a small, wellscoped LNbits patch for S5. S2/S3/S4 are the proper Nostrnative layer and should land in the sprint after.
@ -359,12 +359,12 @@ For an auditor or new contributor doing a walkthrough:
| File | Role | Note |
|---|---|---|
| `~/dev/shared/extensions/satmachineadmin/tasks.py` | LNbits invoice listener. Entry point for all settlements today. | `_handle_payment:56-95` — loadbearing routing. |
| `~/dev/shared/extensions/satmachineadmin/bitspire.py` | Parses Payment.extra. The trust boundary. | `parse_settlement:68-92` — happy vs fallback path. |
| `~/dev/shared/extensions/satmachineadmin/distribution.py` | Threeleg distribution chain. | `process_settlement` — uses claim pattern. |
| `~/dev/shared/extensions/satmachineadmin/crud.py` | Operatorscoped DB layer. | `claim_settlement_for_processing`, `_machine_owned_by`. |
| `~/dev/shared/extensions/satmachineadmin/views_api.py` | 33 routes, all `check_user_exists` except superconfig PUT. | `_assert_wallet_owned_by` is the walletIDOR fix. |
| `~/dev/shared/extensions/satmachineadmin/migrations.py` | Schema. | `dca_settlements` is the audit row; `dca_payments` is the leg row. |
| `~/dev/shared/extensions/spirekeeper/tasks.py` | LNbits invoice listener. Entry point for all settlements today. | `_handle_payment:56-95` — loadbearing routing. |
| `~/dev/shared/extensions/spirekeeper/bitspire.py` | Parses Payment.extra. The trust boundary. | `parse_settlement:68-92` — happy vs fallback path. |
| `~/dev/shared/extensions/spirekeeper/distribution.py` | Threeleg distribution chain. | `process_settlement` — uses claim pattern. |
| `~/dev/shared/extensions/spirekeeper/crud.py` | Operatorscoped DB layer. | `claim_settlement_for_processing`, `_machine_owned_by`. |
| `~/dev/shared/extensions/spirekeeper/views_api.py` | 33 routes, all `check_user_exists` except superconfig PUT. | `_assert_wallet_owned_by` is the walletIDOR fix. |
| `~/dev/shared/extensions/spirekeeper/migrations.py` | Schema. | `dca_settlements` is the audit row; `dca_payments` is the leg row. |
| `~/dev/shocknet/lamassu-next/deploy/nixos/provision-atm.sh` | Where keys land on the ATM today. | `:81-99``VITE_ATM_PRIVATE_KEY` and the Option1 stopgap. |
| `~/dev/lnbits/nostr-transport/lnbits/core/services/nostr_transport/` | LNbits transport handler (upstream we depend on). | NIP44 v2 crypto here; G5/G6/G7 fixes will live here. |
| `~/dev/nostr-protocol/nips/26.md` | Delegation. | Source for S2. |
@ -397,7 +397,7 @@ How we'd test the proposed design endtoend, once S0S5 land:
Once approved:
1. The PDF for printing will be generated postplanmode (requires shell exec). Recommended path: render the markdown via `pandoc` to `~/dev/shared/extensions/satmachineadmin/docs/security-pathway-v1.pdf`; the markdown source will live at `~/dev/shared/extensions/satmachineadmin/docs/security-pathway-v1.md` so future contributors edit it inrepo.
1. The PDF for printing will be generated postplanmode (requires shell exec). Recommended path: render the markdown via `pandoc` to `~/dev/shared/extensions/spirekeeper/docs/security-pathway-v1.pdf`; the markdown source will live at `~/dev/shared/extensions/spirekeeper/docs/security-pathway-v1.md` so future contributors edit it inrepo.
2. Open Forgejo epics on `aiolabs/satmachineadmin` linking back to existing `#9/#11/#12` and adding a new one for "Security pathway hardening (S0S7)."
3. Open a tracking issue on `aiolabs/lnbits` against the `nostr-transport` branch for the LNbitsside primitives (S2, S5, S6).
4. Sequence sprint: **S0 + S1 + S5 first** (highest ratio of security delta to upstream coupling). S2/S3/S4 in the following sprint.

View file

@ -38,7 +38,7 @@
-- Find duplicate transactions
SELECT transaction_id, COUNT(*) as count,
STRING_AGG(id::text, ', ') as record_ids
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
GROUP BY transaction_id
HAVING COUNT(*) > 1;
```
@ -60,16 +60,16 @@ HAVING COUNT(*) > 1;
-- Step 1: Identify duplicate distributions
SELECT lt.transaction_id, lt.id, lt.created_at, lt.base_amount,
COUNT(dp.id) as distribution_count
FROM satoshimachine.lamassu_transactions lt
LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id
FROM spirekeeper.lamassu_transactions lt
LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
GROUP BY lt.id
HAVING COUNT(dp.id) > (SELECT COUNT(*) FROM satoshimachine.dca_clients WHERE remaining_balance > 0);
HAVING COUNT(dp.id) > (SELECT COUNT(*) FROM spirekeeper.dca_clients WHERE remaining_balance > 0);
-- Step 2: Calculate over-distributed amounts per client
SELECT client_id,
SUM(amount_sats) as total_received,
-- Manual calculation of expected amount needed here
FROM satoshimachine.dca_payments
FROM spirekeeper.dca_payments
WHERE lamassu_transaction_id IN (SELECT id FROM duplicates_table)
GROUP BY client_id;
```
@ -94,7 +94,7 @@ if existing:
**Required Database Change**:
```sql
ALTER TABLE satoshimachine.lamassu_transactions
ALTER TABLE spirekeeper.lamassu_transactions
ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
```
@ -117,7 +117,7 @@ ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
-- Check last successful poll
SELECT MAX(created_at) as last_transaction,
EXTRACT(EPOCH FROM (NOW() - MAX(created_at)))/3600 as hours_since_last
FROM satoshimachine.lamassu_transactions;
FROM spirekeeper.lamassu_transactions;
-- If hours_since_last > 24, investigate immediately
```
@ -194,8 +194,8 @@ ssh-keygen -t ed25519 -f ~/.ssh/satmachine_lamassu -C "satmachine-polling"
-- Find stuck/failed payments (older than 1 hour, not completed)
SELECT dp.id, dp.client_id, dp.amount_sats, dp.status, dp.created_at,
c.username, dp.payment_hash
FROM satoshimachine.dca_payments dp
JOIN satoshimachine.dca_clients c ON dp.client_id = c.id
FROM spirekeeper.dca_payments dp
JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
WHERE dp.status != 'completed'
AND dp.created_at < NOW() - INTERVAL '1 hour'
ORDER BY dp.created_at DESC;
@ -207,7 +207,7 @@ ORDER BY dp.created_at DESC;
SELECT
SUM(commission_amount) as total_commission_expected,
-- Manually check actual wallet balance in LNBits
FROM satoshimachine.lamassu_transactions;
FROM spirekeeper.lamassu_transactions;
```
#### Immediate Response
@ -258,9 +258,9 @@ SELECT
c.remaining_balance,
COALESCE(SUM(d.amount), 0) as total_deposits,
COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments
FROM satoshimachine.dca_clients c
LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id
FROM spirekeeper.dca_clients c
LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
GROUP BY c.id;
```
@ -319,9 +319,9 @@ SELECT
COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_distributed,
COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as calculated_balance,
c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0)) as discrepancy
FROM satoshimachine.dca_clients c
LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id
FROM spirekeeper.dca_clients c
LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
GROUP BY c.id, c.username, c.remaining_balance
HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0))) > 1;
```
@ -341,11 +341,11 @@ HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE
```sql
-- Get complete transaction history for client
SELECT 'DEPOSIT' as type, id, amount, status, created_at, confirmed_at
FROM satoshimachine.dca_deposits
FROM spirekeeper.dca_deposits
WHERE client_id = <client_id>
UNION ALL
SELECT 'PAYMENT' as type, id, amount_sats, status, created_at, NULL
FROM satoshimachine.dca_payments
FROM spirekeeper.dca_payments
WHERE client_id = <client_id>
ORDER BY created_at;
```
@ -361,18 +361,18 @@ ORDER BY created_at;
**Option A: Adjustment Entry** (Recommended)
```sql
-- Create compensating deposit for positive discrepancy
INSERT INTO satoshimachine.dca_deposits (client_id, amount, status, note)
INSERT INTO spirekeeper.dca_deposits (client_id, amount, status, note)
VALUES (<client_id>, <adjustment_amount>, 'confirmed', 'Balance correction - reconciliation 2025-10-19');
-- OR Create compensating payment for negative discrepancy
INSERT INTO satoshimachine.dca_payments (client_id, amount_sats, status, note)
INSERT INTO spirekeeper.dca_payments (client_id, amount_sats, status, note)
VALUES (<client_id>, <adjustment_amount>, 'completed', 'Balance correction - reconciliation 2025-10-19');
```
**Option B: Direct Balance Update** (Use with extreme caution)
```sql
-- ONLY if audit trail is complete and discrepancy is unexplained
UPDATE satoshimachine.dca_clients
UPDATE spirekeeper.dca_clients
SET remaining_balance = <correct_balance>,
updated_at = NOW()
WHERE id = <client_id>;
@ -396,11 +396,11 @@ async def daily_reconciliation_check():
**Database Constraints**:
```sql
-- Prevent negative balances
ALTER TABLE satoshimachine.dca_clients
ALTER TABLE spirekeeper.dca_clients
ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0);
-- Prevent confirmed deposits with zero amount
ALTER TABLE satoshimachine.dca_deposits
ALTER TABLE spirekeeper.dca_deposits
ADD CONSTRAINT positive_deposit CHECK (amount > 0);
```
@ -435,7 +435,7 @@ SELECT
-- Calculate differences
base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as base_difference,
commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) as commission_difference
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
WHERE ABS(base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) > 1
OR ABS(commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))))) > 1;
```
@ -485,9 +485,9 @@ SELECT
SUM(lt.base_amount) as total_distributed,
SUM(expected_base) as should_have_distributed,
SUM(expected_base - lt.base_amount) as client_impact
FROM satoshimachine.lamassu_transactions lt
JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id
JOIN satoshimachine.dca_clients c ON dp.client_id = c.id
FROM spirekeeper.lamassu_transactions lt
JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
WHERE -- filter for affected transactions
GROUP BY c.id;
```
@ -505,7 +505,7 @@ GROUP BY c.id;
- Create compensating payments to affected clients:
```sql
-- Add to client balances
UPDATE satoshimachine.dca_clients c
UPDATE spirekeeper.dca_clients c
SET remaining_balance = remaining_balance + adjustment.amount
FROM (
-- Calculate adjustment per client
@ -572,7 +572,7 @@ assert abs(calculated_total - crypto_atoms) <= 1, "Commission calculation error
SELECT
SUM(commission_amount) as expected_total_commission,
-- Compare to actual wallet balance in LNBits dashboard
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
WHERE created_at > '<date-of-last-known-good-balance>';
```
@ -607,7 +607,7 @@ curl -X GET https://<lnbits-host>/api/v1/wallet \
SELECT commission_wallet_id,
LEFT(commission_wallet_adminkey, 10) || '...' as key_preview,
updated_at
FROM satoshimachine.lamassu_config
FROM spirekeeper.lamassu_config
ORDER BY updated_at DESC
LIMIT 1;
```
@ -783,9 +783,9 @@ WITH client_financials AS (
COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments,
COUNT(DISTINCT d.id) as deposit_count,
COUNT(DISTINCT p.id) as payment_count
FROM satoshimachine.dca_clients c
LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id
FROM spirekeeper.dca_clients c
LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
GROUP BY c.id
)
SELECT
@ -813,7 +813,7 @@ SELECT
SUM(commission_amount) as total_commission,
MIN(created_at) as first_transaction,
MAX(created_at) as last_transaction
FROM satoshimachine.lamassu_transactions;
FROM spirekeeper.lamassu_transactions;
-- 3. Failed/Pending Payments Check
SELECT
@ -822,7 +822,7 @@ SELECT
SUM(amount_sats) as total_amount,
MIN(created_at) as oldest,
MAX(created_at) as newest
FROM satoshimachine.dca_payments
FROM spirekeeper.dca_payments
GROUP BY status
ORDER BY
CASE status
@ -839,7 +839,7 @@ SELECT
status,
created_at,
EXTRACT(EPOCH FROM (NOW() - created_at))/3600 as hours_pending
FROM satoshimachine.dca_deposits
FROM spirekeeper.dca_deposits
WHERE status = 'pending'
AND created_at < NOW() - INTERVAL '48 hours'
ORDER BY created_at;
@ -855,7 +855,7 @@ SELECT
commission_amount,
ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as expected_base,
base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as difference
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
ORDER BY created_at DESC
LIMIT 20;
```
@ -876,15 +876,15 @@ psql -h localhost -U lnbits -d lnbits
2. **Direct Configuration Update**:
```sql
-- Update Lamassu config directly
UPDATE satoshimachine.lamassu_config
UPDATE spirekeeper.lamassu_config
SET polling_enabled = false
WHERE id = (SELECT MAX(id) FROM satoshimachine.lamassu_config);
WHERE id = (SELECT MAX(id) FROM spirekeeper.lamassu_config);
```
3. **Manual Client Balance Update**:
```sql
-- ONLY in emergency when dashboard unavailable
UPDATE satoshimachine.dca_clients
UPDATE spirekeeper.dca_clients
SET remaining_balance = <correct_amount>
WHERE id = <client_id>;
-- MUST document this action in incident log
@ -910,19 +910,19 @@ uv run lnbits
```bash
# Export all DCA-related tables to CSV
sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.lamassu_transactions;" \
"SELECT * FROM spirekeeper.lamassu_transactions;" \
> lamassu_transactions_export_$(date +%Y%m%d_%H%M%S).csv
sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.dca_payments;" \
"SELECT * FROM spirekeeper.dca_payments;" \
> dca_payments_export_$(date +%Y%m%d_%H%M%S).csv
sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.dca_deposits;" \
"SELECT * FROM spirekeeper.dca_deposits;" \
> dca_deposits_export_$(date +%Y%m%d_%H%M%S).csv
sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.dca_clients;" \
"SELECT * FROM spirekeeper.dca_clients;" \
> dca_clients_export_$(date +%Y%m%d_%H%M%S).csv
```
@ -946,9 +946,9 @@ SELECT
dp.amount_sats as client_received,
dp.status as payment_status,
dp.payment_hash
FROM satoshimachine.lamassu_transactions lt
LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id
LEFT JOIN satoshimachine.dca_clients c ON dp.client_id = c.id
FROM spirekeeper.lamassu_transactions lt
LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
LEFT JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
ORDER BY lt.created_at DESC, c.username;
```
@ -981,18 +981,18 @@ async def process_lamassu_transaction(txn_data: dict) -> Optional[LamassuTransac
```sql
-- Add unique constraint on transaction_id
ALTER TABLE satoshimachine.lamassu_transactions
ALTER TABLE spirekeeper.lamassu_transactions
ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
-- Prevent negative balances
ALTER TABLE satoshimachine.dca_clients
ALTER TABLE spirekeeper.dca_clients
ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0);
-- Ensure positive amounts
ALTER TABLE satoshimachine.dca_deposits
ALTER TABLE spirekeeper.dca_deposits
ADD CONSTRAINT positive_deposit CHECK (amount > 0);
ALTER TABLE satoshimachine.dca_payments
ALTER TABLE spirekeeper.dca_payments
ADD CONSTRAINT positive_payment CHECK (amount_sats > 0);
```
@ -1196,7 +1196,7 @@ grep "wallet.*api\|payment_hash" lnbits.log | tail -50
### System Access
**LNBits Admin Dashboard**:
- URL: `https://<your-lnbits-host>/satoshimachine`
- URL: `https://<your-lnbits-host>/spirekeeper`
- Requires superuser authentication
**Database Access**:
@ -1205,7 +1205,7 @@ grep "wallet.*api\|payment_hash" lnbits.log | tail -50
sqlite3 /home/padreug/AioLabs/Git/lnbits-extensions/lnbits/data/database.sqlite
# Direct table access
sqlite3 /path/to/db "SELECT * FROM satoshimachine.<table_name>;"
sqlite3 /path/to/db "SELECT * FROM spirekeeper.<table_name>;"
```
**Log Files**:
@ -1274,7 +1274,7 @@ pkill -f lnbits
```sql
-- Disable automatic polling
UPDATE satoshimachine.lamassu_config
UPDATE spirekeeper.lamassu_config
SET polling_enabled = false;
```
@ -1283,7 +1283,7 @@ SET polling_enabled = false;
```sql
-- All client balances summary
SELECT id, username, remaining_balance, created_at
FROM satoshimachine.dca_clients
FROM spirekeeper.dca_clients
ORDER BY remaining_balance DESC;
```
@ -1291,7 +1291,7 @@ ORDER BY remaining_balance DESC;
```sql
SELECT id, transaction_id, created_at, crypto_atoms, base_amount, commission_amount
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
ORDER BY created_at DESC
LIMIT 10;
```
@ -1299,7 +1299,7 @@ LIMIT 10;
### Failed Payments
```sql
SELECT * FROM satoshimachine.dca_payments
SELECT * FROM spirekeeper.dca_payments
WHERE status != 'completed'
ORDER BY created_at DESC;
```
@ -1314,19 +1314,19 @@ BACKUP_DIR="emergency_backup_${DATE}"
mkdir -p $BACKUP_DIR
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.lamassu_transactions;" \
"SELECT * FROM spirekeeper.lamassu_transactions;" \
> ${BACKUP_DIR}/lamassu_transactions.csv
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_payments;" \
"SELECT * FROM spirekeeper.dca_payments;" \
> ${BACKUP_DIR}/dca_payments.csv
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_deposits;" \
"SELECT * FROM spirekeeper.dca_deposits;" \
> ${BACKUP_DIR}/dca_deposits.csv
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_clients;" \
"SELECT * FROM spirekeeper.dca_clients;" \
> ${BACKUP_DIR}/dca_clients.csv
echo "Backup complete in ${BACKUP_DIR}/"

View file

@ -38,7 +38,7 @@
-- Find duplicate transactions
SELECT transaction_id, COUNT(*) as count,
STRING_AGG(id::text, ', ') as record_ids
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
GROUP BY transaction_id
HAVING COUNT(*) > 1;
```
@ -60,16 +60,16 @@ HAVING COUNT(*) > 1;
-- Step 1: Identify duplicate distributions
SELECT lt.transaction_id, lt.id, lt.created_at, lt.base_amount,
COUNT(dp.id) as distribution_count
FROM satoshimachine.lamassu_transactions lt
LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id
FROM spirekeeper.lamassu_transactions lt
LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
GROUP BY lt.id
HAVING COUNT(dp.id) > (SELECT COUNT(*) FROM satoshimachine.dca_clients WHERE remaining_balance > 0);
HAVING COUNT(dp.id) > (SELECT COUNT(*) FROM spirekeeper.dca_clients WHERE remaining_balance > 0);
-- Step 2: Calculate over-distributed amounts per client
SELECT client_id,
SUM(amount_sats) as total_received,
-- Manual calculation of expected amount needed here
FROM satoshimachine.dca_payments
FROM spirekeeper.dca_payments
WHERE lamassu_transaction_id IN (SELECT id FROM duplicates_table)
GROUP BY client_id;
```
@ -94,7 +94,7 @@ if existing:
**Required Database Change**:
```sql
ALTER TABLE satoshimachine.lamassu_transactions
ALTER TABLE spirekeeper.lamassu_transactions
ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
```
@ -117,7 +117,7 @@ ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
-- Check last successful poll
SELECT MAX(created_at) as last_transaction,
EXTRACT(EPOCH FROM (NOW() - MAX(created_at)))/3600 as hours_since_last
FROM satoshimachine.lamassu_transactions;
FROM spirekeeper.lamassu_transactions;
-- If hours_since_last > 24, investigate immediately
```
@ -194,8 +194,8 @@ ssh-keygen -t ed25519 -f ~/.ssh/satmachine_lamassu -C "satmachine-polling"
-- Find stuck/failed payments (older than 1 hour, not completed)
SELECT dp.id, dp.client_id, dp.amount_sats, dp.status, dp.created_at,
c.username, dp.payment_hash
FROM satoshimachine.dca_payments dp
JOIN satoshimachine.dca_clients c ON dp.client_id = c.id
FROM spirekeeper.dca_payments dp
JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
WHERE dp.status != 'completed'
AND dp.created_at < NOW() - INTERVAL '1 hour'
ORDER BY dp.created_at DESC;
@ -207,7 +207,7 @@ ORDER BY dp.created_at DESC;
SELECT
SUM(commission_amount) as total_commission_expected,
-- Manually check actual wallet balance in LNBits
FROM satoshimachine.lamassu_transactions;
FROM spirekeeper.lamassu_transactions;
```
#### Immediate Response
@ -258,9 +258,9 @@ SELECT
c.remaining_balance,
COALESCE(SUM(d.amount), 0) as total_deposits,
COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments
FROM satoshimachine.dca_clients c
LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id
FROM spirekeeper.dca_clients c
LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
GROUP BY c.id;
```
@ -319,9 +319,9 @@ SELECT
COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_distributed,
COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as calculated_balance,
c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0)) as discrepancy
FROM satoshimachine.dca_clients c
LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id
FROM spirekeeper.dca_clients c
LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
GROUP BY c.id, c.username, c.remaining_balance
HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0))) > 1;
```
@ -341,11 +341,11 @@ HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE
```sql
-- Get complete transaction history for client
SELECT 'DEPOSIT' as type, id, amount, status, created_at, confirmed_at
FROM satoshimachine.dca_deposits
FROM spirekeeper.dca_deposits
WHERE client_id = <client_id>
UNION ALL
SELECT 'PAYMENT' as type, id, amount_sats, status, created_at, NULL
FROM satoshimachine.dca_payments
FROM spirekeeper.dca_payments
WHERE client_id = <client_id>
ORDER BY created_at;
```
@ -361,18 +361,18 @@ ORDER BY created_at;
**Option A: Adjustment Entry** (Recommended)
```sql
-- Create compensating deposit for positive discrepancy
INSERT INTO satoshimachine.dca_deposits (client_id, amount, status, note)
INSERT INTO spirekeeper.dca_deposits (client_id, amount, status, note)
VALUES (<client_id>, <adjustment_amount>, 'confirmed', 'Balance correction - reconciliation 2025-10-19');
-- OR Create compensating payment for negative discrepancy
INSERT INTO satoshimachine.dca_payments (client_id, amount_sats, status, note)
INSERT INTO spirekeeper.dca_payments (client_id, amount_sats, status, note)
VALUES (<client_id>, <adjustment_amount>, 'completed', 'Balance correction - reconciliation 2025-10-19');
```
**Option B: Direct Balance Update** (Use with extreme caution)
```sql
-- ONLY if audit trail is complete and discrepancy is unexplained
UPDATE satoshimachine.dca_clients
UPDATE spirekeeper.dca_clients
SET remaining_balance = <correct_balance>,
updated_at = NOW()
WHERE id = <client_id>;
@ -396,11 +396,11 @@ async def daily_reconciliation_check():
**Database Constraints**:
```sql
-- Prevent negative balances
ALTER TABLE satoshimachine.dca_clients
ALTER TABLE spirekeeper.dca_clients
ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0);
-- Prevent confirmed deposits with zero amount
ALTER TABLE satoshimachine.dca_deposits
ALTER TABLE spirekeeper.dca_deposits
ADD CONSTRAINT positive_deposit CHECK (amount > 0);
```
@ -435,7 +435,7 @@ SELECT
-- Calculate differences
base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as base_difference,
commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) as commission_difference
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
WHERE ABS(base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) > 1
OR ABS(commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))))) > 1;
```
@ -485,9 +485,9 @@ SELECT
SUM(lt.base_amount) as total_distributed,
SUM(expected_base) as should_have_distributed,
SUM(expected_base - lt.base_amount) as client_impact
FROM satoshimachine.lamassu_transactions lt
JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id
JOIN satoshimachine.dca_clients c ON dp.client_id = c.id
FROM spirekeeper.lamassu_transactions lt
JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
WHERE -- filter for affected transactions
GROUP BY c.id;
```
@ -505,7 +505,7 @@ GROUP BY c.id;
- Create compensating payments to affected clients:
```sql
-- Add to client balances
UPDATE satoshimachine.dca_clients c
UPDATE spirekeeper.dca_clients c
SET remaining_balance = remaining_balance + adjustment.amount
FROM (
-- Calculate adjustment per client
@ -572,7 +572,7 @@ assert abs(calculated_total - crypto_atoms) <= 1, "Commission calculation error
SELECT
SUM(commission_amount) as expected_total_commission,
-- Compare to actual wallet balance in LNBits dashboard
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
WHERE created_at > '<date-of-last-known-good-balance>';
```
@ -607,7 +607,7 @@ curl -X GET https://<lnbits-host>/api/v1/wallet \
SELECT commission_wallet_id,
LEFT(commission_wallet_adminkey, 10) || '...' as key_preview,
updated_at
FROM satoshimachine.lamassu_config
FROM spirekeeper.lamassu_config
ORDER BY updated_at DESC
LIMIT 1;
```
@ -783,9 +783,9 @@ WITH client_financials AS (
COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments,
COUNT(DISTINCT d.id) as deposit_count,
COUNT(DISTINCT p.id) as payment_count
FROM satoshimachine.dca_clients c
LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id
FROM spirekeeper.dca_clients c
LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
GROUP BY c.id
)
SELECT
@ -813,7 +813,7 @@ SELECT
SUM(commission_amount) as total_commission,
MIN(created_at) as first_transaction,
MAX(created_at) as last_transaction
FROM satoshimachine.lamassu_transactions;
FROM spirekeeper.lamassu_transactions;
-- 3. Failed/Pending Payments Check
SELECT
@ -822,7 +822,7 @@ SELECT
SUM(amount_sats) as total_amount,
MIN(created_at) as oldest,
MAX(created_at) as newest
FROM satoshimachine.dca_payments
FROM spirekeeper.dca_payments
GROUP BY status
ORDER BY
CASE status
@ -839,7 +839,7 @@ SELECT
status,
created_at,
EXTRACT(EPOCH FROM (NOW() - created_at))/3600 as hours_pending
FROM satoshimachine.dca_deposits
FROM spirekeeper.dca_deposits
WHERE status = 'pending'
AND created_at < NOW() - INTERVAL '48 hours'
ORDER BY created_at;
@ -855,7 +855,7 @@ SELECT
commission_amount,
ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as expected_base,
base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as difference
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
ORDER BY created_at DESC
LIMIT 20;
```
@ -876,15 +876,15 @@ psql -h localhost -U lnbits -d lnbits
2. **Direct Configuration Update**:
```sql
-- Update Lamassu config directly
UPDATE satoshimachine.lamassu_config
UPDATE spirekeeper.lamassu_config
SET polling_enabled = false
WHERE id = (SELECT MAX(id) FROM satoshimachine.lamassu_config);
WHERE id = (SELECT MAX(id) FROM spirekeeper.lamassu_config);
```
3. **Manual Client Balance Update**:
```sql
-- ONLY in emergency when dashboard unavailable
UPDATE satoshimachine.dca_clients
UPDATE spirekeeper.dca_clients
SET remaining_balance = <correct_amount>
WHERE id = <client_id>;
-- MUST document this action in incident log
@ -910,19 +910,19 @@ uv run lnbits
```bash
# Export all DCA-related tables to CSV
sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.lamassu_transactions;" \
"SELECT * FROM spirekeeper.lamassu_transactions;" \
> lamassu_transactions_export_$(date +%Y%m%d_%H%M%S).csv
sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.dca_payments;" \
"SELECT * FROM spirekeeper.dca_payments;" \
> dca_payments_export_$(date +%Y%m%d_%H%M%S).csv
sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.dca_deposits;" \
"SELECT * FROM spirekeeper.dca_deposits;" \
> dca_deposits_export_$(date +%Y%m%d_%H%M%S).csv
sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.dca_clients;" \
"SELECT * FROM spirekeeper.dca_clients;" \
> dca_clients_export_$(date +%Y%m%d_%H%M%S).csv
```
@ -946,9 +946,9 @@ SELECT
dp.amount_sats as client_received,
dp.status as payment_status,
dp.payment_hash
FROM satoshimachine.lamassu_transactions lt
LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id
LEFT JOIN satoshimachine.dca_clients c ON dp.client_id = c.id
FROM spirekeeper.lamassu_transactions lt
LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
LEFT JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
ORDER BY lt.created_at DESC, c.username;
```
@ -981,18 +981,18 @@ async def process_lamassu_transaction(txn_data: dict) -> Optional[LamassuTransac
```sql
-- Add unique constraint on transaction_id
ALTER TABLE satoshimachine.lamassu_transactions
ALTER TABLE spirekeeper.lamassu_transactions
ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
-- Prevent negative balances
ALTER TABLE satoshimachine.dca_clients
ALTER TABLE spirekeeper.dca_clients
ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0);
-- Ensure positive amounts
ALTER TABLE satoshimachine.dca_deposits
ALTER TABLE spirekeeper.dca_deposits
ADD CONSTRAINT positive_deposit CHECK (amount > 0);
ALTER TABLE satoshimachine.dca_payments
ALTER TABLE spirekeeper.dca_payments
ADD CONSTRAINT positive_payment CHECK (amount_sats > 0);
```
@ -1196,7 +1196,7 @@ grep "wallet.*api\|payment_hash" lnbits.log | tail -50
### System Access
**LNBits Admin Dashboard**:
- URL: `https://<your-lnbits-host>/satoshimachine`
- URL: `https://<your-lnbits-host>/spirekeeper`
- Requires superuser authentication
**Database Access**:
@ -1205,7 +1205,7 @@ grep "wallet.*api\|payment_hash" lnbits.log | tail -50
sqlite3 /home/padreug/AioLabs/Git/lnbits-extensions/lnbits/data/database.sqlite
# Direct table access
sqlite3 /path/to/db "SELECT * FROM satoshimachine.<table_name>;"
sqlite3 /path/to/db "SELECT * FROM spirekeeper.<table_name>;"
```
**Log Files**:
@ -1274,7 +1274,7 @@ pkill -f lnbits
```sql
-- Disable automatic polling
UPDATE satoshimachine.lamassu_config
UPDATE spirekeeper.lamassu_config
SET polling_enabled = false;
```
@ -1283,7 +1283,7 @@ SET polling_enabled = false;
```sql
-- All client balances summary
SELECT id, username, remaining_balance, created_at
FROM satoshimachine.dca_clients
FROM spirekeeper.dca_clients
ORDER BY remaining_balance DESC;
```
@ -1291,7 +1291,7 @@ ORDER BY remaining_balance DESC;
```sql
SELECT id, transaction_id, created_at, crypto_atoms, base_amount, commission_amount
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
ORDER BY created_at DESC
LIMIT 10;
```
@ -1299,7 +1299,7 @@ LIMIT 10;
### Failed Payments
```sql
SELECT * FROM satoshimachine.dca_payments
SELECT * FROM spirekeeper.dca_payments
WHERE status != 'completed'
ORDER BY created_at DESC;
```
@ -1314,19 +1314,19 @@ BACKUP_DIR="emergency_backup_${DATE}"
mkdir -p $BACKUP_DIR
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.lamassu_transactions;" \
"SELECT * FROM spirekeeper.lamassu_transactions;" \
> ${BACKUP_DIR}/lamassu_transactions.csv
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_payments;" \
"SELECT * FROM spirekeeper.dca_payments;" \
> ${BACKUP_DIR}/dca_payments.csv
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_deposits;" \
"SELECT * FROM spirekeeper.dca_deposits;" \
> ${BACKUP_DIR}/dca_deposits.csv
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_clients;" \
"SELECT * FROM spirekeeper.dca_clients;" \
> ${BACKUP_DIR}/dca_clients.csv
echo "Backup complete in ${BACKUP_DIR}/"