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:
parent
e6f280a50b
commit
4ca1adeb0b
34 changed files with 2898 additions and 0 deletions
218
lamassu-next/.claude/skills/docs.md
Normal file
218
lamassu-next/.claude/skills/docs.md
Normal 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
|
||||
```
|
||||
200
lamassu-next/.claude/skills/hal-check.md
Normal file
200
lamassu-next/.claude/skills/hal-check.md
Normal 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
|
||||
```
|
||||
168
lamassu-next/.claude/skills/lightning-check.md
Normal file
168
lamassu-next/.claude/skills/lightning-check.md
Normal 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
|
||||
```
|
||||
125
lamassu-next/.claude/skills/nostr-check.md
Normal file
125
lamassu-next/.claude/skills/nostr-check.md
Normal 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
|
||||
```
|
||||
105
lamassu-next/.claude/skills/security.md
Normal file
105
lamassu-next/.claude/skills/security.md
Normal 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
|
||||
```
|
||||
201
lamassu-next/.claude/skills/test.md
Normal file
201
lamassu-next/.claude/skills/test.md
Normal 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
71
lamassu-next/.gitignore
vendored
Normal 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
7
lamassu-next/.prettierrc
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100
|
||||
}
|
||||
177
lamassu-next/CLAUDE.md
Normal file
177
lamassu-next/CLAUDE.md
Normal 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
242
lamassu-next/devenv.nix
Normal 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
5
lamassu-next/devenv.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
inputs:
|
||||
nixpkgs:
|
||||
url: github:NixOS/nixpkgs/nixpkgs-unstable
|
||||
pre-commit-hooks:
|
||||
url: github:cachix/pre-commit-hooks.nix
|
||||
63
lamassu-next/docker/docker-compose.dev.yml
Normal file
63
lamassu-next/docker/docker-compose.dev.yml
Normal 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:
|
||||
52
lamassu-next/docker/strfry.conf
Normal file
52
lamassu-next/docker/strfry.conf
Normal 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
86
lamassu-next/flake.nix
Normal 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
26
lamassu-next/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
30
lamassu-next/packages/cashu/package.json
Normal file
30
lamassu-next/packages/cashu/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
31
lamassu-next/packages/clink/package.json
Normal file
31
lamassu-next/packages/clink/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
50
lamassu-next/packages/hal/Cargo.toml
Normal file
50
lamassu-next/packages/hal/Cargo.toml
Normal 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 = []
|
||||
5
lamassu-next/packages/hal/build.rs
Normal file
5
lamassu-next/packages/hal/build.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
extern crate napi_build;
|
||||
|
||||
fn main() {
|
||||
napi_build::setup();
|
||||
}
|
||||
134
lamassu-next/packages/hal/src/dispensers/mock.rs
Normal file
134
lamassu-next/packages/hal/src/dispensers/mock.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
125
lamassu-next/packages/hal/src/dispensers/mod.rs
Normal file
125
lamassu-next/packages/hal/src/dispensers/mod.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
55
lamassu-next/packages/hal/src/dispensers/traits.rs
Normal file
55
lamassu-next/packages/hal/src/dispensers/traits.rs
Normal 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>;
|
||||
}
|
||||
91
lamassu-next/packages/hal/src/error.rs
Normal file
91
lamassu-next/packages/hal/src/error.rs
Normal 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),
|
||||
}
|
||||
94
lamassu-next/packages/hal/src/lib.rs
Normal file
94
lamassu-next/packages/hal/src/lib.rs
Normal 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;
|
||||
147
lamassu-next/packages/hal/src/validators/mock.rs
Normal file
147
lamassu-next/packages/hal/src/validators/mock.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
119
lamassu-next/packages/hal/src/validators/mod.rs
Normal file
119
lamassu-next/packages/hal/src/validators/mod.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
73
lamassu-next/packages/hal/src/validators/traits.rs
Normal file
73
lamassu-next/packages/hal/src/validators/traits.rs
Normal 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>;
|
||||
}
|
||||
32
lamassu-next/packages/lightning/package.json
Normal file
32
lamassu-next/packages/lightning/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
35
lamassu-next/packages/nostr-client/package.json
Normal file
35
lamassu-next/packages/nostr-client/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
30
lamassu-next/packages/state-machine/package.json
Normal file
30
lamassu-next/packages/state-machine/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
40
lamassu-next/packages/ui-shared/package.json
Normal file
40
lamassu-next/packages/ui-shared/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
3
lamassu-next/pnpm-workspace.yaml
Normal file
3
lamassu-next/pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
packages:
|
||||
- 'apps/*'
|
||||
- 'packages/*'
|
||||
35
lamassu-next/tsconfig.json
Normal file
35
lamassu-next/tsconfig.json
Normal 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
23
lamassu-next/turbo.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue