Implement LNURL-withdraw for cash-in flow
Replace CLINK offer with LNURL-withdraw for the "buy bitcoin" flow. LNURL-withdraw is the correct protocol for customers to receive sats: - ATM displays LNURL-withdraw QR code - Customer scans with their Lightning wallet - Wallet automatically creates invoice and sends it to ATM - ATM pays the invoice, sending sats to customer This provides a much better UX than manual invoice entry, especially for a kiosk where users can't paste text. Changes: - Add LnurlWithdrawServer class that implements LUD-03 protocol - Add LNURL HTTP server (port 3333) for wallet callbacks - Update state machine: generatingOffer → generatingLnurlWithdraw - Add lnurlWithdraw context field and generateLnurlWithdraw service - Update CashInView to display LNURL-withdraw QR - Add @scure/base dependency for bech32 encoding Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2356befbcf
commit
5d56ebb642
8 changed files with 427 additions and 19 deletions
|
|
@ -18,6 +18,7 @@
|
|||
"@lamassu/lightning": "workspace:*",
|
||||
"@lamassu/nostr-client": "workspace:*",
|
||||
"@lamassu/state-machine": "workspace:*",
|
||||
"@scure/base": "^1.2.0",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
* Lightning Services
|
||||
*
|
||||
* Connects to Lightning.Pub and provides real ATMServices implementation.
|
||||
* Includes LNURL-withdraw server for cash-in flow.
|
||||
*/
|
||||
|
||||
import { NostrClient, generateIdentity, type MachineIdentity } from '@lamassu/nostr-client'
|
||||
|
|
@ -9,6 +10,7 @@ import { LightningPubClient } from '@lamassu/lightning'
|
|||
import { CLINKClient, createOfferSuccess, createOfferError, CLINKErrorCode } from '@lamassu/clink'
|
||||
import type { OfferRequest } from '@lamassu/clink'
|
||||
import type { ATMServices, ATMContext } from '@lamassu/state-machine'
|
||||
import { LnurlWithdrawServer } from './lnurl-withdraw'
|
||||
|
||||
// Development infrastructure configuration
|
||||
// In production, these would come from environment or secure config
|
||||
|
|
@ -21,17 +23,23 @@ const DEV_CONFIG = {
|
|||
adminToken: 'lamassu-dev-admin-token',
|
||||
// Lightning.Pub HTTP API
|
||||
lightningPubApiUrl: 'http://localhost:1776',
|
||||
// LNURL-withdraw server configuration
|
||||
lnurlPort: 3333,
|
||||
// External host - this needs to be reachable by the customer's wallet
|
||||
// For local development, use localhost. In production, use the ATM's public IP/domain
|
||||
lnurlExternalHost: 'localhost',
|
||||
}
|
||||
|
||||
interface LightningServices {
|
||||
nostrClient: NostrClient
|
||||
lightningPub: LightningPubClient
|
||||
clink: CLINKClient
|
||||
lnurlServer: LnurlWithdrawServer
|
||||
identity: MachineIdentity
|
||||
atmServices: ATMServices
|
||||
/** Set callback for when offer requests are received */
|
||||
onOfferRequest: (callback: OfferRequestCallback) => void
|
||||
/** Set callback for when payments are received */
|
||||
/** Set callback for when payments are received (from CLINK or LNURL-withdraw) */
|
||||
onPaymentReceived: (callback: PaymentReceivedCallback) => void
|
||||
}
|
||||
|
||||
|
|
@ -130,13 +138,48 @@ export async function initializeLightningServices(): Promise<LightningServices>
|
|||
clink.startListening()
|
||||
console.log('[Lightning] CLINK client initialized with offer handler')
|
||||
|
||||
// Create pay invoice function for LNURL-withdraw
|
||||
const payInvoice = async (invoice: string): Promise<{ preimage: string }> => {
|
||||
console.log('[Lightning] Paying invoice:', invoice.slice(0, 32) + '...')
|
||||
try {
|
||||
const result = await lightningPub.payInvoice(invoice)
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Payment failed')
|
||||
}
|
||||
console.log(
|
||||
'[Lightning] Payment successful, preimage:',
|
||||
result.preimage?.slice(0, 16) + '...'
|
||||
)
|
||||
return { preimage: result.preimage! }
|
||||
} catch (error) {
|
||||
console.error('[Lightning] Failed to pay invoice:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Create LNURL-withdraw server
|
||||
const lnurlServer = new LnurlWithdrawServer({
|
||||
port: DEV_CONFIG.lnurlPort,
|
||||
externalHost: DEV_CONFIG.lnurlExternalHost,
|
||||
payInvoice,
|
||||
})
|
||||
|
||||
// Start the LNURL server
|
||||
await lnurlServer.start()
|
||||
console.log('[Lightning] LNURL-withdraw server started on port', DEV_CONFIG.lnurlPort)
|
||||
|
||||
// Create ATM services
|
||||
const atmServices = createATMServices(lightningPub, clink, identity)
|
||||
const atmServices = createATMServices(lightningPub, clink, lnurlServer, identity, (preimage) => {
|
||||
if (paymentReceivedCallback) {
|
||||
paymentReceivedCallback(preimage)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
nostrClient,
|
||||
lightningPub,
|
||||
clink,
|
||||
lnurlServer,
|
||||
identity,
|
||||
atmServices,
|
||||
onOfferRequest: (callback: OfferRequestCallback) => {
|
||||
|
|
@ -154,13 +197,15 @@ export async function initializeLightningServices(): Promise<LightningServices>
|
|||
function createATMServices(
|
||||
lightningPub: LightningPubClient,
|
||||
clink: CLINKClient,
|
||||
_identity: MachineIdentity
|
||||
lnurlServer: LnurlWithdrawServer,
|
||||
_identity: MachineIdentity,
|
||||
onPaymentSuccess: (preimage: string) => void
|
||||
): ATMServices {
|
||||
return {
|
||||
/**
|
||||
* Generate a CLINK offer (noffer) for receiving payment
|
||||
*
|
||||
* For cash-in: customer scans QR to receive sats
|
||||
* For cash-out display (not currently used for main flow)
|
||||
*/
|
||||
generateClinkOffer: async (context: ATMContext): Promise<string> => {
|
||||
console.log('[ATM Service] Generating CLINK offer for', context.satsAmount, 'sats')
|
||||
|
|
@ -178,6 +223,35 @@ function createATMServices(
|
|||
return noffer
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate an LNURL-withdraw link for cash-in
|
||||
*
|
||||
* Customer scans QR with their wallet, wallet automatically creates invoice
|
||||
* and sends it to us, we pay it.
|
||||
*/
|
||||
generateLnurlWithdraw: async (context: ATMContext): Promise<string> => {
|
||||
console.log('[ATM Service] Generating LNURL-withdraw for', context.satsAmount, 'sats')
|
||||
|
||||
const lnurl = lnurlServer.createWithdrawal(
|
||||
context.satsAmount,
|
||||
`Lamassu ATM - Buy ${context.satsAmount} sats`,
|
||||
(preimage) => {
|
||||
console.log(
|
||||
'[ATM Service] LNURL-withdraw payment sent! Preimage:',
|
||||
preimage.slice(0, 16) + '...'
|
||||
)
|
||||
onPaymentSuccess(preimage)
|
||||
},
|
||||
(error) => {
|
||||
console.error('[ATM Service] LNURL-withdraw payment failed:', error)
|
||||
// Note: error handling is done via the state machine's PAYMENT_FAILED event
|
||||
}
|
||||
)
|
||||
|
||||
console.log('[ATM Service] Generated LNURL-withdraw:', lnurl.slice(0, 32) + '...')
|
||||
return lnurl
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a Lightning invoice for payment
|
||||
*
|
||||
|
|
|
|||
310
lamassu-next/apps/machine/src/services/lnurl-withdraw.ts
Normal file
310
lamassu-next/apps/machine/src/services/lnurl-withdraw.ts
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
/**
|
||||
* LNURL-Withdraw Server
|
||||
*
|
||||
* Implements LUD-03 (LNURL-withdraw) protocol for cash-in flow.
|
||||
* Customer scans a QR code with their Lightning wallet, which automatically
|
||||
* creates an invoice and sends it to us for payment.
|
||||
*
|
||||
* Flow:
|
||||
* 1. ATM generates withdrawal with unique k1 token
|
||||
* 2. Customer scans LNURL-withdraw QR
|
||||
* 3. Wallet fetches parameters from our callback URL
|
||||
* 4. Wallet creates invoice and submits to callback
|
||||
* 5. We validate k1, pay the invoice, notify the ATM
|
||||
*
|
||||
* @see https://github.com/lnurl/luds/blob/luds/03.md
|
||||
*/
|
||||
|
||||
import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'http'
|
||||
import { bech32 } from '@scure/base'
|
||||
|
||||
/** Pending withdrawal request */
|
||||
interface PendingWithdrawal {
|
||||
k1: string
|
||||
amountMsats: number
|
||||
description: string
|
||||
createdAt: number
|
||||
/** Callback to pay the invoice when received */
|
||||
payInvoice: (invoice: string) => Promise<{ preimage: string }>
|
||||
/** Callback when payment succeeds */
|
||||
onSuccess: (preimage: string) => void
|
||||
/** Callback when payment fails */
|
||||
onError: (error: string) => void
|
||||
}
|
||||
|
||||
/** LNURL-withdraw server configuration */
|
||||
export interface LnurlWithdrawConfig {
|
||||
/** Port to listen on */
|
||||
port: number
|
||||
/** External host/IP that wallets will connect to */
|
||||
externalHost: string
|
||||
/** Payment function */
|
||||
payInvoice: (invoice: string) => Promise<{ preimage: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* LNURL-withdraw server
|
||||
*/
|
||||
export class LnurlWithdrawServer {
|
||||
private server: Server | null = null
|
||||
private pendingWithdrawals = new Map<string, PendingWithdrawal>()
|
||||
private config: LnurlWithdrawConfig
|
||||
|
||||
constructor(config: LnurlWithdrawConfig) {
|
||||
this.config = config
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the HTTP server
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.server) {
|
||||
return // Already running
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server = createServer((req, res) => this.handleRequest(req, res))
|
||||
|
||||
this.server.on('error', (err) => {
|
||||
console.error('[LNURL] Server error:', err)
|
||||
reject(err)
|
||||
})
|
||||
|
||||
this.server.listen(this.config.port, () => {
|
||||
console.log(`[LNURL] Server listening on port ${this.config.port}`)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the HTTP server
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (this.server) {
|
||||
this.server.close(() => {
|
||||
this.server = null
|
||||
console.log('[LNURL] Server stopped')
|
||||
resolve()
|
||||
})
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new withdrawal request
|
||||
*
|
||||
* @param amountSats Amount in satoshis
|
||||
* @param description Description for the invoice
|
||||
* @param onSuccess Callback when payment succeeds
|
||||
* @param onError Callback when payment fails
|
||||
* @returns LNURL-withdraw string (bech32-encoded)
|
||||
*/
|
||||
createWithdrawal(
|
||||
amountSats: number,
|
||||
description: string,
|
||||
onSuccess: (preimage: string) => void,
|
||||
onError: (error: string) => void
|
||||
): string {
|
||||
// Generate random k1 token
|
||||
const k1 = this.generateK1()
|
||||
|
||||
// Store the pending withdrawal
|
||||
const withdrawal: PendingWithdrawal = {
|
||||
k1,
|
||||
amountMsats: amountSats * 1000,
|
||||
description,
|
||||
createdAt: Date.now(),
|
||||
payInvoice: this.config.payInvoice,
|
||||
onSuccess,
|
||||
onError,
|
||||
}
|
||||
|
||||
this.pendingWithdrawals.set(k1, withdrawal)
|
||||
console.log(`[LNURL] Created withdrawal: ${k1.slice(0, 16)}... for ${amountSats} sats`)
|
||||
|
||||
// Build the callback URL
|
||||
const callbackUrl = `http://${this.config.externalHost}:${this.config.port}/lnurl?k1=${k1}`
|
||||
|
||||
// Encode as LNURL (bech32 with "lnurl" prefix)
|
||||
const lnurl = this.encodeLnurl(callbackUrl)
|
||||
console.log(`[LNURL] Generated LNURL: ${lnurl.slice(0, 32)}...`)
|
||||
|
||||
return lnurl
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a pending withdrawal
|
||||
*/
|
||||
cancelWithdrawal(k1: string): void {
|
||||
if (this.pendingWithdrawals.has(k1)) {
|
||||
this.pendingWithdrawals.delete(k1)
|
||||
console.log(`[LNURL] Cancelled withdrawal: ${k1.slice(0, 16)}...`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming HTTP requests
|
||||
*/
|
||||
private handleRequest(req: IncomingMessage, res: ServerResponse): void {
|
||||
// Enable CORS for wallet compatibility
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
const url = new URL(req.url || '/', `http://${req.headers.host}`)
|
||||
const path = url.pathname
|
||||
const k1 = url.searchParams.get('k1')
|
||||
const pr = url.searchParams.get('pr') // Payment request (invoice)
|
||||
|
||||
console.log(
|
||||
`[LNURL] Request: ${path}?k1=${k1?.slice(0, 16)}...${pr ? `&pr=${pr.slice(0, 20)}...` : ''}`
|
||||
)
|
||||
|
||||
if (path !== '/lnurl') {
|
||||
this.sendError(res, 404, 'Not found')
|
||||
return
|
||||
}
|
||||
|
||||
if (!k1) {
|
||||
this.sendError(res, 400, 'Missing k1 parameter')
|
||||
return
|
||||
}
|
||||
|
||||
const withdrawal = this.pendingWithdrawals.get(k1)
|
||||
if (!withdrawal) {
|
||||
this.sendError(res, 400, 'Unknown or expired k1')
|
||||
return
|
||||
}
|
||||
|
||||
if (pr) {
|
||||
// Step 2: Wallet is submitting an invoice
|
||||
this.handleInvoiceSubmission(res, withdrawal, pr)
|
||||
} else {
|
||||
// Step 1: Wallet is requesting withdrawal parameters
|
||||
this.handleWithdrawalRequest(res, withdrawal)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle initial LNURL-withdraw request (wallet fetching parameters)
|
||||
*/
|
||||
private handleWithdrawalRequest(res: ServerResponse, withdrawal: PendingWithdrawal): void {
|
||||
const callbackUrl = `http://${this.config.externalHost}:${this.config.port}/lnurl?k1=${withdrawal.k1}`
|
||||
|
||||
const response = {
|
||||
tag: 'withdrawRequest',
|
||||
callback: callbackUrl,
|
||||
k1: withdrawal.k1,
|
||||
defaultDescription: withdrawal.description,
|
||||
minWithdrawable: withdrawal.amountMsats,
|
||||
maxWithdrawable: withdrawal.amountMsats,
|
||||
}
|
||||
|
||||
console.log(`[LNURL] Sending withdrawal parameters:`, {
|
||||
...response,
|
||||
k1: response.k1.slice(0, 16) + '...',
|
||||
})
|
||||
|
||||
res.writeHead(200)
|
||||
res.end(JSON.stringify(response))
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle invoice submission from wallet
|
||||
*/
|
||||
private async handleInvoiceSubmission(
|
||||
res: ServerResponse,
|
||||
withdrawal: PendingWithdrawal,
|
||||
invoice: string
|
||||
): Promise<void> {
|
||||
console.log(`[LNURL] Received invoice: ${invoice.slice(0, 32)}...`)
|
||||
|
||||
try {
|
||||
// Validate invoice starts with expected prefix
|
||||
const lowerInvoice = invoice.toLowerCase()
|
||||
if (
|
||||
!lowerInvoice.startsWith('lnbc') &&
|
||||
!lowerInvoice.startsWith('lntb') &&
|
||||
!lowerInvoice.startsWith('lnbcrt')
|
||||
) {
|
||||
this.sendError(res, 400, 'Invalid invoice format')
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Decode and verify invoice amount matches withdrawal amount
|
||||
// For now, trust the wallet created the correct invoice
|
||||
|
||||
console.log(`[LNURL] Paying invoice...`)
|
||||
const result = await withdrawal.payInvoice(invoice)
|
||||
|
||||
// Payment successful
|
||||
console.log(`[LNURL] Payment successful! Preimage: ${result.preimage.slice(0, 16)}...`)
|
||||
|
||||
// Remove from pending
|
||||
this.pendingWithdrawals.delete(withdrawal.k1)
|
||||
|
||||
// Notify the ATM
|
||||
withdrawal.onSuccess(result.preimage)
|
||||
|
||||
// Send success response to wallet
|
||||
res.writeHead(200)
|
||||
res.end(JSON.stringify({ status: 'OK' }))
|
||||
} catch (error) {
|
||||
console.error(`[LNURL] Payment failed:`, error)
|
||||
|
||||
// Notify the ATM
|
||||
withdrawal.onError(error instanceof Error ? error.message : 'Payment failed')
|
||||
|
||||
// Send error to wallet
|
||||
this.sendError(res, 500, error instanceof Error ? error.message : 'Payment failed')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send JSON error response
|
||||
*/
|
||||
private sendError(res: ServerResponse, status: number, message: string): void {
|
||||
res.writeHead(status)
|
||||
res.end(JSON.stringify({ status: 'ERROR', reason: message }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random k1 token (32 bytes hex)
|
||||
*/
|
||||
private generateK1(): string {
|
||||
const bytes = new Uint8Array(32)
|
||||
crypto.getRandomValues(bytes)
|
||||
return Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a URL as LNURL (bech32 with "lnurl" prefix)
|
||||
*/
|
||||
private encodeLnurl(url: string): string {
|
||||
const bytes = new TextEncoder().encode(url)
|
||||
const words = bech32.toWords(bytes)
|
||||
return bech32.encode('lnurl', words, 1023).toUpperCase()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an LNURL back to a URL (for testing/debugging)
|
||||
*/
|
||||
export function decodeLnurl(lnurl: string): string {
|
||||
// Cast to the expected bech32 format type
|
||||
const decoded = bech32.decode(lnurl.toLowerCase() as `${string}1${string}`, 1023)
|
||||
const bytes = bech32.fromWords(decoded.words)
|
||||
return new TextDecoder().decode(new Uint8Array(bytes))
|
||||
}
|
||||
|
|
@ -18,6 +18,12 @@ const mockServices: ATMServices = {
|
|||
return `noffer1mock${Date.now().toString(36)}`
|
||||
},
|
||||
|
||||
generateLnurlWithdraw: async (context) => {
|
||||
console.log('[Mock] Generating LNURL-withdraw for', context.satsAmount, 'sats')
|
||||
// This is a mock LNURL - in reality it would be a bech32 encoded URL
|
||||
return `LNURL1MOCK${Date.now().toString(36).toUpperCase()}`
|
||||
},
|
||||
|
||||
generateInvoice: async (amountMsat) => {
|
||||
console.log('[Mock] Generating invoice for', amountMsat, 'msats')
|
||||
return `lnbc${amountMsat}n1mock${Date.now().toString(36)}`
|
||||
|
|
|
|||
|
|
@ -125,18 +125,18 @@ function formatSats(sats: number): string {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Generating Offer -->
|
||||
<Card v-else-if="nestedState === 'generatingOffer'" class="w-80 border-0 bg-white/5">
|
||||
<!-- Generating LNURL-withdraw -->
|
||||
<Card v-else-if="nestedState === 'generatingLnurlWithdraw'" class="w-80 border-0 bg-white/5">
|
||||
<CardContent class="flex flex-col items-center gap-4 p-8">
|
||||
<Skeleton class="h-10 w-10 rounded-full" />
|
||||
<p class="text-muted-foreground">Generating CLINK offer...</p>
|
||||
<p class="text-muted-foreground">Preparing your withdrawal...</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Display QR -->
|
||||
<!-- Display LNURL-withdraw QR -->
|
||||
<Card v-else-if="nestedState === 'displayingQR'" class="w-96 border-0 bg-white/5">
|
||||
<CardHeader class="text-center">
|
||||
<CardTitle>Scan to Receive</CardTitle>
|
||||
<CardTitle>Scan to Receive Sats</CardTitle>
|
||||
<CardDescription v-if="context">
|
||||
<Badge variant="secondary" class="text-lg">
|
||||
{{ formatSats(context.satsAmount) }} sats
|
||||
|
|
@ -144,9 +144,12 @@ function formatSats(sats: number): string {
|
|||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<QRCode v-if="context?.clinkOffer" :value="context.clinkOffer" :size="200" />
|
||||
<QRCode v-if="context?.lnurlWithdraw" :value="context.lnurlWithdraw" :size="200" />
|
||||
<p class="text-center text-sm text-muted-foreground">
|
||||
Open your Lightning wallet and scan the code
|
||||
Open your Lightning wallet and scan to receive your sats
|
||||
</p>
|
||||
<p class="text-center text-xs text-muted-foreground/70">
|
||||
Your wallet will automatically create an invoice and we'll pay it instantly
|
||||
</p>
|
||||
|
||||
<!-- Dev controls -->
|
||||
|
|
@ -157,7 +160,7 @@ function formatSats(sats: number): string {
|
|||
size="sm"
|
||||
@click="atmStore.paymentReceived('mock-preimage-' + Date.now())"
|
||||
>
|
||||
Dev: Simulate Payment
|
||||
Dev: Simulate Payment Sent
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ export function createATMMachine(services: Partial<ATMServices> = {}) {
|
|||
}
|
||||
return services.generateClinkOffer(input)
|
||||
}),
|
||||
generateLnurlWithdraw: fromPromise(async ({ input }: { input: ATMContext }) => {
|
||||
if (!services.generateLnurlWithdraw) {
|
||||
throw new Error('generateLnurlWithdraw service not provided')
|
||||
}
|
||||
return services.generateLnurlWithdraw(input)
|
||||
}),
|
||||
generateInvoice: fromPromise(async ({ input }: { input: number }) => {
|
||||
if (!services.generateInvoice) {
|
||||
throw new Error('generateInvoice service not provided')
|
||||
|
|
@ -217,21 +223,21 @@ export function createATMMachine(services: Partial<ATMServices> = {}) {
|
|||
},
|
||||
FINISH_INSERTING: {
|
||||
guard: 'hasInsertedBills',
|
||||
target: 'generatingOffer',
|
||||
target: 'generatingLnurlWithdraw',
|
||||
},
|
||||
CANCEL: '#atm.idle',
|
||||
TIMEOUT: '#atm.idle',
|
||||
},
|
||||
},
|
||||
generatingOffer: {
|
||||
generatingLnurlWithdraw: {
|
||||
invoke: {
|
||||
src: 'generateClinkOffer',
|
||||
src: 'generateLnurlWithdraw',
|
||||
input: ({ context }) => context,
|
||||
onDone: {
|
||||
target: 'displayingQR',
|
||||
actions: assign({
|
||||
clinkOffer: ({ event }) => event.output,
|
||||
paymentMethod: () => 'clink_offer' as const,
|
||||
lnurlWithdraw: ({ event }) => event.output,
|
||||
paymentMethod: () => 'lnurl_withdraw' as const,
|
||||
}),
|
||||
},
|
||||
onError: {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,10 @@ export interface ATMContext {
|
|||
// Payment
|
||||
/** BOLT11 invoice for payment */
|
||||
invoice: string | null
|
||||
/** CLINK offer string (noffer) */
|
||||
/** CLINK offer string (noffer) - for cash-out */
|
||||
clinkOffer: string | null
|
||||
/** LNURL-withdraw string - for cash-in (customer receives sats) */
|
||||
lnurlWithdraw: string | null
|
||||
/** Current payment status */
|
||||
paymentStatus: PaymentStatus
|
||||
/** Payment preimage (proof of payment) */
|
||||
|
|
@ -94,6 +96,7 @@ export const initialContext: ATMContext = {
|
|||
feePercent: 0.02,
|
||||
invoice: null,
|
||||
clinkOffer: null,
|
||||
lnurlWithdraw: null,
|
||||
paymentStatus: null,
|
||||
preimage: null,
|
||||
paymentMethod: null,
|
||||
|
|
@ -109,8 +112,10 @@ export const initialContext: ATMContext = {
|
|||
|
||||
/** Service inputs for actors */
|
||||
export interface ATMServices {
|
||||
/** Generate a CLINK offer */
|
||||
/** Generate a CLINK offer (for cash-out) */
|
||||
generateClinkOffer: (context: ATMContext) => Promise<string>
|
||||
/** Generate an LNURL-withdraw link (for cash-in - customer receives sats) */
|
||||
generateLnurlWithdraw: (context: ATMContext) => Promise<string>
|
||||
/** Generate a Lightning invoice */
|
||||
generateInvoice: (amountMsat: number) => Promise<string>
|
||||
/** Send receipt via Nostr */
|
||||
|
|
|
|||
3
lamassu-next/pnpm-lock.yaml
generated
3
lamassu-next/pnpm-lock.yaml
generated
|
|
@ -35,6 +35,9 @@ importers:
|
|||
'@lamassu/state-machine':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/state-machine
|
||||
'@scure/base':
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.6
|
||||
'@vueuse/core':
|
||||
specifier: ^14.1.0
|
||||
version: 14.1.0(vue@3.5.27(typescript@5.9.3))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue