Initialize lamassu-next monorepo with scaffolding and Claude skills

Monorepo Structure:
- apps/machine/ - Tauri + Vue 3 ATM application (placeholder)
- apps/dashboard/ - Vue 3 operator dashboard (placeholder)
- apps/relay/ - strfry relay configuration
- packages/hal/ - Rust Hardware Abstraction Layer with napi-rs
- packages/nostr-client/ - Nostr client library
- packages/clink/ - CLINK protocol implementation
- packages/state-machine/ - XState v5 ATM state machine
- packages/lightning/ - Lightning.Pub client
- packages/cashu/ - Cashu ecash integration
- packages/ui-shared/ - Shared Vue 3 components

Development Environment:
- devenv.nix with pre-commit hooks (prettier, eslint, rustfmt, clippy)
- Security hooks (detect-secrets, no console.log in production)
- Custom Nostr schema validation hook
- Docker Compose for strfry relay and Lightning.Pub

Claude Code Skills:
- /security - Security review for Bitcoin/Lightning/ATM vulnerabilities
- /nostr-check - Nostr NIP conformity validation
- /lightning-check - Lightning.Pub and CLINK protocol validation
- /test - Testing agent for coverage and flow validation
- /docs - Documentation synchronization
- /hal-check - HAL driver porting validation

HAL Foundation:
- Rust crate with napi-rs bindings
- BillValidator and BillDispenser traits
- Mock implementations for development
- Error types for all hardware operations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-01-22 16:25:25 -05:00
commit 4ca1adeb0b
34 changed files with 2898 additions and 0 deletions

View file

@ -0,0 +1,218 @@
# /docs - Documentation Agent
## Purpose
Keep documentation synchronized with code, generate API docs, and maintain architecture diagrams.
## Invocation
```
/docs [command] [target]
```
Commands:
- `sync` - Sync docs with code changes
- `api` - Generate API documentation
- `diagram` - Update architecture diagrams
- `readme` - Update package README
- `changelog` - Generate changelog entry
## Documentation Structure
```
docs/
├── architecture/
│ ├── overview.md # High-level architecture
│ ├── nostr-protocol.md # Nostr integration details
│ ├── 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
├── guides/
│ ├── development.md # Dev setup guide
│ ├── hardware-testing.md # Testing with real hardware
│ └── deployment.md # NixOS deployment guide
└── adrs/ # Architecture Decision Records
├── 001-nostr-backbone.md
├── 002-clink-over-bolt12.md
└── ...
```
## Sync Tasks
### Code → Docs Sync
When code changes:
1. Detect modified files
2. Check if related docs exist
3. Flag outdated documentation
4. Suggest updates
### Doc → Code Validation
Ensure docs reference actual code:
- [ ] Function names match
- [ ] Parameter types correct
- [ ] Return types accurate
- [ ] Examples compile/run
## API Documentation
### TypeScript Packages
Generate from TSDoc comments:
```typescript
/**
* Create a CLINK offer for receiving payments
*
* @param identity - Machine identity (nsec/npub)
* @param relays - Relay URLs to include in offer
* @param priceType - 'fixed' | 'variable' | 'spontaneous'
* @param amount - Amount in sats (required for fixed)
* @returns Encoded noffer string
*
* @example
* ```typescript
* const offer = createOffer(identity, ['wss://relay.example'], 'fixed', 10000)
* // Returns: noffer1...
* ```
*/
export function createOffer(...): string
```
### Rust Crate (HAL)
Generate from rustdoc comments:
```rust
/// Connect to a bill validator
///
/// # Arguments
/// * `port` - Serial port path (e.g., "/dev/ttyUSB0")
///
/// # Errors
/// Returns `ValidatorError::ConnectionFailed` if port unavailable
///
/// # Example
/// ```
/// let mut validator = Id003Validator::new("/dev/ttyUSB0", "USD");
/// validator.connect().await?;
/// ```
pub async fn connect(&mut self) -> Result<(), ValidatorError>
```
## Diagram Updates
### Mermaid Diagrams
Keep architecture diagrams current:
```mermaid
sequenceDiagram
participant U as User
participant A as ATM
participant R as Relay
participant L as Lightning.Pub
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
```
### Auto-update triggers
- New event kinds added → Update protocol diagram
- State machine changes → Update flow diagram
- New hardware driver → Update HAL diagram
## Changelog Generation
### Format (Keep a Changelog)
```markdown
## [Unreleased]
### Added
- CLINK offer support for cash-in flow
- Puloon dispenser driver
### Changed
- Upgraded to XState v5 actor model
### Fixed
- Race condition in bill acceptance
### Security
- Added NIP-44 encryption for receipts
```
### Auto-detection
From git commits since last release:
- `feat:` → Added
- `fix:` → Fixed
- `refactor:` → Changed
- `security:` → Security
- `BREAKING:` → Breaking Changes
## Output Format
### Sync Report
```markdown
## Documentation Sync: [target]
### Outdated Docs
| Doc | Code Change | Status |
|-----|-------------|--------|
| api/clink.md | createOffer params | ⚠️ Outdated |
| guides/development.md | New env var | ⚠️ Missing |
### Suggested Updates
1. `api/clink.md:45` - Add `timeout` parameter to createOffer
2. `guides/development.md` - Add LIGHTNING_PUB_URL env var
### Missing Documentation
- `packages/cashu/src/wallet.ts` - No API docs
```
### Generated Docs
```markdown
## Generated: [file]
### API Reference
#### `createOffer(identity, relays, priceType, amount?)`
Creates a CLINK offer for receiving Lightning payments.
**Parameters:**
| Name | Type | Description |
|------|------|-------------|
| identity | MachineIdentity | Machine nsec/npub |
| relays | string[] | Relay URLs |
| priceType | 'fixed' \| 'variable' | Pricing model |
| amount | number? | Amount in sats |
**Returns:** `string` - Encoded noffer
**Example:**
```typescript
const offer = createOffer(identity, ['wss://relay'], 'fixed', 10000)
```
```
## Example Usage
```
/docs sync packages/clink/
```
```
/docs api packages/nostr-client/src/
```
```
/docs changelog --since v0.1.0
```

View file

@ -0,0 +1,200 @@
# /hal-check - Hardware Abstraction Layer Agent
## Purpose
Validate HAL driver implementations against existing lamassu-machine drivers and hardware specifications.
## Invocation
```
/hal-check [command] [driver]
```
Commands:
- `port` - Validate porting from lamassu-machine
- `protocol` - Check protocol implementation
- `safety` - Rust safety review
- `mock` - Validate mock implementation
Drivers:
- `id003`, `ccnet`, `cashflow`, `bnr`, `genmega`, `hcm2`, `gsr50` (validators)
- `puloon`, `f56`, `genmega`, `hcm2`, `gsr50` (dispensers)
- `nippon`, `zebra`, `genmega` (printers)
## Porting Validation
### Source Reference
Each Rust driver should map 1:1 with lamassu-machine JavaScript:
| 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` |
### 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
#### ID003 (JCM)
| Command | Code | JS Reference |
|---------|------|--------------|
| RESET | 0x40 | id003.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 |
|---------|------|--------------|
| RESET | 0x44 | puloonrs232.js |
| DISPENSE | 0x45 | puloonrs232.js |
| STATUS | 0x46 | puloonrs232.js |
## Safety Review (Rust-specific)
### Memory Safety
- [ ] No `unsafe` without justification comment
- [ ] Buffer sizes validated before read/write
- [ ] No panic paths in production code
- [ ] Proper error propagation with Result
### Concurrency Safety
- [ ] Serial port access properly synchronized
- [ ] Event channels bounded
- [ ] No deadlock potential
- [ ] Timeout on all blocking operations
### Hardware Safety
- [ ] Dispenser amounts validated (prevent over-dispense)
- [ ] Bill count cross-checked
- [ ] Error states trigger hardware reset
- [ ] Graceful degradation on hardware failure
## Mock Validation
### Mock Requirements
Mocks must simulate:
1. Normal operation flow
2. Error conditions
3. Timing (realistic delays)
4. State persistence
### 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
### 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
}
```
Against JavaScript:
```javascript
// lamassu-machine/lib/id003/id003rs232.js
function buildPacket(data) {
const buf = Buffer.alloc(data.length + 4)
buf[0] = 0x02 // SYNC
buf[1] = data.length + 4
data.copy(buf, 2)
const crc = calculateCrc(buf.slice(0, -2))
buf.writeUInt16LE(crc, buf.length - 2)
return buf
}
```
## Output Format
### Port Validation
```markdown
## HAL Port Validation: id003
### Command Coverage
| Command | JS | Rust | Match |
|---------|-------|------|-------|
| RESET | ✅ | ✅ | ✅ |
| ENABLE | ✅ | ✅ | ✅ |
| STATUS | ✅ | ⚠️ | Partial |
### Protocol Differences
- [ ] `id003.rs:78` - CRC uses different polynomial than JS
### Missing Implementations
- [ ] `HOLD` command not implemented (used in id003fsm.js:holdBill)
### Recommendations
1. Verify CRC calculation against test vectors from JS
2. Add HOLD command for escrow mode
```
### Safety Review
```markdown
## HAL Safety Review: puloon
### Memory Safety
- [x] No unsafe blocks
- [x] Buffer bounds checked
- [ ] `dispense()` at line 145 - unwrap() could panic
### Concurrency Safety
- [x] Serial port mutex protected
- [x] Event channel bounded (16)
### Hardware Safety
- [x] Amount validation in dispense()
- [ ] No cassette empty check before dispense
### Critical Issues
1. `puloon.rs:145` - Replace unwrap() with proper error handling
2. `puloon.rs:178` - Add cassette level check before dispensing
```
## Example Usage
```
/hal-check port id003
```
```
/hal-check safety packages/hal/src/dispensers/
```
```
/hal-check mock --all
```

View file

@ -0,0 +1,168 @@
# /lightning-check - Lightning.Pub Conformity Agent
## Purpose
Validate Lightning.Pub integration and CLINK protocol conformity.
## Invocation
```
/lightning-check [target] [--clink] [--wallet]
```
Where:
- `target` - File or directory to check
- `--clink` - Focus on CLINK offer/debit flow
- `--wallet` - Focus on wallet operations
## Lightning.Pub Integration
### Connection
Lightning.Pub uses Nostr for all communication:
```typescript
// Connection via nprofile
const nprofile = 'nprofile1...' // Contains pubkey + relay hints
// All operations are Nostr events to Lightning.Pub's pubkey
await relay.publish({
kind: 21002, // CLINK debit
content: encrypted_request,
tags: [['p', lightningPubPubkey]],
})
```
### 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
## CLINK Protocol
### Offer Flow (Kind 21001)
```
User scans noffer → Wallet sends 21002 → ATM responds with invoice → User pays
```
Validation:
- [ ] noffer encoding is valid
- [ ] Relays included in offer
- [ ] Price type correctly specified (fixed/variable/spontaneous)
- [ ] Amount bounds validated
### Debit Flow (Kind 21002)
Request structure:
```json
{
"method": "pay_invoice",
"params": {
"invoice": "lnbc...",
"amount_msat": 100000
}
}
```
Response structure:
```json
{
"result": {
"preimage": "...",
"fee_msat": 1000
}
}
```
Validation:
- [ ] Invoice format valid (BOLT11)
- [ ] Amount matches invoice
- [ ] Preimage verified against payment hash
- [ ] Fee within acceptable bounds
- [ ] Timeout handling
### Manage Flow (Kind 21003)
For operator commands:
```json
{
"method": "get_balance",
"params": {}
}
```
Validation:
- [ ] Only authorized operators can send
- [ ] Commands are properly authenticated
- [ ] Responses handled securely
## 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
```markdown
## Lightning.Pub Conformity: [target]
### CLINK Protocol
| Flow | Status | Notes |
|------|--------|-------|
| Offer (21001) | ✅ | |
| Debit (21002) | ⚠️ | Missing timeout |
| Manage (21003) | ✅ | |
### Invoice Handling
- [ ] `file:line` - Issue description
### Integration Issues
- [ ] Description with fix suggestion
### Recommendations
- [ ] Performance/UX improvements
```
## Example Usage
```
/lightning-check packages/lightning/src/ --clink
```
Output:
```markdown
## Lightning.Pub Conformity: packages/lightning/src/
### CLINK Protocol
| Flow | Status | Notes |
|------|--------|-------|
| Offer (21001) | ✅ | Properly encoded |
| Debit (21002) | ⚠️ | No timeout handling |
### 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
```

View file

@ -0,0 +1,125 @@
# /nostr-check - Nostr Conformity Agent
## Purpose
Validate Nostr Implementation Proposals (NIP) conformity and optimize Nostr-related code.
## Invocation
```
/nostr-check [target] [--nips NIP1,NIP2,...]
```
Where `target` can be:
- A file path
- A directory
- `--events` to validate event structures
- `--relay` to check relay configuration
## Relevant NIPs for Lamassu ATM
### Core NIPs (Must Implement)
| NIP | Description | Usage in Lamassu |
|-----|-------------|------------------|
| NIP-01 | Basic protocol | Event structure, relay communication |
| NIP-19 | bech32 entities | npub, nsec, nprofile encoding |
| NIP-42 | Auth | Private relay authentication |
| NIP-44 | Encrypted payloads | Secure DMs, receipts |
| NIP-59 | Gift wrapping | Anonymous message delivery |
### Application NIPs
| NIP | Description | Usage in Lamassu |
|-----|-------------|------------------|
| NIP-17 | Private DMs | Receipt delivery |
| NIP-47 | Nostr Wallet Connect | Potential wallet integration |
| NIP-57 | Lightning Zaps | Optional tipping |
### Custom Event Kinds
| Kind | Description | Structure |
|------|-------------|-----------|
| 21001 | CLINK Offer | `{ pubkey, relays, priceType, amount? }` |
| 21002 | CLINK Debit | Payment request/response |
| 21003 | CLINK Manage | Operator commands |
| 30078 | Machine Status | Replaceable, `d` tag = "status" |
| 30079 | Transaction Record | Replaceable, `d` tag = "tx:{txid}" |
## Validation Checks
### Event Structure (NIP-01)
```typescript
interface Event {
id: string // 32-byte hex
pubkey: string // 32-byte hex
created_at: number // Unix timestamp
kind: number // Event kind
tags: string[][] // Array of tag arrays
content: string // Arbitrary string
sig: string // 64-byte hex signature
}
```
- [ ] `id` is valid SHA256 of serialized event
- [ ] `pubkey` is valid 32-byte hex
- [ ] `created_at` is reasonable (not far future/past)
- [ ] `kind` is valid for application
- [ ] `tags` are properly formatted
- [ ] `sig` is valid Schnorr signature
### bech32 Encoding (NIP-19)
- [ ] npub/nsec properly encoded
- [ ] nprofile includes relay hints
- [ ] nevent includes author pubkey
- [ ] No confusion between hex and bech32
### Encryption (NIP-44)
- [ ] Using NIP-44 (not deprecated NIP-04)
- [ ] Proper key derivation
- [ ] Random nonce for each message
- [ ] MAC verification before decryption
### Auth (NIP-42)
- [ ] Challenge-response implemented
- [ ] Auth events have proper `relay` tag
- [ ] Auth events signed correctly
- [ ] Timeout handling for auth flow
## Optimization Suggestions
### Relay Communication
- Use connection pooling
- Implement proper reconnection with backoff
- Batch event publishing when possible
- Use REQ filters efficiently
### Event Handling
- Verify signatures before processing
- Cache verified events
- Use proper indexing for event lookups
- Implement proper subscription management
## Output Format
```markdown
## Nostr Conformity: [target]
### NIP Compliance
| NIP | Status | Notes |
|-----|--------|-------|
| NIP-01 | ✅ Pass | |
| NIP-19 | ⚠️ Issue | See below |
### Issues Found
- [ ] `file:line` - Description
### Optimization Opportunities
- [ ] Description with rationale
### Custom Event Validation
| Kind | Valid | Notes |
|------|-------|-------|
| 30078 | ✅ | Machine status properly formatted |
```
## Example Usage
```
/nostr-check packages/nostr-client/src/events.ts --nips NIP-01,NIP-19
```

View file

@ -0,0 +1,105 @@
# /security - Security Review Agent
## Purpose
Perform security audits on code changes, focusing on Bitcoin/Lightning ATM-specific vulnerabilities.
## Invocation
```
/security [target]
```
Where `target` can be:
- A file path (e.g., `packages/lightning/src/client.ts`)
- A directory (e.g., `packages/hal/`)
- `--staged` for staged git changes
- `--all` for full codebase scan
## Review Checklist
### 1. Bitcoin/Lightning Security
- [ ] Private keys never logged or exposed
- [ ] nsec (Nostr secret keys) protected with proper permissions (0600)
- [ ] Invoice amounts validated before payment
- [ ] Payment preimages handled securely
- [ ] No hardcoded mnemonics, seeds, or keys
- [ ] Proper BOLT11/BOLT12 invoice validation
### 2. Hardware Security (ATM-specific)
- [ ] Bill validator amounts cross-checked
- [ ] Dispenser commands validated (prevent over-dispensing)
- [ ] Hardware error states handled gracefully
- [ ] No race conditions in cash handling
- [ ] Timeout handling for hardware operations
### 3. Nostr Security
- [ ] NIP-44 encryption used for sensitive messages
- [ ] Event signatures verified before processing
- [ ] Relay URLs validated (no injection)
- [ ] NIP-42 auth implemented for private relay
- [ ] No pubkey/npub confusion (proper type safety)
### 4. General Security
- [ ] Input validation on all external data
- [ ] SQL injection prevention (parameterized queries)
- [ ] No command injection in Bash/shell calls
- [ ] Proper error handling (no sensitive data in errors)
- [ ] Rate limiting on API endpoints
- [ ] HTTPS/WSS enforced in production
### 5. Rust-specific (HAL)
- [ ] No `unsafe` blocks without justification
- [ ] Error handling with Result, no unwrap() in production
- [ ] Buffer bounds checking for serial communication
- [ ] Proper lifetime management
### 6. TypeScript-specific
- [ ] Strict null checks honored
- [ ] No `any` types
- [ ] Zod validation on external data
- [ ] No eval() or dynamic code execution
## Output Format
```markdown
## Security Review: [target]
### Critical Issues
- [ ] Issue description with file:line reference
### High Priority
- [ ] Issue description with file:line reference
### Medium Priority
- [ ] Issue description with file:line reference
### Low Priority / Suggestions
- [ ] Suggestion with rationale
### Passed Checks
- [x] Check that passed
```
## Example Usage
```
/security packages/lightning/src/client.ts
```
Output:
```markdown
## Security Review: packages/lightning/src/client.ts
### Critical Issues
None found.
### High Priority
- [ ] `client.ts:45` - Invoice amount not validated before `payInvoice()` call
### Medium Priority
- [ ] `client.ts:78` - Error message includes full stack trace, may leak internal paths
### Passed Checks
- [x] Private keys not exposed in logs
- [x] NIP-44 encryption used for DMs
- [x] Proper TypeScript strict mode
```

View file

@ -0,0 +1,201 @@
# /test - Testing Agent
## Purpose
Run tests, analyze coverage, generate test cases, and validate ATM transaction flows.
## Invocation
```
/test [command] [target]
```
Commands:
- `run` - Run tests (default)
- `coverage` - Run with coverage report
- `generate` - Generate test cases for a file
- `flow` - Validate transaction flow
- `hardware` - Run hardware mock tests
## Test Categories
### 1. Unit Tests
Location: `*.test.ts` or `*.spec.ts` alongside source files
```bash
pnpm test # All tests
pnpm test --filter @lamassu/nostr-client # Specific package
```
### 2. Integration Tests
Location: `tests/integration/`
Tests cross-package interactions:
- Nostr client + CLINK
- State machine + Hardware mocks
- Lightning client + Invoice handling
### 3. E2E Tests (Playwright)
Location: `tests/e2e/`
Full transaction flows:
- Cash-in flow (bill insert → Lightning payment)
- Cash-out flow (Lightning receive → dispense)
- Error recovery flows
### 4. Hardware Mock Tests
Test against mock hardware:
```bash
pnpm test:hardware # With mock validator/dispenser
REAL_HARDWARE=true pnpm test:hardware # With real hardware (CI skip)
```
## Test Generation
### For a new file:
```
/test generate packages/lightning/src/client.ts
```
Generates test skeleton:
```typescript
import { describe, it, expect, vi } from 'vitest'
import { LightningPubClient } from './client'
describe('LightningPubClient', () => {
describe('createInvoice', () => {
it('should create valid BOLT11 invoice', async () => {
// TODO: Implement
})
it('should handle network errors', async () => {
// TODO: Implement
})
it('should validate amount bounds', async () => {
// TODO: Implement
})
})
})
```
### Test Case Suggestions
Based on code analysis, suggest test cases for:
- Happy path
- Edge cases (null, empty, max values)
- Error conditions
- Timeout scenarios
- Concurrent operations
## Transaction Flow Validation
### Cash-In Flow
```
/test flow cash-in
```
Validates:
1. Idle → Select Cash-In
2. Bill insertion events received
3. Amount calculation correct
4. CLINK offer generated
5. Payment received event
6. Receipt sent (optional)
7. Return to Idle
### Cash-Out Flow
```
/test flow cash-out
```
Validates:
1. Idle → Select Cash-Out
2. Amount selection
3. Invoice generated
4. Payment received
5. Cash dispensed
6. Bills removed detection (F56)
7. Return to Idle
### Error Flows
```
/test flow errors
```
Validates:
- Hardware timeout recovery
- Payment failure handling
- Network disconnection
- Partial dispense handling
## Coverage Requirements
### Minimum Coverage Targets
| Package | Statements | Branches | Functions |
|---------|------------|----------|-----------|
| nostr-client | 80% | 75% | 80% |
| clink | 80% | 75% | 80% |
| state-machine | 90% | 85% | 90% |
| lightning | 80% | 75% | 80% |
| hal (mocks) | 70% | 65% | 70% |
### Critical Paths (100% coverage required)
- Payment processing
- Cash dispensing logic
- Key handling
- Amount calculations
## Output Format
### Test Run
```markdown
## Test Results: [target]
### Summary
- Total: 42
- Passed: 40
- Failed: 2
- Skipped: 0
### Failed Tests
1. `client.test.ts` > createInvoice > should validate amount
- Expected: Error thrown
- Received: Invoice created with negative amount
### Coverage
| File | Statements | Branches | Functions |
|------|------------|----------|-----------|
| client.ts | 85% | 78% | 90% |
```
### Test Generation
```markdown
## Generated Tests: [file]
### Test File
`packages/lightning/src/client.test.ts`
### Suggested Test Cases
1. **createInvoice**
- ✅ Valid amount creates invoice
- ✅ Zero amount rejected
- ✅ Negative amount rejected
- ✅ Network error handled
### Missing Coverage
- `payInvoice` has no tests
- Error handling branch at line 45 untested
```
## Example Usage
```
/test coverage packages/state-machine/
```
```
/test generate packages/clink/src/offer.ts
```
```
/test flow cash-out
```

71
lamassu-next/.gitignore vendored Normal file
View file

@ -0,0 +1,71 @@
# Dependencies
node_modules/
.pnpm-store/
# Build outputs
dist/
.next/
.nuxt/
.output/
target/
*.node
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# Environment
.env
.env.*
!.env.example
# Secrets
*.nsec
*.pem
*.key
.secrets.baseline
# Logs
*.log
npm-debug.log*
pnpm-debug.log*
# Testing
coverage/
.nyc_output/
# Caches
.turbo/
.cache/
.parcel-cache/
.eslintcache
*.tsbuildinfo
# OS
.DS_Store
Thumbs.db
# Tauri
apps/machine/src-tauri/target/
apps/machine/src-tauri/WixTools/
apps/machine/src-tauri/icons/
# Rust
Cargo.lock
**/*.rs.bk
# devenv
.devenv/
.direnv/
.pre-commit-config.yaml
# Docker
docker/**/data/
# Temporary
tmp/
temp/
*.tmp

7
lamassu-next/.prettierrc Normal file
View file

@ -0,0 +1,7 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"printWidth": 100
}

177
lamassu-next/CLAUDE.md Normal file
View file

@ -0,0 +1,177 @@
# CLAUDE.md
This file provides guidance to Claude Code when working with the Lamassu Next codebase.
## Project Overview
**Lamassu Next** is a Nostr-native Lightning ATM system. Key principles:
- **KYC-Free**: No identity collection, no compliance theater
- **Lightning-Native**: Security encapsulated in Lightning protocol
- **Nostr as Infrastructure**: Relay for communication, keypairs for identity
- **Open Source First**: Every component auditable and forkable
## Architecture
```
lamassu-next/
├── apps/
│ ├── machine/ # Tauri + Vue 3 ATM kiosk application
│ ├── dashboard/ # Vue 3 operator dashboard
│ └── relay/ # strfry relay configuration
├── packages/
│ ├── hal/ # Rust Hardware Abstraction Layer (napi-rs)
│ ├── nostr-client/ # Nostr client library
│ ├── clink/ # CLINK protocol implementation
│ ├── state-machine/ # XState v5 ATM state machine
│ ├── lightning/ # Lightning.Pub client
│ ├── cashu/ # Cashu ecash (offline mode)
│ └── ui-shared/ # Shared Vue components
└── docker/ # Development infrastructure
```
## Commands
```bash
# Enter development environment
devenv shell
# Start development
pnpm dev
# Build all packages
pnpm build
# Run tests
pnpm test
# Start infrastructure (Nostr relay, Lightning.Pub)
infra-up
# Stop infrastructure
infra-down
```
## Key Technologies
| Component | Technology | Notes |
|-----------|------------|-------|
| Runtime | Node.js 22 LTS | Strict TypeScript |
| ATM Shell | Tauri 2.x | Rust core, Vue 3 UI |
| State Machine | XState v5 | Actor model |
| Hardware | Rust + napi-rs | Ported from lamassu-machine |
| Messaging | Nostr | NIP-01, NIP-17, NIP-42, NIP-44 |
| Payments | CLINK | Kind 21001/21002/21003 |
| Backend | Lightning.Pub | Nostr-native account system |
## Custom Skills
The following skills are available for development assistance:
### `/security` - Security Review
Audit code for Bitcoin/Lightning/ATM-specific vulnerabilities.
```
/security packages/lightning/src/
/security --staged
```
### `/nostr-check` - Nostr Conformity
Validate NIP compliance and Nostr protocol implementation.
```
/nostr-check packages/nostr-client/src/events.ts --nips NIP-01,NIP-44
```
### `/lightning-check` - Lightning.Pub Conformity
Validate CLINK protocol and Lightning.Pub integration.
```
/lightning-check packages/clink/src/ --clink
```
### `/test` - Testing Agent
Run tests, generate test cases, validate transaction flows.
```
/test coverage packages/state-machine/
/test flow cash-out
/test generate packages/lightning/src/client.ts
```
### `/docs` - Documentation Agent
Keep documentation synchronized with code.
```
/docs sync packages/clink/
/docs api packages/nostr-client/src/
```
### `/hal-check` - HAL Validation
Validate Rust HAL drivers against lamassu-machine implementations.
```
/hal-check port id003
/hal-check safety packages/hal/src/dispensers/
```
## Code Style
### TypeScript
- ESM only (`import`/`export`)
- Strict mode with `strictNullChecks` and `noUncheckedIndexedAccess`
- Zod for runtime validation
- No `any` types
### Rust (HAL)
- Stable toolchain
- `#![deny(unsafe_code)]` unless justified
- Error handling with `thiserror`
- Async with `tokio`
### Formatting
- Prettier for TypeScript (2 spaces, no semicolons, single quotes)
- rustfmt for Rust
- Pre-commit hooks enforce formatting
## Hardware Drivers
Drivers are ported from `lamassu-machine/lib/`:
| Category | Drivers |
|----------|---------|
| Validators | id003, ccnet, cashflow_sc, bnr_advance, genmega, hcm2, gsr50 |
| Dispensers | puloon, f56, genmega, hcm2, gsr50 |
| Printers | nippon, zebra, genmega |
When porting:
1. Read JS driver thoroughly
2. Document protocol from JS code
3. Implement Rust version
4. Test against same hardware
5. Use `/hal-check port <driver>` to validate
## Nostr Event Kinds
| Kind | Description |
|------|-------------|
| 21001 | CLINK Offer |
| 21002 | CLINK Debit |
| 21003 | CLINK Manage |
| 30078 | Machine Status (replaceable) |
| 30079 | Transaction Record (replaceable) |
## Security Priorities
1. **Private keys** - Never log nsec, protect with 0600 permissions
2. **Payments** - Validate invoices, verify preimages, prevent double-pay
3. **Hardware** - Validate dispense amounts, handle errors gracefully
4. **Encryption** - Use NIP-44 for all sensitive data
## Testing Requirements
- Unit tests for all packages
- Integration tests for cross-package interactions
- E2E tests for full transaction flows
- **Cash-out flow is critical path** (95%+ of activity)
## Related Documentation
- `../docs/implementation-plan.md` - Full implementation roadmap
- `../docs/nostr-native-architecture.md` - Architecture details
- `../docs/architecture-review.md` - KYC-free vision
- `.claude/skills/*.md` - Skill documentation

242
lamassu-next/devenv.nix Normal file
View file

@ -0,0 +1,242 @@
{ pkgs, lib, ... }:
{
# Project metadata
name = "lamassu-next";
# ============================================
# Languages
# ============================================
languages.javascript = {
enable = true;
package = pkgs.nodejs_22;
pnpm = {
enable = true;
install.enable = true;
};
};
languages.typescript.enable = true;
languages.rust = {
enable = true;
channel = "stable";
components = [ "rustc" "cargo" "clippy" "rustfmt" "rust-analyzer" ];
};
# ============================================
# Services
# ============================================
services.postgres = {
enable = true;
initialDatabases = [{ name = "lamassu_dev"; }];
listen_addresses = "127.0.0.1";
};
# ============================================
# Packages
# ============================================
packages = with pkgs; [
# Build tools
pkg-config
openssl
openssl.dev
# Tauri dependencies (Linux)
gtk3
webkitgtk
libappindicator-gtk3
librsvg
# Hardware access
libusb1
libnfc
udev
# Serial port access
picocom
# Nostr tools
nak # Nostr army knife CLI
# Development utilities
just # Task runner
jq # JSON processing
httpie # HTTP client
websocat # WebSocket client
# Database tools
pgcli
# Container tools (for Lightning.Pub, strfry)
docker-compose
];
# ============================================
# Pre-commit Hooks
# ============================================
pre-commit.hooks = {
# TypeScript/JavaScript
prettier = {
enable = true;
excludes = [ "pnpm-lock.yaml" ];
};
eslint = {
enable = true;
entry = "pnpm eslint --fix";
types = [ "javascript" "typescript" ];
};
# Rust
rustfmt.enable = true;
clippy = {
enable = true;
entry = "cargo clippy --all-targets --all-features -- -D warnings";
pass_filenames = false;
};
# TypeScript type checking
typecheck = {
enable = true;
entry = "pnpm typecheck";
pass_filenames = false;
stages = [ "pre-push" ]; # Only on push, not every commit
};
# Security: detect secrets
detect-secrets = {
enable = true;
entry = "${pkgs.detect-secrets}/bin/detect-secrets-hook --baseline .secrets.baseline";
};
# Nix formatting
nixpkgs-fmt.enable = true;
# TOML/YAML validation
check-toml.enable = true;
check-yaml.enable = true;
# Prevent large files
check-added-large-files = {
enable = true;
entry = "check-added-large-files --maxkb=500";
};
# Custom: Nostr event schema validation
nostr-schema = {
enable = true;
entry = "pnpm --filter @lamassu/nostr-client run validate-schemas";
files = "packages/nostr-client/.*\\.(ts|json)$";
pass_filenames = false;
};
# Custom: No console.log in production code
no-console-log = {
enable = true;
entry = ''
${pkgs.gnugrep}/bin/grep -rn "console\\.log" --include="*.ts" --include="*.tsx" \
apps/ packages/ \
--exclude-dir=node_modules \
--exclude-dir=dist \
--exclude="*.test.ts" \
--exclude="*.spec.ts" \
&& echo "ERROR: console.log found in production code" && exit 1 || exit 0
'';
pass_filenames = false;
};
};
# ============================================
# Environment Variables
# ============================================
env = {
RUST_BACKTRACE = "1";
DATABASE_URL = "postgresql://localhost/lamassu_dev";
# Nostr development relay
NOSTR_RELAY_URL = "ws://localhost:7777";
# Lightning.Pub development
LIGHTNING_PUB_URL = "http://localhost:3000";
};
# ============================================
# Scripts (available as commands in shell)
# ============================================
scripts = {
dev.exec = "pnpm turbo dev";
build.exec = "pnpm turbo build";
test.exec = "pnpm turbo test";
lint.exec = "pnpm turbo lint";
# Start infrastructure
infra-up.exec = ''
echo "Starting development infrastructure..."
docker-compose -f docker/docker-compose.dev.yml up -d
echo "Waiting for services..."
sleep 5
echo "Infrastructure ready!"
echo " - Nostr relay: ws://localhost:7777"
echo " - Lightning.Pub: http://localhost:3000"
'';
infra-down.exec = "docker-compose -f docker/docker-compose.dev.yml down";
# Hardware testing
mock-bill.exec = ''
# Simulate bill insertion for testing
echo "Simulating $1 bill insertion..."
# TODO: Implement mock event publisher
'';
# Nostr tools
nostr-publish.exec = ''
nak event --content "$1" ws://localhost:7777
'';
nostr-subscribe.exec = ''
nak req --kinds "$1" ws://localhost:7777
'';
};
# ============================================
# Shell Hook
# ============================================
enterShell = ''
echo ""
echo " ⚡ Lamassu Next - Nostr-Native Lightning ATM"
echo " ─────────────────────────────────────────────"
echo " Node.js: $(node --version)"
echo " Rust: $(rustc --version | cut -d' ' -f2)"
echo " pnpm: $(pnpm --version)"
echo ""
echo " Commands:"
echo " dev Start development servers"
echo " build Build all packages"
echo " test Run tests"
echo " infra-up Start Docker infrastructure"
echo " infra-down Stop Docker infrastructure"
echo ""
echo " Pre-commit hooks are enabled. Run 'pre-commit run --all-files' to check."
echo ""
'';
# ============================================
# Process Compose (optional background services)
# ============================================
# Uncomment to auto-start services in devenv up
# processes = {
# relay.exec = "docker-compose -f docker/docker-compose.dev.yml up strfry";
# lightning-pub.exec = "docker-compose -f docker/docker-compose.dev.yml up lightning-pub";
# };
}

5
lamassu-next/devenv.yaml Normal file
View file

@ -0,0 +1,5 @@
inputs:
nixpkgs:
url: github:NixOS/nixpkgs/nixpkgs-unstable
pre-commit-hooks:
url: github:cachix/pre-commit-hooks.nix

View file

@ -0,0 +1,63 @@
version: '3.8'
services:
# Private Nostr relay for ATM communication
strfry:
image: ghcr.io/hoytech/strfry:latest
container_name: lamassu-relay
ports:
- "7777:7777"
volumes:
- ./strfry.conf:/etc/strfry.conf:ro
- strfry-data:/app/strfry-db
restart: unless-stopped
# Lightning.Pub - Nostr-native Lightning account system
lightning-pub:
image: ghcr.io/shocknet/lightning-pub:latest
container_name: lamassu-lightning-pub
ports:
- "3000:3000"
- "9735:9735"
volumes:
- lightning-pub-data:/root/lightning_pub
environment:
- NETWORK=regtest # Use regtest for development
restart: unless-stopped
# Polar LND node for regtest (optional - for testing without Lightning.Pub)
# Uncomment if you want a separate LND instance
# lnd:
# image: lightninglabs/lnd:v0.18.0-beta
# container_name: lamassu-lnd
# ports:
# - "10009:10009"
# - "9736:9735"
# volumes:
# - lnd-data:/root/.lnd
# command: >
# --bitcoin.active
# --bitcoin.regtest
# --bitcoin.node=neutrino
# --neutrino.connect=faucet.lightning.community
# restart: unless-stopped
# PostgreSQL for any server-side state (optional)
postgres:
image: postgres:16-alpine
container_name: lamassu-postgres
ports:
- "5432:5432"
environment:
POSTGRES_DB: lamassu_dev
POSTGRES_USER: lamassu
POSTGRES_PASSWORD: lamassu_dev_password
volumes:
- postgres-data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
strfry-data:
lightning-pub-data:
postgres-data:
# lnd-data:

View file

@ -0,0 +1,52 @@
##
## strfry configuration for Lamassu ATM development
##
relay {
bind = "0.0.0.0"
port = 7777
info {
name = "Lamassu Dev Relay"
description = "Private Nostr relay for ATM development and testing"
pubkey = ""
contact = ""
}
# Maximum message size in bytes
maxWebsocketPayloadSize = 131072
# Connection limits
maxWebsockets = 100
maxConnections = 1000
}
# Event policies
events {
# Maximum event size
maxEventSize = 65536
# Rate limiting
rejectEventsNewerThanSeconds = 900
rejectEventsOlderThanSeconds = 94608000
# Event kinds we care about
# 14: Private DMs (NIP-17)
# 21001-21003: CLINK events
# 22242: NIP-42 auth
# 30078: Machine status (replaceable)
# 30079: Transaction records (replaceable)
}
# Negentropy sync (for relay federation)
negentropy {
enabled = true
syncOnConnect = false
}
# Plugins (for NIP-42 auth in production)
# plugins {
# authRequired = true
# writePolicy = "accept"
# readPolicy = "accept"
# }

86
lamassu-next/flake.nix Normal file
View file

@ -0,0 +1,86 @@
{
description = "Lamassu Next - Nostr-Native Lightning ATM";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
devenv = {
url = "github:cachix/devenv";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, flake-utils, rust-overlay, devenv }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs { inherit system overlays; };
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" "rust-analyzer" ];
targets = [ "wasm32-unknown-unknown" ];
};
in
{
# Development shell via devenv
devShells.default = devenv.lib.mkShell {
inherit pkgs;
modules = [ ./devenv.nix ];
};
# Packages
packages = {
# HAL Rust crate
hal = pkgs.rustPlatform.buildRustPackage {
pname = "lamassu-hal";
version = "0.1.0";
src = ./packages/hal;
cargoLock.lockFile = ./packages/hal/Cargo.lock;
nativeBuildInputs = with pkgs; [
pkg-config
];
buildInputs = with pkgs; [
openssl
libudev-zero
];
};
# Full application (TODO)
default = self.packages.${system}.hal;
};
# NixOS module for deployment
nixosModules.default = { config, lib, pkgs, ... }: {
options.services.lamassu-atm = {
enable = lib.mkEnableOption "Lamassu ATM service";
relayUrl = lib.mkOption {
type = lib.types.str;
description = "Nostr relay URL";
};
lightningPubUrl = lib.mkOption {
type = lib.types.str;
description = "Lightning.Pub connection URL";
};
identityPath = lib.mkOption {
type = lib.types.path;
default = "/etc/lamassu/machine.nsec";
description = "Path to machine identity (nsec)";
};
};
config = lib.mkIf config.services.lamassu-atm.enable {
# TODO: systemd service, firewall rules, etc.
};
};
}
);
}

26
lamassu-next/package.json Normal file
View file

@ -0,0 +1,26 @@
{
"name": "lamassu-next",
"version": "0.1.0",
"private": true,
"description": "Nostr-native Lightning ATM - KYC-free, open-source",
"type": "module",
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
"test": "turbo test",
"lint": "turbo lint",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "turbo typecheck"
},
"devDependencies": {
"@types/node": "^22.0.0",
"prettier": "^3.4.0",
"turbo": "^2.3.0",
"typescript": "^5.7.0"
},
"packageManager": "pnpm@9.15.0",
"engines": {
"node": ">=22.0.0"
}
}

View file

@ -0,0 +1,30 @@
{
"name": "@lamassu/cashu",
"version": "0.1.0",
"description": "Cashu ecash integration for offline ATM operation",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"lint": "eslint src/"
},
"dependencies": {
"@cashu/cashu-ts": "^2.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0",
"vitest": "^2.1.0"
}
}

View file

@ -0,0 +1,31 @@
{
"name": "@lamassu/clink",
"version": "0.1.0",
"description": "CLINK protocol implementation for Nostr-native Lightning payments",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"lint": "eslint src/"
},
"dependencies": {
"@lamassu/nostr-client": "workspace:*",
"nostr-tools": "^2.10.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0",
"vitest": "^2.1.0"
}
}

View file

@ -0,0 +1,50 @@
[package]
name = "lamassu-hal"
version = "0.1.0"
edition = "2021"
description = "Hardware Abstraction Layer for Lamassu ATM"
license = "MIT"
repository = "https://github.com/lamassu/lamassu-next"
[lib]
crate-type = ["cdylib"]
[dependencies]
# napi-rs for Node.js bindings
napi = { version = "2", features = ["async", "tokio_rt"] }
napi-derive = "2"
# Async runtime
tokio = { version = "1", features = ["full"] }
# Serial port communication
tokio-serial = "5"
# Error handling
thiserror = "1"
# Async traits
async-trait = "0.1"
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[build-dependencies]
napi-build = "2"
[profile.release]
lto = true
strip = true
codegen-units = 1
[features]
default = []
# Enable hardware drivers (for production builds)
hardware = []
# Mock-only build (for testing/development)
mock-only = []

View file

@ -0,0 +1,5 @@
extern crate napi_build;
fn main() {
napi_build::setup();
}

View file

@ -0,0 +1,134 @@
//! Mock bill dispenser for testing
use async_trait::async_trait;
use crate::error::DispenserError;
use super::traits::{BillDispenser, CassetteStatus};
/// Mock bill dispenser for development and testing
pub struct MockDispenser {
connected: bool,
cassettes: Vec<CassetteStatus>,
bills_at_exit: bool,
}
impl MockDispenser {
/// Create a new mock dispenser with default cassettes
pub fn new() -> Self {
Self {
connected: false,
cassettes: vec![
CassetteStatus {
denomination: 20,
count: 500,
capacity: 500,
},
CassetteStatus {
denomination: 50,
count: 200,
capacity: 200,
},
],
bills_at_exit: false,
}
}
/// Create with custom cassette configuration
pub fn with_cassettes(cassettes: Vec<CassetteStatus>) -> Self {
Self {
connected: false,
cassettes,
bills_at_exit: false,
}
}
}
impl Default for MockDispenser {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl BillDispenser for MockDispenser {
fn driver_name(&self) -> &'static str {
"mock"
}
async fn connect(&mut self) -> Result<(), DispenserError> {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
self.connected = true;
tracing::info!("MockDispenser connected");
Ok(())
}
async fn disconnect(&mut self) -> Result<(), DispenserError> {
self.connected = false;
tracing::info!("MockDispenser disconnected");
Ok(())
}
async fn get_cassette_status(&self) -> Result<Vec<CassetteStatus>, DispenserError> {
Ok(self.cassettes.clone())
}
async fn dispense(&mut self, denomination: u32, count: u32) -> Result<u32, DispenserError> {
if !self.connected {
return Err(DispenserError::ConnectionFailed("Not connected".into()));
}
// Find cassette with this denomination
let cassette = self
.cassettes
.iter_mut()
.find(|c| c.denomination == denomination)
.ok_or_else(|| {
DispenserError::HardwareError(format!(
"No cassette for denomination {}",
denomination
))
})?;
// Check if we have enough bills
if cassette.count < count {
return Err(DispenserError::InsufficientBills(count, cassette.count));
}
// Simulate dispense time
tokio::time::sleep(tokio::time::Duration::from_millis(count as u64 * 200)).await;
// Update count
cassette.count -= count;
self.bills_at_exit = true;
tracing::info!(
"MockDispenser dispensed {} x ${} bills ({} remaining)",
count,
denomination,
cassette.count
);
Ok(count)
}
async fn reset(&mut self) -> Result<(), DispenserError> {
tracing::info!("MockDispenser reset");
self.bills_at_exit = false;
Ok(())
}
async fn is_ready(&self) -> Result<bool, DispenserError> {
Ok(self.connected && !self.bills_at_exit)
}
async fn bills_present(&self) -> Result<bool, DispenserError> {
Ok(self.bills_at_exit)
}
async fn wait_for_bills_removed(&self) -> Result<(), DispenserError> {
// Simulate customer taking bills
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
tracing::info!("MockDispenser: bills removed");
Ok(())
}
}

View file

@ -0,0 +1,125 @@
//! Bill dispenser drivers
//!
//! This module contains implementations for various bill dispenser protocols:
//! - Puloon LCDM series
//! - Fujitsu F53/F56
//! - Genmega
//! - HCM2 (Hitachi recycler)
//! - GSR50 (recycler)
pub mod traits;
pub mod mock;
// pub mod puloon;
// pub mod f56;
// pub mod genmega;
// pub mod hcm2;
// pub mod gsr50;
pub use traits::*;
pub use mock::MockDispenser;
use napi::bindgen_prelude::*;
use napi_derive::napi;
use crate::{DispenserDriver, error::DispenserError};
/// Wrapper for bill dispenser that exposes napi-rs bindings
#[napi]
pub struct BillDispenserWrapper {
inner: Box<dyn BillDispenser>,
}
#[napi]
impl BillDispenserWrapper {
/// Create a new bill dispenser instance
#[napi(constructor)]
pub fn new(
driver: DispenserDriver,
port: Option<String>,
fiat_code: Option<String>,
) -> Result<Self> {
let _fiat = fiat_code.unwrap_or_else(|| "USD".to_string());
let dispenser: Box<dyn BillDispenser> = match driver {
DispenserDriver::Mock => Box::new(MockDispenser::new()),
// TODO: Implement other drivers
_ => {
return Err(Error::from_reason(format!(
"Driver {:?} not yet implemented. Use Mock for development.",
driver
)))
}
};
Ok(Self { inner: dispenser })
}
/// Get the driver name
#[napi(getter)]
pub fn driver_name(&self) -> String {
self.inner.driver_name().to_string()
}
/// Connect to the dispenser
#[napi]
pub async fn connect(&mut self) -> Result<()> {
self.inner
.connect()
.await
.map_err(|e| Error::from_reason(e.to_string()))
}
/// Disconnect from the dispenser
#[napi]
pub async fn disconnect(&mut self) -> Result<()> {
self.inner
.disconnect()
.await
.map_err(|e| Error::from_reason(e.to_string()))
}
/// Dispense bills
#[napi]
pub async fn dispense(&mut self, denomination: u32, count: u32) -> Result<u32> {
self.inner
.dispense(denomination, count)
.await
.map_err(|e| Error::from_reason(e.to_string()))
}
/// Reset the dispenser
#[napi]
pub async fn reset(&mut self) -> Result<()> {
self.inner
.reset()
.await
.map_err(|e| Error::from_reason(e.to_string()))
}
/// Check if dispenser is ready
#[napi]
pub async fn is_ready(&self) -> Result<bool> {
self.inner
.is_ready()
.await
.map_err(|e| Error::from_reason(e.to_string()))
}
/// Check if bills are present at exit
#[napi]
pub async fn bills_present(&self) -> Result<bool> {
self.inner
.bills_present()
.await
.map_err(|e| Error::from_reason(e.to_string()))
}
/// Wait for bills to be removed
#[napi]
pub async fn wait_for_bills_removed(&self) -> Result<()> {
self.inner
.wait_for_bills_removed()
.await
.map_err(|e| Error::from_reason(e.to_string()))
}
}

View file

@ -0,0 +1,55 @@
//! Bill dispenser trait definitions
use async_trait::async_trait;
use crate::error::DispenserError;
/// Status of a cassette in the dispenser
#[derive(Debug, Clone)]
pub struct CassetteStatus {
/// Bill denomination in this cassette
pub denomination: u32,
/// Current bill count
pub count: u32,
/// Maximum capacity
pub capacity: u32,
}
/// Unified interface for all bill dispensers
///
/// Implementations: Puloon, F56, Genmega, HCM2, GSR50, Mock
#[async_trait]
pub trait BillDispenser: Send + Sync {
/// Get dispenser driver name (for logging/debugging)
fn driver_name(&self) -> &'static str;
/// Connect to the dispenser
async fn connect(&mut self) -> Result<(), DispenserError>;
/// Disconnect from the dispenser
async fn disconnect(&mut self) -> Result<(), DispenserError>;
/// Get status of all cassettes
async fn get_cassette_status(&self) -> Result<Vec<CassetteStatus>, DispenserError>;
/// Dispense bills from a specific cassette
///
/// Returns the number of bills actually dispensed.
async fn dispense(&mut self, denomination: u32, count: u32) -> Result<u32, DispenserError>;
/// Reset the dispenser after a jam or error
async fn reset(&mut self) -> Result<(), DispenserError>;
/// Check if dispenser is ready to dispense
async fn is_ready(&self) -> Result<bool, DispenserError>;
/// Check if bills are present at the exit (for customer to take)
///
/// Not all dispensers support this - some will always return true.
async fn bills_present(&self) -> Result<bool, DispenserError>;
/// Wait for bills to be removed by customer
///
/// Returns immediately for dispensers that don't support detection.
async fn wait_for_bills_removed(&self) -> Result<(), DispenserError>;
}

View file

@ -0,0 +1,91 @@
//! Error types for the HAL
use thiserror::Error;
/// Errors that can occur during bill validator operations
#[derive(Debug, Error)]
pub enum ValidatorError {
/// Failed to connect to the validator
#[error("Connection failed: {0}")]
ConnectionFailed(String),
/// Communication error during operation
#[error("Communication error: {0}")]
CommunicationError(String),
/// Bill was rejected by the validator
#[error("Bill rejected: {0}")]
BillRejected(String),
/// Stacker is full
#[error("Stacker full")]
StackerFull,
/// General hardware error
#[error("Hardware error: {0}")]
HardwareError(String),
/// Invalid state for requested operation
#[error("Invalid state: {0}")]
InvalidState(String),
/// Operation timed out
#[error("Operation timed out")]
Timeout,
}
/// Errors that can occur during bill dispenser operations
#[derive(Debug, Error)]
pub enum DispenserError {
/// Failed to connect to the dispenser
#[error("Connection failed: {0}")]
ConnectionFailed(String),
/// Communication error during operation
#[error("Communication error: {0}")]
CommunicationError(String),
/// Not enough bills to fulfill request
#[error("Insufficient bills: need {0}, have {1}")]
InsufficientBills(u32, u32),
/// Cassette is empty
#[error("Cassette empty: denomination {0}")]
CassetteEmpty(u32),
/// Bill jam detected
#[error("Bill jam")]
BillJam,
/// General hardware error
#[error("Hardware error: {0}")]
HardwareError(String),
/// Operation timed out
#[error("Operation timed out")]
Timeout,
}
/// Errors that can occur during printer operations
#[derive(Debug, Error)]
pub enum PrinterError {
/// Failed to connect to the printer
#[error("Connection failed: {0}")]
ConnectionFailed(String),
/// Communication error during operation
#[error("Communication error: {0}")]
CommunicationError(String),
/// Printer is out of paper
#[error("Out of paper")]
OutOfPaper,
/// Paper jam detected
#[error("Paper jam")]
PaperJam,
/// General hardware error
#[error("Hardware error: {0}")]
HardwareError(String),
}

View file

@ -0,0 +1,94 @@
//! Lamassu Hardware Abstraction Layer
//!
//! This crate provides Rust implementations of hardware drivers for bill validators,
//! dispensers, printers, and other ATM peripherals. These are exposed to Node.js
//! via napi-rs bindings.
//!
//! # Supported Hardware
//!
//! ## Bill Validators
//! - ID003 (JCM) - Default validator protocol
//! - CCNET (CashCode)
//! - MEI CashFlow SC
//! - MEI BNR Advance
//! - Genmega
//! - HCM2 (Hitachi recycler)
//! - GSR50 (recycler)
//!
//! ## Bill Dispensers
//! - Puloon LCDM series
//! - Fujitsu F53/F56
//! - Genmega
//! - HCM2 (Hitachi recycler)
//! - GSR50 (recycler)
//!
//! ## Printers
//! - Nippon (ESC/POS)
//! - Zebra (ZPL)
//! - Genmega
//!
//! # Usage
//!
//! ```typescript
//! import { BillValidatorWrapper, ValidatorDriver } from '@lamassu/hal'
//!
//! const validator = new BillValidatorWrapper(ValidatorDriver.Id003, '/dev/ttyUSB0', 'USD')
//! await validator.connect()
//! await validator.enable()
//! ```
#![deny(unsafe_code)]
#![warn(missing_docs)]
#![warn(clippy::all)]
use napi_derive::napi;
pub mod error;
pub mod validators;
pub mod dispensers;
// pub mod printer;
// pub mod scanner;
// pub mod leds;
// pub mod nfc;
/// Supported bill validator drivers
#[napi]
pub enum ValidatorDriver {
/// JCM ID-003 protocol (default)
Id003,
/// CashCode CCNET protocol
Ccnet,
/// MEI CashFlow SC
CashflowSc,
/// MEI BNR Advance
BnrAdvance,
/// Genmega validator
Genmega,
/// Hitachi HCM2 recycler
Hcm2,
/// GSR50 recycler
Gsr50,
/// Mock validator for testing
Mock,
}
/// Supported bill dispenser drivers
#[napi]
pub enum DispenserDriver {
/// Puloon LCDM series
Puloon,
/// Fujitsu F53/F56
F56,
/// Genmega dispenser
Genmega,
/// Hitachi HCM2 recycler
Hcm2,
/// GSR50 recycler
Gsr50,
/// Mock dispenser for testing
Mock,
}
// Re-exports for convenience
pub use validators::BillValidatorWrapper;
pub use dispensers::BillDispenserWrapper;

View file

@ -0,0 +1,147 @@
//! Mock bill validator for testing
use async_trait::async_trait;
use tokio::sync::broadcast;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::error::ValidatorError;
use super::traits::{BillValidator, BillEvent, BillEventType};
/// Mock bill validator for development and testing
pub struct MockValidator {
connected: bool,
enabled: bool,
escrowed_bill: Option<u32>,
bill_count: u32,
event_tx: broadcast::Sender<BillEvent>,
fiat_code: String,
}
impl MockValidator {
/// Create a new mock validator
pub fn new() -> Self {
let (event_tx, _) = broadcast::channel(16);
Self {
connected: false,
enabled: false,
escrowed_bill: None,
bill_count: 0,
event_tx,
fiat_code: "USD".to_string(),
}
}
/// Simulate a bill being inserted (for testing)
pub fn simulate_bill(&mut self, denomination: u32) {
if self.enabled {
self.escrowed_bill = Some(denomination);
let _ = self.event_tx.send(BillEvent {
denomination,
currency: self.fiat_code.clone(),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
event_type: BillEventType::Inserted,
});
}
}
fn current_timestamp(&self) -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}
}
impl Default for MockValidator {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl BillValidator for MockValidator {
fn driver_name(&self) -> &'static str {
"mock"
}
async fn connect(&mut self) -> Result<(), ValidatorError> {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
self.connected = true;
tracing::info!("MockValidator connected");
Ok(())
}
async fn disconnect(&mut self) -> Result<(), ValidatorError> {
self.connected = false;
self.enabled = false;
tracing::info!("MockValidator disconnected");
Ok(())
}
async fn enable(&mut self) -> Result<(), ValidatorError> {
if !self.connected {
return Err(ValidatorError::ConnectionFailed("Not connected".into()));
}
self.enabled = true;
tracing::info!("MockValidator enabled");
Ok(())
}
async fn disable(&mut self) -> Result<(), ValidatorError> {
self.enabled = false;
tracing::info!("MockValidator disabled");
Ok(())
}
async fn accept(&mut self) -> Result<(), ValidatorError> {
match self.escrowed_bill.take() {
Some(denomination) => {
self.bill_count += 1;
let _ = self.event_tx.send(BillEvent {
denomination,
currency: self.fiat_code.clone(),
timestamp: self.current_timestamp(),
event_type: BillEventType::Stacked,
});
tracing::info!("MockValidator accepted ${}", denomination);
Ok(())
}
None => Err(ValidatorError::InvalidState("No bill escrowed".into())),
}
}
async fn reject(&mut self) -> Result<(), ValidatorError> {
match self.escrowed_bill.take() {
Some(denomination) => {
let _ = self.event_tx.send(BillEvent {
denomination,
currency: self.fiat_code.clone(),
timestamp: self.current_timestamp(),
event_type: BillEventType::Rejected,
});
tracing::info!("MockValidator rejected ${}", denomination);
Ok(())
}
None => Err(ValidatorError::InvalidState("No bill escrowed".into())),
}
}
async fn get_bill_count(&self) -> Result<u32, ValidatorError> {
Ok(self.bill_count)
}
fn subscribe(&self) -> broadcast::Receiver<BillEvent> {
self.event_tx.subscribe()
}
async fn run(&mut self) -> Result<(), ValidatorError> {
// Mock validator doesn't need continuous polling
// In real implementation, this would poll the hardware
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
}
}

View file

@ -0,0 +1,119 @@
//! Bill validator drivers
//!
//! This module contains implementations for various bill validator protocols:
//! - ID003 (JCM) - Default protocol
//! - CCNET (CashCode)
//! - MEI CashFlow SC
//! - MEI BNR Advance
//! - Genmega
//! - HCM2 (Hitachi recycler)
//! - GSR50 (recycler)
pub mod traits;
pub mod mock;
// pub mod id003;
// pub mod ccnet;
// pub mod mei_cashflow;
// pub mod genmega;
// pub mod hcm2;
// pub mod gsr50;
pub use traits::*;
pub use mock::MockValidator;
use napi::bindgen_prelude::*;
use napi_derive::napi;
use crate::{ValidatorDriver, error::ValidatorError};
/// Wrapper for bill validator that exposes napi-rs bindings
#[napi]
pub struct BillValidatorWrapper {
inner: Box<dyn BillValidator>,
}
#[napi]
impl BillValidatorWrapper {
/// Create a new bill validator instance
#[napi(constructor)]
pub fn new(
driver: ValidatorDriver,
port: Option<String>,
fiat_code: Option<String>,
) -> Result<Self> {
let _fiat = fiat_code.unwrap_or_else(|| "USD".to_string());
let validator: Box<dyn BillValidator> = match driver {
ValidatorDriver::Mock => Box::new(MockValidator::new()),
// TODO: Implement other drivers
_ => {
return Err(Error::from_reason(format!(
"Driver {:?} not yet implemented. Use Mock for development.",
driver
)))
}
};
Ok(Self { inner: validator })
}
/// Get the driver name
#[napi(getter)]
pub fn driver_name(&self) -> String {
self.inner.driver_name().to_string()
}
/// Connect to the validator
#[napi]
pub async fn connect(&mut self) -> Result<()> {
self.inner
.connect()
.await
.map_err(|e| Error::from_reason(e.to_string()))
}
/// Disconnect from the validator
#[napi]
pub async fn disconnect(&mut self) -> Result<()> {
self.inner
.disconnect()
.await
.map_err(|e| Error::from_reason(e.to_string()))
}
/// Enable bill acceptance
#[napi]
pub async fn enable(&mut self) -> Result<()> {
self.inner
.enable()
.await
.map_err(|e| Error::from_reason(e.to_string()))
}
/// Disable bill acceptance
#[napi]
pub async fn disable(&mut self) -> Result<()> {
self.inner
.disable()
.await
.map_err(|e| Error::from_reason(e.to_string()))
}
/// Accept the currently escrowed bill
#[napi]
pub async fn accept(&mut self) -> Result<()> {
self.inner
.accept()
.await
.map_err(|e| Error::from_reason(e.to_string()))
}
/// Reject the currently escrowed bill
#[napi]
pub async fn reject(&mut self) -> Result<()> {
self.inner
.reject()
.await
.map_err(|e| Error::from_reason(e.to_string()))
}
}

View file

@ -0,0 +1,73 @@
//! Bill validator trait definitions
use async_trait::async_trait;
use tokio::sync::broadcast;
use crate::error::ValidatorError;
/// Event types for bill validator operations
#[derive(Debug, Clone)]
pub enum BillEventType {
/// Bill detected and validated
Inserted,
/// Bill accepted into stacker
Accepted,
/// Bill rejected (returned to customer)
Rejected,
/// Bill successfully stacked
Stacked,
/// Bill jammed
Jammed,
}
/// Event emitted by bill validators
#[derive(Debug, Clone)]
pub struct BillEvent {
/// Denomination of the bill
pub denomination: u32,
/// Currency code (e.g., "USD")
pub currency: String,
/// Unix timestamp
pub timestamp: u64,
/// Type of event
pub event_type: BillEventType,
}
/// Unified interface for all bill validators
///
/// Implementations: ID003, CCNET, MEI CashFlow, MEI BNR, Genmega, HCM2, GSR50, Mock
#[async_trait]
pub trait BillValidator: Send + Sync {
/// Get validator driver name (for logging/debugging)
fn driver_name(&self) -> &'static str;
/// Connect to the validator
async fn connect(&mut self) -> Result<(), ValidatorError>;
/// Disconnect from the validator
async fn disconnect(&mut self) -> Result<(), ValidatorError>;
/// Enable bill acceptance
async fn enable(&mut self) -> Result<(), ValidatorError>;
/// Disable bill acceptance
async fn disable(&mut self) -> Result<(), ValidatorError>;
/// Accept the currently held bill into stacker
async fn accept(&mut self) -> Result<(), ValidatorError>;
/// Reject the currently held bill
async fn reject(&mut self) -> Result<(), ValidatorError>;
/// Get current bill count in stacker (if supported)
async fn get_bill_count(&self) -> Result<u32, ValidatorError>;
/// Subscribe to bill events
fn subscribe(&self) -> broadcast::Receiver<BillEvent>;
/// Run the validator state machine (poll for events)
///
/// Most validators need continuous polling. This method should be
/// called in a separate task and will run indefinitely.
async fn run(&mut self) -> Result<(), ValidatorError>;
}

View file

@ -0,0 +1,32 @@
{
"name": "@lamassu/lightning",
"version": "0.1.0",
"description": "Lightning.Pub client for Nostr-native Lightning operations",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"lint": "eslint src/"
},
"dependencies": {
"@lamassu/nostr-client": "workspace:*",
"@lamassu/clink": "workspace:*",
"nostr-tools": "^2.10.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0",
"vitest": "^2.1.0"
}
}

View file

@ -0,0 +1,35 @@
{
"name": "@lamassu/nostr-client",
"version": "0.1.0",
"description": "Nostr client library for Lamassu ATM",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"lint": "eslint src/",
"validate-schemas": "tsx scripts/validate-schemas.ts"
},
"dependencies": {
"nostr-tools": "^2.10.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0",
"vitest": "^2.1.0",
"tsx": "^4.19.0"
},
"peerDependencies": {
"typescript": "^5.0.0"
}
}

View file

@ -0,0 +1,30 @@
{
"name": "@lamassu/state-machine",
"version": "0.1.0",
"description": "XState v5 state machine for ATM transaction flows",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"lint": "eslint src/"
},
"dependencies": {
"xstate": "^5.18.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0",
"vitest": "^2.1.0"
}
}

View file

@ -0,0 +1,40 @@
{
"name": "@lamassu/ui-shared",
"version": "0.1.0",
"description": "Shared Vue 3 components for Lamassu ATM and dashboard",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./components/*": {
"types": "./dist/components/*.d.ts",
"import": "./dist/components/*.js"
}
},
"scripts": {
"build": "vite build",
"dev": "vite build --watch",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "vue-tsc --noEmit",
"lint": "eslint src/"
},
"dependencies": {
"vue": "^3.5.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@vitejs/plugin-vue": "^5.2.0",
"typescript": "^5.7.0",
"vite": "^6.0.0",
"vitest": "^2.1.0",
"vue-tsc": "^2.1.0"
},
"peerDependencies": {
"vue": "^3.5.0"
}
}

View file

@ -0,0 +1,3 @@
packages:
- 'apps/*'
- 'packages/*'

View file

@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"strict": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"paths": {
"@lamassu/nostr-client": ["./packages/nostr-client/src"],
"@lamassu/clink": ["./packages/clink/src"],
"@lamassu/state-machine": ["./packages/state-machine/src"],
"@lamassu/lightning": ["./packages/lightning/src"],
"@lamassu/cashu": ["./packages/cashu/src"],
"@lamassu/ui-shared": ["./packages/ui-shared/src"],
"@lamassu/hal": ["./packages/hal"]
}
},
"exclude": ["node_modules", "dist", "**/dist", "**/*.test.ts", "**/*.spec.ts"]
}

23
lamassu-next/turbo.json Normal file
View file

@ -0,0 +1,23 @@
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"typecheck": {
"dependsOn": ["^build"]
},
"lint": {
"dependsOn": ["^build"]
}
}
}