feat(docker): add dev.sh with auto-funding and ATM app setup
- Add dev.sh script for managing regtest development environment - Implement cmd_fund to fund ATM app owner via Lightning.Pub API - Add --fund flag to cmd_up for automatic funding on startup - Update setup_atm_app to write VITE_APP_ID to machine .env - Fix Electron IPC to pass appId and extensionApiUrl to renderer - Restructure repo from nested lamassu-next/ to root The dev.sh script now supports: - ./dev.sh up --fund # Start regtest and auto-fund ATM - ./dev.sh fund # Fund existing ATM app - ./dev.sh status # Show environment status - ./dev.sh reset # Clean restart Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
30a2eb2199
commit
c98f126ba7
180 changed files with 2695 additions and 9587 deletions
46
packages/nostr-client/src/__tests__/encryption.test.ts
Normal file
46
packages/nostr-client/src/__tests__/encryption.test.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { generateIdentity } from '../identity.js'
|
||||
import { encryptContent, decryptContent, decryptJSON } from '../encryption.js'
|
||||
|
||||
describe('encryption', () => {
|
||||
describe('encryptContent / decryptContent', () => {
|
||||
it('should encrypt and decrypt string content', () => {
|
||||
const sender = generateIdentity()
|
||||
const recipient = generateIdentity()
|
||||
const message = 'Hello, Nostr!'
|
||||
|
||||
const encrypted = encryptContent(sender, recipient.publicKey, message)
|
||||
|
||||
expect(encrypted).not.toBe(message)
|
||||
expect(typeof encrypted).toBe('string')
|
||||
|
||||
const decrypted = decryptContent(recipient, sender.publicKey, encrypted)
|
||||
|
||||
expect(decrypted).toBe(message)
|
||||
})
|
||||
|
||||
it('should encrypt and decrypt object content', () => {
|
||||
const sender = generateIdentity()
|
||||
const recipient = generateIdentity()
|
||||
const data = { amount: 1000, currency: 'USD', timestamp: Date.now() }
|
||||
|
||||
const encrypted = encryptContent(sender, recipient.publicKey, data)
|
||||
const decrypted = decryptContent(recipient, sender.publicKey, encrypted)
|
||||
|
||||
expect(JSON.parse(decrypted)).toEqual(data)
|
||||
})
|
||||
})
|
||||
|
||||
describe('decryptJSON', () => {
|
||||
it('should decrypt and parse JSON directly', () => {
|
||||
const sender = generateIdentity()
|
||||
const recipient = generateIdentity()
|
||||
const data = { test: true, nested: { value: 42 } }
|
||||
|
||||
const encrypted = encryptContent(sender, recipient.publicKey, data)
|
||||
const decrypted = decryptJSON<typeof data>(recipient, sender.publicKey, encrypted)
|
||||
|
||||
expect(decrypted).toEqual(data)
|
||||
})
|
||||
})
|
||||
})
|
||||
82
packages/nostr-client/src/__tests__/events.test.ts
Normal file
82
packages/nostr-client/src/__tests__/events.test.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { generateIdentity } from '../identity.js'
|
||||
import {
|
||||
createSignedEvent,
|
||||
createMachineStatusEvent,
|
||||
createAuthEvent,
|
||||
validateEvent,
|
||||
generateTxId,
|
||||
} from '../events.js'
|
||||
import { LamassuEventKind, type MachineStatus } from '../types.js'
|
||||
|
||||
describe('events', () => {
|
||||
describe('createSignedEvent', () => {
|
||||
it('should create a properly signed event', () => {
|
||||
const identity = generateIdentity()
|
||||
const event = createSignedEvent(identity, {
|
||||
kind: 1,
|
||||
content: 'test',
|
||||
tags: [],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
})
|
||||
|
||||
expect(event.pubkey).toBe(identity.publicKey)
|
||||
expect(event.kind).toBe(1)
|
||||
expect(event.content).toBe('test')
|
||||
expect(event.id).toMatch(/^[0-9a-f]{64}$/)
|
||||
expect(event.sig).toMatch(/^[0-9a-f]{128}$/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createMachineStatusEvent', () => {
|
||||
it('should create encrypted status event', () => {
|
||||
const machine = generateIdentity()
|
||||
const operator = generateIdentity()
|
||||
|
||||
const status: MachineStatus = {
|
||||
online: true,
|
||||
lastTransaction: Date.now(),
|
||||
cashLevels: {
|
||||
validator: 1000,
|
||||
dispenser: [{ denomination: 20, count: 100, capacity: 500 }],
|
||||
},
|
||||
errors: [],
|
||||
version: '1.0.0',
|
||||
}
|
||||
|
||||
const event = createMachineStatusEvent(machine, operator.publicKey, status)
|
||||
|
||||
expect(event.kind).toBe(LamassuEventKind.MachineStatus)
|
||||
expect(event.pubkey).toBe(machine.publicKey)
|
||||
expect(event.tags).toContainEqual(['d', 'status'])
|
||||
expect(event.tags).toContainEqual(['p', operator.publicKey])
|
||||
// Content should be encrypted (not readable JSON)
|
||||
expect(() => JSON.parse(event.content)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createAuthEvent', () => {
|
||||
it('should create NIP-42 auth event', () => {
|
||||
const identity = generateIdentity()
|
||||
const relayUrl = 'wss://relay.test.com'
|
||||
const challenge = 'random-challenge-string'
|
||||
|
||||
const event = createAuthEvent(identity, relayUrl, challenge)
|
||||
|
||||
expect(event.kind).toBe(LamassuEventKind.Auth)
|
||||
expect(event.content).toBe('')
|
||||
expect(event.tags).toContainEqual(['relay', relayUrl])
|
||||
expect(event.tags).toContainEqual(['challenge', challenge])
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateTxId', () => {
|
||||
it('should generate unique IDs', () => {
|
||||
const ids = new Set<string>()
|
||||
for (let i = 0; i < 100; i++) {
|
||||
ids.add(generateTxId())
|
||||
}
|
||||
expect(ids.size).toBe(100)
|
||||
})
|
||||
})
|
||||
})
|
||||
61
packages/nostr-client/src/__tests__/identity.test.ts
Normal file
61
packages/nostr-client/src/__tests__/identity.test.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
generateIdentity,
|
||||
loadIdentityFromNsec,
|
||||
exportToNsec,
|
||||
parsePublicKey,
|
||||
} from '../identity.js'
|
||||
|
||||
describe('identity', () => {
|
||||
describe('generateIdentity', () => {
|
||||
it('should generate a valid identity', () => {
|
||||
const identity = generateIdentity()
|
||||
|
||||
expect(identity.privateKey).toBeInstanceOf(Uint8Array)
|
||||
expect(identity.privateKey.length).toBe(32)
|
||||
expect(identity.publicKey).toMatch(/^[0-9a-f]{64}$/)
|
||||
expect(identity.npub).toMatch(/^npub1[a-z0-9]{58}$/)
|
||||
})
|
||||
|
||||
it('should generate unique identities', () => {
|
||||
const id1 = generateIdentity()
|
||||
const id2 = generateIdentity()
|
||||
|
||||
expect(id1.publicKey).not.toBe(id2.publicKey)
|
||||
})
|
||||
})
|
||||
|
||||
describe('exportToNsec / loadIdentityFromNsec', () => {
|
||||
it('should round-trip identity through nsec', () => {
|
||||
const original = generateIdentity()
|
||||
const nsec = exportToNsec(original)
|
||||
|
||||
expect(nsec).toMatch(/^nsec1[a-z0-9]{58}$/)
|
||||
|
||||
const restored = loadIdentityFromNsec(nsec)
|
||||
|
||||
expect(restored.publicKey).toBe(original.publicKey)
|
||||
expect(restored.npub).toBe(original.npub)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parsePublicKey', () => {
|
||||
it('should parse hex public key', () => {
|
||||
const identity = generateIdentity()
|
||||
const parsed = parsePublicKey(identity.publicKey)
|
||||
|
||||
expect(parsed).toBe(identity.publicKey)
|
||||
})
|
||||
|
||||
it('should parse npub', () => {
|
||||
const identity = generateIdentity()
|
||||
const parsed = parsePublicKey(identity.npub)
|
||||
|
||||
expect(parsed).toBe(identity.publicKey)
|
||||
})
|
||||
|
||||
it('should throw on invalid format', () => {
|
||||
expect(() => parsePublicKey('invalid')).toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue