feat: implement ndebit QR code display for cash-in flow
Replace LNURL-withdraw with CLINK ndebit for cash-in transactions: - Add ndebit.ts with TLV-based bech32 encoding/decoding - Generate clink:ndebit1...?amount=<sats> URIs - Display QR code with copyable URI in CashInView - Update state machine flow: generatingNdebit → displayingQR - Fix shadcn-vue Input binding with :model-value prop Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
66a3c054e7
commit
cf053730c6
8 changed files with 378 additions and 92 deletions
|
|
@ -19,7 +19,14 @@ import {
|
|||
type MachineIdentity,
|
||||
} from '@lamassu/nostr-client'
|
||||
import { LightningPubClient } from '@lamassu/lightning'
|
||||
import { CLINKClient, createOfferSuccess, createOfferError, CLINKErrorCode } from '@lamassu/clink'
|
||||
import {
|
||||
CLINKClient,
|
||||
createOfferSuccess,
|
||||
createOfferError,
|
||||
CLINKErrorCode,
|
||||
encodeNdebit,
|
||||
formatNdebitUri,
|
||||
} from '@lamassu/clink'
|
||||
import type { OfferRequest } from '@lamassu/clink'
|
||||
import type { ATMServices, ATMContext } from '@lamassu/state-machine'
|
||||
|
||||
|
|
@ -186,6 +193,28 @@ function createATMServices(
|
|||
_onPaymentSuccess: (preimage: string) => void
|
||||
): ATMServices {
|
||||
return {
|
||||
/**
|
||||
* Generate an ndebit URI for cash-in
|
||||
*
|
||||
* The ndebit encodes the ATM's pubkey and relay, with amount as query param.
|
||||
* Format: clink:ndebit1<bech32>?amount=<sats>
|
||||
*/
|
||||
generateNdebit: async (context: ATMContext): Promise<string> => {
|
||||
console.log('[ATM Service] Generating ndebit for', context.satsAmount, 'sats')
|
||||
|
||||
// Create ndebit with ATM's pubkey and relay
|
||||
const ndebit = encodeNdebit({
|
||||
pubkey: _identity.publicKey,
|
||||
relay: DEV_CONFIG.relayUrl,
|
||||
})
|
||||
|
||||
// Format as full URI with amount
|
||||
const uri = formatNdebitUri(ndebit, context.satsAmount)
|
||||
|
||||
console.log('[ATM Service] Generated ndebit URI:', uri.slice(0, 60) + '...')
|
||||
return uri
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a CLINK offer (noffer) for receiving payment
|
||||
*
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ const mockServices: ATMServices = {
|
|||
return `noffer1mock${Date.now().toString(36)}`
|
||||
},
|
||||
|
||||
generateNdebit: async (context) => {
|
||||
console.log('[Mock] Generating ndebit for', context.satsAmount, 'sats')
|
||||
// Mock ndebit URI with amount
|
||||
return `clink:ndebit1mock${Date.now().toString(36)}?amount=${context.satsAmount}`
|
||||
},
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -13,16 +13,31 @@ import QRCode from '@/components/QRCode.vue'
|
|||
const router = useRouter()
|
||||
const atmStore = useAtmStore()
|
||||
|
||||
// Input modes
|
||||
type PaymentMode = 'clink' | 'invoice'
|
||||
const paymentMode = ref<PaymentMode>('clink')
|
||||
|
||||
// CLINK Debit: customer's npub/pubkey
|
||||
const pubkeyInput = ref('')
|
||||
|
||||
// Invoice input for manual payment
|
||||
const invoiceInput = ref('')
|
||||
|
||||
// Copy state for ndebit URI
|
||||
const copied = ref(false)
|
||||
|
||||
// Copy ndebit URI to clipboard
|
||||
async function copyNdebit() {
|
||||
const uri = atmStore.context?.ndebitUri
|
||||
if (!uri) return
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(uri)
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 2000)
|
||||
} catch (err) {
|
||||
console.error('[CashIn] Failed to copy:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate back to idle when transaction completes
|
||||
watch(
|
||||
() => atmStore.isIdle,
|
||||
|
|
@ -180,54 +195,71 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Generating LNURL-withdraw -->
|
||||
<Card v-else-if="nestedState === 'generatingLnurlWithdraw'" class="w-80 border-0 bg-white/5">
|
||||
<!-- Generating NDebit -->
|
||||
<Card v-else-if="nestedState === 'generatingNdebit'" 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">Preparing your withdrawal...</p>
|
||||
<p class="text-muted-foreground">Preparing payment code...</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Display Payment Options -->
|
||||
<!-- Display NDebit QR -->
|
||||
<Card v-else-if="nestedState === 'displayingQR'" class="w-[480px] border-0 bg-white/5">
|
||||
<CardHeader class="text-center">
|
||||
<CardTitle>Receive Your Sats</CardTitle>
|
||||
<CardTitle>Scan to Receive Sats</CardTitle>
|
||||
<CardDescription v-if="context">
|
||||
<Badge variant="secondary" class="text-lg">
|
||||
{{ formatSats(context.satsAmount) }} sats
|
||||
</Badge>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- Payment mode tabs -->
|
||||
<div class="flex gap-2 rounded-lg bg-white/5 p-1">
|
||||
<Button
|
||||
:variant="paymentMode === 'clink' ? 'default' : 'ghost'"
|
||||
size="sm"
|
||||
class="flex-1"
|
||||
@click="paymentMode = 'clink'"
|
||||
>
|
||||
CLINK Debit (Nostr)
|
||||
</Button>
|
||||
<Button
|
||||
:variant="paymentMode === 'invoice' ? 'default' : 'ghost'"
|
||||
size="sm"
|
||||
class="flex-1"
|
||||
@click="paymentMode = 'invoice'"
|
||||
>
|
||||
Paste Invoice
|
||||
</Button>
|
||||
<CardContent class="space-y-6">
|
||||
<!-- NDebit QR Code (Primary) -->
|
||||
<div class="flex flex-col items-center">
|
||||
<QRCode v-if="context?.ndebitUri" :value="context.ndebitUri" :size="220" />
|
||||
<p class="mt-4 text-center text-sm text-muted-foreground">
|
||||
Scan with Shock Wallet or compatible CLINK wallet
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- CLINK Debit Mode -->
|
||||
<div v-if="paymentMode === 'clink'" class="space-y-4">
|
||||
<Alert class="border-blue-500/50 bg-blue-500/10">
|
||||
<AlertDescription>
|
||||
<p class="mb-2 text-sm font-medium text-blue-400">CLINK Debit (Nostr-Native)</p>
|
||||
<p class="mb-3 text-xs text-muted-foreground">
|
||||
Enter your Shock Wallet's npub or pubkey. We'll send a debit request and your
|
||||
wallet will automatically pay.
|
||||
</p>
|
||||
<!-- Copyable NDebit URI -->
|
||||
<div v-if="context?.ndebitUri" class="space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<Input :model-value="context.ndebitUri" readonly class="flex-1 font-mono text-xs" />
|
||||
<Button variant="outline" size="sm" @click="copyNdebit">
|
||||
{{ copied ? '✓ Copied' : 'Copy' }}
|
||||
</Button>
|
||||
</div>
|
||||
<p class="text-center text-xs text-muted-foreground">
|
||||
Or copy and paste into your wallet
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Instructions -->
|
||||
<Alert class="border-green-500/50 bg-green-500/10">
|
||||
<AlertDescription>
|
||||
<p class="mb-2 text-sm font-medium text-green-400">How it works:</p>
|
||||
<ol class="space-y-1 text-xs text-muted-foreground">
|
||||
<li>1. Scan the QR code with your Lightning wallet</li>
|
||||
<li>
|
||||
2. Your wallet creates an invoice for
|
||||
{{ formatSats(context?.satsAmount || 0) }} sats
|
||||
</li>
|
||||
<li>3. Confirm the claim in your wallet</li>
|
||||
<li>4. Receive your sats instantly!</li>
|
||||
</ol>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<!-- Alternative: Manual Options (collapsed) -->
|
||||
<details class="rounded-lg bg-white/5 p-3">
|
||||
<summary class="cursor-pointer text-sm text-muted-foreground">
|
||||
Alternative: Manual payment options
|
||||
</summary>
|
||||
<div class="mt-4 space-y-4">
|
||||
<!-- CLINK Debit by pubkey -->
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-medium text-blue-400">Enter your npub/pubkey:</p>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
v-model="pubkeyInput"
|
||||
|
|
@ -236,33 +268,19 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
|
|||
:disabled="isProcessing"
|
||||
/>
|
||||
<Button
|
||||
variant="default"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="!pubkeyInput.trim() || isProcessing"
|
||||
@click="submitDebitRequest"
|
||||
>
|
||||
{{ atmStore.isRequestingDebit ? 'Requesting...' : 'Request' }}
|
||||
{{ atmStore.isRequestingDebit ? '...' : 'Send' }}
|
||||
</Button>
|
||||
</div>
|
||||
<p v-if="atmStore.paymentError" class="mt-2 text-xs text-red-400">
|
||||
Error: {{ atmStore.paymentError }}
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<p class="text-center text-xs text-muted-foreground">
|
||||
Your Shock Wallet must be connected to relay: ws://192.168.1.122:7777
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invoice Paste Mode -->
|
||||
<div v-if="paymentMode === 'invoice'" class="space-y-4">
|
||||
<Alert class="border-orange-500/50 bg-orange-500/10">
|
||||
<AlertDescription>
|
||||
<p class="mb-2 text-sm font-medium text-orange-400">Manual Invoice Payment</p>
|
||||
<p class="mb-3 text-xs text-muted-foreground">
|
||||
Create a {{ formatSats(context?.satsAmount || 0) }} sat invoice in your wallet and
|
||||
paste it below. We'll pay it instantly.
|
||||
</p>
|
||||
<!-- Invoice paste -->
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-medium text-orange-400">Or paste your invoice:</p>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
v-model="invoiceInput"
|
||||
|
|
@ -271,41 +289,21 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
|
|||
:disabled="isProcessing"
|
||||
/>
|
||||
<Button
|
||||
variant="default"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="!invoiceInput.trim() || isProcessing"
|
||||
@click="submitInvoice"
|
||||
>
|
||||
{{ atmStore.isPayingInvoice ? 'Paying...' : 'Pay' }}
|
||||
{{ atmStore.isPayingInvoice ? '...' : 'Pay' }}
|
||||
</Button>
|
||||
</div>
|
||||
<p v-if="atmStore.paymentError" class="mt-2 text-xs text-red-400">
|
||||
Error: {{ atmStore.paymentError }}
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<p class="text-center text-xs text-muted-foreground">
|
||||
Note: Invoice must be on same network (regtest for dev)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="relative flex items-center justify-center py-2">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<span class="w-full border-t border-white/20" />
|
||||
<p v-if="atmStore.paymentError" class="text-xs text-red-400">
|
||||
Error: {{ atmStore.paymentError }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="relative bg-background px-3 text-xs text-muted-foreground">
|
||||
or scan QR
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- LNURL QR (for wallet scanning - mock in browser) -->
|
||||
<div class="flex flex-col items-center">
|
||||
<QRCode v-if="context?.lnurlWithdraw" :value="context.lnurlWithdraw" :size="150" />
|
||||
<p class="mt-2 text-center text-xs text-muted-foreground">
|
||||
LNURL-withdraw (mock in browser dev)
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Debug: Simulate payment button -->
|
||||
<div v-if="atmStore.debugMode" class="pt-2 text-center">
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ export {
|
|||
type CLINKOffer,
|
||||
type PriceType,
|
||||
NofferTLV,
|
||||
// Ndebit types
|
||||
type DebitPointer,
|
||||
NdebitTLV,
|
||||
// Function types
|
||||
type GenerateInvoice,
|
||||
type PayInvoice,
|
||||
|
|
@ -82,6 +85,15 @@ export {
|
|||
// Noffer encoding/decoding
|
||||
export { encodeNoffer, decodeNoffer, isValidNoffer, npubToHex, hexToNpub } from './noffer.js'
|
||||
|
||||
// Ndebit encoding/decoding
|
||||
export {
|
||||
encodeNdebit,
|
||||
decodeNdebit,
|
||||
isValidNdebit,
|
||||
formatNdebitUri,
|
||||
parseNdebitAmount,
|
||||
} from './ndebit.js'
|
||||
|
||||
// Client
|
||||
export {
|
||||
CLINKClient,
|
||||
|
|
|
|||
206
lamassu-next/packages/clink/src/ndebit.ts
Normal file
206
lamassu-next/packages/clink/src/ndebit.ts
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
/**
|
||||
* ndebit encoding/decoding
|
||||
*
|
||||
* ndebits are bech32-encoded debit pointers that contain
|
||||
* pubkey, relay, and optional pointer information.
|
||||
*
|
||||
* Format: ndebit1<bech32-encoded-tlv-data>
|
||||
*
|
||||
* For ATM cash-in flow, the URI format includes amount:
|
||||
* clink:ndebit1<bech32data>?amount=<sats>
|
||||
*/
|
||||
|
||||
import { bech32 } from '@scure/base'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import type { DebitPointer } from './types.js'
|
||||
import { NdebitTLV } from './types.js'
|
||||
|
||||
const NDEBIT_PREFIX = 'ndebit'
|
||||
const BECH32_LIMIT = 2000
|
||||
|
||||
/**
|
||||
* Encode a debit pointer as an ndebit string
|
||||
*/
|
||||
export function encodeNdebit(pointer: DebitPointer): string {
|
||||
const tlvData: number[] = []
|
||||
|
||||
// Pubkey (TLV 0) - convert npub to hex if needed
|
||||
const hexPubkey = npubToHex(pointer.pubkey)
|
||||
const pubkeyBytes = hexToBytes(hexPubkey)
|
||||
writeTLV(tlvData, NdebitTLV.Pubkey, pubkeyBytes)
|
||||
|
||||
// Relay (TLV 1)
|
||||
const relayBytes = new TextEncoder().encode(pointer.relay)
|
||||
writeTLV(tlvData, NdebitTLV.Relay, relayBytes)
|
||||
|
||||
// Pointer (TLV 2) - if present
|
||||
if (pointer.pointer) {
|
||||
const pointerBytes = new TextEncoder().encode(pointer.pointer)
|
||||
writeTLV(tlvData, NdebitTLV.Pointer, pointerBytes)
|
||||
}
|
||||
|
||||
// Convert to Uint8Array and bech32 encode
|
||||
const bytes = new Uint8Array(tlvData)
|
||||
const words = bech32.toWords(bytes)
|
||||
|
||||
return bech32.encode(NDEBIT_PREFIX, words, BECH32_LIMIT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an ndebit string to a debit pointer
|
||||
*/
|
||||
export function decodeNdebit(ndebit: string): DebitPointer {
|
||||
// Remove clink: prefix if present
|
||||
let cleaned = ndebit
|
||||
if (cleaned.toLowerCase().startsWith('clink:')) {
|
||||
cleaned = cleaned.slice(6)
|
||||
}
|
||||
if (cleaned.toLowerCase().startsWith('lightning:')) {
|
||||
cleaned = cleaned.slice(10)
|
||||
}
|
||||
|
||||
// Remove query parameters if present
|
||||
const queryIndex = cleaned.indexOf('?')
|
||||
if (queryIndex !== -1) {
|
||||
cleaned = cleaned.slice(0, queryIndex)
|
||||
}
|
||||
|
||||
// Decode bech32 - cast to expected type (contains "1" separator)
|
||||
const { prefix, words } = bech32.decode(cleaned as `${string}1${string}`, BECH32_LIMIT)
|
||||
|
||||
if (prefix !== NDEBIT_PREFIX) {
|
||||
throw new Error(`Invalid ndebit prefix: ${prefix}`)
|
||||
}
|
||||
|
||||
const bytes = bech32.fromWords(words)
|
||||
const data = new Uint8Array(bytes)
|
||||
|
||||
// Parse TLV entries
|
||||
let pubkey: string | undefined
|
||||
let relay: string | undefined
|
||||
let pointer: string | undefined
|
||||
|
||||
let offset = 0
|
||||
while (offset < data.length) {
|
||||
const type = data[offset]
|
||||
offset++
|
||||
|
||||
if (offset >= data.length) break
|
||||
|
||||
const length = data[offset]
|
||||
offset++
|
||||
|
||||
if (offset + (length ?? 0) > data.length) {
|
||||
throw new Error('Invalid TLV: data truncated')
|
||||
}
|
||||
|
||||
const value = data.slice(offset, offset + (length ?? 0))
|
||||
offset += length ?? 0
|
||||
|
||||
switch (type) {
|
||||
case NdebitTLV.Pubkey:
|
||||
pubkey = bytesToHex(value)
|
||||
break
|
||||
case NdebitTLV.Relay:
|
||||
relay = new TextDecoder().decode(value)
|
||||
break
|
||||
case NdebitTLV.Pointer:
|
||||
pointer = new TextDecoder().decode(value)
|
||||
break
|
||||
// Ignore unknown TLV types for forward compatibility
|
||||
}
|
||||
}
|
||||
|
||||
if (!pubkey) {
|
||||
throw new Error('Invalid ndebit: missing pubkey')
|
||||
}
|
||||
|
||||
if (!relay) {
|
||||
throw new Error('Invalid ndebit: missing relay')
|
||||
}
|
||||
|
||||
return {
|
||||
pubkey,
|
||||
relay,
|
||||
pointer,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an ndebit string without fully decoding
|
||||
*/
|
||||
export function isValidNdebit(ndebit: string): boolean {
|
||||
try {
|
||||
decodeNdebit(ndebit)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an ndebit as a full clink: URI with amount
|
||||
*/
|
||||
export function formatNdebitUri(ndebit: string, amountSats: number): string {
|
||||
// Ensure ndebit doesn't already have clink: prefix
|
||||
let cleaned = ndebit
|
||||
if (cleaned.toLowerCase().startsWith('clink:')) {
|
||||
cleaned = cleaned.slice(6)
|
||||
}
|
||||
if (cleaned.toLowerCase().startsWith('lightning:')) {
|
||||
cleaned = cleaned.slice(10)
|
||||
}
|
||||
|
||||
return `clink:${cleaned}?amount=${amountSats}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse amount from ndebit URI query parameters
|
||||
*/
|
||||
export function parseNdebitAmount(uri: string): number | undefined {
|
||||
const match = uri.match(/[?&]amount=(\d+)/)
|
||||
if (match && match[1]) {
|
||||
return parseInt(match[1], 10)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// TLV helpers
|
||||
|
||||
function writeTLV(data: number[], type: number, value: number[] | Uint8Array): void {
|
||||
data.push(type)
|
||||
data.push(value.length)
|
||||
for (const byte of value) {
|
||||
data.push(byte)
|
||||
}
|
||||
}
|
||||
|
||||
function hexToBytes(hex: string): Uint8Array {
|
||||
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 bytesToHex(bytes: Uint8Array): string {
|
||||
return Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert npub to hex pubkey
|
||||
* Accepts either npub1... or hex format, returns hex
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
|
@ -269,6 +269,30 @@ export enum NofferTLV {
|
|||
Description = 4,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Ndebit (Debit Pointer) encoding
|
||||
// ============================================================================
|
||||
|
||||
/** Debit pointer for ndebit encoding */
|
||||
export interface DebitPointer {
|
||||
/** Public key of the service that will pay (hex) */
|
||||
pubkey: string
|
||||
/** Relay URL for communication */
|
||||
relay: string
|
||||
/** Optional session/voucher identifier */
|
||||
pointer?: string
|
||||
}
|
||||
|
||||
/** ndebit TLV types */
|
||||
export enum NdebitTLV {
|
||||
/** Public key (32 bytes) */
|
||||
Pubkey = 0,
|
||||
/** Relay URL (variable length string) */
|
||||
Relay = 1,
|
||||
/** Pointer/session identifier (variable length string) */
|
||||
Pointer = 2,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Function types for service injection
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ export function createATMMachine(services: Partial<ATMServices> = {}) {
|
|||
}
|
||||
return services.generateClinkOffer(input)
|
||||
}),
|
||||
generateNdebit: fromPromise(async ({ input }: { input: ATMContext }) => {
|
||||
if (!services.generateNdebit) {
|
||||
throw new Error('generateNdebit service not provided')
|
||||
}
|
||||
return services.generateNdebit(input)
|
||||
}),
|
||||
generateLnurlWithdraw: fromPromise(async ({ input }: { input: ATMContext }) => {
|
||||
if (!services.generateLnurlWithdraw) {
|
||||
throw new Error('generateLnurlWithdraw service not provided')
|
||||
|
|
@ -288,21 +294,21 @@ export function createATMMachine(services: Partial<ATMServices> = {}) {
|
|||
},
|
||||
FINISH_INSERTING: {
|
||||
guard: 'hasInsertedBills',
|
||||
target: 'generatingLnurlWithdraw',
|
||||
target: 'generatingNdebit',
|
||||
},
|
||||
CANCEL: '#atm.idle',
|
||||
TIMEOUT: '#atm.idle',
|
||||
},
|
||||
},
|
||||
generatingLnurlWithdraw: {
|
||||
generatingNdebit: {
|
||||
invoke: {
|
||||
src: 'generateLnurlWithdraw',
|
||||
src: 'generateNdebit',
|
||||
input: ({ context }) => context,
|
||||
onDone: {
|
||||
target: 'displayingQR',
|
||||
actions: assign({
|
||||
lnurlWithdraw: ({ event }) => event.output,
|
||||
paymentMethod: () => 'lnurl_withdraw' as const,
|
||||
ndebitUri: ({ event }) => event.output,
|
||||
paymentMethod: () => 'clink_offer' as const,
|
||||
}),
|
||||
},
|
||||
onError: {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@ export interface ATMContext {
|
|||
clinkOffer: string | null
|
||||
/** noffer string displayed to user for cash-out */
|
||||
nofferString: string | null
|
||||
/** LNURL-withdraw string - for cash-in (customer receives sats) */
|
||||
/** ndebit URI for cash-in (clink:ndebit1...?amount=X) */
|
||||
ndebitUri: string | null
|
||||
/** LNURL-withdraw string - for cash-in (customer receives sats) - legacy */
|
||||
lnurlWithdraw: string | null
|
||||
/** Current payment status */
|
||||
paymentStatus: PaymentStatus
|
||||
|
|
@ -116,6 +118,7 @@ export const initialContext: ATMContext = {
|
|||
invoice: null,
|
||||
clinkOffer: null,
|
||||
nofferString: null,
|
||||
ndebitUri: null,
|
||||
lnurlWithdraw: null,
|
||||
paymentStatus: null,
|
||||
preimage: null,
|
||||
|
|
@ -135,7 +138,9 @@ export const initialContext: ATMContext = {
|
|||
export interface ATMServices {
|
||||
/** Generate a CLINK offer (for cash-out) */
|
||||
generateClinkOffer: (context: ATMContext) => Promise<string>
|
||||
/** Generate an LNURL-withdraw link (for cash-in - customer receives sats) */
|
||||
/** Generate an ndebit URI for cash-in (clink:ndebit1...?amount=X) */
|
||||
generateNdebit: (context: ATMContext) => Promise<string>
|
||||
/** Generate an LNURL-withdraw link (for cash-in - customer receives sats) - legacy */
|
||||
generateLnurlWithdraw: (context: ATMContext) => Promise<string>
|
||||
/** Generate a Lightning invoice */
|
||||
generateInvoice: (amountMsat: number) => Promise<string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue