feat: implement ndebit cash-in flow with clink: URI scheme
Adds mock ATM machine and infrastructure for testing the ndebit cash-in flow where users scan a QR code to withdraw sats. Key changes: - Add mock-machine.mjs: displays ndebit QR with clink: scheme - Add ndebit-cash-in-flow.md: comprehensive implementation guide - Update docker-compose: configure relay for browser access - Use clink: URI scheme (protocol-agnostic, supports future Cashu/Fedimint) Flow: ATM displays clink:ndebit1...?amount=X → User scans with wallet → Wallet creates invoice → Sends Kind 21002 debit request → ATM pays Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e7fac251b7
commit
a7a7b16dc1
14 changed files with 2608 additions and 13 deletions
|
|
@ -101,8 +101,9 @@ services:
|
||||||
|
|
||||||
# Lightning.Pub - Nostr-native Lightning account system
|
# Lightning.Pub - Nostr-native Lightning account system
|
||||||
# Note: Lightning.Pub connects to LND for actual Lightning operations
|
# Note: Lightning.Pub connects to LND for actual Lightning operations
|
||||||
|
# Using patched image with Kind 0 profile publishing support
|
||||||
lightning-pub:
|
lightning-pub:
|
||||||
image: ghcr.io/shocknet/lightning-pub:latest
|
image: lightning-pub-patched:latest
|
||||||
container_name: lamassu-lightning-pub
|
container_name: lamassu-lightning-pub
|
||||||
depends_on:
|
depends_on:
|
||||||
lnd:
|
lnd:
|
||||||
|
|
@ -119,10 +120,9 @@ services:
|
||||||
- LND_ADDRESS=lnd:10009
|
- LND_ADDRESS=lnd:10009
|
||||||
- LND_CERT_PATH=/root/.lnd/tls.cert
|
- LND_CERT_PATH=/root/.lnd/tls.cert
|
||||||
- LND_MACAROON_PATH=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon
|
- LND_MACAROON_PATH=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon
|
||||||
# Use local strfry relay - external URL first for nprofile generation
|
# Relay URLs (space-separated). Docker-internal relay FIRST for publishing,
|
||||||
# Lightning.Pub uses relays[0] for nprofiles, so put external URL first
|
# then backup. Mock ATM rewrites ndebit relay for browser access.
|
||||||
# NOTE: Change 192.168.1.122 to your machine's local IP for cross-device testing
|
- NOSTR_RELAYS=ws://strfry:7777
|
||||||
- NOSTR_RELAYS=ws://192.168.1.122:7777 ws://strfry:7777
|
|
||||||
# Disable external liquidity provider for regtest
|
# Disable external liquidity provider for regtest
|
||||||
- DISABLE_LIQUIDITY_PROVIDER=true
|
- DISABLE_LIQUIDITY_PROVIDER=true
|
||||||
# Admin token for HTTP API access (development only)
|
# Admin token for HTTP API access (development only)
|
||||||
|
|
|
||||||
460
lamassu-next/docs/ndebit-cash-in-flow.md
Normal file
460
lamassu-next/docs/ndebit-cash-in-flow.md
Normal file
|
|
@ -0,0 +1,460 @@
|
||||||
|
# NDebit Cash-In Flow Implementation Guide
|
||||||
|
|
||||||
|
This document describes how to implement the ndebit scanning flow for ATM cash-in, where a user scans an ndebit QR code from an ATM to withdraw sats to their wallet.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
**Flow Summary:**
|
||||||
|
|
||||||
|
1. ATM displays QR code containing `clink:ndebit1...?amount=X`
|
||||||
|
2. User scans QR with wallet → navigates to Claim screen
|
||||||
|
3. Wallet creates a Lightning invoice for the specified amount
|
||||||
|
4. Wallet sends Kind 21002 debit request to the relay encoded in the ndebit
|
||||||
|
5. Lightning.Pub (ATM's backend) receives the request and pays the invoice
|
||||||
|
6. User receives sats
|
||||||
|
|
||||||
|
**Key Insight:** The user is _receiving_ sats, not sending. The ndebit flow is a RECEIVE action from the wallet's perspective.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ ATM Machine │ │ Nostr Relay │ │ User's Wallet │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ Displays QR: │ │ ws://relay:7777│ │ ShockWallet │
|
||||||
|
│ clink: │ │ │ │ │
|
||||||
|
│ ndebit1... │ │ │ │ │
|
||||||
|
│ ?amount=1000 │ │ │ │ │
|
||||||
|
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
||||||
|
│ │ │
|
||||||
|
│ │ 1. Scan QR │
|
||||||
|
│ │◄──────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ │ 2. Create invoice │
|
||||||
|
│ │ (via Lightning.Pub)│
|
||||||
|
│ │◄──────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ │ 3. Kind 21002 debit │
|
||||||
|
│ │ request with │
|
||||||
|
│ │ invoice │
|
||||||
|
│ │◄──────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ 4. Lightning.Pub │ │
|
||||||
|
│ receives request │ │
|
||||||
|
│◄──────────────────────┤ │
|
||||||
|
│ │ │
|
||||||
|
│ 5. ATM validates & │ │
|
||||||
|
│ pays invoice │ │
|
||||||
|
│──────────────────────►│ │
|
||||||
|
│ │ │
|
||||||
|
│ │ 6. Payment received │
|
||||||
|
│ │──────────────────────►│
|
||||||
|
│ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
## NDebit Format
|
||||||
|
|
||||||
|
### Bech32 Encoding
|
||||||
|
|
||||||
|
The ndebit string is bech32-encoded with the following TLV data:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface DebitPointer {
|
||||||
|
pubkey: string // Lightning.Pub's pubkey (hex)
|
||||||
|
relay: string // Relay URL for communication
|
||||||
|
pointer?: string // Optional session/voucher identifier
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### URI Format with Amount
|
||||||
|
|
||||||
|
Following the BIP-21 pattern for unified QR codes, but using `clink:` as the scheme since CLINK is protocol-agnostic (could work with Cashu/Fedimint, not just Lightning):
|
||||||
|
|
||||||
|
```
|
||||||
|
clink:ndebit1<bech32data>?amount=<sats>
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
clink:ndebit1qgpkzardqyfhwue69uhkcmmrv9kxsmmnwsarwdehxuqzq34ndmupyc4vrc3tx0rp0km6xmyldrwmum0afjc58rzjegz9hh0m7up5w0?amount=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** The amount is NOT encoded in the bech32 ndebit itself - it's passed as a query parameter. This allows a static ndebit to be used with dynamic amounts.
|
||||||
|
|
||||||
|
### Why `clink:` Instead of `lightning:`?
|
||||||
|
|
||||||
|
We use `clink:` as the URI scheme instead of `lightning:` for several reasons:
|
||||||
|
|
||||||
|
1. **Protocol-agnostic** - CLINK is not Lightning-specific. The same ndebit flow could work with Cashu mints or Fedimint in the future.
|
||||||
|
|
||||||
|
2. **Accuracy** - `lightning:` was designed for BOLT11 invoices (`lnbc...`) and LNURL. Using it for `ndebit1...` strings is semantically incorrect.
|
||||||
|
|
||||||
|
3. **Future-proofing** - As CLINK expands to support more payment backends, `clink:` remains accurate while `lightning:` would be misleading.
|
||||||
|
|
||||||
|
**Browser Support Note:** Neither `clink:` nor `lightning:` are IANA-registered schemes, so browsers treat them identically. For QR code scanning (the primary use case), both work fine since the camera app hands the URI directly to the OS for app routing.
|
||||||
|
|
||||||
|
Wallets SHOULD support both schemes for compatibility:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const CLINK_SCHEME = '(?:clink:|lightning:)?'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wallet Implementation
|
||||||
|
|
||||||
|
### 1. Regex for Parsing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In lib/regex.ts
|
||||||
|
// Support both clink: (preferred) and lightning: (legacy) schemes
|
||||||
|
const CLINK_SCHEME = '(?:clink:|lightning:)?'
|
||||||
|
const BECH32_DATA = '[02-9ac-hj-np-z]'
|
||||||
|
|
||||||
|
export const NDEBIT_REGEX = new RegExp(
|
||||||
|
`^${CLINK_SCHEME}(ndebit1${BECH32_DATA}+)(?:\\?amount=(\\d+))?`,
|
||||||
|
'i'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Type Definitions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In lib/types/parse.ts
|
||||||
|
import type { DebitPointer } from '@shocknet/clink-sdk'
|
||||||
|
import type { Satoshi } from './units'
|
||||||
|
|
||||||
|
export enum InputClassification {
|
||||||
|
// ... other types
|
||||||
|
NDEBIT = 'Ndebit',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParsedNdebitInput {
|
||||||
|
type: InputClassification.NDEBIT
|
||||||
|
data: string // The raw ndebit string
|
||||||
|
ndebit: DebitPointer
|
||||||
|
amount?: Satoshi // From ?amount= query parameter
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Input Parser
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In lib/parse.ts
|
||||||
|
import { decodeBech32 } from '@shocknet/clink-sdk';
|
||||||
|
|
||||||
|
// Add to VALIDATORS array:
|
||||||
|
{
|
||||||
|
type: InputClassification.NDEBIT,
|
||||||
|
test: (s) => {
|
||||||
|
const m = s.match(NDEBIT_REGEX);
|
||||||
|
if (m) {
|
||||||
|
const ndebit = m[1].toLowerCase();
|
||||||
|
const amount = m[2]; // may be undefined
|
||||||
|
return {
|
||||||
|
classification: InputClassification.NDEBIT,
|
||||||
|
value: amount ? `${ndebit}?amount=${amount}` : ndebit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In parseBitcoinInput switch:
|
||||||
|
case InputClassification.NDEBIT: {
|
||||||
|
const [ndebitPart, queryPart] = input.split("?");
|
||||||
|
|
||||||
|
let amount: Satoshi | undefined;
|
||||||
|
if (queryPart) {
|
||||||
|
const amountMatch = queryPart.match(/amount=(\d+)/);
|
||||||
|
if (amountMatch) {
|
||||||
|
amount = parseInt(amountMatch[1], 10) as Satoshi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = decodeBech32(ndebitPart);
|
||||||
|
if (decoded.type !== "ndebit") {
|
||||||
|
throw new Error("Invalid ndebit string");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: InputClassification.NDEBIT,
|
||||||
|
data: ndebitPart,
|
||||||
|
ndebit: decoded.data,
|
||||||
|
amount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Claim Thunk
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In State/scoped/backups/sources/history/claimNdebitThunk.ts
|
||||||
|
import { getNostrClient } from '@/Api/nostr'
|
||||||
|
import { SendNdebitRequest } from '@shocknet/clink-sdk'
|
||||||
|
import { SimplePool } from 'nostr-tools'
|
||||||
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
|
|
||||||
|
export const claimNdebitThunk = ({
|
||||||
|
sourceId,
|
||||||
|
parsedInput,
|
||||||
|
amount,
|
||||||
|
note,
|
||||||
|
showToast,
|
||||||
|
}: {
|
||||||
|
sourceId: string
|
||||||
|
parsedInput: ParsedNdebitInput
|
||||||
|
amount: Satoshi
|
||||||
|
note?: string
|
||||||
|
showToast: ShowToast
|
||||||
|
}): AppThunk<Promise<boolean>> => {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const selectedSource = selectSourceViewById(getState(), sourceId)
|
||||||
|
if (!selectedSource || selectedSource.type !== SourceType.NPROFILE_SOURCE) {
|
||||||
|
showToast({ message: 'Source not found', color: 'danger' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Create invoice to RECEIVE payment
|
||||||
|
let client
|
||||||
|
try {
|
||||||
|
client = await getNostrClient(
|
||||||
|
{ pubkey: selectedSource.lpk, relays: selectedSource.relays },
|
||||||
|
selectedSource.keys
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Cannot connect to Lightning.Pub')
|
||||||
|
}
|
||||||
|
|
||||||
|
let invoiceRes
|
||||||
|
try {
|
||||||
|
invoiceRes = await client.NewInvoice({
|
||||||
|
amountSats: amount,
|
||||||
|
memo: note || `Debit request for ${amount} sats`,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Failed to create invoice')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invoiceRes.status === 'ERROR') {
|
||||||
|
throw new Error(invoiceRes.reason || 'Failed to create invoice')
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoice = invoiceRes.invoice
|
||||||
|
|
||||||
|
// Step 2: Send debit request
|
||||||
|
showToast({ message: 'Waiting for approval...', color: 'primary' })
|
||||||
|
|
||||||
|
const pool = new SimplePool()
|
||||||
|
const ndebitRes = await SendNdebitRequest(
|
||||||
|
pool,
|
||||||
|
hexToBytes(selectedSource.keys.privateKey),
|
||||||
|
[parsedInput.ndebit.relay],
|
||||||
|
parsedInput.ndebit.pubkey,
|
||||||
|
{
|
||||||
|
bolt11: invoice,
|
||||||
|
amount_sats: amount,
|
||||||
|
pointer: parsedInput.ndebit.pointer,
|
||||||
|
},
|
||||||
|
30 // 30 second timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ndebitRes.res === 'GFY') {
|
||||||
|
throw new Error(ndebitRes.error || 'Debit request denied')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success!
|
||||||
|
showToast({
|
||||||
|
message: `Received ${amount} sats`,
|
||||||
|
color: 'success',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Refresh history
|
||||||
|
dispatch(historyFetchSourceRequested({ sourceId }))
|
||||||
|
return true
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast({
|
||||||
|
message: err?.message || 'Debit request failed',
|
||||||
|
color: 'danger',
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. UI Component (Claim Tab)
|
||||||
|
|
||||||
|
Place the ndebit claim UI in the **Receive** page (not Send!) since the user is receiving sats.
|
||||||
|
|
||||||
|
Key UI elements:
|
||||||
|
|
||||||
|
- Text input for pasting ndebit strings
|
||||||
|
- QR scanner button
|
||||||
|
- Amount display (pre-filled if from URI, editable if not)
|
||||||
|
- "Amount set by sender" indicator when amount comes from URI
|
||||||
|
- Claim button
|
||||||
|
|
||||||
|
## ATM/Service Implementation
|
||||||
|
|
||||||
|
### Generating the NDebit QR
|
||||||
|
|
||||||
|
The ATM service needs to:
|
||||||
|
|
||||||
|
1. **Get the ndebit from Lightning.Pub:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const user = await getAtmUser() // API call to Lightning.Pub
|
||||||
|
const ndebit = user.info.ndebit
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Rewrite relay for browser access** (if needed):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { decodeBech32, ndebitEncode } from '@shocknet/clink-sdk'
|
||||||
|
|
||||||
|
function rewriteNdebitRelay(ndebitString, browserRelay) {
|
||||||
|
const decoded = decodeBech32(ndebitString)
|
||||||
|
if (decoded.type !== 'ndebit') return ndebitString
|
||||||
|
|
||||||
|
return ndebitEncode({
|
||||||
|
pubkey: decoded.data.pubkey,
|
||||||
|
relay: browserRelay, // e.g., 'ws://localhost:7777'
|
||||||
|
pointer: decoded.data.pointer,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Format the URI with amount:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Using clink: scheme (protocol-agnostic, supports Cashu/Fedimint in future)
|
||||||
|
function formatNdebitUri(ndebit, amountSats) {
|
||||||
|
const rewritten = rewriteNdebitRelay(ndebit, 'ws://localhost:7777')
|
||||||
|
return `clink:${rewritten}?amount=${amountSats}`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Generate QR code:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import QRCode from 'qrcode'
|
||||||
|
|
||||||
|
const uri = formatNdebitUri(ndebit, 1000)
|
||||||
|
const qrDataUrl = await QRCode.toDataURL(uri, { width: 300 })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handling Debit Requests (Lightning.Pub)
|
||||||
|
|
||||||
|
Lightning.Pub handles Kind 21002 debit requests automatically. It will:
|
||||||
|
|
||||||
|
1. Receive the debit request with the invoice
|
||||||
|
2. Validate the request (check pointer, amount limits, etc.)
|
||||||
|
3. Pay the invoice from the ATM user's balance
|
||||||
|
4. Send response back via relay
|
||||||
|
|
||||||
|
**Important:** The ATM user must have sufficient balance to pay invoices.
|
||||||
|
|
||||||
|
## Infrastructure Requirements
|
||||||
|
|
||||||
|
### Relay Configuration
|
||||||
|
|
||||||
|
The relay must be accessible from:
|
||||||
|
|
||||||
|
1. **Lightning.Pub** (for publishing responses)
|
||||||
|
2. **User's browser** (for subscribing to responses)
|
||||||
|
|
||||||
|
For Docker setups:
|
||||||
|
|
||||||
|
- Lightning.Pub uses internal Docker DNS: `ws://strfry:7777`
|
||||||
|
- Browser uses host mapping: `ws://localhost:7777`
|
||||||
|
- Both resolve to the same relay
|
||||||
|
|
||||||
|
**Lightning.Pub docker-compose config:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- NOSTR_RELAYS=ws://strfry:7777 # Docker internal
|
||||||
|
```
|
||||||
|
|
||||||
|
**NDebit must encode browser-accessible relay:**
|
||||||
|
|
||||||
|
```
|
||||||
|
ws://localhost:7777 # NOT ws://strfry:7777
|
||||||
|
```
|
||||||
|
|
||||||
|
### Funding the ATM
|
||||||
|
|
||||||
|
The ATM user needs a balance to pay invoices:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create invoice for ATM user
|
||||||
|
curl -X POST "http://localhost:1776/api/app/user/add/invoice" \
|
||||||
|
-H "Authorization: Bearer $APP_TOKEN" \
|
||||||
|
-d '{"receiver_identifier": "atm", "payer_identifier": "external",
|
||||||
|
"invoice_req": {"amountSats": 50000, "memo": "Fund ATM"}}'
|
||||||
|
|
||||||
|
# Pay from external node
|
||||||
|
lncli payinvoice <invoice>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### 1. Wrong Page for NDebit UI
|
||||||
|
|
||||||
|
**Problem:** Putting ndebit in the Send page
|
||||||
|
**Solution:** NDebit is a RECEIVE action - put it in the Receive page
|
||||||
|
|
||||||
|
### 2. Relay Mismatch
|
||||||
|
|
||||||
|
**Problem:** NDebit encodes Docker-internal relay (`ws://strfry:7777`)
|
||||||
|
**Solution:** Rewrite relay to browser-accessible URL (`ws://localhost:7777`)
|
||||||
|
|
||||||
|
### 3. Pubkey Mismatch
|
||||||
|
|
||||||
|
**Problem:** Hardcoded Lightning.Pub pubkey doesn't match after container recreation
|
||||||
|
**Solution:** Dynamically fetch pubkey from ndebit or API
|
||||||
|
|
||||||
|
### 4. Zero Balance
|
||||||
|
|
||||||
|
**Problem:** Debit request fails with "Error in single invoice payment"
|
||||||
|
**Solution:** Fund the ATM user before testing
|
||||||
|
|
||||||
|
### 5. LND Not Synced
|
||||||
|
|
||||||
|
**Problem:** All RPC calls timeout
|
||||||
|
**Solution:** Mine blocks to sync LND: `bitcoin-cli -generate 10`
|
||||||
|
|
||||||
|
### 6. Amount Not in Bech32
|
||||||
|
|
||||||
|
**Problem:** Trying to encode amount in the ndebit bech32 string
|
||||||
|
**Solution:** Use query parameter: `?amount=1000`
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
1. [ ] Lightning.Pub health check returns OK
|
||||||
|
2. [ ] LND is synced (`synced_to_chain: true`)
|
||||||
|
3. [ ] ATM user exists and has balance
|
||||||
|
4. [ ] Relay is accessible from browser
|
||||||
|
5. [ ] Wallet source configured with correct relay
|
||||||
|
6. [ ] NDebit QR contains `clink:` prefix
|
||||||
|
7. [ ] NDebit QR contains `?amount=` parameter
|
||||||
|
8. [ ] NDebit relay is browser-accessible
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@shocknet/clink-sdk": "^x.x.x",
|
||||||
|
"nostr-tools": "^2.x.x",
|
||||||
|
"@noble/hashes": "^1.x.x",
|
||||||
|
"qrcode": "^1.x.x"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [CLINK Protocol Spec](https://github.com/shocknet/clink)
|
||||||
|
- [Lightning.Pub](https://github.com/shocknet/Lightning.Pub)
|
||||||
|
- [NIP-19 (bech32 encoding)](https://github.com/nostr-protocol/nips/blob/master/19.md)
|
||||||
|
- [BIP-21 (URI scheme)](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki)
|
||||||
|
|
@ -80,7 +80,7 @@ export {
|
||||||
} from './types.js'
|
} from './types.js'
|
||||||
|
|
||||||
// Noffer encoding/decoding
|
// Noffer encoding/decoding
|
||||||
export { encodeNoffer, decodeNoffer, isValidNoffer } from './noffer.js'
|
export { encodeNoffer, decodeNoffer, isValidNoffer, npubToHex, hexToNpub } from './noffer.js'
|
||||||
|
|
||||||
// Client
|
// Client
|
||||||
export {
|
export {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { bech32 } from '@scure/base'
|
import { bech32 } from '@scure/base'
|
||||||
|
import { nip19 } from 'nostr-tools'
|
||||||
import type { CLINKOffer, PriceType } from './types.js'
|
import type { CLINKOffer, PriceType } from './types.js'
|
||||||
import { NofferTLV } from './types.js'
|
import { NofferTLV } from './types.js'
|
||||||
|
|
||||||
|
|
@ -20,8 +21,9 @@ const BECH32_LIMIT = 2000
|
||||||
export function encodeNoffer(offer: CLINKOffer): string {
|
export function encodeNoffer(offer: CLINKOffer): string {
|
||||||
const tlvData: number[] = []
|
const tlvData: number[] = []
|
||||||
|
|
||||||
// Pubkey (TLV 0)
|
// Pubkey (TLV 0) - convert npub to hex if needed
|
||||||
const pubkeyBytes = hexToBytes(offer.pubkey)
|
const hexPubkey = npubToHex(offer.pubkey)
|
||||||
|
const pubkeyBytes = hexToBytes(hexPubkey)
|
||||||
writeTLV(tlvData, NofferTLV.Pubkey, pubkeyBytes)
|
writeTLV(tlvData, NofferTLV.Pubkey, pubkeyBytes)
|
||||||
|
|
||||||
// Relays (TLV 1) - one entry per relay
|
// Relays (TLV 1) - one entry per relay
|
||||||
|
|
@ -208,3 +210,26 @@ function bytesToHex(bytes: Uint8Array): string {
|
||||||
.map((b) => b.toString(16).padStart(2, '0'))
|
.map((b) => b.toString(16).padStart(2, '0'))
|
||||||
.join('')
|
.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert npub to hex pubkey
|
||||||
|
* Accepts either npub1... or hex format, returns hex
|
||||||
|
*/
|
||||||
|
export function npubToHex(pubkey: string): string {
|
||||||
|
if (pubkey.startsWith('npub1')) {
|
||||||
|
const decoded = nip19.decode(pubkey)
|
||||||
|
if (decoded.type !== 'npub') {
|
||||||
|
throw new Error(`Invalid npub: expected npub type, got ${decoded.type}`)
|
||||||
|
}
|
||||||
|
return decoded.data
|
||||||
|
}
|
||||||
|
// Already hex
|
||||||
|
return pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert hex pubkey to npub
|
||||||
|
*/
|
||||||
|
export function hexToNpub(hex: string): string {
|
||||||
|
return nip19.npubEncode(hex)
|
||||||
|
}
|
||||||
|
|
|
||||||
270
lamassu-next/packages/nostr-client/atm-debit-agent.mjs
Normal file
270
lamassu-next/packages/nostr-client/atm-debit-agent.mjs
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
/**
|
||||||
|
* ATM Debit Authorization Agent
|
||||||
|
*
|
||||||
|
* This script demonstrates how an ATM can act as the authorization authority
|
||||||
|
* for CLINK debit requests, similar to an "admin macaroon" for Lightning.
|
||||||
|
*
|
||||||
|
* Flow:
|
||||||
|
* 1. Generate keypair for ATM (or load from secure storage)
|
||||||
|
* 2. Link keypair to Lightning.Pub user account
|
||||||
|
* 3. Subscribe to live debit requests
|
||||||
|
* 4. Auto-approve requests (within configured limits)
|
||||||
|
*
|
||||||
|
* Prerequisites:
|
||||||
|
* - Get a linking token from Lightning.Pub HTTP API
|
||||||
|
* - Run: curl -X POST "http://localhost:1776/api/app/user/npub/token/reset" \
|
||||||
|
* -H "Authorization: Bearer $APP_TOKEN" \
|
||||||
|
* -H "Content-Type: application/json" \
|
||||||
|
* -d '{"user_identifier": "YOUR_USER_IDENTIFIER"}'
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Relay } from 'nostr-tools/relay'
|
||||||
|
import { finalizeEvent, getPublicKey, generateSecretKey } from 'nostr-tools'
|
||||||
|
import * as nip44v1 from './nip44v1.mjs'
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const LINKING_TOKEN = process.argv[2]
|
||||||
|
const LIGHTNING_PUB_PUBKEY = '6c59284e3da31b776cb1c06324c25f4a0b0308177af9f8aec5ebef07b44c3fdf'
|
||||||
|
const RELAY_URL = process.env.RELAY_URL || 'ws://localhost:7777'
|
||||||
|
|
||||||
|
// Generate ATM keypair (in production, this would be stored securely)
|
||||||
|
const ATM_PRIVATE_KEY = generateSecretKey()
|
||||||
|
const ATM_PUBLIC_KEY = getPublicKey(ATM_PRIVATE_KEY)
|
||||||
|
const ATM_PRIVATE_KEY_HEX = Buffer.from(ATM_PRIVATE_KEY).toString('hex')
|
||||||
|
|
||||||
|
if (!LINKING_TOKEN) {
|
||||||
|
console.log('ATM Debit Authorization Agent')
|
||||||
|
console.log('==============================')
|
||||||
|
console.log('')
|
||||||
|
console.log('Usage: node atm-debit-agent.mjs <linking-token>')
|
||||||
|
console.log('')
|
||||||
|
console.log('Get a linking token:')
|
||||||
|
console.log(' curl -X POST "http://localhost:1776/api/app/user/npub/token/reset" \\')
|
||||||
|
console.log(' -H "Authorization: Bearer $APP_TOKEN" \\')
|
||||||
|
console.log(' -H "Content-Type: application/json" \\')
|
||||||
|
console.log(' -d \'{"user_identifier": "YOUR_USER_IDENTIFIER"}\'')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('=== ATM Debit Authorization Agent ===')
|
||||||
|
console.log('')
|
||||||
|
console.log('ATM Pubkey:', ATM_PUBLIC_KEY)
|
||||||
|
console.log('Lightning.Pub Pubkey:', LIGHTNING_PUB_PUBKEY)
|
||||||
|
console.log('Linking Token:', LINKING_TOKEN.substring(0, 16) + '...')
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Connect to relay
|
||||||
|
console.log('Connecting to relay...')
|
||||||
|
const relay = await Relay.connect(RELAY_URL)
|
||||||
|
console.log('Connected!')
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// Create conversation key for NIP-44 v1 encryption (used by Kind 21000 RPC)
|
||||||
|
const conversationKey = nip44v1.getConversationKey(ATM_PRIVATE_KEY_HEX, LIGHTNING_PUB_PUBKEY)
|
||||||
|
|
||||||
|
// Step 1: Link NPub through token
|
||||||
|
console.log('Step 1: Linking ATM keypair to user account...')
|
||||||
|
|
||||||
|
const linkRequest = {
|
||||||
|
rpcName: 'LinkNPubThroughToken',
|
||||||
|
authIdentifier: ATM_PUBLIC_KEY,
|
||||||
|
body: {
|
||||||
|
token: LINKING_TOKEN,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkEvent = finalizeEvent(
|
||||||
|
{
|
||||||
|
kind: 21000,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [['p', LIGHTNING_PUB_PUBKEY]],
|
||||||
|
content: nip44v1.encrypt(JSON.stringify(linkRequest), conversationKey),
|
||||||
|
},
|
||||||
|
ATM_PRIVATE_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
// Subscribe for response
|
||||||
|
let linkingComplete = false
|
||||||
|
const linkSub = relay.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
kinds: [21000],
|
||||||
|
authors: [LIGHTNING_PUB_PUBKEY],
|
||||||
|
'#p': [ATM_PUBLIC_KEY],
|
||||||
|
since: Math.floor(Date.now() / 1000) - 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
onevent(evt) {
|
||||||
|
try {
|
||||||
|
const decrypted = nip44v1.decrypt(evt.content, conversationKey)
|
||||||
|
const response = JSON.parse(decrypted)
|
||||||
|
console.log('Link response:', JSON.stringify(response))
|
||||||
|
if (response.status === 'OK') {
|
||||||
|
linkingComplete = true
|
||||||
|
console.log('Keypair linked successfully!')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Failed to decrypt link response:', err.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await relay.publish(linkEvent)
|
||||||
|
console.log(
|
||||||
|
'Link request sent (event id:',
|
||||||
|
linkEvent.id.substring(0, 16) + '...), waiting for confirmation...'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wait for linking to complete
|
||||||
|
for (let i = 0; i < 10 && !linkingComplete; i++) {
|
||||||
|
await new Promise((r) => setTimeout(r, 1000))
|
||||||
|
if (i % 3 === 2) console.log('Still waiting for link confirmation...')
|
||||||
|
}
|
||||||
|
linkSub.close()
|
||||||
|
|
||||||
|
if (!linkingComplete) {
|
||||||
|
console.log('Warning: Did not receive linking confirmation, continuing anyway...')
|
||||||
|
}
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// Step 2: Subscribe to live debit requests
|
||||||
|
console.log('Step 2: Subscribing to live debit requests...')
|
||||||
|
|
||||||
|
const subscribeRequest = {
|
||||||
|
rpcName: 'GetLiveDebitRequests',
|
||||||
|
authIdentifier: ATM_PUBLIC_KEY,
|
||||||
|
body: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const subEvent = finalizeEvent(
|
||||||
|
{
|
||||||
|
kind: 21000,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [['p', LIGHTNING_PUB_PUBKEY]],
|
||||||
|
content: nip44v1.encrypt(JSON.stringify(subscribeRequest), conversationKey),
|
||||||
|
},
|
||||||
|
ATM_PRIVATE_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
// Subscribe for debit requests and responses
|
||||||
|
console.log('Listening for debit requests...')
|
||||||
|
console.log('(Scan the ndebit QR code with ShockWallet to test)')
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
const debitSub = relay.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
kinds: [21000],
|
||||||
|
authors: [LIGHTNING_PUB_PUBKEY],
|
||||||
|
'#p': [ATM_PUBLIC_KEY],
|
||||||
|
since: Math.floor(Date.now() / 1000) - 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
async onevent(evt) {
|
||||||
|
try {
|
||||||
|
const decrypted = nip44v1.decrypt(evt.content, conversationKey)
|
||||||
|
const message = JSON.parse(decrypted)
|
||||||
|
|
||||||
|
// Check if this is a debit request (has request_id and debit fields)
|
||||||
|
if (message.requestId === 'GetLiveDebitRequests' && message.debit) {
|
||||||
|
console.log('')
|
||||||
|
console.log('========================================')
|
||||||
|
console.log('Received debit request!')
|
||||||
|
console.log(' Request ID:', message.request_id)
|
||||||
|
console.log(' From npub:', message.npub)
|
||||||
|
console.log(' Debit type:', message.debit.type)
|
||||||
|
|
||||||
|
if (message.debit.type === 'invoice' && message.debit.invoice) {
|
||||||
|
console.log(' Invoice:', message.debit.invoice.substring(0, 50) + '...')
|
||||||
|
|
||||||
|
// Auto-approve by responding with INVOICE type
|
||||||
|
console.log('')
|
||||||
|
console.log('Auto-approving debit request...')
|
||||||
|
|
||||||
|
const approveRequest = {
|
||||||
|
rpcName: 'RespondToDebit',
|
||||||
|
authIdentifier: ATM_PUBLIC_KEY,
|
||||||
|
body: {
|
||||||
|
npub: message.npub,
|
||||||
|
request_id: message.request_id,
|
||||||
|
response: {
|
||||||
|
type: 'invoice',
|
||||||
|
invoice: message.debit.invoice,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const approveEvent = finalizeEvent(
|
||||||
|
{
|
||||||
|
kind: 21000,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [['p', LIGHTNING_PUB_PUBKEY]],
|
||||||
|
content: nip44v1.encrypt(JSON.stringify(approveRequest), conversationKey),
|
||||||
|
},
|
||||||
|
ATM_PRIVATE_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
await relay.publish(approveEvent)
|
||||||
|
console.log('Approval sent! (event id:', approveEvent.id.substring(0, 16) + '...)')
|
||||||
|
} else if (message.debit.type === 'budget') {
|
||||||
|
console.log(' Budget request - ignoring for now')
|
||||||
|
} else if (message.debit.type === 'fullAccess') {
|
||||||
|
console.log(' Full access request - ignoring for now')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('========================================')
|
||||||
|
console.log('')
|
||||||
|
} else if (message.rpcName) {
|
||||||
|
// This is a response to our RPC request
|
||||||
|
console.log('RPC response:', message.rpcName, ':', message.status || 'received')
|
||||||
|
} else {
|
||||||
|
// Log other messages for debugging
|
||||||
|
console.log('Message received:', JSON.stringify(message).substring(0, 100))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore decryption failures (may be messages for other clients)
|
||||||
|
if (!err.message.includes('Unsupported')) {
|
||||||
|
console.log('Error processing message:', err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await relay.publish(subEvent)
|
||||||
|
console.log('Subscription request sent (event id:', subEvent.id.substring(0, 16) + '...)')
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// Keep running
|
||||||
|
console.log('ATM Debit Agent running. Press Ctrl+C to exit.')
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// Handle graceful shutdown
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('\nShutting down...')
|
||||||
|
debitSub.close()
|
||||||
|
relay.close()
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Keep alive with heartbeat
|
||||||
|
let heartbeatCount = 0
|
||||||
|
while (true) {
|
||||||
|
await new Promise((r) => setTimeout(r, 10000))
|
||||||
|
heartbeatCount++
|
||||||
|
if (heartbeatCount % 6 === 0) {
|
||||||
|
// Every minute
|
||||||
|
console.log('Still listening... (' + heartbeatCount * 10 + 's)')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('Error:', err.message)
|
||||||
|
console.error(err.stack)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
@ -3,7 +3,9 @@ import { finalizeEvent } from 'nostr-tools'
|
||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
|
|
||||||
const DEV_PRIVATE_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
const DEV_PRIVATE_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
||||||
const LIGHTNING_PUB_PUBKEY = 'f454c5eec4ec4474128e19b57b45e49c3d0851d01eea960b39ce391cdba76fc6'
|
const LIGHTNING_PUB_PUBKEY =
|
||||||
|
process.env.LIGHTNING_PUB_PUBKEY ||
|
||||||
|
'4a72e400a254bf74a70cc711ab97e461b8d4fd9738b1ac3b5194cdeb3192ab91'
|
||||||
const RELAY_URL = process.env.NOSTR_RELAY_URL || 'ws://localhost:7777'
|
const RELAY_URL = process.env.NOSTR_RELAY_URL || 'ws://localhost:7777'
|
||||||
const FUND_AMOUNT = parseInt(process.env.FUND_AMOUNT || '100000', 10)
|
const FUND_AMOUNT = parseInt(process.env.FUND_AMOUNT || '100000', 10)
|
||||||
|
|
||||||
|
|
|
||||||
84
lamassu-next/packages/nostr-client/generate-ndebit.mjs
Normal file
84
lamassu-next/packages/nostr-client/generate-ndebit.mjs
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
/**
|
||||||
|
* Generate an ndebit string for ShockWallet to scan
|
||||||
|
*
|
||||||
|
* Usage: node generate-ndebit.mjs [pointer]
|
||||||
|
*
|
||||||
|
* The ndebit allows ShockWallet to send an invoice that Lightning.Pub will pay.
|
||||||
|
* This is the LNURL-withdraw equivalent for CLINK.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { bech32 } from '@scure/base'
|
||||||
|
|
||||||
|
const LIGHTNING_PUB_PUBKEY =
|
||||||
|
process.env.LIGHTNING_PUB_PUBKEY ||
|
||||||
|
'4a72e400a254bf74a70cc711ab97e461b8d4fd9738b1ac3b5194cdeb3192ab91'
|
||||||
|
const RELAY_URL = process.env.NOSTR_RELAY_URL || 'wss://strfry.shock.network'
|
||||||
|
const POINTER = process.argv[2] || 'atm-cashin-' + Date.now()
|
||||||
|
|
||||||
|
function hexToBytes(hex) {
|
||||||
|
const bytes = new Uint8Array(hex.length / 2)
|
||||||
|
for (let i = 0; i < hex.length; i += 2) {
|
||||||
|
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16)
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeTLV(tlv) {
|
||||||
|
const entries = []
|
||||||
|
Object.entries(tlv)
|
||||||
|
.reverse()
|
||||||
|
.forEach(([t, vs]) => {
|
||||||
|
vs.forEach((v) => {
|
||||||
|
const entry = new Uint8Array(v.length + 2)
|
||||||
|
entry.set([parseInt(t)], 0)
|
||||||
|
entry.set([v.length], 1)
|
||||||
|
entry.set(v, 2)
|
||||||
|
entries.push(entry)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Concatenate all entries
|
||||||
|
const totalLength = entries.reduce((sum, e) => sum + e.length, 0)
|
||||||
|
const result = new Uint8Array(totalLength)
|
||||||
|
let offset = 0
|
||||||
|
for (const entry of entries) {
|
||||||
|
result.set(entry, offset)
|
||||||
|
offset += entry.length
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function ndebitEncode(debit) {
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
|
||||||
|
const tlv = {
|
||||||
|
0: [hexToBytes(debit.pubkey)],
|
||||||
|
1: [encoder.encode(debit.relay)],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debit.pointer) {
|
||||||
|
tlv[2] = [encoder.encode(debit.pointer)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = encodeTLV(tlv)
|
||||||
|
const words = bech32.toWords(data)
|
||||||
|
return bech32.encode('ndebit', words, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate ndebit
|
||||||
|
const ndebit = ndebitEncode({
|
||||||
|
pubkey: LIGHTNING_PUB_PUBKEY,
|
||||||
|
relay: RELAY_URL,
|
||||||
|
pointer: POINTER,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('=== NDEBIT for ShockWallet ===')
|
||||||
|
console.log('')
|
||||||
|
console.log('Pubkey:', LIGHTNING_PUB_PUBKEY)
|
||||||
|
console.log('Relay:', RELAY_URL)
|
||||||
|
console.log('Pointer:', POINTER)
|
||||||
|
console.log('')
|
||||||
|
console.log('ndebit string:')
|
||||||
|
console.log(ndebit)
|
||||||
|
console.log('')
|
||||||
|
console.log('ShockWallet should scan this QR code to receive sats from the ATM.')
|
||||||
628
lamassu-next/packages/nostr-client/mock-machine.mjs
Normal file
628
lamassu-next/packages/nostr-client/mock-machine.mjs
Normal file
|
|
@ -0,0 +1,628 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Mock ATM Machine - Simulates the ATM cash-in flow with CLINK
|
||||||
|
*
|
||||||
|
* Usage: node mock-machine.mjs
|
||||||
|
*
|
||||||
|
* This creates a web server that:
|
||||||
|
* 1. Displays the ndebit QR code for customers to scan
|
||||||
|
* 2. Runs the ATM debit agent to authorize payments
|
||||||
|
* 3. Shows real-time status updates
|
||||||
|
*/
|
||||||
|
|
||||||
|
import http from 'http'
|
||||||
|
import { WebSocketServer } from 'ws'
|
||||||
|
import { Relay } from 'nostr-tools/relay'
|
||||||
|
import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools'
|
||||||
|
import { getConversationKey, encrypt, decrypt } from './nip44v1.mjs'
|
||||||
|
import { randomUUID } from 'crypto'
|
||||||
|
import QRCode from 'qrcode'
|
||||||
|
import { decodeBech32, ndebitEncode } from '@shocknet/clink-sdk'
|
||||||
|
|
||||||
|
const PORT = 3456
|
||||||
|
const RELAY_URL = 'ws://localhost:7777'
|
||||||
|
// Relay URL for browser access (different from Docker internal strfry:7777)
|
||||||
|
const BROWSER_RELAY_URL = 'ws://localhost:7777'
|
||||||
|
const LIGHTNING_PUB_HTTP = 'http://localhost:1776'
|
||||||
|
const ADMIN_TOKEN = 'lamassu-dev-admin-token'
|
||||||
|
|
||||||
|
// Lightning.Pub pubkey - fetched dynamically from the ATM user's ndebit
|
||||||
|
let LIGHTNING_PUB_PUBKEY = null
|
||||||
|
|
||||||
|
// ATM keypair (persistent for this session)
|
||||||
|
const ATM_PRIVATE_KEY = generateSecretKey()
|
||||||
|
const ATM_PUBLIC_KEY = getPublicKey(ATM_PRIVATE_KEY)
|
||||||
|
|
||||||
|
// State
|
||||||
|
let relay = null
|
||||||
|
let appToken = null
|
||||||
|
let ndebit = null
|
||||||
|
let ndebitQR = null
|
||||||
|
let atmBalance = 0
|
||||||
|
let wsClients = []
|
||||||
|
let isLinked = false
|
||||||
|
let withdrawAmount = 1000 // Default withdrawal amount in sats
|
||||||
|
|
||||||
|
// Rewrite ndebit relay for browser access (strfry:7777 -> localhost:7777)
|
||||||
|
function rewriteNdebitRelay(ndebitString) {
|
||||||
|
try {
|
||||||
|
const decoded = decodeBech32(ndebitString)
|
||||||
|
if (decoded.type !== 'ndebit') return ndebitString
|
||||||
|
|
||||||
|
// Replace Docker internal relay with browser-accessible relay
|
||||||
|
const data = {
|
||||||
|
pubkey: decoded.data.pubkey,
|
||||||
|
relay: BROWSER_RELAY_URL,
|
||||||
|
pointer: decoded.data.pointer,
|
||||||
|
}
|
||||||
|
return ndebitEncode(data)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to rewrite ndebit relay:', e)
|
||||||
|
return ndebitString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format ndebit with clink: prefix and amount parameter
|
||||||
|
// Using clink: instead of lightning: because CLINK is protocol-agnostic
|
||||||
|
// (could work with Cashu/Fedimint, not just Lightning)
|
||||||
|
function formatNdebitUri(ndebitString, amount) {
|
||||||
|
const rewritten = rewriteNdebitRelay(ndebitString)
|
||||||
|
return `clink:${rewritten}?amount=${amount}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcast(type, data) {
|
||||||
|
const msg = JSON.stringify({ type, ...data })
|
||||||
|
wsClients.forEach((ws) => {
|
||||||
|
if (ws.readyState === 1) ws.send(msg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
const timestamp = new Date().toLocaleTimeString()
|
||||||
|
console.log(`[${timestamp}] ${message}`)
|
||||||
|
broadcast('log', { message: `[${timestamp}] ${message}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAppToken() {
|
||||||
|
const res = await fetch(`${LIGHTNING_PUB_HTTP}/api/admin/app/auth`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${ADMIN_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name: 'wallet' }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
return data.auth_token
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAtmUser() {
|
||||||
|
const res = await fetch(`${LIGHTNING_PUB_HTTP}/api/app/user/get`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${appToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ user_identifier: 'atm' }),
|
||||||
|
})
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLinkingToken() {
|
||||||
|
const res = await fetch(`${LIGHTNING_PUB_HTTP}/api/app/user/npub/token/reset`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${appToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ user_identifier: 'atm' }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
return data.token
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendRPC(rpcName, body) {
|
||||||
|
const requestId = randomUUID()
|
||||||
|
const request = {
|
||||||
|
rpcName,
|
||||||
|
authIdentifier: ATM_PUBLIC_KEY,
|
||||||
|
body,
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversationKey = getConversationKey(ATM_PRIVATE_KEY, LIGHTNING_PUB_PUBKEY)
|
||||||
|
const encryptedContent = encrypt(JSON.stringify(request), conversationKey)
|
||||||
|
|
||||||
|
const event = finalizeEvent(
|
||||||
|
{
|
||||||
|
kind: 21000,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [['p', LIGHTNING_PUB_PUBKEY]],
|
||||||
|
content: encryptedContent,
|
||||||
|
},
|
||||||
|
ATM_PRIVATE_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => reject(new Error('RPC timeout')), 30000)
|
||||||
|
|
||||||
|
const sub = relay.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
kinds: [21000],
|
||||||
|
authors: [LIGHTNING_PUB_PUBKEY],
|
||||||
|
since: Math.floor(Date.now() / 1000) - 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
onevent(evt) {
|
||||||
|
const pTags = evt.tags.filter((t) => t[0] === 'p')
|
||||||
|
if (!pTags.some((t) => t[1] === ATM_PUBLIC_KEY)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(decrypt(evt.content, conversationKey))
|
||||||
|
clearTimeout(timeout)
|
||||||
|
sub.close()
|
||||||
|
resolve(response)
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
relay.publish(event)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function linkKeypair(token) {
|
||||||
|
log('Linking ATM keypair to user account...')
|
||||||
|
const response = await sendRPC('LinkNPubThroughToken', { token })
|
||||||
|
if (response.status === 'OK') {
|
||||||
|
log('[OK] Keypair linked successfully!')
|
||||||
|
isLinked = true
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
log(`[ERROR] Link failed: ${response.reason}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function subscribeToDebitRequests() {
|
||||||
|
log('Subscribing to debit requests...')
|
||||||
|
|
||||||
|
const conversationKey = getConversationKey(ATM_PRIVATE_KEY, LIGHTNING_PUB_PUBKEY)
|
||||||
|
|
||||||
|
// Subscribe to Kind 21000 messages from Lightning.Pub
|
||||||
|
relay.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
kinds: [21000],
|
||||||
|
authors: [LIGHTNING_PUB_PUBKEY],
|
||||||
|
since: Math.floor(Date.now() / 1000) - 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
onevent(evt) {
|
||||||
|
const pTags = evt.tags.filter((t) => t[0] === 'p')
|
||||||
|
if (!pTags.some((t) => t[1] === ATM_PUBLIC_KEY)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(decrypt(evt.content, conversationKey))
|
||||||
|
|
||||||
|
if (message.debit) {
|
||||||
|
handleDebitRequest(message, conversationKey)
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Send GetLiveDebitRequests to start the stream
|
||||||
|
const request = {
|
||||||
|
rpcName: 'GetLiveDebitRequests',
|
||||||
|
authIdentifier: ATM_PUBLIC_KEY,
|
||||||
|
body: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedContent = encrypt(JSON.stringify(request), conversationKey)
|
||||||
|
const event = finalizeEvent(
|
||||||
|
{
|
||||||
|
kind: 21000,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [['p', LIGHTNING_PUB_PUBKEY]],
|
||||||
|
content: encryptedContent,
|
||||||
|
},
|
||||||
|
ATM_PRIVATE_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
await relay.publish(event)
|
||||||
|
log('[OK] Listening for debit requests...')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDebitRequest(message, conversationKey) {
|
||||||
|
const { debit } = message
|
||||||
|
log(`[ALERT] DEBIT REQUEST RECEIVED!`)
|
||||||
|
log(` Amount: ${debit.amount || 'invoice amount'} sats`)
|
||||||
|
log(` Type: ${debit.type}`)
|
||||||
|
|
||||||
|
broadcast('debit_request', { debit })
|
||||||
|
|
||||||
|
// Auto-approve after 2 seconds
|
||||||
|
setTimeout(async () => {
|
||||||
|
log('[OK] Auto-approving debit request...')
|
||||||
|
|
||||||
|
const approval = {
|
||||||
|
rpcName: 'RespondToDebit',
|
||||||
|
authIdentifier: ATM_PUBLIC_KEY,
|
||||||
|
body: {
|
||||||
|
npub: message.npub,
|
||||||
|
request_id: message.request_id,
|
||||||
|
response: {
|
||||||
|
type: 'invoice',
|
||||||
|
invoice: debit.invoice,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedContent = encrypt(JSON.stringify(approval), conversationKey)
|
||||||
|
const event = finalizeEvent(
|
||||||
|
{
|
||||||
|
kind: 21000,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [['p', LIGHTNING_PUB_PUBKEY]],
|
||||||
|
content: encryptedContent,
|
||||||
|
},
|
||||||
|
ATM_PRIVATE_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
await relay.publish(event)
|
||||||
|
log('[OK] Approval sent!')
|
||||||
|
|
||||||
|
// Update balance after a short delay
|
||||||
|
setTimeout(updateBalance, 2000)
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateBalance() {
|
||||||
|
const user = await getAtmUser()
|
||||||
|
if (user.status === 'OK') {
|
||||||
|
atmBalance = user.info.balance
|
||||||
|
ndebit = user.info.ndebit
|
||||||
|
await regenerateQR()
|
||||||
|
broadcast('status', {
|
||||||
|
balance: atmBalance,
|
||||||
|
ndebit,
|
||||||
|
ndebitQR,
|
||||||
|
ndebitUri: formatNdebitUri(ndebit, withdrawAmount),
|
||||||
|
withdrawAmount,
|
||||||
|
})
|
||||||
|
log(`Balance updated: ${atmBalance} sats`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function regenerateQR() {
|
||||||
|
if (ndebit) {
|
||||||
|
const uri = formatNdebitUri(ndebit, withdrawAmount)
|
||||||
|
ndebitQR = await QRCode.toDataURL(uri, { width: 300, margin: 2 })
|
||||||
|
log(`QR code generated for ${withdrawAmount} sats`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWithdrawAmount(amount) {
|
||||||
|
withdrawAmount = amount
|
||||||
|
regenerateQR()
|
||||||
|
broadcast('status', {
|
||||||
|
balance: atmBalance,
|
||||||
|
ndebit,
|
||||||
|
ndebitQR,
|
||||||
|
ndebitUri: formatNdebitUri(ndebit, withdrawAmount),
|
||||||
|
withdrawAmount,
|
||||||
|
})
|
||||||
|
log(`Withdrawal amount set to ${amount} sats`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initialize() {
|
||||||
|
log('Starting Mock ATM Machine...')
|
||||||
|
|
||||||
|
// Get app token
|
||||||
|
appToken = await getAppToken()
|
||||||
|
log('Got app token')
|
||||||
|
|
||||||
|
// Get ATM user info
|
||||||
|
const user = await getAtmUser()
|
||||||
|
if (user.status === 'OK') {
|
||||||
|
atmBalance = user.info.balance
|
||||||
|
ndebit = user.info.ndebit
|
||||||
|
|
||||||
|
// Extract Lightning.Pub pubkey from ndebit
|
||||||
|
const decoded = decodeBech32(ndebit)
|
||||||
|
LIGHTNING_PUB_PUBKEY = decoded.data.pubkey
|
||||||
|
log(`ATM user found: ${atmBalance} sats balance`)
|
||||||
|
log(`Lightning.Pub pubkey: ${LIGHTNING_PUB_PUBKEY.substring(0, 16)}...`)
|
||||||
|
} else {
|
||||||
|
log('ATM user not found - please create one first')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to relay
|
||||||
|
log('Connecting to relay...')
|
||||||
|
relay = await Relay.connect(RELAY_URL)
|
||||||
|
log('Connected to relay')
|
||||||
|
|
||||||
|
// Get linking token and link keypair
|
||||||
|
const token = await getLinkingToken()
|
||||||
|
await linkKeypair(token)
|
||||||
|
|
||||||
|
// Subscribe to debit requests
|
||||||
|
await subscribeToDebitRequests()
|
||||||
|
|
||||||
|
// Generate QR code with amount
|
||||||
|
await regenerateQR()
|
||||||
|
|
||||||
|
broadcast('status', {
|
||||||
|
balance: atmBalance,
|
||||||
|
ndebit,
|
||||||
|
ndebitQR,
|
||||||
|
ndebitUri: formatNdebitUri(ndebit, withdrawAmount),
|
||||||
|
withdrawAmount,
|
||||||
|
isLinked,
|
||||||
|
})
|
||||||
|
log('[READY] Mock ATM Machine ready!')
|
||||||
|
log(` Open http://localhost:${PORT} to see the QR code`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML page
|
||||||
|
const html = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Mock ATM Machine</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
color: #fff;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container { max-width: 600px; margin: 0 auto; }
|
||||||
|
h1 { text-align: center; margin-bottom: 20px; color: #00d4ff; }
|
||||||
|
.card {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
.balance {
|
||||||
|
font-size: 48px;
|
||||||
|
text-align: center;
|
||||||
|
color: #00ff88;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.balance span { font-size: 24px; opacity: 0.7; }
|
||||||
|
#qr-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
#qr-container canvas { max-width: 100%; height: auto !important; }
|
||||||
|
.ndebit-code {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
word-break: break-all;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.logs {
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.log-entry { margin: 4px 0; }
|
||||||
|
.status { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
||||||
|
.status-dot {
|
||||||
|
width: 12px; height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #00ff88;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
.instructions {
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.amount-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.amount-input input {
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border: 2px solid #00d4ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #fff;
|
||||||
|
width: 150px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.amount-input input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #00ff88;
|
||||||
|
}
|
||||||
|
.amount-input button {
|
||||||
|
background: #00d4ff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.amount-input button:hover {
|
||||||
|
background: #00ff88;
|
||||||
|
}
|
||||||
|
.amount-label {
|
||||||
|
font-size: 18px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Mock ATM Machine</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="status">
|
||||||
|
<div class="status-dot"></div>
|
||||||
|
<span>Ready for deposits</span>
|
||||||
|
</div>
|
||||||
|
<div class="balance"><span id="balance">0</span> <span>sats</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Scan to Withdraw</h3>
|
||||||
|
<div class="amount-input">
|
||||||
|
<span class="amount-label">Amount:</span>
|
||||||
|
<input type="number" id="amount" value="1000" min="1" />
|
||||||
|
<span class="amount-label">sats</span>
|
||||||
|
<button id="update-btn">Update QR</button>
|
||||||
|
</div>
|
||||||
|
<div id="qr-container">
|
||||||
|
<img id="qr" alt="QR Code" />
|
||||||
|
</div>
|
||||||
|
<p class="instructions">Scan with ShockWallet to claim <span id="display-amount">1000</span> sats</p>
|
||||||
|
<div class="ndebit-code" id="ndebit-code">Loading...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Activity Log</h3>
|
||||||
|
<div class="logs" id="logs"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const ws = new WebSocket('ws://localhost:3456')
|
||||||
|
const logs = document.getElementById('logs')
|
||||||
|
const balance = document.getElementById('balance')
|
||||||
|
const ndebitCode = document.getElementById('ndebit-code')
|
||||||
|
const qrImg = document.getElementById('qr')
|
||||||
|
const amountInput = document.getElementById('amount')
|
||||||
|
const updateBtn = document.getElementById('update-btn')
|
||||||
|
const displayAmount = document.getElementById('display-amount')
|
||||||
|
|
||||||
|
updateBtn.onclick = () => {
|
||||||
|
const amount = parseInt(amountInput.value, 10)
|
||||||
|
if (amount > 0) {
|
||||||
|
ws.send(JSON.stringify({ type: 'set_amount', amount }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
amountInput.onkeypress = (e) => {
|
||||||
|
if (e.key === 'Enter') updateBtn.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data)
|
||||||
|
|
||||||
|
if (data.type === 'log') {
|
||||||
|
const entry = document.createElement('div')
|
||||||
|
entry.className = 'log-entry'
|
||||||
|
entry.textContent = data.message
|
||||||
|
logs.appendChild(entry)
|
||||||
|
logs.scrollTop = logs.scrollHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'status') {
|
||||||
|
balance.textContent = data.balance.toLocaleString()
|
||||||
|
if (data.ndebitUri) {
|
||||||
|
ndebitCode.textContent = data.ndebitUri
|
||||||
|
}
|
||||||
|
if (data.ndebitQR) {
|
||||||
|
qrImg.src = data.ndebitQR
|
||||||
|
}
|
||||||
|
if (data.withdrawAmount) {
|
||||||
|
amountInput.value = data.withdrawAmount
|
||||||
|
displayAmount.textContent = data.withdrawAmount.toLocaleString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'debit_request') {
|
||||||
|
const entry = document.createElement('div')
|
||||||
|
entry.className = 'log-entry'
|
||||||
|
entry.style.color = '#00d4ff'
|
||||||
|
entry.textContent = '[ALERT] DEBIT REQUEST: ' + JSON.stringify(data.debit)
|
||||||
|
logs.appendChild(entry)
|
||||||
|
logs.scrollTop = logs.scrollHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
const entry = document.createElement('div')
|
||||||
|
entry.textContent = 'Connected to ATM server'
|
||||||
|
logs.appendChild(entry)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
// Create HTTP server
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' })
|
||||||
|
res.end(html)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create WebSocket server
|
||||||
|
const wss = new WebSocketServer({ server })
|
||||||
|
|
||||||
|
wss.on('connection', (ws) => {
|
||||||
|
wsClients.push(ws)
|
||||||
|
ws.on('close', () => {
|
||||||
|
wsClients = wsClients.filter((c) => c !== ws)
|
||||||
|
})
|
||||||
|
|
||||||
|
ws.on('message', (data) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(data)
|
||||||
|
if (msg.type === 'set_amount' && typeof msg.amount === 'number') {
|
||||||
|
setWithdrawAmount(msg.amount)
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Send current state
|
||||||
|
if (ndebit) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'status',
|
||||||
|
balance: atmBalance,
|
||||||
|
ndebit,
|
||||||
|
ndebitQR,
|
||||||
|
ndebitUri: formatNdebitUri(ndebit, withdrawAmount),
|
||||||
|
withdrawAmount,
|
||||||
|
isLinked,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
server.listen(PORT, async () => {
|
||||||
|
console.log(`Mock ATM Machine running at http://localhost:${PORT}`)
|
||||||
|
await initialize()
|
||||||
|
})
|
||||||
111
lamassu-next/packages/nostr-client/nip44v1.mjs
Normal file
111
lamassu-next/packages/nostr-client/nip44v1.mjs
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
/**
|
||||||
|
* NIP-44 v1 Implementation
|
||||||
|
*
|
||||||
|
* This is the XChaCha20-based encryption used by Lightning.Pub for Kind 21000 RPC events.
|
||||||
|
* It differs from standard NIP-44 v2 (used in nostr-tools) which uses ChaCha20-Poly1305.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { base64 } from '@scure/base'
|
||||||
|
import { randomBytes } from '@noble/hashes/utils.js'
|
||||||
|
import { streamXOR as xchacha20 } from '@stablelib/xchacha20'
|
||||||
|
import { secp256k1 } from '@noble/curves/secp256k1.js'
|
||||||
|
import { sha256 } from '@noble/hashes/sha2.js'
|
||||||
|
|
||||||
|
const XCHACHA20_VERSION = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert hex string to Uint8Array
|
||||||
|
* @param {string} hex - Hex string
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
function hexToBytes(hex) {
|
||||||
|
const bytes = new Uint8Array(hex.length / 2)
|
||||||
|
for (let i = 0; i < hex.length; i += 2) {
|
||||||
|
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16)
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get shared secret for NIP-44 v1 encryption
|
||||||
|
* @param {Uint8Array|string} privateKey - Private key (32 bytes or hex string)
|
||||||
|
* @param {string} publicKey - Public key (32 bytes hex string, no prefix)
|
||||||
|
* @returns {Uint8Array} - 32-byte shared secret
|
||||||
|
*/
|
||||||
|
export function getConversationKey(privateKey, publicKey) {
|
||||||
|
// Convert private key to Uint8Array if it's a hex string
|
||||||
|
const privKeyBytes = typeof privateKey === 'string' ? hexToBytes(privateKey) : privateKey
|
||||||
|
|
||||||
|
// Convert public key (with 02 prefix) to Uint8Array
|
||||||
|
const pubKeyBytes = hexToBytes('02' + publicKey)
|
||||||
|
|
||||||
|
// Get ECDH shared point
|
||||||
|
const sharedPoint = secp256k1.getSharedSecret(privKeyBytes, pubKeyBytes)
|
||||||
|
|
||||||
|
// Hash the x-coordinate of the shared point
|
||||||
|
return sha256(sharedPoint.slice(1, 33))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt content using NIP-44 v1 (XChaCha20)
|
||||||
|
* @param {string} content - Plaintext content to encrypt
|
||||||
|
* @param {Uint8Array} conversationKey - 32-byte conversation key from getConversationKey
|
||||||
|
* @returns {string} - Base64-encoded encrypted payload
|
||||||
|
*/
|
||||||
|
export function encrypt(content, conversationKey) {
|
||||||
|
const nonce = randomBytes(24)
|
||||||
|
const plaintext = new TextEncoder().encode(content)
|
||||||
|
|
||||||
|
// XChaCha20 stream cipher - encrypts in place
|
||||||
|
const ciphertext = new Uint8Array(plaintext.length)
|
||||||
|
xchacha20(conversationKey, nonce, plaintext, ciphertext)
|
||||||
|
|
||||||
|
// Encode: version byte + nonce + ciphertext
|
||||||
|
return base64.encode(new Uint8Array([XCHACHA20_VERSION, ...nonce, ...ciphertext]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt content using NIP-44 v1 (XChaCha20)
|
||||||
|
* @param {string} content - Base64-encoded encrypted payload
|
||||||
|
* @param {Uint8Array} conversationKey - 32-byte conversation key from getConversationKey
|
||||||
|
* @returns {string} - Decrypted plaintext
|
||||||
|
*/
|
||||||
|
export function decrypt(content, conversationKey) {
|
||||||
|
const payload = decodePayload(content)
|
||||||
|
|
||||||
|
// XChaCha20 stream cipher - decrypts in place
|
||||||
|
const plaintext = new Uint8Array(payload.ciphertext.length)
|
||||||
|
xchacha20(conversationKey, payload.nonce, payload.ciphertext, plaintext)
|
||||||
|
|
||||||
|
return new TextDecoder().decode(plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode encrypted payload (supports both formats)
|
||||||
|
* @param {string} content - Base64-encoded or JSON-encoded payload
|
||||||
|
* @returns {{nonce: Uint8Array, ciphertext: Uint8Array}}
|
||||||
|
*/
|
||||||
|
function decodePayload(content) {
|
||||||
|
// Check for JSON format
|
||||||
|
if (content.startsWith('{') && content.endsWith('}')) {
|
||||||
|
const parsed = JSON.parse(content)
|
||||||
|
if (parsed.v !== XCHACHA20_VERSION) {
|
||||||
|
throw new Error(`Unsupported encryption version: ${parsed.v}`)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
nonce: base64.decode(parsed.nonce),
|
||||||
|
ciphertext: base64.decode(parsed.ciphertext),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary format: version byte + nonce (24 bytes) + ciphertext
|
||||||
|
const buf = base64.decode(content)
|
||||||
|
if (buf[0] !== XCHACHA20_VERSION) {
|
||||||
|
throw new Error(`Unsupported encryption version: ${buf[0]}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nonce: buf.subarray(1, 25),
|
||||||
|
ciphertext: buf.subarray(25),
|
||||||
|
}
|
||||||
|
}
|
||||||
726
lamassu-next/packages/nostr-client/package-lock.json
generated
Normal file
726
lamassu-next/packages/nostr-client/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,726 @@
|
||||||
|
{
|
||||||
|
"name": "@lamassu/nostr-client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@lamassu/nostr-client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/curves": "^2.0.1",
|
||||||
|
"@noble/hashes": "^2.0.1",
|
||||||
|
"@scure/base": "^1.2.6",
|
||||||
|
"@shocknet/clink-sdk": "^1.5.4",
|
||||||
|
"@stablelib/xchacha20": "^2.0.1",
|
||||||
|
"nostr-tools": "^2.10.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.19.7",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"tsx": "^4.19.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"vitest": "^2.1.0",
|
||||||
|
"ws": "^8.19.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../node_modules/.pnpm/@noble+curves@2.0.1/node_modules/@noble/curves": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "2.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@paulmillr/jsbt": "0.4.4",
|
||||||
|
"@types/node": "24.2.1",
|
||||||
|
"fast-check": "4.2.0",
|
||||||
|
"prettier": "3.6.2",
|
||||||
|
"typescript": "5.9.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../node_modules/.pnpm/@noble+hashes@2.0.1/node_modules/@noble/hashes": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@paulmillr/jsbt": "0.4.4",
|
||||||
|
"@types/node": "24.2.1",
|
||||||
|
"fast-check": "4.2.0",
|
||||||
|
"prettier": "3.6.2",
|
||||||
|
"typescript": "5.9.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../node_modules/.pnpm/@scure+base@1.2.6/node_modules/@scure/base": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@noble/hashes": "1.8.0",
|
||||||
|
"@paulmillr/jsbt": "0.3.3",
|
||||||
|
"@types/node": "22.15.23",
|
||||||
|
"fast-check": "4.1.1",
|
||||||
|
"micro-bmark": "0.4.2",
|
||||||
|
"micro-should": "0.5.3",
|
||||||
|
"prettier": "3.5.3",
|
||||||
|
"typescript": "5.8.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../node_modules/.pnpm/@shocknet+clink-sdk@1.5.4/node_modules/@shocknet/clink-sdk": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "^1.8.0",
|
||||||
|
"@scure/base": "^1.2.5",
|
||||||
|
"nostr-tools": "2.15.1",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.15.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../node_modules/.pnpm/@stablelib+xchacha20@2.0.1/node_modules/@stablelib/xchacha20": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@stablelib/binary": "^2.0.1",
|
||||||
|
"@stablelib/chacha": "^2.0.1",
|
||||||
|
"@stablelib/wipe": "^2.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@stablelib/benchmark": "^2.0.0",
|
||||||
|
"@stablelib/hex": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../node_modules/.pnpm/@types+node@22.19.7/node_modules/@types/node": {
|
||||||
|
"version": "22.19.7",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../node_modules/.pnpm/nostr-tools@2.19.4_typescript@5.9.3/node_modules/nostr-tools": {
|
||||||
|
"version": "2.19.4",
|
||||||
|
"license": "Unlicense",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/ciphers": "^0.5.1",
|
||||||
|
"@noble/curves": "1.2.0",
|
||||||
|
"@noble/hashes": "1.3.1",
|
||||||
|
"@scure/base": "1.1.1",
|
||||||
|
"@scure/bip32": "1.3.1",
|
||||||
|
"@scure/bip39": "1.2.1",
|
||||||
|
"nostr-wasm": "0.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18.13.0",
|
||||||
|
"@types/node-fetch": "^2.6.3",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.5.0",
|
||||||
|
"@typescript-eslint/parser": "^6.5.0",
|
||||||
|
"bun-types": "^1.0.18",
|
||||||
|
"esbuild": "0.16.9",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"mitata": "^0.1.6",
|
||||||
|
"mock-socket": "^9.3.1",
|
||||||
|
"node-fetch": "^2.6.9",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"typescript": "^5.8.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../node_modules/.pnpm/tsx@4.21.0/node_modules/tsx": {
|
||||||
|
"version": "4.21.0",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "~0.27.0",
|
||||||
|
"get-tsconfig": "^4.7.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tsx": "dist/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@dprint/formatter": "^0.4.1",
|
||||||
|
"@dprint/typescript": "0.93.4",
|
||||||
|
"@esfx/canceltoken": "^1.0.0",
|
||||||
|
"@eslint/js": "^9.20.0",
|
||||||
|
"@octokit/rest": "^21.1.1",
|
||||||
|
"@types/chai": "^4.3.20",
|
||||||
|
"@types/diff": "^7.0.1",
|
||||||
|
"@types/minimist": "^1.2.5",
|
||||||
|
"@types/mocha": "^10.0.10",
|
||||||
|
"@types/ms": "^0.7.34",
|
||||||
|
"@types/node": "latest",
|
||||||
|
"@types/source-map-support": "^0.5.10",
|
||||||
|
"@types/which": "^3.0.4",
|
||||||
|
"@typescript-eslint/rule-tester": "^8.24.1",
|
||||||
|
"@typescript-eslint/type-utils": "^8.24.1",
|
||||||
|
"@typescript-eslint/utils": "^8.24.1",
|
||||||
|
"azure-devops-node-api": "^14.1.0",
|
||||||
|
"c8": "^10.1.3",
|
||||||
|
"chai": "^4.5.0",
|
||||||
|
"chokidar": "^4.0.3",
|
||||||
|
"diff": "^7.0.0",
|
||||||
|
"dprint": "^0.49.0",
|
||||||
|
"esbuild": "^0.25.0",
|
||||||
|
"eslint": "^9.20.1",
|
||||||
|
"eslint-formatter-autolinkable-stylish": "^1.4.0",
|
||||||
|
"eslint-plugin-regexp": "^2.7.0",
|
||||||
|
"fast-xml-parser": "^4.5.2",
|
||||||
|
"glob": "^10.4.5",
|
||||||
|
"globals": "^15.15.0",
|
||||||
|
"hereby": "^1.10.0",
|
||||||
|
"jsonc-parser": "^3.3.1",
|
||||||
|
"knip": "^5.44.4",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
|
"mocha": "^10.8.2",
|
||||||
|
"mocha-fivemat-progress-reporter": "^0.1.0",
|
||||||
|
"monocart-coverage-reports": "^2.12.1",
|
||||||
|
"ms": "^2.1.3",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"playwright": "^1.50.1",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.24.1",
|
||||||
|
"which": "^3.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.7_lightningcss@1.30.2/node_modules/vitest": {
|
||||||
|
"version": "2.1.9",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/expect": "2.1.9",
|
||||||
|
"@vitest/mocker": "2.1.9",
|
||||||
|
"@vitest/pretty-format": "^2.1.9",
|
||||||
|
"@vitest/runner": "2.1.9",
|
||||||
|
"@vitest/snapshot": "2.1.9",
|
||||||
|
"@vitest/spy": "2.1.9",
|
||||||
|
"@vitest/utils": "2.1.9",
|
||||||
|
"chai": "^5.1.2",
|
||||||
|
"debug": "^4.3.7",
|
||||||
|
"expect-type": "^1.1.0",
|
||||||
|
"magic-string": "^0.30.12",
|
||||||
|
"pathe": "^1.1.2",
|
||||||
|
"std-env": "^3.8.0",
|
||||||
|
"tinybench": "^2.9.0",
|
||||||
|
"tinyexec": "^0.3.1",
|
||||||
|
"tinypool": "^1.0.1",
|
||||||
|
"tinyrainbow": "^1.2.0",
|
||||||
|
"vite": "^5.0.0",
|
||||||
|
"vite-node": "2.1.9",
|
||||||
|
"why-is-node-running": "^2.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vitest": "vitest.mjs"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@ampproject/remapping": "^2.3.0",
|
||||||
|
"@antfu/install-pkg": "^0.4.1",
|
||||||
|
"@edge-runtime/vm": "^4.0.4",
|
||||||
|
"@sinonjs/fake-timers": "11.1.0",
|
||||||
|
"@types/debug": "^4.1.12",
|
||||||
|
"@types/estree": "^1.0.6",
|
||||||
|
"@types/istanbul-lib-coverage": "^2.0.6",
|
||||||
|
"@types/istanbul-reports": "^3.0.4",
|
||||||
|
"@types/jsdom": "^21.1.7",
|
||||||
|
"@types/micromatch": "^4.0.9",
|
||||||
|
"@types/node": "^22.9.0",
|
||||||
|
"@types/prompts": "^2.4.9",
|
||||||
|
"@types/sinonjs__fake-timers": "^8.1.5",
|
||||||
|
"acorn-walk": "^8.3.4",
|
||||||
|
"birpc": "0.2.19",
|
||||||
|
"cac": "^6.7.14",
|
||||||
|
"chai-subset": "^1.6.0",
|
||||||
|
"cli-truncate": "^4.0.0",
|
||||||
|
"fast-glob": "3.3.2",
|
||||||
|
"find-up": "^6.3.0",
|
||||||
|
"flatted": "^3.3.1",
|
||||||
|
"get-tsconfig": "^4.8.1",
|
||||||
|
"happy-dom": "^15.11.4",
|
||||||
|
"jsdom": "^25.0.1",
|
||||||
|
"local-pkg": "^0.5.0",
|
||||||
|
"log-update": "^5.0.1",
|
||||||
|
"micromatch": "^4.0.8",
|
||||||
|
"pretty-format": "^29.7.0",
|
||||||
|
"prompts": "^2.4.2",
|
||||||
|
"strip-literal": "^2.1.0",
|
||||||
|
"ws": "^8.18.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.0.0 || >=20.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@edge-runtime/vm": "*",
|
||||||
|
"@types/node": "^18.0.0 || >=20.0.0",
|
||||||
|
"@vitest/browser": "2.1.9",
|
||||||
|
"@vitest/ui": "2.1.9",
|
||||||
|
"happy-dom": "*",
|
||||||
|
"jsdom": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@edge-runtime/vm": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/ui": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"happy-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jsdom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/curves": {
|
||||||
|
"resolved": "../../node_modules/.pnpm/@noble+curves@2.0.1/node_modules/@noble/curves",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/@noble/hashes": {
|
||||||
|
"resolved": "../../node_modules/.pnpm/@noble+hashes@2.0.1/node_modules/@noble/hashes",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/@scure/base": {
|
||||||
|
"resolved": "../../node_modules/.pnpm/@scure+base@1.2.6/node_modules/@scure/base",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/@shocknet/clink-sdk": {
|
||||||
|
"resolved": "../../node_modules/.pnpm/@shocknet+clink-sdk@1.5.4/node_modules/@shocknet/clink-sdk",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/@stablelib/xchacha20": {
|
||||||
|
"resolved": "../../node_modules/.pnpm/@stablelib+xchacha20@2.0.1/node_modules/@stablelib/xchacha20",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"resolved": "../../node_modules/.pnpm/@types+node@22.19.7/node_modules/@types/node",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/camelcase": {
|
||||||
|
"version": "5.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
|
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"wrap-ansi": "^6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dijkstrajs": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/find-up": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"locate-path": "^5.0.0",
|
||||||
|
"path-exists": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/locate-path": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-locate": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nostr-tools": {
|
||||||
|
"resolved": "../../node_modules/.pnpm/nostr-tools@2.19.4_typescript@5.9.3/node_modules/nostr-tools",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/p-limit": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-try": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-locate": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-limit": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-try": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-exists": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pngjs": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dijkstrajs": "^1.0.1",
|
||||||
|
"pngjs": "^5.0.0",
|
||||||
|
"yargs": "^15.3.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"qrcode": "bin/qrcode"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/require-main-filename": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/set-blocking": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx": {
|
||||||
|
"resolved": "../../node_modules/.pnpm/tsx@4.21.0/node_modules/tsx",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"resolved": "../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/vitest": {
|
||||||
|
"resolved": "../../node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.7_lightningcss@1.30.2/node_modules/vitest",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/which-module": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "15.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^6.0.0",
|
||||||
|
"decamelize": "^1.2.0",
|
||||||
|
"find-up": "^4.1.0",
|
||||||
|
"get-caller-file": "^2.0.1",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"require-main-filename": "^2.0.0",
|
||||||
|
"set-blocking": "^2.0.0",
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"which-module": "^2.0.0",
|
||||||
|
"y18n": "^4.0.0",
|
||||||
|
"yargs-parser": "^18.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "18.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||||
|
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": "^5.0.0",
|
||||||
|
"decamelize": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,15 +23,18 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/curves": "^2.0.1",
|
"@noble/curves": "^2.0.1",
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.0.1",
|
||||||
"@scure/base": "^1.2.0",
|
"@scure/base": "^1.2.6",
|
||||||
|
"@shocknet/clink-sdk": "^1.5.4",
|
||||||
"@stablelib/xchacha20": "^2.0.1",
|
"@stablelib/xchacha20": "^2.0.1",
|
||||||
"nostr-tools": "^2.10.0"
|
"nostr-tools": "^2.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.19.7",
|
"@types/node": "^22.19.7",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.0",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
"vitest": "^2.1.0"
|
"vitest": "^2.1.0",
|
||||||
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
|
|
|
||||||
182
lamassu-next/packages/nostr-client/test-debit.mjs
Normal file
182
lamassu-next/packages/nostr-client/test-debit.mjs
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
/**
|
||||||
|
* Test CLINK Debit flow
|
||||||
|
*
|
||||||
|
* This simulates what ShockWallet does when scanning an ndebit:
|
||||||
|
* 1. Decode the ndebit to get pubkey, relay, pointer
|
||||||
|
* 2. Create a bolt11 invoice (we'll get one from Alice)
|
||||||
|
* 3. Send Kind 21002 debit request to Lightning.Pub
|
||||||
|
* 4. Wait for payment response
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Relay } from 'nostr-tools/relay'
|
||||||
|
import { finalizeEvent, getPublicKey } from 'nostr-tools'
|
||||||
|
import { nip44 } from 'nostr-tools'
|
||||||
|
import { bech32 } from '@scure/base'
|
||||||
|
import { randomBytes } from 'crypto'
|
||||||
|
|
||||||
|
// Generate a random keypair for this test (simulates ShockWallet)
|
||||||
|
const WALLET_PRIVATE_KEY = randomBytes(32)
|
||||||
|
const WALLET_PUBLIC_KEY = getPublicKey(WALLET_PRIVATE_KEY)
|
||||||
|
|
||||||
|
// The ndebit to test
|
||||||
|
const NDEBIT = process.argv[2]
|
||||||
|
// The bolt11 invoice to be paid
|
||||||
|
const BOLT11 = process.argv[3]
|
||||||
|
|
||||||
|
if (!NDEBIT || !BOLT11) {
|
||||||
|
console.log('Usage: node test-debit.mjs <ndebit> <bolt11>')
|
||||||
|
console.log('')
|
||||||
|
console.log('Example:')
|
||||||
|
console.log(' # First create an invoice on Alice:')
|
||||||
|
console.log(' docker exec lamassu-lnd-alice lncli --network=regtest addinvoice --amt 1000')
|
||||||
|
console.log('')
|
||||||
|
console.log(' # Then test the debit:')
|
||||||
|
console.log(' node test-debit.mjs ndebit1... lnbcrt...')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeNdebit(ndebit) {
|
||||||
|
const { prefix, words } = bech32.decode(ndebit, 5000)
|
||||||
|
if (prefix !== 'ndebit') throw new Error('Invalid ndebit prefix')
|
||||||
|
|
||||||
|
const data = new Uint8Array(bech32.fromWords(words))
|
||||||
|
|
||||||
|
let pubkey, relay, pointer
|
||||||
|
let offset = 0
|
||||||
|
|
||||||
|
while (offset < data.length) {
|
||||||
|
const type = data[offset]
|
||||||
|
const length = data[offset + 1]
|
||||||
|
const value = data.slice(offset + 2, offset + 2 + length)
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 0:
|
||||||
|
pubkey = Buffer.from(value).toString('hex')
|
||||||
|
break
|
||||||
|
case 1:
|
||||||
|
relay = new TextDecoder().decode(value)
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
pointer = new TextDecoder().decode(value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += 2 + length
|
||||||
|
}
|
||||||
|
|
||||||
|
return { pubkey, relay, pointer }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('=== CLINK Debit Test ===')
|
||||||
|
console.log('')
|
||||||
|
console.log('Test wallet pubkey:', WALLET_PUBLIC_KEY)
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// Decode ndebit
|
||||||
|
const debit = decodeNdebit(NDEBIT)
|
||||||
|
console.log('Decoded ndebit:')
|
||||||
|
console.log(' Pubkey:', debit.pubkey)
|
||||||
|
console.log(' Relay:', debit.relay)
|
||||||
|
console.log(' Pointer:', debit.pointer || '(none)')
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// Connect to relay (override Docker internal hostnames with localhost for local testing)
|
||||||
|
const relayUrl = debit.relay
|
||||||
|
.replace('host.docker.internal', 'localhost')
|
||||||
|
.replace('ws://strfry:', 'ws://localhost:')
|
||||||
|
console.log('Connecting to relay:', relayUrl)
|
||||||
|
const relay = await Relay.connect(relayUrl)
|
||||||
|
console.log('Connected!')
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// Build debit request payload
|
||||||
|
const requestPayload = {
|
||||||
|
pointer: debit.pointer,
|
||||||
|
bolt11: BOLT11,
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Request payload:', JSON.stringify(requestPayload, null, 2))
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// Encrypt with NIP-44
|
||||||
|
const conversationKey = nip44.getConversationKey(WALLET_PRIVATE_KEY, debit.pubkey)
|
||||||
|
const encryptedContent = nip44.encrypt(JSON.stringify(requestPayload), conversationKey)
|
||||||
|
|
||||||
|
// Create Kind 21002 event
|
||||||
|
const event = finalizeEvent(
|
||||||
|
{
|
||||||
|
kind: 21002,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [
|
||||||
|
['p', debit.pubkey],
|
||||||
|
['clink_version', '1'],
|
||||||
|
],
|
||||||
|
content: encryptedContent,
|
||||||
|
},
|
||||||
|
WALLET_PRIVATE_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('Publishing debit request (event id:', event.id.substring(0, 16) + '...)...')
|
||||||
|
|
||||||
|
// Subscribe to responses
|
||||||
|
let responseReceived = false
|
||||||
|
const sub = relay.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
kinds: [21002],
|
||||||
|
authors: [debit.pubkey],
|
||||||
|
'#p': [WALLET_PUBLIC_KEY],
|
||||||
|
'#e': [event.id],
|
||||||
|
since: Math.floor(Date.now() / 1000) - 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
onevent(evt) {
|
||||||
|
console.log('')
|
||||||
|
console.log('Got response event:', evt.id.substring(0, 16) + '...')
|
||||||
|
try {
|
||||||
|
const decrypted = nip44.decrypt(evt.content, conversationKey)
|
||||||
|
const response = JSON.parse(decrypted)
|
||||||
|
console.log('Response:', JSON.stringify(response, null, 2))
|
||||||
|
|
||||||
|
if (response.res === 'ok') {
|
||||||
|
console.log('')
|
||||||
|
console.log('✅ DEBIT SUCCESS!')
|
||||||
|
if (response.preimage) {
|
||||||
|
console.log('Preimage:', response.preimage)
|
||||||
|
}
|
||||||
|
} else if (response.res === 'GFY') {
|
||||||
|
console.log('')
|
||||||
|
console.log('❌ DEBIT FAILED:', response.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
responseReceived = true
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Failed to decrypt:', err.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Publish request
|
||||||
|
await relay.publish(event)
|
||||||
|
console.log('Request published, waiting for response...')
|
||||||
|
|
||||||
|
// Wait for response
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
await new Promise((r) => setTimeout(r, 1000))
|
||||||
|
if (responseReceived) break
|
||||||
|
if (i % 5 === 4) console.log('Still waiting... (' + (i + 1) + 's)')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseReceived) {
|
||||||
|
console.log('')
|
||||||
|
console.log('❌ No response received within timeout')
|
||||||
|
}
|
||||||
|
|
||||||
|
sub.close()
|
||||||
|
relay.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error)
|
||||||
|
|
@ -4,7 +4,9 @@ import { finalizeEvent } from 'nostr-tools'
|
||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
|
|
||||||
const DEV_PRIVATE_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
const DEV_PRIVATE_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
||||||
const LIGHTNING_PUB_PUBKEY = 'f454c5eec4ec4474128e19b57b45e49c3d0851d01eea960b39ce391cdba76fc6'
|
const LIGHTNING_PUB_PUBKEY =
|
||||||
|
process.env.LIGHTNING_PUB_PUBKEY ||
|
||||||
|
'4a72e400a254bf74a70cc711ab97e461b8d4fd9738b1ac3b5194cdeb3192ab91'
|
||||||
const RELAY_URL = process.env.NOSTR_RELAY_URL || 'ws://localhost:7777'
|
const RELAY_URL = process.env.NOSTR_RELAY_URL || 'ws://localhost:7777'
|
||||||
|
|
||||||
// Get a fresh invoice from Alice first:
|
// Get a fresh invoice from Alice first:
|
||||||
|
|
|
||||||
104
lamassu-next/pnpm-lock.yaml
generated
104
lamassu-next/pnpm-lock.yaml
generated
|
|
@ -169,8 +169,11 @@ importers:
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
'@scure/base':
|
'@scure/base':
|
||||||
specifier: ^1.2.0
|
specifier: ^1.2.6
|
||||||
version: 1.2.6
|
version: 1.2.6
|
||||||
|
'@shocknet/clink-sdk':
|
||||||
|
specifier: ^1.5.4
|
||||||
|
version: 1.5.4
|
||||||
'@stablelib/xchacha20':
|
'@stablelib/xchacha20':
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
|
|
@ -723,6 +726,14 @@ packages:
|
||||||
'@internationalized/number@3.6.5':
|
'@internationalized/number@3.6.5':
|
||||||
resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==}
|
resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==}
|
||||||
|
|
||||||
|
'@isaacs/balanced-match@4.0.1':
|
||||||
|
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
'@isaacs/brace-expansion@5.0.0':
|
||||||
|
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||||
|
|
||||||
|
|
@ -912,6 +923,9 @@ packages:
|
||||||
'@scure/bip39@1.2.1':
|
'@scure/bip39@1.2.1':
|
||||||
resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==}
|
resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==}
|
||||||
|
|
||||||
|
'@shocknet/clink-sdk@1.5.4':
|
||||||
|
resolution: {integrity: sha512-YrKR7oFjzUmBhm4p8pT6mP3YC7UdWbIlh5PMXHSwSLfJhL8Ddx91NLIfOHUTcQa1cDf27uvillMFXYrVUMCRuQ==}
|
||||||
|
|
||||||
'@stablelib/binary@2.0.1':
|
'@stablelib/binary@2.0.1':
|
||||||
resolution: {integrity: sha512-U9iAO8lXgEDONsA0zPPSgcf3HUBNAqHiJmSHgZz62OvC3Hi2Bhc5kTnQ3S1/L+sthDTHtCMhcEiklmIly6uQ3w==}
|
resolution: {integrity: sha512-U9iAO8lXgEDONsA0zPPSgcf3HUBNAqHiJmSHgZz62OvC3Hi2Bhc5kTnQ3S1/L+sthDTHtCMhcEiklmIly6uQ3w==}
|
||||||
|
|
||||||
|
|
@ -1336,6 +1350,10 @@ packages:
|
||||||
get-tsconfig@4.13.0:
|
get-tsconfig@4.13.0:
|
||||||
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
||||||
|
|
||||||
|
glob@13.0.0:
|
||||||
|
resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
|
|
@ -1420,6 +1438,10 @@ packages:
|
||||||
loupe@3.2.1:
|
loupe@3.2.1:
|
||||||
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
||||||
|
|
||||||
|
lru-cache@11.2.4:
|
||||||
|
resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
lucide-vue-next@0.563.0:
|
lucide-vue-next@0.563.0:
|
||||||
resolution: {integrity: sha512-zsE/lCKtmaa7bGfhSpN84br1K9YoQ5pCN+2oKWjQQG3Lo6ufUUKBuHSjNFI6RvUevxaajNXb8XwFUKeTXG3sIA==}
|
resolution: {integrity: sha512-zsE/lCKtmaa7bGfhSpN84br1K9YoQ5pCN+2oKWjQQG3Lo6ufUUKBuHSjNFI6RvUevxaajNXb8XwFUKeTXG3sIA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -1428,10 +1450,18 @@ packages:
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
|
minimatch@10.1.1:
|
||||||
|
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
minimatch@9.0.5:
|
minimatch@9.0.5:
|
||||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
|
minipass@7.1.2:
|
||||||
|
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
|
@ -1443,6 +1473,14 @@ packages:
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
nostr-tools@2.15.1:
|
||||||
|
resolution: {integrity: sha512-LpetHDR9ltnkpJDkva/SONgyKBbsoV+5yLB8DWc0/U3lCWGtoWJw6Nbc2vR2Ai67RIQYrBQeZLyMlhwVZRK/9A==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=5.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
nostr-tools@2.19.4:
|
nostr-tools@2.19.4:
|
||||||
resolution: {integrity: sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg==}
|
resolution: {integrity: sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -1457,9 +1495,16 @@ packages:
|
||||||
ohash@2.0.11:
|
ohash@2.0.11:
|
||||||
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
|
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
|
||||||
|
|
||||||
|
package-json-from-dist@1.0.1:
|
||||||
|
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||||
|
|
||||||
path-browserify@1.0.1:
|
path-browserify@1.0.1:
|
||||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||||
|
|
||||||
|
path-scurry@2.0.1:
|
||||||
|
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
pathe@1.1.2:
|
pathe@1.1.2:
|
||||||
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||||
|
|
||||||
|
|
@ -1505,6 +1550,11 @@ packages:
|
||||||
resolve-pkg-maps@1.0.0:
|
resolve-pkg-maps@1.0.0:
|
||||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||||
|
|
||||||
|
rimraf@6.1.2:
|
||||||
|
resolution: {integrity: sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
rollup@4.56.0:
|
rollup@4.56.0:
|
||||||
resolution: {integrity: sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==}
|
resolution: {integrity: sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
|
|
@ -2024,6 +2074,12 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@swc/helpers': 0.5.18
|
'@swc/helpers': 0.5.18
|
||||||
|
|
||||||
|
'@isaacs/balanced-match@4.0.1': {}
|
||||||
|
|
||||||
|
'@isaacs/brace-expansion@5.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@isaacs/balanced-match': 4.0.1
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
@ -2165,6 +2221,14 @@ snapshots:
|
||||||
'@noble/hashes': 1.3.2
|
'@noble/hashes': 1.3.2
|
||||||
'@scure/base': 1.1.1
|
'@scure/base': 1.1.1
|
||||||
|
|
||||||
|
'@shocknet/clink-sdk@1.5.4':
|
||||||
|
dependencies:
|
||||||
|
'@noble/hashes': 1.8.0
|
||||||
|
'@scure/base': 1.2.6
|
||||||
|
nostr-tools: 2.15.1(typescript@5.9.3)
|
||||||
|
rimraf: 6.1.2
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
'@stablelib/binary@2.0.1':
|
'@stablelib/binary@2.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@stablelib/int': 2.0.1
|
'@stablelib/int': 2.0.1
|
||||||
|
|
@ -2639,6 +2703,12 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
resolve-pkg-maps: 1.0.0
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
||||||
|
glob@13.0.0:
|
||||||
|
dependencies:
|
||||||
|
minimatch: 10.1.1
|
||||||
|
minipass: 7.1.2
|
||||||
|
path-scurry: 2.0.1
|
||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
he@1.2.0: {}
|
he@1.2.0: {}
|
||||||
|
|
@ -2696,6 +2766,8 @@ snapshots:
|
||||||
|
|
||||||
loupe@3.2.1: {}
|
loupe@3.2.1: {}
|
||||||
|
|
||||||
|
lru-cache@11.2.4: {}
|
||||||
|
|
||||||
lucide-vue-next@0.563.0(vue@3.5.27(typescript@5.9.3)):
|
lucide-vue-next@0.563.0(vue@3.5.27(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.27(typescript@5.9.3)
|
vue: 3.5.27(typescript@5.9.3)
|
||||||
|
|
@ -2704,16 +2776,34 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
minimatch@10.1.1:
|
||||||
|
dependencies:
|
||||||
|
'@isaacs/brace-expansion': 5.0.0
|
||||||
|
|
||||||
minimatch@9.0.5:
|
minimatch@9.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 2.0.2
|
brace-expansion: 2.0.2
|
||||||
|
|
||||||
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
muggle-string@0.4.1: {}
|
muggle-string@0.4.1: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
nostr-tools@2.15.1(typescript@5.9.3):
|
||||||
|
dependencies:
|
||||||
|
'@noble/ciphers': 0.5.3
|
||||||
|
'@noble/curves': 1.2.0
|
||||||
|
'@noble/hashes': 1.3.1
|
||||||
|
'@scure/base': 1.1.1
|
||||||
|
'@scure/bip32': 1.3.1
|
||||||
|
'@scure/bip39': 1.2.1
|
||||||
|
nostr-wasm: 0.1.0
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
nostr-tools@2.19.4(typescript@5.9.3):
|
nostr-tools@2.19.4(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/ciphers': 0.5.3
|
'@noble/ciphers': 0.5.3
|
||||||
|
|
@ -2730,8 +2820,15 @@ snapshots:
|
||||||
|
|
||||||
ohash@2.0.11: {}
|
ohash@2.0.11: {}
|
||||||
|
|
||||||
|
package-json-from-dist@1.0.1: {}
|
||||||
|
|
||||||
path-browserify@1.0.1: {}
|
path-browserify@1.0.1: {}
|
||||||
|
|
||||||
|
path-scurry@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
lru-cache: 11.2.4
|
||||||
|
minipass: 7.1.2
|
||||||
|
|
||||||
pathe@1.1.2: {}
|
pathe@1.1.2: {}
|
||||||
|
|
||||||
pathval@2.0.1: {}
|
pathval@2.0.1: {}
|
||||||
|
|
@ -2781,6 +2878,11 @@ snapshots:
|
||||||
|
|
||||||
resolve-pkg-maps@1.0.0: {}
|
resolve-pkg-maps@1.0.0: {}
|
||||||
|
|
||||||
|
rimraf@6.1.2:
|
||||||
|
dependencies:
|
||||||
|
glob: 13.0.0
|
||||||
|
package-json-from-dist: 1.0.1
|
||||||
|
|
||||||
rollup@4.56.0:
|
rollup@4.56.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue