docs: finish the LNbits-era doc sweep across docs/ + .claude/skills/
Second + final batch of the doc refresh. README + CLAUDE went out in
8c9ae29; deploy/nixos/README + obsolete-flow flags in 924844f. This
commit covers everything left.
docs/machine-installation.md
Was describing a manual AppImage scp deploy + a `lamassu-kiosk`
systemd unit that hasn't been the deployment path for months.
Replaced with a high-level "what the pipeline does and why"
overview that points at deploy/nixos/README.md for the full
command-by-command walkthrough. Includes the BATM3 chassis-mod
note (custom Dell OptiPlex retrofit, not a stock Dell).
docs/architecture-comparison.md
Rewrote the comparison to be lamassu-server (≤ v8.1.5) vs bitSpire
(LNbits-backed) instead of the original lamassu-server vs LP-backed
lamassu-next framing. Updated the cash-out + cash-in flow diagrams
to show the actual nostr-transport path (LNbits-bundled nostrrelay
extension at ws://<host>:5001/nostrrelay/test, no separate strfry
container). Replaced the migration-path section with a softer
"when to choose what" framing that includes Lamassu's current
commercial offering as a legitimate third option. Added a header
pointer to the Acknowledgements section.
docs/business-model.md
Light touch-ups: Lightning.Pub → LNbits where it appeared, swapped
the [[ndebit-cash-in-flow]] link for [[architecture-comparison]],
noted the kind-30078 service beacon for availability broadcasts.
docs/device-configuration.md
Dropped "Lamassu" branding from the machine-model headings
(Sintra / tejo / douro / batm3 are referenced by hardware identity
here, not by Lamassu's product line). Added the Sintra-specific
ttyS4-vs-placeholder-ttyS1..3 gotcha we hard-learned during the
first flash. Corrected the BATM3 entry: stock GeneralBytes chassis
with a Dell OptiPlex 9030 AIO motherboard physically grafted in,
NOT a Dell out of the box. Updated the example /dev/ttyJ* symlink
output to match what a healthy Sintra actually shows.
docs/adr/001-hal-architecture.md
ADRs are historical artifacts — kept the original decision text
intact. Added a postscript noting:
- The package rename @lamassu/hal → @bitSpire/hal
- The v8.1.5 boundary on any lamassu-machine source-tree
references (Lamassu's 2024-01-26 license transition)
- That the "Remaining Work" list is complete and the first
successful Sintra hardware integration ran on 2026-05-13
.claude/skills/lightning-check.md
Rewrote end-to-end. Was Lightning.Pub-flavoured with CLINK kinds
21001/21002 as the primary flows; now validates the LNbits nostr-
transport surface (kind-21000 envelope, NIP-44 v2 encryption,
subscribe_payments filter discipline, lnurlw link composition).
Preserved a --clink mode for the still-live kind-21003 operator-
management surface. Includes a "what to check" rubric for cash-out
vs cash-in flows that mirrors the actual code in
apps/machine/src/services/lightning.ts.
.claude/skills/hal-check.md
Two pivots: (1) acknowledge ADR-001's TypeScript-not-Rust choice
and reframe all the safety checklists in TS-flavour (type safety,
discriminated unions, single-writer serial, bounded emitters)
instead of Rust-flavour (unsafe, borrow checker). (2) Add explicit
v8.1.5 provenance boundary plus a "forbidden operations" section
that prohibits diffing or porting from v8.1.6+ lamassu-machine
source. Updated the port-validation source-reference table to
list TS file paths under packages/hal/ instead of Rust paths.
.claude/skills/docs.md
@lamassu/* → @bitSpire/*. Replaced the Lightning.Pub mermaid
diagram with a current cash-out flow showing the nostr-transport
RPC + subscribe_payments push path. Left the createOffer noffer
example in the API-docs template section since it's illustrative
("here's what a good TSDoc block looks like") rather than current
reference documentation.
.claude/skills/test.md
One-line: @lamassu/nostr-client → @bitSpire/nostr-client in the
pnpm-filter example.
deploy/nixos/README.md
Single touch-up: clarified the douro/batm3 hardware-module comments
to reflect that BATM3 is a custom-installed Dell board in a
GeneralBytes BATM3 chassis (not a Dell OEM).
Files NOT touched in this sweep (intentionally):
- packages/hal/src/**/*.ts attribution comments — those reference
"lamassu-machine" in their port-source headers. Those are
factually accurate (the drivers ARE ported from there, up to
v8.1.5) and constitute necessary license/attribution metadata.
Editing them would erase the provenance trail.
- .claude/skills/{nostr-check,security}.md — already protocol-
neutral, no LP/lamassu references to clean up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0b94bef4be
commit
53b0d382e8
10 changed files with 692 additions and 792 deletions
|
|
@ -25,10 +25,11 @@ docs/
|
|||
│ ├── lightning-flow.md # Payment flows
|
||||
│ └── hardware-hal.md # HAL documentation
|
||||
├── api/
|
||||
│ ├── nostr-client.md # @lamassu/nostr-client API
|
||||
│ ├── clink.md # @lamassu/clink API
|
||||
│ ├── state-machine.md # @lamassu/state-machine API
|
||||
│ └── hal.md # @lamassu/hal API
|
||||
│ ├── nostr-client.md # @bitSpire/nostr-client API
|
||||
│ ├── lnbits.md # @bitSpire/lnbits API (kind-21000 RPC surface)
|
||||
│ ├── clink.md # @bitSpire/clink API (dormant on dev; 21003 management still wired)
|
||||
│ ├── state-machine.md # @bitSpire/state-machine API
|
||||
│ └── hal.md # @bitSpire/hal API
|
||||
├── guides/
|
||||
│ ├── development.md # Dev setup guide
|
||||
│ ├── hardware-testing.md # Testing with real hardware
|
||||
|
|
@ -106,22 +107,25 @@ Keep architecture diagrams current:
|
|||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant U as Customer Wallet
|
||||
participant A as ATM
|
||||
participant R as Relay
|
||||
participant L as Lightning.Pub
|
||||
participant R as Relay (LNbits-bundled nostrrelay)
|
||||
participant L as LNbits (nostr-transport)
|
||||
|
||||
U->>A: Insert $20 bill
|
||||
A->>A: Validate bill
|
||||
A->>R: Publish CLINK offer
|
||||
U->>R: Send payment request
|
||||
R->>A: Forward request
|
||||
A->>L: Generate invoice
|
||||
L->>A: Return invoice
|
||||
A->>R: Send invoice to user
|
||||
U->>L: Pay invoice
|
||||
L->>A: Payment confirmed
|
||||
A->>U: Display success
|
||||
Note over A,L: cash-out flow (customer pays ATM, gets cash)
|
||||
A->>R: kind-21000 RPC "create_invoice"
|
||||
R->>L: forward to LNbits server pubkey
|
||||
L->>R: kind-21000 ACK with BOLT11 invoice
|
||||
R->>A: forward ACK
|
||||
A->>U: display BOLT11 QR
|
||||
A->>R: kind-21000 "subscribe_payments" by payment_hash
|
||||
R->>L: forward
|
||||
L->>R: kind-21000 ACK with subscription_id
|
||||
R->>A: forward
|
||||
U->>L: pay BOLT11 via Lightning Network
|
||||
L->>R: kind-21000 push (settlement) tagged subscription_id
|
||||
R->>A: forward
|
||||
A->>A: dispense cash
|
||||
```
|
||||
|
||||
### Auto-update triggers
|
||||
|
|
|
|||
|
|
@ -1,133 +1,162 @@
|
|||
# /hal-check - Hardware Abstraction Layer Agent
|
||||
# /hal-check — Hardware Abstraction Layer Agent
|
||||
|
||||
## Purpose
|
||||
Validate HAL driver implementations against existing lamassu-machine drivers and hardware specifications.
|
||||
|
||||
Validate HAL driver implementations in `packages/hal/` against published hardware protocol specs (JCM ID003, Fujitsu F56 DLE/STX, Puloon LCDM, MEI EBDS, etc.) and against the v8.1.5 release line of `lamassu-machine` — which is the **last** lamassu-machine release published under a fully-open license.
|
||||
|
||||
> **Provenance boundary.** Drivers in `packages/hal/` derive from `lamassu-machine` at v8.1.5 and earlier (plus hardware-vendor protocol specs). Lamassu Industries AG transitioned to a proprietary source-available license on 2024-01-26 with v8.1.6+ gated behind a paid Operator Support Agreement. **Do not** reference, port, or diff against v8.1.6+ — the only safe upstream tree for porting is `v8.1.5` or earlier. See [CLAUDE.md → Provenance + legal status](../../CLAUDE.md#provenance--legal-status) for the operating rules.
|
||||
|
||||
> **Language note.** ADR-001 selected TypeScript-in-Electron over Rust-in-Tauri for the HAL. Earlier versions of this skill referenced Rust patterns; that's obsolete. All checks below are TypeScript-flavored.
|
||||
|
||||
## Invocation
|
||||
|
||||
```
|
||||
/hal-check [command] [driver]
|
||||
```
|
||||
|
||||
Commands:
|
||||
- `port` - Validate porting from lamassu-machine
|
||||
- `protocol` - Check protocol implementation
|
||||
- `safety` - Rust safety review
|
||||
- `mock` - Validate mock implementation
|
||||
|
||||
- `port` — Validate that a driver matches its lamassu-machine v8.1.5 reference (where the driver was ported from one)
|
||||
- `protocol` — Check protocol implementation against published vendor specs
|
||||
- `safety` — Type safety, error handling, hardware safety review
|
||||
- `mock` — Validate mock implementation completeness
|
||||
|
||||
Drivers:
|
||||
- `id003`, `ccnet`, `cashflow`, `bnr`, `genmega`, `hcm2`, `gsr50` (validators)
|
||||
- `puloon`, `f56`, `genmega`, `hcm2`, `gsr50` (dispensers)
|
||||
- `nippon`, `zebra`, `genmega` (printers)
|
||||
|
||||
## Porting Validation
|
||||
- Validators: `id003`, `ccnet`, `cashflow_sc` (EBDS), `bnr_advance`, `genmega`, `hcm2`, `gsr50`
|
||||
- Dispensers: `puloon`, `f56`, `genmega`, `hcm2`, `gsr50`
|
||||
- Printers: `nippon`, `zebra`, `genmega`
|
||||
|
||||
### Source Reference
|
||||
Each Rust driver should map 1:1 with lamassu-machine JavaScript:
|
||||
## Porting validation (`port` command)
|
||||
|
||||
| Rust File | JavaScript Source |
|
||||
|-----------|-------------------|
|
||||
| `validators/id003.rs` | `lib/id003/*.js` |
|
||||
| `validators/ccnet.rs` | `lib/ccnet/*.js` |
|
||||
| `dispensers/puloon.rs` | `lib/puloon/*.js` |
|
||||
| `dispensers/f56.rs` | `lib/f56/*.js` |
|
||||
### Source reference
|
||||
|
||||
Each TS driver in `packages/hal/` maps to (at most) one JS source in lamassu-machine v8.1.5 (the last fully-open release):
|
||||
|
||||
| TS Driver | JS Source (v8.1.5 release tree) |
|
||||
|---|---|
|
||||
| `validators/id003/*.ts` | `lib/id003/*.js` |
|
||||
| `validators/ccnet/*.ts` | `lib/ccnet/*.js` |
|
||||
| `validators/ebds/*.ts` | `lib/mei/cashflow_sc.js` + `lib/mei/*.js` |
|
||||
| `dispensers/puloon/*.ts` | `lib/puloon/*.js` |
|
||||
| `dispensers/f56/*.ts` | `lib/f56/*.js` |
|
||||
|
||||
If a driver in `packages/hal/` references a JS source NOT under v8.1.5 (or no JS source at all because it was re-derived from vendor specs), the attribution comment at the top of the TS file should say so.
|
||||
|
||||
### Porting checklist
|
||||
|
||||
### Porting Checklist
|
||||
```
|
||||
/hal-check port id003
|
||||
```
|
||||
|
||||
Validates:
|
||||
- [ ] All protocol commands implemented
|
||||
- [ ] State machine matches JS FSM
|
||||
- [ ] CRC/checksum calculation identical
|
||||
- [ ] Timeout values match
|
||||
- [ ] Error codes mapped correctly
|
||||
- [ ] Denomination tables match
|
||||
|
||||
### Protocol Commands
|
||||
- [ ] All protocol commands implemented (RESET, ENABLE, DISABLE, STACK, RETURN, STATUS — see ID003 spec or `lib/id003/id003rs232.js` from v8.1.5)
|
||||
- [ ] State machine matches the JS FSM (`lib/id003/id003fsm.js`)
|
||||
- [ ] CRC/checksum calculation byte-identical to the JS version
|
||||
- [ ] Timeout values match the JS source
|
||||
- [ ] Error codes mapped correctly to TypeScript enum/union
|
||||
- [ ] Denomination tables match (currency-specific)
|
||||
- [ ] Attribution comment at top of TS file points at the specific v8.1.5 file
|
||||
|
||||
### Protocol commands
|
||||
|
||||
#### ID003 (JCM)
|
||||
| Command | Code | JS Reference |
|
||||
|---------|------|--------------|
|
||||
| RESET | 0x40 | id003.js:reset() |
|
||||
|
||||
| Command | Code | JS Reference (v8.1.5) |
|
||||
|---|---|---|
|
||||
| RESET | 0x40 | id003rs232.js:reset() |
|
||||
| ENABLE | 0x13 | id003fsm.js:enable |
|
||||
| DISABLE | 0x14 | id003fsm.js:disable |
|
||||
| STACK | 0x15 | id003fsm.js:stack |
|
||||
| RETURN | 0x16 | id003fsm.js:return |
|
||||
| STATUS | 0x10 | id003fsm.js:poll |
|
||||
|
||||
#### Puloon
|
||||
| Command | Code | JS Reference |
|
||||
|---------|------|--------------|
|
||||
#### Puloon LCDM
|
||||
|
||||
| Command | Code | JS Reference (v8.1.5) |
|
||||
|---|---|---|
|
||||
| RESET | 0x44 | puloonrs232.js |
|
||||
| DISPENSE | 0x45 | puloonrs232.js |
|
||||
| STATUS | 0x46 | puloonrs232.js |
|
||||
|
||||
## Safety Review (Rust-specific)
|
||||
## Safety review (`safety` command)
|
||||
|
||||
### Memory Safety
|
||||
- [ ] No `unsafe` without justification comment
|
||||
- [ ] Buffer sizes validated before read/write
|
||||
- [ ] No panic paths in production code
|
||||
- [ ] Proper error propagation with Result
|
||||
### Type safety
|
||||
|
||||
### Concurrency Safety
|
||||
- [ ] Serial port access properly synchronized
|
||||
- [ ] Event channels bounded
|
||||
- [ ] No deadlock potential
|
||||
- [ ] Timeout on all blocking operations
|
||||
- [ ] No `any` types in driver public API
|
||||
- [ ] Discriminated unions for state machine states (not just string literals)
|
||||
- [ ] Error types are typed `Result<T, DriverError>` (or equivalent) at the driver boundary — never `throw` raw strings
|
||||
- [ ] Buffer reads are bounds-checked; offsets validated before slicing
|
||||
|
||||
### Hardware Safety
|
||||
- [ ] Dispenser amounts validated (prevent over-dispense)
|
||||
- [ ] Bill count cross-checked
|
||||
- [ ] Error states trigger hardware reset
|
||||
- [ ] Graceful degradation on hardware failure
|
||||
### Concurrency safety
|
||||
|
||||
## Mock Validation
|
||||
- [ ] Serial port access is single-writer (TypeScript single-threaded helps, but ensure no two `await write()` calls can interleave with each other's expected response)
|
||||
- [ ] Event emitters are bounded (max listeners set; back-pressure handled)
|
||||
- [ ] Timeouts on all blocking I/O (`Promise.race` with `setTimeout`)
|
||||
- [ ] No unbounded queues — a runaway validator polling loop shouldn't fill RAM
|
||||
|
||||
### Hardware safety
|
||||
|
||||
- [ ] Dispenser amounts validated against cassette inventory before dispensing
|
||||
- [ ] Bill count cross-checked between requested and dispensed
|
||||
- [ ] Error states trigger hardware reset (`disable + reset + enable` cycle for validators)
|
||||
- [ ] Graceful degradation: partial dispense reports correct dispensed-vs-rejected counts
|
||||
- [ ] Validator goes to `disable` on app shutdown — bills inserted while the app is dead get returned, not stacked uncounted
|
||||
|
||||
## Mock validation (`mock` command)
|
||||
|
||||
### Mock requirements
|
||||
|
||||
Mocks under `packages/hal/src/**/mock.ts` must simulate:
|
||||
|
||||
### Mock Requirements
|
||||
Mocks must simulate:
|
||||
1. Normal operation flow
|
||||
2. Error conditions
|
||||
3. Timing (realistic delays)
|
||||
4. State persistence
|
||||
2. Error conditions (jam, empty cassette, comms failure)
|
||||
3. Realistic timing (validator escrow takes ~600ms; dispenser cycle takes ~2s)
|
||||
4. State persistence within a session
|
||||
|
||||
### Mock test coverage
|
||||
|
||||
### Mock Test Coverage
|
||||
```
|
||||
/hal-check mock puloon
|
||||
```
|
||||
|
||||
Validates mock implements:
|
||||
- [ ] `connect()` - Success and failure paths
|
||||
- [ ] `dispense()` - Full and partial dispense
|
||||
- [ ] `reset()` - Error recovery
|
||||
- [ ] Event emission timing
|
||||
- [ ] Cassette state tracking
|
||||
|
||||
## Protocol Analysis
|
||||
- [ ] `connect()` — success and failure paths
|
||||
- [ ] `dispense()` — full, partial, and over-requested paths
|
||||
- [ ] `reset()` — error recovery
|
||||
- [ ] Event emission timing roughly matches real hardware
|
||||
- [ ] Cassette state tracking (count decrements properly)
|
||||
|
||||
## Protocol analysis (`protocol` command)
|
||||
|
||||
### Packet structure validation
|
||||
|
||||
### Packet Structure Validation
|
||||
```
|
||||
/hal-check protocol id003
|
||||
```
|
||||
|
||||
Compares Rust packet building with JS:
|
||||
```rust
|
||||
// Rust implementation
|
||||
fn build_packet(&self, data: &[u8]) -> Vec<u8> {
|
||||
let mut packet = vec![0x02]; // SYNC
|
||||
packet.push(data.len() as u8 + 4);
|
||||
packet.extend_from_slice(data);
|
||||
let crc = self.calculate_crc(&packet);
|
||||
packet.push((crc & 0xFF) as u8);
|
||||
packet.push((crc >> 8) as u8);
|
||||
packet
|
||||
Compares TS packet building with the v8.1.5 JS reference:
|
||||
|
||||
```typescript
|
||||
// TypeScript implementation
|
||||
function buildPacket(data: Uint8Array): Uint8Array {
|
||||
const packet = new Uint8Array(data.length + 4)
|
||||
packet[0] = 0x02 // SYNC
|
||||
packet[1] = data.length + 4 // length includes header + CRC
|
||||
packet.set(data, 2)
|
||||
const crc = calculateCrc(packet.slice(0, -2))
|
||||
packet[packet.length - 2] = crc & 0xff
|
||||
packet[packet.length - 1] = (crc >> 8) & 0xff
|
||||
return packet
|
||||
}
|
||||
```
|
||||
|
||||
Against JavaScript:
|
||||
Against the v8.1.5 JS reference (line numbers may vary by tag):
|
||||
|
||||
```javascript
|
||||
// lamassu-machine/lib/id003/id003rs232.js
|
||||
// lamassu-machine v8.1.5 — lib/id003/id003rs232.js
|
||||
function buildPacket(data) {
|
||||
const buf = Buffer.alloc(data.length + 4)
|
||||
buf[0] = 0x02 // SYNC
|
||||
|
|
@ -139,62 +168,72 @@ function buildPacket(data) {
|
|||
}
|
||||
```
|
||||
|
||||
## Output Format
|
||||
A discrepancy here (different CRC polynomial, different framing, different endianness) is a serious port bug.
|
||||
|
||||
## Output format
|
||||
|
||||
### Port validation
|
||||
|
||||
### Port Validation
|
||||
```markdown
|
||||
## HAL Port Validation: id003
|
||||
|
||||
### Command Coverage
|
||||
| Command | JS | Rust | Match |
|
||||
|---------|-------|------|-------|
|
||||
### Command coverage
|
||||
| Command | v8.1.5 JS | TS | Match |
|
||||
|---|---|---|---|
|
||||
| RESET | ✅ | ✅ | ✅ |
|
||||
| ENABLE | ✅ | ✅ | ✅ |
|
||||
| STATUS | ✅ | ⚠️ | Partial |
|
||||
|
||||
### Protocol Differences
|
||||
- [ ] `id003.rs:78` - CRC uses different polynomial than JS
|
||||
### Protocol differences
|
||||
- [ ] `id003-rs232.ts:78` — CRC uses different polynomial than v8.1.5 JS
|
||||
|
||||
### Missing Implementations
|
||||
- [ ] `HOLD` command not implemented (used in id003fsm.js:holdBill)
|
||||
### Missing implementations
|
||||
- [ ] HOLD command not implemented (v8.1.5 `id003fsm.js:holdBill`)
|
||||
|
||||
### Recommendations
|
||||
1. Verify CRC calculation against test vectors from JS
|
||||
2. Add HOLD command for escrow mode
|
||||
### Attribution
|
||||
- [x] File header points at `lib/id003/id003rs232.js` (v8.1.5)
|
||||
```
|
||||
|
||||
### Safety Review
|
||||
### Safety review
|
||||
|
||||
```markdown
|
||||
## HAL Safety Review: puloon
|
||||
|
||||
### Memory Safety
|
||||
- [x] No unsafe blocks
|
||||
- [x] Buffer bounds checked
|
||||
- [ ] `dispense()` at line 145 - unwrap() could panic
|
||||
### Type safety
|
||||
- [x] No `any` types in public surface
|
||||
- [x] Discriminated union for DispenserState
|
||||
|
||||
### Concurrency Safety
|
||||
- [x] Serial port mutex protected
|
||||
- [x] Event channel bounded (16)
|
||||
### Concurrency safety
|
||||
- [x] Single-writer over serial port
|
||||
- [x] Event emitter has bounded listener count
|
||||
- [ ] `puloon-rs232.ts:142` — no timeout on response wait; could hang forever
|
||||
|
||||
### Hardware Safety
|
||||
- [x] Amount validation in dispense()
|
||||
- [ ] No cassette empty check before dispense
|
||||
### Hardware safety
|
||||
- [x] Amount validated against cassette inventory in dispense()
|
||||
- [ ] `puloon.ts:178` — partial dispense doesn't update cassette inventory
|
||||
|
||||
### Critical Issues
|
||||
1. `puloon.rs:145` - Replace unwrap() with proper error handling
|
||||
2. `puloon.rs:178` - Add cassette level check before dispensing
|
||||
### Critical issues
|
||||
1. `puloon-rs232.ts:142` — wrap response wait in `Promise.race([response, timeout(5000)])`
|
||||
2. `puloon.ts:178` — decrement cassette count by actual dispensed, not requested
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
## Example usage
|
||||
|
||||
```
|
||||
/hal-check port id003
|
||||
```
|
||||
|
||||
```
|
||||
/hal-check safety packages/hal/src/dispensers/
|
||||
```
|
||||
|
||||
```
|
||||
/hal-check safety packages/hal/src/dispensers/puloon/
|
||||
/hal-check mock --all
|
||||
/hal-check protocol ebds # validates EBDS framing against v8.1.5 JS + MEI vendor spec
|
||||
```
|
||||
|
||||
## Related skills
|
||||
|
||||
- `/security` — broader Bitcoin/Lightning/ATM vulnerability scan
|
||||
- `/test` — runs HAL-specific tests, generates coverage reports
|
||||
- `/lightning-check` — validates the Lightning backend side (LNbits transport)
|
||||
|
||||
## Forbidden operations
|
||||
|
||||
- Diff or read `lamassu-machine` source at v8.1.6 or later. Only `v8.1.5` (and the historical commit range leading up to it) is permissible to reference.
|
||||
- "Backport" any fix or feature from v8.1.6+ JS sources into TypeScript. If a bug fix is needed, implement from the protocol spec or hardware traces.
|
||||
- Include attribution comments pointing at v8.1.6+ files even if the implementation is your own — readers should be able to trust file-header attributions as accurate.
|
||||
|
|
|
|||
|
|
@ -1,168 +1,208 @@
|
|||
# /lightning-check - Lightning.Pub Conformity Agent
|
||||
# /lightning-check — Lightning backend conformity
|
||||
|
||||
## Purpose
|
||||
Validate Lightning.Pub integration and CLINK protocol conformity.
|
||||
|
||||
Validate the LNbits nostr-transport integration in bitSpire — wire envelope, encryption, subscription handling, invoice/withdraw correctness. Replaces the original Lightning.Pub-flavored version of this skill; the LP backend was removed from `dev` in commit `a51d7af`.
|
||||
|
||||
> The `--clink` mode is preserved for validating CLINK kind-21003 management commands, which are the one piece of CLINK still wired on `dev` (operator dispense commands). CLINK kinds 21001/21002 are dormant on `dev`.
|
||||
|
||||
## Invocation
|
||||
|
||||
```
|
||||
/lightning-check [target] [--clink] [--wallet]
|
||||
/lightning-check [target] [--lnbits] [--lnurlw] [--clink]
|
||||
```
|
||||
|
||||
Where:
|
||||
- `target` - File or directory to check
|
||||
- `--clink` - Focus on CLINK offer/debit flow
|
||||
- `--wallet` - Focus on wallet operations
|
||||
|
||||
## Lightning.Pub Integration
|
||||
- `target` — file or directory to check (default: `packages/lnbits/src/` + `apps/machine/src/services/lightning.ts`)
|
||||
- `--lnbits` — focus on LNbits nostr-transport RPC + subscription surface
|
||||
- `--lnurlw` — focus on LNURL-withdraw cash-in flow (link creation, callback URL composition, subscription)
|
||||
- `--clink` — focus on the kind-21003 management surface (operator commands)
|
||||
|
||||
### Connection
|
||||
Lightning.Pub uses Nostr for all communication:
|
||||
## What to validate
|
||||
|
||||
### 1. Wire envelope (kind 21000)
|
||||
|
||||
Each RPC request/response is a kind-21000 event with NIP-44 v2-encrypted JSON in `content`. The plaintext shape is:
|
||||
|
||||
```jsonc
|
||||
// Request
|
||||
{
|
||||
"rpc_name": "create_invoice",
|
||||
"request_id": "create_invoice-7-ab12cd",
|
||||
"wallet_id": "<uuid>", // present for AUTH_WALLET RPCs
|
||||
"body": { /* RPC-specific */ },
|
||||
"query": { /* RPC-specific */ } // optional
|
||||
}
|
||||
|
||||
// One-shot reply
|
||||
{
|
||||
"status": "OK" | "ERROR",
|
||||
"request_id": "create_invoice-7-ab12cd",
|
||||
"data": { /* RPC-specific */ },
|
||||
"error": "..." // present when status==ERROR
|
||||
}
|
||||
|
||||
// Subscription push
|
||||
{
|
||||
"status": "OK",
|
||||
"request_id": "sub-9-ef34gh",
|
||||
"subscription_id": "<server-issued>",
|
||||
"data": { "payment": { /* Payment */ } } |
|
||||
{ "closed": true, "reason": "ttl"|"unsubscribed" }
|
||||
}
|
||||
```
|
||||
|
||||
Required checks:
|
||||
|
||||
- [ ] `request_id` is unique per outbound RPC (no reuse across sessions)
|
||||
- [ ] The pending map is keyed by `request_id` and cleaned up on resolve/reject
|
||||
- [ ] Subscription pushes are matched by `subscription_id` (top-level field, not in `data`)
|
||||
- [ ] The handler resolves the subscription's `subscriptionId` only after the ACK arrives — pre-ACK pushes (rare but possible) are handled
|
||||
- [ ] The reply listener decrypts with `decryptContentV2(identity, serverPubkey, ev.content)` and silently skips events that don't decrypt (wrong sender)
|
||||
- [ ] Events are signed with `finalizeEvent` from `nostr-tools` using the ATM's `identity.privateKey` — server identifies the calling account from the signature alone
|
||||
|
||||
### 2. Encryption (NIP-44 v2)
|
||||
|
||||
LNbits transport requires NIP-44 v2, NOT NIP-04. Required checks:
|
||||
|
||||
- [ ] All kind-21000 content is `encryptContentV2`/`decryptContentV2` (not `encryptContent`/`decryptContent`, which are the v1/NIP-04 paths kept around for CLINK compatibility)
|
||||
- [ ] The shared secret is derived from `identity.privateKey × serverPubkey` (or the reverse on the server side — symmetric)
|
||||
- [ ] No code logs ciphertext or shared secrets
|
||||
|
||||
### 3. Authentication model
|
||||
|
||||
LNbits derives the calling account from the event signature. There is **no admin token, no API key, no client cert** on the ATM. Required checks:
|
||||
|
||||
- [ ] No code stores or references an admin token
|
||||
- [ ] No code makes outbound HTTP to LNbits (other than via `VITE_LNBITS_HTTP_URL` for the LNURL-withdraw callback, which is the customer wallet's path — the ATM itself doesn't hit HTTP)
|
||||
- [ ] The ATM's `identity.privateKey` is loaded once from `/var/lib/bitspire/.env` and never logged
|
||||
|
||||
### 4. Cash-out flow (`--lnbits`)
|
||||
|
||||
In `services/lightning.ts → generateInvoice / watchInvoice`:
|
||||
|
||||
```typescript
|
||||
// Connection via nprofile
|
||||
const nprofile = 'nprofile1...' // Contains pubkey + relay hints
|
||||
// Generate
|
||||
const payment = await lnbits.createInvoice(walletId, {
|
||||
amount: amountSats,
|
||||
memo: 'bitSpire - Cash Out',
|
||||
unit: 'sat',
|
||||
})
|
||||
|
||||
// All operations are Nostr events to Lightning.Pub's pubkey
|
||||
await relay.publish({
|
||||
kind: 21002, // CLINK debit
|
||||
content: encrypted_request,
|
||||
tags: [['p', lightningPubPubkey]],
|
||||
// Watch — must filter by payment_hash to avoid acting on unrelated settlements
|
||||
const decoded = await lnbits.decodePayment(invoice)
|
||||
const paymentHash = decoded.payment_hash
|
||||
const subId = await lnbits.subscribePayments(walletId, {
|
||||
payment_hash: paymentHash,
|
||||
max_seconds: 600,
|
||||
}, push => {
|
||||
if (push.payment_hash !== paymentHash) return // belt-and-suspenders
|
||||
if (push.status !== 'success') return
|
||||
callback(push.preimage ?? 'payment-confirmed')
|
||||
})
|
||||
```
|
||||
|
||||
### Required Checks
|
||||
- [ ] Using correct event kinds (21001, 21002, 21003)
|
||||
- [ ] Proper NIP-44 encryption for requests
|
||||
- [ ] Handling async responses via subscription
|
||||
- [ ] Proper error handling for payment failures
|
||||
Required checks:
|
||||
|
||||
## CLINK Protocol
|
||||
- [ ] `subscribePayments` filter always specifies `payment_hash` for invoice watching (filter-less subscriptions are server-rejected; broader filters expose us to acting on unrelated settlements)
|
||||
- [ ] `max_seconds` is clamped — LNbits clamps server-side to [1, 600], so values outside that range are silently changed
|
||||
- [ ] The callback checks `push.payment_hash` matches the expected one (defense in depth)
|
||||
- [ ] The callback checks `push.status === 'success'` before treating as paid
|
||||
- [ ] On cleanup, `unsubscribe(walletId, subId)` is called
|
||||
|
||||
### Offer Flow (Kind 21001)
|
||||
```
|
||||
User scans noffer → Wallet sends 21002 → ATM responds with invoice → User pays
|
||||
### 5. Cash-in flow (`--lnurlw`)
|
||||
|
||||
In `services/lightning.ts → generateLnurlWithdraw`:
|
||||
|
||||
```typescript
|
||||
const link = await lnbits.createWithdrawLink(walletId, {
|
||||
title: '...',
|
||||
min_withdrawable: ctx.satsAmount,
|
||||
max_withdrawable: ctx.satsAmount,
|
||||
uses: 1,
|
||||
wait_time: 1,
|
||||
is_unique: false,
|
||||
})
|
||||
|
||||
const callbackUrl = `${CONFIG.lnbitsHttpUrl.replace(/\/+$/, '')}/withdraw/api/v1/lnurl/${link.unique_hash}`
|
||||
const lnurl = encodeLnurl(callbackUrl) // bech32 HRP="lnurl", uppercase
|
||||
|
||||
const subId = await lnbits.subscribePayments(walletId, {
|
||||
tag: 'withdraw',
|
||||
link_id: link.id,
|
||||
max_seconds: 600,
|
||||
}, push => { /* mark session claimed, dispense */ })
|
||||
```
|
||||
|
||||
Validation:
|
||||
- [ ] noffer encoding is valid
|
||||
- [ ] Relays included in offer
|
||||
- [ ] Price type correctly specified (fixed/variable/spontaneous)
|
||||
- [ ] Amount bounds validated
|
||||
Required checks:
|
||||
|
||||
### Debit Flow (Kind 21002)
|
||||
Request structure:
|
||||
```json
|
||||
{
|
||||
"method": "pay_invoice",
|
||||
"params": {
|
||||
"invoice": "lnbc...",
|
||||
"amount_msat": 100000
|
||||
}
|
||||
}
|
||||
```
|
||||
- [ ] `uses: 1` — single-use prevents replay
|
||||
- [ ] `min_withdrawable === max_withdrawable === ctx.satsAmount` — exact amount, no operator slippage
|
||||
- [ ] The LNURL is composed from `VITE_LNBITS_HTTP_URL` + `link.unique_hash` (the transport `lnurlw_create_link` returns `link.lnurl === null` — those fields are filled by HTTP views, not the create RPC)
|
||||
- [ ] bech32 encoding uses HRP `"lnurl"` and the result is uppercased per BOLT/LNURL convention
|
||||
- [ ] Subscription filter is `tag: 'withdraw'` + `link_id: link.id` — this is what the withdraw extension stamps on settlement events
|
||||
- [ ] On session abort, the cleanup closure both unsubscribes AND deletes the withdraw link (so a half-completed session doesn't leave a redeemable QR alive on LNbits)
|
||||
|
||||
Response structure:
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"preimage": "...",
|
||||
"fee_msat": 1000
|
||||
}
|
||||
}
|
||||
```
|
||||
### 6. Operator commands (`--clink`, kind-21003)
|
||||
|
||||
Validation:
|
||||
- [ ] Invoice format valid (BOLT11)
|
||||
- [ ] Amount matches invoice
|
||||
- [ ] Preimage verified against payment hash
|
||||
- [ ] Fee within acceptable bounds
|
||||
- [ ] Timeout handling
|
||||
The kind-21003 management surface is the one piece of CLINK still wired on `dev`. It's LP-independent (operators talk directly to the ATM over nostr) so it survived the migration.
|
||||
|
||||
### Manage Flow (Kind 21003)
|
||||
For operator commands:
|
||||
```json
|
||||
{
|
||||
"method": "get_balance",
|
||||
"params": {}
|
||||
}
|
||||
```
|
||||
Required checks:
|
||||
|
||||
Validation:
|
||||
- [ ] Only authorized operators can send
|
||||
- [ ] Commands are properly authenticated
|
||||
- [ ] Responses handled securely
|
||||
- [ ] `CLINKClient` is initialized with `operatorPubkey: CONFIG.operatorPubkeys` (no Lightning.Pub pubkey mixed in — that's LP-era)
|
||||
- [ ] `clink.onManagement` handler routes through `handleManagementCommand` which dispatches by `request.command` (manual dispense, status query, etc.)
|
||||
- [ ] Operator authentication is by pubkey allowlist — not a separate token
|
||||
- [ ] Sensitive ops (`dispenseCash`) only fire when the state machine is idle (`isIdle.value` check)
|
||||
|
||||
## Invoice Validation
|
||||
|
||||
### BOLT11 Checks
|
||||
- [ ] Valid bech32 encoding
|
||||
- [ ] Expiry not passed
|
||||
- [ ] Amount matches expected
|
||||
- [ ] Description hash valid (if used)
|
||||
- [ ] Payment hash extractable
|
||||
|
||||
### Security Checks
|
||||
- [ ] Never pay same invoice twice
|
||||
- [ ] Amount limits enforced
|
||||
- [ ] Rate limiting on payments
|
||||
- [ ] Proper logging (no sensitive data)
|
||||
|
||||
## Wallet Operations
|
||||
|
||||
### Balance Queries
|
||||
- [ ] Cached appropriately (not every render)
|
||||
- [ ] Error handling for offline
|
||||
- [ ] Display in correct units (sats, not msat)
|
||||
|
||||
### Invoice Generation
|
||||
- [ ] Unique payment hashes
|
||||
- [ ] Reasonable expiry times
|
||||
- [ ] Description for record-keeping
|
||||
- [ ] Amount in correct units
|
||||
|
||||
## Output Format
|
||||
## Output format
|
||||
|
||||
```markdown
|
||||
## Lightning.Pub Conformity: [target]
|
||||
## Lightning backend conformity: [target]
|
||||
|
||||
### CLINK Protocol
|
||||
| Flow | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| Offer (21001) | ✅ | |
|
||||
| Debit (21002) | ⚠️ | Missing timeout |
|
||||
| Manage (21003) | ✅ | |
|
||||
### LNbits transport
|
||||
| Concern | Status | Notes |
|
||||
|---|---|---|
|
||||
| Wire envelope | ✅ | |
|
||||
| NIP-44 v2 encryption | ✅ | |
|
||||
| Signature-only auth | ✅ | No admin tokens found |
|
||||
| Subscription handling | ⚠️ | Missing payment_hash filter on watchInvoice cleanup |
|
||||
|
||||
### Invoice Handling
|
||||
- [ ] `file:line` - Issue description
|
||||
### Cash-out / Cash-in flows
|
||||
- [ ] `file:line` — Issue description + fix
|
||||
|
||||
### Integration Issues
|
||||
- [ ] Description with fix suggestion
|
||||
### Operator (kind-21003)
|
||||
- [ ] `file:line` — Issue description
|
||||
|
||||
### Recommendations
|
||||
- [ ] Performance/UX improvements
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
## Example
|
||||
|
||||
```
|
||||
/lightning-check packages/lightning/src/ --clink
|
||||
/lightning-check packages/lnbits/src/ --lnbits
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```markdown
|
||||
## Lightning.Pub Conformity: packages/lightning/src/
|
||||
## Lightning backend conformity: packages/lnbits/src/
|
||||
|
||||
### CLINK Protocol
|
||||
| Flow | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| Offer (21001) | ✅ | Properly encoded |
|
||||
| Debit (21002) | ⚠️ | No timeout handling |
|
||||
### LNbits transport
|
||||
| Concern | Status | Notes |
|
||||
|---|---|---|
|
||||
| Wire envelope | ✅ | request_id uniqueness OK, pending map cleaned up on timeout |
|
||||
| NIP-44 v2 encryption | ✅ | encryptContentV2/decryptContentV2 used exclusively |
|
||||
| Subscription handling | ✅ | subscriptionId resolved after ACK, push handlers race-safe |
|
||||
| Cleanup | ⚠️ | unsubscribe error swallowed silently — log at warn level |
|
||||
|
||||
### Issues Found
|
||||
- [ ] `client.ts:89` - Debit request has no timeout, could hang indefinitely
|
||||
- [ ] `client.ts:112` - Payment hash not verified against preimage
|
||||
|
||||
### Recommendations
|
||||
- Add 30-second timeout for CLINK debit requests
|
||||
- Implement invoice deduplication to prevent double-pay
|
||||
### Cash-out flow
|
||||
- [ ] `packages/lnbits/src/client.ts:212` — watchInvoice resolves on first success push but doesn't unsubscribe until subIdRef is assigned; if push lands before subscribePayments resolves, leaks the subscription
|
||||
```
|
||||
|
||||
## Related skills
|
||||
|
||||
- `/nostr-check` — validates NIP-01 / NIP-44 conformance at the nostr-client layer
|
||||
- `/security` — Bitcoin/Lightning vuln audit
|
||||
- `/test` — runs vitest + flow validation
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ Location: `*.test.ts` or `*.spec.ts` alongside source files
|
|||
|
||||
```bash
|
||||
pnpm test # All tests
|
||||
pnpm test --filter @lamassu/nostr-client # Specific package
|
||||
pnpm test --filter @bitSpire/nostr-client # Specific package
|
||||
```
|
||||
|
||||
### 2. Integration Tests
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ deploy/nixos/
|
|||
├── configuration.nix # Base system (NixOS 24.05, locale, kernel, packages, lamassu user)
|
||||
├── live.nix # Live USB variant (squashfs + tmpfs root) — used by mkLiveConfig
|
||||
├── hardware/
|
||||
│ ├── douro.nix # Dell OptiPlex 9030 AIO (batm3 sibling; SATA SSD, eGalax touch)
|
||||
│ ├── batm3.nix # GeneralBytes BATM3 (Dell board, WireGuard wired in)
|
||||
│ ├── douro.nix # Dell OptiPlex 9030 AIO (stock Douro motherboard; SATA SSD, eGalax touch)
|
||||
│ ├── batm3.nix # GeneralBytes BATM3 chassis with a Dell OptiPlex 9030 AIO grafted in (custom mod; WireGuard wired in)
|
||||
│ └── upboard.nix # Aaeon UP Board (Sintra + tejo; eMMC root via sdhci-acpi + mmc_block)
|
||||
├── udev/
|
||||
│ └── 99-lamassu-hardware.rules # additional udev rules (loaded via configuration.nix)
|
||||
|
|
|
|||
|
|
@ -134,7 +134,24 @@ If a compelling reason for Rust emerges (it likely won't), the HAL interface is
|
|||
|
||||
## References
|
||||
|
||||
- `packages/hal/` - TypeScript HAL implementation
|
||||
- `packages/state-machine/src/types.ts` - ATMServices interface
|
||||
- `hardware/codebase/upboard/sintra/device_config.json` - Sintra device paths
|
||||
- lamassu-machine `lib/id003/`, `lib/f56/` - Original JS drivers
|
||||
- `packages/hal/` — TypeScript HAL implementation
|
||||
- `packages/state-machine/src/types.ts` — ATMServices interface
|
||||
- `deploy/nixos/hardware/upboard.nix` — Sintra device paths and udev rules
|
||||
- lamassu-machine `lib/id003/`, `lib/f56/` — Original JS drivers (v8.1.5 release line and earlier; see postscript below)
|
||||
|
||||
---
|
||||
|
||||
## Postscript (2026-05-13)
|
||||
|
||||
This ADR is still in effect — TypeScript HAL in Electron remains the right call for the same reasons documented above. A few clarifications for readers in 2026 and beyond:
|
||||
|
||||
1. **Package naming.** The HAL package was originally `@lamassu/hal` (per the line above in "Completed"). It was renamed to `@bitSpire/hal` during the project rename from `lamassu-next` to `bitSpire` (commit `924844f` in the 2a sweep). All references to the package scope should now read `@bitSpire/hal`.
|
||||
|
||||
2. **Source-tree provenance, post-Lamassu license change.** The "Original JS drivers" reference above refers to lamassu-machine's `lib/id003/` and `lib/f56/` directories in the **v8.1.5 release line and earlier**, which were published under a fully-open license. Lamassu Industries AG transitioned to a proprietary source-available license on 2024-01-26 (v8.1.6+). bitSpire incorporates no code from v8.1.6 or later; the protocol-level reimplementations we have are derived from v8.1.5 + protocol specs published by JCM, Fujitsu, etc. See [CLAUDE.md → Provenance + legal status](../../CLAUDE.md#provenance--legal-status) for the operating rules going forward.
|
||||
|
||||
3. **Implementation status.** The "Remaining Work" list above is complete:
|
||||
- `apps/machine` runs on Electron (the original Tauri scaffolding was removed early on)
|
||||
- HAL is wired to the state machine via the ATMServices interface
|
||||
- Device configuration ships via `deploy/nixos/hardware/upboard.nix` + the `services.bitspire` NixOS module
|
||||
- First successful Sintra hardware integration test ran on 2026-05-13 (cash-out flow verified end-to-end against a regtest LNbits instance)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,338 +1,306 @@
|
|||
# Architecture Comparison: Nostr-Native vs Traditional Lamassu
|
||||
# Architecture Comparison: bitSpire vs Traditional Lamassu
|
||||
|
||||
This document compares the Nostr-native Lightning ATM architecture (Lamassu Next) with the traditional lamassu-server/machine implementation to help operators evaluate the transition.
|
||||
This document compares the bitSpire architecture — a Nostr-native Lightning ATM talking to LNbits over the nostr-native-transport — with the traditional `lamassu-server` + `lamassu-machine` architecture. The goal is to help an operator evaluating an open-source Lightning ATM stack understand what they're choosing between.
|
||||
|
||||
## Executive Summary
|
||||
> A note on prior art: bitSpire's hardware drivers and cash-flow state machine derive from Lamassu Industries AG's open-source `lamassu-machine` and `lamassu-server` projects up to and including the v8.1.5 release line (the last published under a fully-open license). Lamassu [transitioned to a proprietary source-available license](https://blog.lamassu.is/updates-to-our-lamassu-software-license/) on 2024-01-26; bitSpire does **not** incorporate code from v8.1.6 or later and is not affiliated with Lamassu Industries AG. See the [Acknowledgements](../README.md#acknowledgements) section of the top-level README.
|
||||
|
||||
| Aspect | Traditional (lamassu-server) | Nostr-Native (lamassu-next) |
|
||||
| ------------------------ | ---------------------------- | ---------------------------------- |
|
||||
| **Communication** | Custom WebSocket protocol | Nostr relay (NIP-01) |
|
||||
| **Identity** | Server-issued credentials | Cryptographic keypairs (npub/nsec) |
|
||||
| **Account System** | PostgreSQL + custom auth | Lightning.Pub (Nostr-native) |
|
||||
| **Payment Protocol** | Direct LND RPC | CLINK protocol (kinds 21001-21003) |
|
||||
| **Infrastructure** | Server + DB + Admin UI | Relay (optional self-hosted) |
|
||||
| **Wallet Compatibility** | Lamassu-specific | Any CLINK-compatible wallet |
|
||||
## Executive summary
|
||||
|
||||
| Aspect | Traditional (`lamassu-server` ≤ v8.1.5) | bitSpire |
|
||||
|---|---|---|
|
||||
| **Software license** | AGPL-3.0 (v8.1.5); v8.1.6+ is proprietary SLA | AGPL-3.0 |
|
||||
| **Communication** | Custom WebSocket + GraphQL over HTTPS | Nostr kind-21000 events (NIP-01, NIP-44 v2) over a relay |
|
||||
| **Identity** | Server-issued client certs | Nostr keypairs (`nsec`/`npub`) |
|
||||
| **Account system** | PostgreSQL + custom auth | LNbits wallet auto-created from ATM's nostr pubkey |
|
||||
| **Payment protocol** | Direct LND RPC | BOLT11 (cash-out) + LNURL-withdraw (cash-in) |
|
||||
| **Server infrastructure** | App server + DB + Admin UI | A relay + an LNbits instance (both can be one container) |
|
||||
| **Wallet compatibility** | Any Lightning wallet (cash-out); custom flow for cash-in | Any Lightning wallet (BOLT11 + LNURL-withdraw — both widely supported) |
|
||||
| **State location** | Centralized in PostgreSQL | Distributed: LNbits holds Lightning state, ATM holds session state |
|
||||
|
||||
---
|
||||
|
||||
## Traditional Architecture (lamassu-server/machine)
|
||||
|
||||
### Overview
|
||||
## Traditional architecture (lamassu-server / lamassu-machine, v8.1.5)
|
||||
|
||||
```
|
||||
┌─────────────────┐ WebSocket ┌─────────────────────┐
|
||||
│ ATM Machine │◄─────────────────────────►│ lamassu-server │
|
||||
│ (lamassu-machine)│ │ │
|
||||
└─────────────────┘ │ ┌───────────────┐ │
|
||||
│ │ PostgreSQL │ │
|
||||
┌─────────────────┐ HTTPS │ └───────────────┘ │
|
||||
│ Admin UI │◄─────────────────────────►│ │
|
||||
│ (React SPA) │ GraphQL │ ┌───────────────┐ │
|
||||
└─────────────────┘ │ │ LND │ │
|
||||
│ └───────────────┘ │
|
||||
┌─────────────────┐ │ │
|
||||
│ Customer Wallet │◄── Lightning Invoice ─────│ ┌───────────────┐ │
|
||||
│ (any LN wallet) │ │ │ Compliance │ │
|
||||
└─────────────────┘ │ │ Services │ │
|
||||
│ └───────────────┘ │
|
||||
└─────────────────────┘
|
||||
┌──────────────────┐ WebSocket ┌─────────────────────┐
|
||||
│ ATM (kiosk) │◄──────────────────────────►│ lamassu-server │
|
||||
│ lamassu-machine │ client-cert auth │ │
|
||||
└──────────────────┘ │ ┌───────────────┐ │
|
||||
│ │ PostgreSQL │ │
|
||||
┌──────────────────┐ HTTPS │ └───────────────┘ │
|
||||
│ Admin UI │◄──────────────────────────►│ │
|
||||
│ (React SPA) │ GraphQL │ ┌───────────────┐ │
|
||||
└──────────────────┘ │ │ LND │ │
|
||||
│ └───────────────┘ │
|
||||
┌──────────────────┐ │ │
|
||||
│ Customer Wallet │◄── Lightning Invoice ──────│ ┌───────────────┐ │
|
||||
│ (any LN wallet) │ │ │ Compliance │ │
|
||||
└──────────────────┘ │ │ Services │ │
|
||||
│ └───────────────┘ │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
| Component | Description |
|
||||
| ------------------- | ------------------------------------------------ |
|
||||
| **lamassu-server** | Node.js Express + Apollo GraphQL backend |
|
||||
| **lamassu-machine** | Node.js kiosk application with hardware drivers |
|
||||
| **PostgreSQL** | Central database for transactions, users, config |
|
||||
| **Admin UI** | React dashboard for fleet management |
|
||||
| **LND** | Lightning Network Daemon for payments |
|
||||
| Component | Description |
|
||||
|---|---|
|
||||
| `lamassu-server` | Node.js (Express + Apollo GraphQL) backend |
|
||||
| `lamassu-machine` | Node.js kiosk app with hardware drivers |
|
||||
| PostgreSQL | Central DB for transactions, users, config |
|
||||
| Admin UI | React dashboard for fleet management |
|
||||
| LND | Lightning Network Daemon for payments |
|
||||
|
||||
### Communication Flow
|
||||
### Cash-out flow
|
||||
|
||||
1. Machine boots and authenticates with server via client certificate
|
||||
2. Server pushes configuration and state via WebSocket
|
||||
3. Customer initiates transaction at machine
|
||||
4. Machine requests invoice/address from server
|
||||
1. Machine boots, authenticates via client cert
|
||||
2. Server pushes config + state over WebSocket
|
||||
3. Customer initiates cash-out at machine
|
||||
4. Machine asks server for an invoice
|
||||
5. Server creates invoice via LND, stores in PostgreSQL
|
||||
6. Customer pays invoice
|
||||
7. Server detects payment, updates database
|
||||
8. Server notifies machine to dispense cash
|
||||
9. Transaction logged in PostgreSQL for compliance
|
||||
6. Customer pays the invoice
|
||||
7. Server polls LND, detects payment, updates DB
|
||||
8. Server notifies machine to dispense
|
||||
9. Compliance services log the transaction
|
||||
|
||||
### Characteristics
|
||||
|
||||
- **Centralized**: All state lives in server's PostgreSQL
|
||||
- **Operator-managed**: Requires significant infrastructure
|
||||
- **Custom protocol**: WebSocket messages are Lamassu-specific
|
||||
- **Tight coupling**: Machine depends entirely on server availability
|
||||
- **Compliance-ready**: Built-in KYC/AML features
|
||||
- **Centralized**: all state in `lamassu-server`'s PostgreSQL
|
||||
- **Operator-managed**: requires real infrastructure (server, DB, certs, monitoring)
|
||||
- **Tightly coupled**: machine doesn't transact if server is unreachable
|
||||
- **Compliance-ready**: KYC/AML hooks built in (relevant in regulated jurisdictions)
|
||||
|
||||
---
|
||||
|
||||
## Nostr-Native Architecture (lamassu-next)
|
||||
|
||||
### Overview
|
||||
## bitSpire architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ ATM Machine │ │ Customer │
|
||||
│ (npub_atm) │ │ Wallet │
|
||||
└────────┬────────┘ │ (npub_user) │
|
||||
│ └────────┬────────┘
|
||||
│ NIP-01 events │
|
||||
│ NIP-44 encrypted │ CLINK protocol
|
||||
▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Nostr Relay │
|
||||
│ (strfry, nostr-rs, etc.) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│ │
|
||||
│ Kind 21000 (RPC) │
|
||||
│ Kind 21001-21003 (CLINK) │
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ Lightning.Pub │ │ Lightning.Pub │
|
||||
│ (ATM account) │◄── Lightning Payment ─────►│ (User account) │
|
||||
└────────┬────────┘ └─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ LND │
|
||||
└─────────────────┘
|
||||
┌──────────────────┐ ┌──────────────────┐
|
||||
│ ATM (kiosk) │ │ Customer wallet │
|
||||
│ npub_atm │ │ (any LN wallet) │
|
||||
└────────┬─────────┘ └────────┬─────────┘
|
||||
│ kind-21000 │
|
||||
│ NIP-44 v2 encrypted │ BOLT11
|
||||
▼ │ LNURL-withdraw
|
||||
┌───────────────────────────────┐ │
|
||||
│ Nostr Relay │ │
|
||||
│ (LNbits' bundled │ │
|
||||
│ nostrrelay extension, or │ │
|
||||
│ any standalone relay) │ │
|
||||
└────────────────┬──────────────┘ │
|
||||
│ │
|
||||
│ kind-21000 RPC + push │
|
||||
▼ │
|
||||
┌───────────────────────┐ ◄──────────────────────┘
|
||||
│ LNbits │ BOLT11 settle / LNURLw redeem
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ FakeWallet (dev) │ │
|
||||
│ │ or LND/CLN/etc. │ │
|
||||
│ └──────────────────┘ │
|
||||
└───────────────────────┘
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
| Component | Description |
|
||||
| ------------------- | ----------------------------------------------- |
|
||||
| **Nostr Relay** | Message broker (strfry, nostr-rs-relay) |
|
||||
| **Lightning.Pub** | Nostr-native account system wrapping LND |
|
||||
| **ATM Machine** | Tauri + Vue 3 kiosk identified by npub |
|
||||
| **Customer Wallet** | Any CLINK-compatible wallet (ShockWallet, etc.) |
|
||||
| Component | Description |
|
||||
|---|---|
|
||||
| Nostr relay | Message broker. In the dev compose, this is LNbits's bundled `nostrrelay` extension at `ws://<host>:5001/nostrrelay/test`. Production can use any standalone relay both peers subscribe to |
|
||||
| LNbits | Lightning wallet platform; runs the `nostr-native-transport` branch. Auto-creates a wallet on first contact from any nostr pubkey |
|
||||
| ATM machine | Electron + Vue 3 kiosk identified by its `npub`. Signing key IS the credential to LNbits |
|
||||
| Customer wallet | Any LN wallet that handles BOLT11 (cash-out) and LNURL-withdraw (cash-in) — most do |
|
||||
|
||||
### Communication Flow (Cash-In Example)
|
||||
### Cash-out flow (customer pays ATM, gets cash)
|
||||
|
||||
1. ATM generates ephemeral keypair or uses persistent npub
|
||||
2. Customer scans QR containing `ndebit` (debit authorization)
|
||||
3. Customer's wallet connects to relay, finds ATM's Lightning.Pub
|
||||
4. Wallet creates invoice via Lightning.Pub RPC (kind 21000)
|
||||
5. Wallet sends debit request (kind 21002) to ATM's npub
|
||||
6. ATM verifies request, approves payment
|
||||
7. Lightning.Pub pays invoice, returns preimage
|
||||
8. ATM detects payment confirmation
|
||||
9. ATM dispenses cash
|
||||
10. Transaction recorded as Nostr event (optional)
|
||||
1. ATM publishes a kind-21000 event to LNbits: `{rpc_name: "create_invoice", body: {amount: <sats>, memo: "..."}}`
|
||||
2. LNbits replies on the same relay with the BOLT11 invoice
|
||||
3. ATM displays the BOLT11 as a QR
|
||||
4. Customer scans, pays from any LN wallet
|
||||
5. LNbits's Lightning backend (FakeWallet / LND / CLN / etc.) settles the payment
|
||||
6. LNbits pushes a kind-21000 settlement event to the ATM, filtered by `payment_hash`
|
||||
7. ATM dispenses cash
|
||||
|
||||
### Cash-in flow (customer hands ATM cash, gets sats)
|
||||
|
||||
1. ATM publishes `lnurlw_create_link` (kind-21000) to LNbits with `{uses: 1, max_withdrawable: <sats>}`
|
||||
2. LNbits replies with a `WithdrawLink` (a `unique_hash` plus metadata)
|
||||
3. ATM composes the callback URL: `{LNBITS_HTTP_URL}/withdraw/api/v1/lnurl/{unique_hash}` and bech32-encodes it as an LNURL
|
||||
4. ATM displays the LNURL as a QR
|
||||
5. Customer scans with any LNURL-withdraw-capable wallet, redeems it
|
||||
6. LNbits settles the withdrawal via its Lightning backend
|
||||
7. LNbits pushes a kind-21000 settlement event to the ATM, filtered by `tag="withdraw"` + `link_id`
|
||||
8. ATM marks the session complete (cash already physically accepted earlier in the flow)
|
||||
|
||||
### Characteristics
|
||||
|
||||
- **Decentralized**: No single server owns state
|
||||
- **Interoperable**: Standard protocols (Nostr, CLINK, Lightning)
|
||||
- **Keypair identity**: ATM is identified by npub, not server account
|
||||
- **Loose coupling**: ATM can work with any relay/Lightning.Pub
|
||||
- **Privacy-first**: No central transaction logs required
|
||||
- **Decentralized message layer**: any nostr relay works, no proprietary protocol
|
||||
- **No admin tokens on the kiosk**: the ATM's signing key IS the credential — there are no HTTP API tokens or client certs to leak. LNbits derives the calling identity from the event signature.
|
||||
- **Loose coupling**: ATM and LNbits don't need to share a network segment; only a relay both reach
|
||||
- **Privacy-first**: no central transaction logs unless the operator builds them. LNbits stores its own ledger, but bitSpire writes no user-identifying data anywhere on its side.
|
||||
|
||||
---
|
||||
|
||||
## Detailed Comparison
|
||||
## Detailed comparison
|
||||
|
||||
### 1. Infrastructure Requirements
|
||||
### 1. Infrastructure requirements
|
||||
|
||||
| Requirement | Traditional | Nostr-Native |
|
||||
| -------------------- | ------------------------------------ | -------------------------------- |
|
||||
| **Server Hardware** | Dedicated server (4+ GB RAM, SSD) | Optional (can use public relays) |
|
||||
| **Database** | PostgreSQL (backup, maintenance) | None required |
|
||||
| **SSL Certificates** | Required (client certs for machines) | Not required |
|
||||
| **Domain Name** | Required | Optional |
|
||||
| **Static IP** | Recommended | Not required |
|
||||
| **Firewall Rules** | Complex (multiple ports) | Simple (outbound WebSocket only) |
|
||||
| Requirement | Traditional | bitSpire |
|
||||
|---|---|---|
|
||||
| Server hardware | Dedicated server (4+ GB RAM, SSD) | An LNbits instance (1 GB RAM is plenty in production) |
|
||||
| Database | PostgreSQL (operator managed) | LNbits's SQLite (or its configured DB; LNbits manages it) |
|
||||
| Client TLS | Required (client certs per machine) | Not required (signature-based auth over a nostr relay) |
|
||||
| Domain | Required | Optional (only if exposing LNbits HTTP for LNURL redemption from the public internet) |
|
||||
| Static IP | Recommended | Not required |
|
||||
| Firewall | Multiple inbound ports | Outbound WebSocket to a relay; inbound only on the LNbits HTTP port for LNURL callbacks |
|
||||
|
||||
**Advantage: Nostr-Native** - Dramatically simpler infrastructure. An operator can start with public relays and self-host later.
|
||||
**Edge: bitSpire.** A single-container LNbits with FakeWallet is enough for an operator to evaluate the whole stack end-to-end.
|
||||
|
||||
### 2. Security Model
|
||||
### 2. Security model
|
||||
|
||||
| Aspect | Traditional | Nostr-Native |
|
||||
| -------------------- | ------------------------------- | ------------------------ |
|
||||
| **Machine Auth** | Client certificates | Keypair (nsec) |
|
||||
| **Message Security** | TLS | NIP-44 encryption |
|
||||
| **Identity Theft** | Steal cert + key | Steal nsec only |
|
||||
| **Compromise Scope** | All machines if server breached | Individual machine only |
|
||||
| **Key Storage** | Server manages | Machine manages own keys |
|
||||
| Aspect | Traditional | bitSpire |
|
||||
|---|---|---|
|
||||
| Machine auth | Client certificates | Nostr signing key |
|
||||
| Transport security | TLS | NIP-44 v2 (XChaCha20 + HMAC-SHA256) |
|
||||
| Compromise scope | Server breach exposes all machines | One machine's `nsec` compromises only that machine |
|
||||
| Key storage | Server manages | Machine manages its own keys (`/var/lib/bitspire/.env`, mode 0600) |
|
||||
| Revocation | CA-style cert revocation | Operator can stop honoring a pubkey at the LNbits layer; no fleet-wide blast radius |
|
||||
|
||||
**Advantage: Nostr-Native** - Compromise of one machine doesn't affect others. No central point of failure.
|
||||
**Edge: bitSpire.** No central authority that, if compromised, exposes the whole fleet. Per-machine compromise stays contained.
|
||||
|
||||
### 3. Payment Flow
|
||||
### 3. Payment flow
|
||||
|
||||
| Aspect | Traditional | Nostr-Native |
|
||||
| ------------------------ | ---------------------- | ------------------------------ |
|
||||
| **Invoice Creation** | Server creates via LND | Lightning.Pub via Nostr RPC |
|
||||
| **Payment Detection** | Server polls LND | Subscription to payment events |
|
||||
| **Wallet Compatibility** | Any Lightning wallet | CLINK-compatible wallets |
|
||||
| **Offline Capability** | None | Cashu ecash (planned) |
|
||||
| **Payment Protocol** | BOLT11 only | BOLT11 + CLINK + noffer |
|
||||
| Aspect | Traditional | bitSpire |
|
||||
|---|---|---|
|
||||
| Invoice creation | Server creates via LND | LNbits creates via its Lightning backend, returned over kind-21000 |
|
||||
| Payment detection | Server polls LND | LNbits pushes a settlement event over kind-21000 (`subscribe_payments` filtered by `payment_hash`) |
|
||||
| Cash-in detection | Custom flow | LNbits pushes a `tag="withdraw"` + `link_id` event when the LNURL is redeemed |
|
||||
| Wallet compatibility | Any BOLT11 wallet | Any BOLT11 wallet (cash-out) + any LNURL-withdraw wallet (cash-in) |
|
||||
| Offline capability | None | Cashu ecash (placeholder package, not yet wired) |
|
||||
|
||||
**Mixed**: Traditional has broader wallet compatibility today. Nostr-Native enables advanced features (ndebit, noffer) but requires CLINK wallets.
|
||||
**Tied.** Both architectures end up using BOLT11 + LNURL-withdraw at the customer-wallet boundary. The difference is on the ATM side: traditional polls a server it owns; bitSpire subscribes to a push channel on a wallet it shares with anyone who can reach the relay.
|
||||
|
||||
### 4. Operator Experience
|
||||
### 4. Operator experience
|
||||
|
||||
| Aspect | Traditional | Nostr-Native |
|
||||
| ----------------------- | ------------------------------- | -------------------------- |
|
||||
| **Setup Time** | Hours (server, DB, certs) | Minutes (connect to relay) |
|
||||
| **Maintenance** | DB backups, updates, monitoring | Minimal |
|
||||
| **Fleet Management** | Admin UI (full-featured) | Dashboard (planned) |
|
||||
| **Transaction History** | PostgreSQL queries | Nostr event queries |
|
||||
| **Configuration** | Server-pushed | Local or relay-stored |
|
||||
| Aspect | Traditional | bitSpire |
|
||||
|---|---|---|
|
||||
| Setup time | Hours (provision server, DB, TLS, cert each machine) | Minutes (run an LNbits instance + a relay; flash + provision each ATM) |
|
||||
| Maintenance | DB backups, server updates, monitoring | Update LNbits + the bitSpire ATMs (the latter auto-upgrades from the `dev` branch nightly) |
|
||||
| Fleet management | Mature Admin UI | Per-ATM `journalctl`; centralized dashboard is planned, not built |
|
||||
| Transaction history | PostgreSQL queries | Per-ATM `/var/lib/bitspire/state.db` (SQLite); operator-side aggregation is on them to build |
|
||||
| Configuration | Server-pushed | Per-ATM `.env` written via `provision-atm.sh` |
|
||||
|
||||
**Mixed**: Traditional has mature tooling. Nostr-Native is simpler but dashboard is still in development.
|
||||
**Mixed.** Traditional has mature operator tooling. bitSpire is simpler to stand up but the fleet-management story is currently "ssh + scripts" rather than a UI.
|
||||
|
||||
### 5. Customer Experience
|
||||
### 5. Customer experience
|
||||
|
||||
| Aspect | Traditional | Nostr-Native |
|
||||
| ----------------------- | ------------------------ | ----------------------- |
|
||||
| **Cash-In** | Scan invoice, pay | Scan ndebit, authorize |
|
||||
| **Cash-Out** | Enter phone, receive SMS | Scan noffer, receive |
|
||||
| **Wallet Requirements** | Any Lightning wallet | CLINK-compatible wallet |
|
||||
| **Account Required** | Sometimes (compliance) | Never |
|
||||
| **Transaction Speed** | ~10 seconds | ~5 seconds |
|
||||
| Aspect | Traditional | bitSpire |
|
||||
|---|---|---|
|
||||
| Cash-out | Scan BOLT11, pay | Scan BOLT11, pay |
|
||||
| Cash-in | Varies by deployment (LNURL-withdraw common, sometimes custom) | Scan LNURL-withdraw QR, redeem |
|
||||
| Wallet requirements | Any LN wallet for cash-out | Any LN wallet for cash-out + any LNURL-w wallet for cash-in |
|
||||
| Account required | Sometimes (compliance) | Never |
|
||||
| Transaction speed | ~10 seconds | ~3-5 seconds (push-based; no server polling delay) |
|
||||
|
||||
**Advantage: Nostr-Native** - Faster, more private, no accounts. But requires specific wallet support.
|
||||
**Tied on protocol; slight edge to bitSpire on speed** because LNbits push events skip the polling cycle that traditional setups have between LND and the application server.
|
||||
|
||||
### 6. Compliance & Regulation
|
||||
### 6. Compliance & regulation
|
||||
|
||||
| Aspect | Traditional | Nostr-Native |
|
||||
| -------------------- | ----------------------- | --------------------- |
|
||||
| **KYC Integration** | Built-in | Not included |
|
||||
| **Transaction Logs** | PostgreSQL | Optional Nostr events |
|
||||
| **Audit Trail** | Comprehensive | Operator-defined |
|
||||
| **Reporting** | Admin UI exports | Custom tooling needed |
|
||||
| **Regulatory Fit** | Designed for compliance | Privacy-first design |
|
||||
| Aspect | Traditional | bitSpire |
|
||||
|---|---|---|
|
||||
| KYC integration | Built-in | Not included |
|
||||
| Transaction logs | PostgreSQL (server) | Per-ATM SQLite + whatever LNbits records on its side |
|
||||
| Audit trail | Comprehensive | Operator-defined; LNbits's ledger is the canonical Lightning-side record |
|
||||
| Reporting | Admin UI exports | Custom tooling needed |
|
||||
| Regulatory fit | Designed for KYC/AML jurisdictions | Privacy-first design; suits jurisdictions without identity-collection requirements |
|
||||
|
||||
**Advantage: Traditional** - If you need KYC/AML, traditional is ready. Nostr-Native assumes jurisdictions without these requirements.
|
||||
**Edge: Traditional** if you need built-in compliance. bitSpire targets KYC-free operation; operators in regulated jurisdictions would need to add compliance hooks themselves.
|
||||
|
||||
### 7. Resilience & Availability
|
||||
### 7. Resilience & availability
|
||||
|
||||
| Aspect | Traditional | Nostr-Native |
|
||||
| --------------------------- | ---------------------- | ------------------------- |
|
||||
| **Server Down** | All machines offline | Machines continue working |
|
||||
| **Network Partition** | Transactions fail | Can use multiple relays |
|
||||
| **Database Corruption** | Catastrophic | No database to corrupt |
|
||||
| **Recovery** | Restore from backup | Re-sync from relay |
|
||||
| **Geographic Distribution** | Single server location | Relay anywhere |
|
||||
| Aspect | Traditional | bitSpire |
|
||||
|---|---|---|
|
||||
| Server down | All machines stop transacting | Machines stop transacting if LNbits is down; relay outage means RPCs hang |
|
||||
| Network partition | Transactions fail | Can connect to multiple relays for failover |
|
||||
| Database corruption | Catastrophic (cascading impact across fleet) | LNbits-side issue affects only the affected operator's wallet; per-ATM SQLite is local and independent |
|
||||
| Recovery | Restore from backup | LNbits-side restore is its concern; ATMs come back up clean by re-reading `.env` |
|
||||
| Geographic distribution | Single server | LNbits can be regional; relays can be anywhere |
|
||||
|
||||
**Advantage: Nostr-Native** - No single point of failure. Machines are autonomous.
|
||||
**Edge: bitSpire** for blast-radius reasons. LNbits-side failures are no worse than `lamassu-server`-side failures, but bitSpire's per-ATM state is genuinely isolated.
|
||||
|
||||
### 8. Development & Extensibility
|
||||
### 8. Development & extensibility
|
||||
|
||||
| Aspect | Traditional | Nostr-Native |
|
||||
| --------------------------- | ----------------- | ------------------------- |
|
||||
| **Codebase** | Large monolith | Modular packages |
|
||||
| **Protocol** | Proprietary | Open standards |
|
||||
| **Third-party Integration** | Custom API needed | Standard Nostr/CLINK |
|
||||
| **Community** | Lamassu operators | Nostr + Bitcoin ecosystem |
|
||||
| **Forkability** | Complex | Straightforward |
|
||||
| Aspect | Traditional | bitSpire |
|
||||
|---|---|---|
|
||||
| Codebase | Large monolith (`lamassu-server` is hundreds of files) | Modular workspace; ATM, transport client, HAL, state machine are separate packages |
|
||||
| Protocol | Custom WebSocket schema | NIP-01 + NIP-44 v2 + a thin documented kind-21000 envelope |
|
||||
| Third-party integration | Custom API needed | Any nostr/Lightning client can talk to LNbits over the transport |
|
||||
| Community | Lamassu operators (and ex-operators after the license change) | Nostr + Bitcoin ecosystem; LNbits community |
|
||||
| Forkability | Complex (server + machine are coupled) | Straightforward (replace any one package without touching the others) |
|
||||
|
||||
**Advantage: Nostr-Native** - Open protocols mean broader ecosystem participation.
|
||||
**Edge: bitSpire.** Open protocols mean a wider ecosystem of compatible clients, wallets, and tools.
|
||||
|
||||
---
|
||||
|
||||
## Migration Path
|
||||
## When to choose what
|
||||
|
||||
### Phase 1: Parallel Operation
|
||||
### Choose bitSpire if
|
||||
|
||||
Run both systems simultaneously:
|
||||
- You operate in a jurisdiction without mandatory KYC/AML
|
||||
- You want minimal infrastructure
|
||||
- You value customer privacy (no PII collection, no central server-side ledger of who-paid-what)
|
||||
- You're comfortable with emerging tooling (no admin UI yet)
|
||||
- You want a license that stays open (AGPL-3.0)
|
||||
|
||||
- Existing machines continue with lamassu-server
|
||||
- New machines or test units use lamassu-next
|
||||
- Compare reliability and customer feedback
|
||||
|
||||
### Phase 2: Hybrid Mode
|
||||
|
||||
Bridge the systems:
|
||||
|
||||
- lamassu-server creates Nostr events for transactions
|
||||
- Dashboard reads from both PostgreSQL and relay
|
||||
- Gradual feature parity validation
|
||||
|
||||
### Phase 3: Full Migration
|
||||
|
||||
Complete transition:
|
||||
|
||||
- All machines run lamassu-next firmware
|
||||
- Retire lamassu-server infrastructure
|
||||
- Optional: Keep PostgreSQL as read-only archive
|
||||
|
||||
### Migration Considerations
|
||||
|
||||
| Consideration | Notes |
|
||||
| -------------------------- | ----------------------------------------------- |
|
||||
| **Hardware Compatibility** | Same bill validators/dispensers, new software |
|
||||
| **Customer Education** | May need CLINK-compatible wallet guidance |
|
||||
| **Operator Training** | Different mental model (events vs database) |
|
||||
| **Regulatory Review** | Verify compliance in your jurisdiction |
|
||||
| **Rollback Plan** | Keep lamassu-server available during transition |
|
||||
|
||||
---
|
||||
|
||||
## Trade-offs & Honest Assessment
|
||||
|
||||
### Where Nostr-Native Excels
|
||||
|
||||
1. **Simplicity**: No server to manage, no database to backup
|
||||
2. **Privacy**: No central transaction logs
|
||||
3. **Resilience**: No single point of failure
|
||||
4. **Interoperability**: Works with any CLINK wallet
|
||||
5. **Speed**: Direct relay communication is faster
|
||||
6. **Cost**: No server hosting costs (can use public relays)
|
||||
|
||||
### Where Traditional May Be Better
|
||||
|
||||
1. **Compliance**: Built-in KYC/AML if required by law
|
||||
2. **Wallet Support**: Works with any Lightning wallet today
|
||||
3. **Maturity**: Battle-tested over years of operation
|
||||
4. **Tooling**: Full-featured admin dashboard exists
|
||||
5. **Support**: Established support channels and documentation
|
||||
|
||||
### Current Limitations of Nostr-Native
|
||||
|
||||
| Limitation | Status | Mitigation |
|
||||
| ---------------------- | ------------- | ---------------------------------- |
|
||||
| Dashboard not complete | In progress | Use relay queries directly |
|
||||
| Limited wallet support | Growing | ShockWallet, others adopting CLINK |
|
||||
| No offline mode yet | Cashu planned | Requires internet currently |
|
||||
| Less documentation | Improving | This document helps |
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The Nostr-native architecture represents a fundamental shift from centralized to decentralized ATM operation. It trades the comprehensive compliance features of lamassu-server for simplicity, privacy, and resilience.
|
||||
|
||||
**Choose Nostr-Native if:**
|
||||
|
||||
- You operate in jurisdictions without KYC requirements
|
||||
- You want minimal infrastructure overhead
|
||||
- You value customer privacy
|
||||
- You're comfortable with emerging technology
|
||||
|
||||
**Stick with Traditional if:**
|
||||
### Stick with `lamassu-server` v8.1.5 if
|
||||
|
||||
- You need built-in compliance features
|
||||
- You require extensive fleet management tools today
|
||||
- You need to support any Lightning wallet
|
||||
- You prefer mature, battle-tested systems
|
||||
- You need a mature admin dashboard *today*
|
||||
- You have existing infrastructure investment in PostgreSQL + LND tooling
|
||||
- You're comfortable being on a code line that no longer receives upstream updates (v8.1.5 is the last open release; bug fixes in v8.1.6+ are paid-license-only)
|
||||
|
||||
**Consider Hybrid if:**
|
||||
### Choose Lamassu's current commercial offering if
|
||||
|
||||
- You want to evaluate both approaches
|
||||
- You're planning a gradual migration
|
||||
- You have mixed regulatory requirements across locations
|
||||
- You want vendor support, official updates, and a commercial SLA
|
||||
- You're willing to pay the per-machine subscription
|
||||
- You operate in a regulated jurisdiction where KYC tooling matters
|
||||
|
||||
This last option is outside the scope of this document — see [lamassu.is](https://lamassu.is) for their current commercial terms.
|
||||
|
||||
---
|
||||
|
||||
## Further Reading
|
||||
## Trade-offs & honest assessment
|
||||
|
||||
- [CLINK Protocol Specification](https://github.com/shocknet/clink)
|
||||
- [Lightning.Pub Documentation](https://github.com/shocknet/Lightning.Pub)
|
||||
- [Nostr Protocol (NIP-01)](https://github.com/nostr-protocol/nips/blob/master/01.md)
|
||||
- [NIP-44 Encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)
|
||||
- [ndebit Cash-In Flow](./ndebit-cash-in-flow.md)
|
||||
### Where bitSpire excels
|
||||
|
||||
1. **Simplicity** — LNbits + a relay is a much smaller surface than a stateful application server + DB + admin tooling
|
||||
2. **Privacy** — no central transaction logs by design
|
||||
3. **Resilience** — no single point of failure on the ATM side; LNbits is replaceable
|
||||
4. **Interoperability** — works with any BOLT11 / LNURL-withdraw wallet
|
||||
5. **Speed** — push-based settlement detection skips the LND-poll cycle
|
||||
6. **Cost** — no Lamassu license fees; no commercial admin-server hosting
|
||||
|
||||
### Where traditional (`lamassu-server` ≤ v8.1.5) is still better
|
||||
|
||||
1. **Compliance** — built-in KYC/AML if your jurisdiction requires it
|
||||
2. **Tooling** — mature admin dashboard, fleet management, transaction reporting
|
||||
3. **Maturity** — battle-tested in production over many years
|
||||
4. **Documentation** — established support channels for operators (community-maintained for v8.1.5; commercial for v8.1.6+)
|
||||
|
||||
### Current limitations of bitSpire
|
||||
|
||||
| Limitation | Status | Mitigation |
|
||||
|---|---|---|
|
||||
| No fleet dashboard | Planned, not built | Per-ATM `journalctl` + custom scripts |
|
||||
| Limited transaction reporting | SQLite per ATM | Aggregate via operator-side ETL if needed |
|
||||
| No offline mode | `@bitSpire/cashu` is a placeholder | Requires LNbits connectivity currently |
|
||||
| Smaller operator community | Growing | This doc + the README walkthrough |
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
- [LNbits nostr-native-transport docs](https://git.atitlan.io/aiolabs/lnbits) — the wire spec for what's actually happening on kind-21000
|
||||
- [NIP-01 Nostr basics](https://github.com/nostr-protocol/nips/blob/master/01.md)
|
||||
- [NIP-44 v2 encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)
|
||||
- [LUD-03 LNURL-withdraw](https://github.com/lnurl/luds/blob/luds/03.md)
|
||||
- [CLINK Protocol](./clink-protocol.md) — historical reference, dormant on `dev`
|
||||
- [bitSpire deployment walkthrough](../deploy/nixos/README.md)
|
||||
- [machine-installation.md](./machine-installation.md) — high-level deployment overview
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ The location buys (or finances) the machine and runs their own stack.
|
|||
| Aspect | Details |
|
||||
| ----------------- | ---------------------------------------- |
|
||||
| Machine ownership | Them |
|
||||
| Liquidity | Their own Lightning node + Lightning.Pub |
|
||||
| Liquidity | Their own Lightning node + LNbits |
|
||||
| Software updates | Them (we provide releases) |
|
||||
| Cash management | Them |
|
||||
| Fee configuration | Them |
|
||||
|
|
@ -107,15 +107,15 @@ We're selective about where we place machines. The goal is to find locations wit
|
|||
|
||||
For the managed model to work at scale, we need:
|
||||
|
||||
- **Remote monitoring** — machine status, cash levels, error alerts over [[clink-protocol|CLINK]]/Nostr
|
||||
- **Operator dashboard** — manage cassettes, view transactions, update config remotely
|
||||
- **Multi-machine support** — one Lightning.Pub instance serving multiple ATMs
|
||||
- **Remote monitoring** — machine status, cash levels, error alerts over nostr (kind-30078 service beacons + kind-21003 management commands)
|
||||
- **Operator dashboard** — manage cassettes, view transactions, update config remotely (planned)
|
||||
- **Multi-machine support** — one LNbits instance can host wallets for many ATMs; each ATM auto-creates its own wallet from its nostr pubkey
|
||||
- **Fee configuration** — per-machine fee % set by operator
|
||||
- **Availability broadcast** — public Nostr events indicating cash-in/cash-out availability (see [[ndebit-cash-in-flow|cash-in flow]])
|
||||
- **Availability broadcast** — public kind-30078 events indicating cash-in/cash-out availability and reserves
|
||||
|
||||
## Related
|
||||
|
||||
- [[device-configuration]] — hardware setup for different machine models
|
||||
- [[machine-installation]] — NixOS installation on ATM hardware
|
||||
- [[clink-protocol]] — CLINK protocol for ATM-wallet communication
|
||||
- [[ndebit-cash-in-flow]] — cash-in payment flow
|
||||
- [[machine-installation]] — deployment overview for ATM hardware
|
||||
- [Deploy/NixOS README](../deploy/nixos/README.md) — full deployment walkthrough
|
||||
- [[architecture-comparison]] — bitSpire vs traditional lamassu-server
|
||||
|
|
|
|||
|
|
@ -4,23 +4,23 @@ This document describes how to configure the ATM hardware for different machine
|
|||
|
||||
## Overview
|
||||
|
||||
The ATM application supports multiple Lamassu machine models out of the box. Configuration is handled through:
|
||||
bitSpire ships built-in presets for the hardware platforms we've tested on. Configuration is handled through:
|
||||
|
||||
1. **Machine presets** - Built-in defaults for known hardware (Sintra, Gaia)
|
||||
2. **Environment variables** - Override any setting at runtime
|
||||
3. **Runtime overrides** - Programmatic configuration
|
||||
1. **Machine presets** — built-in defaults for known hardware (Sintra, tejo, douro, batm3)
|
||||
2. **Environment variables** — override any setting at runtime
|
||||
3. **Runtime overrides** — programmatic configuration
|
||||
|
||||
## Supported Machine Models
|
||||
## Supported machine models
|
||||
|
||||
### Sintra (Default)
|
||||
### Sintra
|
||||
|
||||
The Lamassu Sintra (Gen 2) uses:
|
||||
The Sintra (Aaeon UP Board-based, Intel Atom x5-Z8350) uses:
|
||||
|
||||
| Device | Protocol | Path |
|
||||
| -------------- | -------- | ------------ |
|
||||
| Bill Validator | ID003 | `/dev/ttyJ5` |
|
||||
| Bill Dispenser | F56 | `/dev/ttyJ7` |
|
||||
| Printer | Nippon | `/dev/ttyJ4` |
|
||||
| Device | Protocol | Path | Underlying device |
|
||||
|---|---|---|---|
|
||||
| Bill Validator | ID003 | `/dev/ttyJ5` | FTDI USB-serial → `/dev/ttyUSB1` |
|
||||
| Bill Dispenser | F56 | `/dev/ttyJ7` | SoC MMIO UART → `/dev/ttyS4` |
|
||||
| Printer | Nippon | `/dev/ttyJ4` | FTDI USB-serial → `/dev/ttyUSB0` |
|
||||
|
||||
**Hardware:**
|
||||
|
||||
|
|
@ -28,9 +28,19 @@ The Lamassu Sintra (Gen 2) uses:
|
|||
- Validator: JCM iVIZION
|
||||
- Dispenser: Fujitsu F53/F56
|
||||
|
||||
### Gaia
|
||||
**Sintra-specific gotcha:** the kernel allocates `ttyS0..ttyS3` as placeholder serial nodes that error on any I/O. Only `ttyS0` (legacy 8250 at I/O 0x3f8) and `ttyS4` (SoC MMIO 16550A at 0xa171b000) are real on this hardware. The `bitspire-atm` NixOS module's `upboard.nix` keeps the kernel console on `tty0` only (not `ttyS4`) so userspace can claim `ttyS4` for the F56 dispenser.
|
||||
|
||||
The Lamassu Gaia uses similar hardware with different device paths. Verify paths on your specific unit.
|
||||
### tejo
|
||||
|
||||
The Tejo runs on the same Aaeon UP Board family as Sintra; same validator + dispenser layout. Uses the same `upboard.nix` hardware module.
|
||||
|
||||
### Douro
|
||||
|
||||
Runs on a Dell OptiPlex 9030 AIO (Intel i7-4790S, Intel HD 4600) — Lamassu's stock Douro motherboard. SATA SSD instead of eMMC, eGalax touchscreen, dispenser/validator on USB-attached FTDI bridges. See `deploy/nixos/hardware/douro.nix` for the specifics.
|
||||
|
||||
### BATM3
|
||||
|
||||
The "batm3" target in this repo is **not the stock GeneralBytes BATM3** — it's a custom modification where the original ARM/Android board has been physically replaced with a Dell OptiPlex 9030 AIO (same hardware family as the Douro). The chassis, cash-handling units (MEI SCR validator/recycler + Puloon F56 dispenser), and screen are stock BATM3; everything compute-side is grafted in. See `deploy/nixos/hardware/batm3.nix` for the boot configuration; functionally it shares the Dell-board layout with Douro but with different cash hardware on the inside.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
|
|
@ -155,15 +165,19 @@ On a Sintra running Linux, verify the serial devices exist:
|
|||
ls -la /dev/ttyJ*
|
||||
```
|
||||
|
||||
Expected output:
|
||||
Expected output on a working Sintra:
|
||||
|
||||
```
|
||||
lrwxrwxrwx 1 root root 10 Jan 29 12:00 /dev/ttyJ4 -> ttyS4
|
||||
lrwxrwxrwx 1 root root 10 Jan 29 12:00 /dev/ttyJ5 -> ttyS5
|
||||
lrwxrwxrwx 1 root root 10 Jan 29 12:00 /dev/ttyJ7 -> ttyS7
|
||||
lrwxrwxrwx 1 root root 7 May 13 09:22 /dev/ttyJ4 -> ttyUSB0
|
||||
lrwxrwxrwx 1 root root 7 May 13 09:22 /dev/ttyJ5 -> ttyUSB1
|
||||
lrwxrwxrwx 1 root root 5 May 13 09:22 /dev/ttyJ7 -> ttyS4
|
||||
```
|
||||
|
||||
If the symlinks don't exist, check the udev rules or use the underlying `/dev/ttyS*` devices directly.
|
||||
(`ttyUSB0`/`ttyUSB1` are the two FTDI USB-serial bridges — printer + validator. `ttyS4` is the SoC's on-carrier MMIO UART used for the F56 dispenser.)
|
||||
|
||||
If the symlinks don't exist, check the udev rules in `deploy/nixos/hardware/upboard.nix`, then run `mdev -s` (or `udevadm trigger` on a non-Alpine system) to repopulate `/dev`.
|
||||
|
||||
If you see a symlink pointing at `ttyS1`/`ttyS2`/`ttyS3` instead, those are kernel-allocated placeholder nodes that always error on I/O — the udev rule needs to map to `ttyS4` for Sintra. The current `upboard.nix` covers all the cases (`ttyS1`, `ttyS4`, `ttyS5`) so whichever real device shows up gets the `ttyJ7` alias.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
|
|||
|
|
@ -1,278 +1,96 @@
|
|||
# Machine Installation
|
||||
# Deploying bitSpire to a Sintra (or other ATM)
|
||||
|
||||
This document describes how to deploy the bitSpire ATM software to a Sintra machine. (Historical name: "Lamassu Next" — renamed on the `dev` branch as part of the LNbits-backend transition.)
|
||||
This document gives the high-level shape of an ATM deployment. **For the step-by-step walkthrough — every command from `nix build` to a kiosk on a real Sintra — see [deploy/nixos/README.md](../deploy/nixos/README.md).** This file is the orientation doc that explains *why* the pipeline looks the way it does.
|
||||
|
||||
## Prerequisites
|
||||
> The pre-NixOS workflow described in earlier versions of this file (manual AppImage scp, hand-written systemd unit, ad-hoc env files) has been retired. NixOS is the supported deployment path on `dev`. The historical AppImage build still exists for one-off testing on non-NixOS dev boxes, but it is not how production ATMs are installed.
|
||||
|
||||
### On the Sintra
|
||||
## The pipeline at a glance
|
||||
|
||||
- Linux OS with X11 display server
|
||||
- Network connectivity (WiFi or Ethernet)
|
||||
- User account with `dialout` group membership (for serial port access)
|
||||
|
||||
### Backend Services
|
||||
|
||||
The ATM requires network access to:
|
||||
|
||||
| Service | Purpose | Required |
|
||||
| ------------- | ------------------- | -------- |
|
||||
| Nostr relay | CLINK communication | Yes |
|
||||
| Lightning.Pub | Payment processing | Yes |
|
||||
|
||||
These can be self-hosted or provided by a third party.
|
||||
|
||||
## Building the Application
|
||||
|
||||
### Development Machine Setup
|
||||
|
||||
1. Enter the development environment:
|
||||
|
||||
```bash
|
||||
cd lamassu-next
|
||||
devenv shell
|
||||
```
|
||||
|
||||
2. Build the Electron application:
|
||||
|
||||
```bash
|
||||
cd apps/machine
|
||||
pnpm build:electron
|
||||
```
|
||||
|
||||
3. Build artifacts are created in `apps/machine/release/`:
|
||||
|
||||
```
|
||||
release/
|
||||
├── bitSpire-0.1.0.AppImage # Portable executable (recommended)
|
||||
└── linux-unpacked/ # Unpacked application directory
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Option A: AppImage (Recommended)
|
||||
|
||||
The AppImage is a self-contained executable that works on any Linux distribution.
|
||||
|
||||
1. Copy to Sintra:
|
||||
|
||||
```bash
|
||||
scp "apps/machine/release/bitSpire-0.1.0.AppImage" user@sintra:/opt/lamassu/
|
||||
```
|
||||
|
||||
2. Make executable:
|
||||
|
||||
```bash
|
||||
ssh user@sintra
|
||||
chmod +x "/opt/lamassu/bitSpire-0.1.0.AppImage"
|
||||
```
|
||||
|
||||
3. Test manually:
|
||||
|
||||
```bash
|
||||
export DISPLAY=:0
|
||||
"/opt/lamassu/bitSpire-0.1.0.AppImage" --no-sandbox
|
||||
```
|
||||
|
||||
### Option B: Unpacked Directory
|
||||
|
||||
For faster startup times, deploy the unpacked application:
|
||||
|
||||
1. Copy to Sintra:
|
||||
|
||||
```bash
|
||||
scp -r apps/machine/release/linux-unpacked user@sintra:/opt/lamassu/
|
||||
```
|
||||
|
||||
2. Test manually:
|
||||
|
||||
```bash
|
||||
ssh user@sintra
|
||||
export DISPLAY=:0
|
||||
/opt/lamassu/linux-unpacked/lamassu-machine --no-sandbox
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a configuration file at `/opt/lamassu/.env`:
|
||||
|
||||
```bash
|
||||
# Machine model (sintra, gaia, or custom)
|
||||
VITE_LAMASSU_MACHINE_MODEL=sintra
|
||||
|
||||
# Fiat currency code (ISO 4217)
|
||||
VITE_LAMASSU_FIAT_CODE=USD
|
||||
|
||||
# Custom device paths (optional, uses preset defaults if not set)
|
||||
# VITE_LAMASSU_VALIDATOR_DEVICE=/dev/ttyJ5
|
||||
# VITE_LAMASSU_DISPENSER_DEVICE=/dev/ttyJ7
|
||||
|
||||
# Cassette configuration (optional)
|
||||
# VITE_LAMASSU_CASSETTES='[{"denomination":20,"count":100}]'
|
||||
```
|
||||
┌─────────────────┐ nix build .# ┌─────────────────────────┐
|
||||
│ dev box │ ─disk-image-sintra──▶ │ result/nixos.img │
|
||||
│ (Nix + flake) │ │ (8.5 GB sparse, GPT) │
|
||||
└─────────────────┘ └────────────┬────────────┘
|
||||
│ dd
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ USB stick │
|
||||
└──────────┬───────────┘
|
||||
│ carry to ATM
|
||||
▼
|
||||
┌─────────────────┐ ┌─────────────────────┐
|
||||
│ Alpine live USB │ boot Sintra │ Sintra (UP Board) │
|
||||
│ (toolchain) │ ─────────────────────▶ │ eMMC: empty │
|
||||
└────────┬────────┘ └──────────┬──────────┘
|
||||
│ apk add + dd USB → eMMC + parted resize + reboot
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ Sintra booted from eMMC, bitspire.service in needs-prov state │
|
||||
└──────────────────────────────┬─────────────────────────────────┘
|
||||
│ provision-atm.sh from dev box
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ /var/lib/bitspire/.env populated, service restarted, kiosk │
|
||||
│ connects to LNbits over nostr-transport, ready for transactions │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
See [Device Configuration](./device-configuration.md) for detailed configuration options.
|
||||
## Why this pipeline
|
||||
|
||||
### Lightning.Pub Connection
|
||||
Three properties matter for ATM software running unattended in retail locations:
|
||||
|
||||
The ATM needs to connect to a Lightning.Pub instance. Configure via environment:
|
||||
1. **Reproducible boot media.** Two ATMs flashed from the same `nixos.img` boot identical environments — same kernel, same systemd units, same Electron build, same nix-store closure. No "works on my machine" drift between fleet units.
|
||||
2. **Auditable provenance.** Every file on the ATM traces back to a derivation in `/nix/store/`, and every derivation traces back to a commit in this repo. There's no `pip install` reaching out to PyPI, no `npm install` pulling un-pinned packages, no `apt update` mutating the system underfoot.
|
||||
3. **Safe in-place updates.** `nixos-rebuild switch` against the `dev` branch atomically installs a new generation; if the new generation fails to boot or activate, the previous one stays bootable. Auto-upgrade at 04:00 daily means an operator can patch the fleet by pushing to `dev`.
|
||||
|
||||
```bash
|
||||
# Nostr relay WebSocket URL (required)
|
||||
VITE_RELAY_URL=wss://your-relay.example.com
|
||||
The disk-image approach (versus `nixos-install` from a live USB) is specifically to avoid the human-in-the-loop installation step. Every Sintra gets the same image; the only per-machine variation is the `.env` written at provisioning time.
|
||||
|
||||
# Lightning.Pub's Nostr public key (required)
|
||||
# Get from: docker logs lamassu-lightning-pub | grep pubkey
|
||||
VITE_LIGHTNING_PUB_PUBKEY=4be8e203a3341bb2b74a4dcbf8774e061437f63ec21af7ec3144c8d0a68e2f39
|
||||
## What's in the image
|
||||
|
||||
# Lightning.Pub HTTP API URL (optional, for admin operations)
|
||||
VITE_LIGHTNING_PUB_API_URL=https://lp.operator.com
|
||||
Conceptually:
|
||||
|
||||
# ATM's Nostr private key (recommended for persistent identity)
|
||||
# Generate with: npx @lamassu/nostr-client generate-keypair
|
||||
# If not set, a new ephemeral identity is generated on each restart
|
||||
VITE_ATM_PRIVATE_KEY=0123456789abcdef...
|
||||
```
|
||||
| Layer | Source | Purpose |
|
||||
|---|---|---|
|
||||
| **Kernel + initrd** | `nixpkgs` 24.05 + `upboard.nix` initrd modules | Boot the Sintra hardware (eMMC via `sdhci-acpi`, validator/dispenser at `ttyJ5`/`ttyJ7`) |
|
||||
| **NixOS base** | `nixpkgs` 24.05 | systemd, Xorg, openbox, the `lamassu` user, sshd for provisioning |
|
||||
| **bitspire.service** | `deploy/nixos/bitspire-atm.nix` | systemd unit that launches the Electron kiosk |
|
||||
| **The Electron app** | `apps/machine` built into a nix derivation | The actual ATM UI + state machine + Lightning client |
|
||||
| **Hardware-specific config** | `deploy/nixos/hardware/upboard.nix` (or `douro.nix`, `batm3.nix`) | udev rules, kernel modules, panel calibration |
|
||||
|
||||
**Important:** The `VITE_LIGHTNING_PUB_PUBKEY` is required. Without it, the ATM cannot communicate with Lightning.Pub.
|
||||
## What's NOT in the image
|
||||
|
||||
## Auto-Start on Boot
|
||||
The image is **identity-free** by design. After flashing, the ATM has no concept of:
|
||||
- Which LNbits server to talk to
|
||||
- Which nostr relay to use
|
||||
- Its own nostr identity (signing key)
|
||||
- Which fiat currency to display (defaulted from build args, but overridable)
|
||||
|
||||
### Systemd Service
|
||||
All of these come from `/var/lib/bitspire/.env`, which is written by `provision-atm.sh` after first boot. This separation means a single image flavor can serve dev, staging, and prod just by changing the provisioning data.
|
||||
|
||||
Create `/etc/systemd/system/lamassu-kiosk.service`:
|
||||
## Pre-deployment checklist
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=bitSpire ATM Kiosk
|
||||
After=graphical.target network-online.target
|
||||
Wants=network-online.target
|
||||
Before flashing a Sintra you'll want:
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=lamassu
|
||||
Environment=DISPLAY=:0
|
||||
EnvironmentFile=/opt/lamassu/.env
|
||||
WorkingDirectory=/opt/lamassu
|
||||
ExecStart=/opt/lamassu/linux-unpacked/lamassu-machine --no-sandbox
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
- [ ] **An Alpine live USB** for use as the installer-OS on the Sintra (Alpine is small, has a working busybox toolchain, and apk is fast). The bitSpire image itself is *not* a live system — it expects to live on the eMMC.
|
||||
- [ ] **A second USB stick** to flash the bitSpire image onto (this is what you'll dd between on the Sintra).
|
||||
- [ ] **A running LNbits instance** with the nostr-native-transport branch built in. The dev compose at `~/dev/local/docker/regtest` provides one; production deployments point at `lnbits.aiolabs.dev` or your operator's instance.
|
||||
- [ ] **Network reachability between the Sintra and the LNbits host.** The ATM connects to the relay endpoint at `ws://<lnbits-host>:5001/nostrrelay/test` (no separate strfry container — LNbits ships its own `nostrrelay` extension).
|
||||
- [ ] **The LNbits server pubkey** (`docker logs <lnbits-container> | grep 'Public key (share this)'`).
|
||||
- [ ] **A freshly generated nostr private key** for the ATM (`openssl rand -hex 32`). Each ATM should have its own; never share keys between machines.
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical.target
|
||||
```
|
||||
## After deployment
|
||||
|
||||
Enable and start:
|
||||
Once the kiosk is up, useful things to know:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable lamassu-kiosk
|
||||
sudo systemctl start lamassu-kiosk
|
||||
```
|
||||
- **Service status:** `ssh lamassu@<atm> 'sudo systemctl status bitspire'`
|
||||
- **Live log tail:** `ssh lamassu@<atm> 'sudo journalctl -u bitspire -f'`
|
||||
- **Re-provision (e.g., wrong relay URL):** rerun `provision-atm.sh` from the dev box with the new env vars
|
||||
- **Push a code change without reflashing:** `nixos-rebuild switch --flake .#sintra-installed --target-host lamassu@<atm> --use-remote-sudo`
|
||||
- **Inspect transaction history:** `ssh lamassu@<atm> 'sudo bash /etc/nixos/atm-transactions.sh'` (queries `/var/lib/bitspire/state.db`)
|
||||
|
||||
### Monitoring
|
||||
## Related documentation
|
||||
|
||||
```bash
|
||||
# Check status
|
||||
sudo systemctl status lamassu-kiosk
|
||||
|
||||
# View logs
|
||||
sudo journalctl -u lamassu-kiosk -f
|
||||
|
||||
# Restart after configuration changes
|
||||
sudo systemctl restart lamassu-kiosk
|
||||
```
|
||||
|
||||
## Hardware Verification
|
||||
|
||||
### Serial Port Access
|
||||
|
||||
1. Verify devices exist:
|
||||
|
||||
```bash
|
||||
ls -la /dev/ttyJ*
|
||||
# Expected: /dev/ttyJ4 (printer), /dev/ttyJ5 (validator), /dev/ttyJ7 (dispenser)
|
||||
```
|
||||
|
||||
2. Check user permissions:
|
||||
|
||||
```bash
|
||||
groups
|
||||
# Should include 'dialout'
|
||||
```
|
||||
|
||||
3. If not in dialout group:
|
||||
|
||||
```bash
|
||||
sudo usermod -a -G dialout $USER
|
||||
# Logout and login for changes to take effect
|
||||
```
|
||||
|
||||
### Display Configuration
|
||||
|
||||
The Sintra uses a 1080x1920 portrait display. Verify X11 is running:
|
||||
|
||||
```bash
|
||||
echo $DISPLAY
|
||||
# Should output :0 or similar
|
||||
|
||||
xdpyinfo | head -5
|
||||
# Should show display information
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Application Won't Start
|
||||
|
||||
1. **Missing display**: Ensure `DISPLAY=:0` is set
|
||||
2. **Sandbox error**: Use `--no-sandbox` flag
|
||||
3. **Permission denied**: Check file is executable (`chmod +x`)
|
||||
|
||||
### Hardware Not Responding
|
||||
|
||||
1. **Check serial ports exist**: `ls -la /dev/ttyJ*`
|
||||
2. **Check permissions**: User must be in `dialout` group
|
||||
3. **Check connections**: Ensure cables are properly seated
|
||||
4. **Power cycle hardware**: Turn validator/dispenser off and on
|
||||
|
||||
### Network Issues
|
||||
|
||||
1. **Test relay connection**: `websocat wss://your-relay.example.com`
|
||||
2. **Check DNS resolution**: `ping your-relay.example.com`
|
||||
3. **Verify firewall**: Ensure outbound WebSocket connections allowed
|
||||
|
||||
### Logs
|
||||
|
||||
Application logs are written to:
|
||||
|
||||
- **Systemd**: `journalctl -u lamassu-kiosk`
|
||||
- **Electron**: `~/.config/lamassu-machine/logs/`
|
||||
|
||||
## Updating
|
||||
|
||||
1. Build new version on development machine
|
||||
2. Stop the service:
|
||||
|
||||
```bash
|
||||
sudo systemctl stop lamassu-kiosk
|
||||
```
|
||||
|
||||
3. Replace application files:
|
||||
|
||||
```bash
|
||||
scp -r apps/machine/release/linux-unpacked/* user@sintra:/opt/lamassu/linux-unpacked/
|
||||
```
|
||||
|
||||
4. Restart service:
|
||||
|
||||
```bash
|
||||
sudo systemctl start lamassu-kiosk
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
- The `--no-sandbox` flag is required for Electron on some Linux configurations. This is acceptable for a dedicated kiosk machine.
|
||||
- Keep the ATM's nsec private key secure. It authorizes all transactions from this machine.
|
||||
- Use a dedicated user account (`lamassu`) with minimal privileges.
|
||||
- Consider firewall rules to restrict network access to only required services.
|
||||
- [deploy/nixos/README.md](../deploy/nixos/README.md) — the full step-by-step walkthrough and NixOS module reference
|
||||
- [device-configuration.md](./device-configuration.md) — hardware-specific configuration (validator types, cassette layouts, fiat currency)
|
||||
- [adr/001-hal-architecture.md](./adr/001-hal-architecture.md) — why HAL is TypeScript-in-Node rather than Rust-via-Tauri
|
||||
- [CLAUDE.md](../CLAUDE.md) — the dev-facing overview, including the hardware-specific gotchas we hard-learned on the first real Sintra flash
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue