feat(state-machine): fetch balance with exchange rate on cash-in

Add getRateAndBalance actor that fetches exchange rate and available
balance in parallel during cash-in flow. Add generateLnurlWithdraw
service type for LNURL-withdraw support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-02-16 16:57:30 -05:00
commit c199431b8e
2 changed files with 19 additions and 2 deletions

View file

@ -68,6 +68,17 @@ export function createATMMachine(services: Partial<ATMServices> = {}) {
} }
return services.getExchangeRate(input) return services.getExchangeRate(input)
}), }),
// Fetch both exchange rate and available balance in parallel for cash-in
getRateAndBalance: fromPromise(async ({ input }: { input: string }) => {
if (!services.getExchangeRate) {
throw new Error('getExchangeRate service not provided')
}
const [exchangeRate, availableBalance] = await Promise.all([
services.getExchangeRate(input),
services.getAvailableBalance ? services.getAvailableBalance() : Promise.resolve(0),
])
return { exchangeRate, availableBalance }
}),
generateNoffer: fromPromise(async () => { generateNoffer: fromPromise(async () => {
if (!services.generateNoffer) { if (!services.generateNoffer) {
throw new Error('generateNoffer service not provided') throw new Error('generateNoffer service not provided')
@ -363,12 +374,13 @@ export function createATMMachine(services: Partial<ATMServices> = {}) {
states: { states: {
fetchingRate: { fetchingRate: {
invoke: { invoke: {
src: 'getExchangeRate', src: 'getRateAndBalance',
input: ({ context }) => context.currency, input: ({ context }) => context.currency,
onDone: { onDone: {
target: 'insertingBills', target: 'insertingBills',
actions: assign({ actions: assign({
exchangeRate: ({ event }) => event.output, exchangeRate: ({ event }) => event.output.exchangeRate,
availableBalance: ({ event }) => event.output.availableBalance,
}), }),
}, },
onError: { onError: {

View file

@ -33,6 +33,8 @@ export interface ATMContext {
exchangeRate: number exchangeRate: number
/** Fee percentage (0.02 = 2%) */ /** Fee percentage (0.02 = 2%) */
feePercent: number feePercent: number
/** ATM's available balance in sats (for limiting cash-in transactions) */
availableBalance: number
// Payment // Payment
/** BOLT11 invoice for payment */ /** BOLT11 invoice for payment */
@ -128,6 +130,7 @@ export const initialContext: ATMContext = {
currency: 'USD', currency: 'USD',
exchangeRate: 0, exchangeRate: 0,
feePercent: 0.02, feePercent: 0.02,
availableBalance: 0,
invoice: null, invoice: null,
clinkOffer: null, clinkOffer: null,
nofferString: null, nofferString: null,
@ -167,6 +170,8 @@ export interface ATMServices {
dispenseCash: (amounts: { denomination: number; count: number }[]) => Promise<void> dispenseCash: (amounts: { denomination: number; count: number }[]) => Promise<void>
/** Get current exchange rate */ /** Get current exchange rate */
getExchangeRate: (currency: string) => Promise<number> getExchangeRate: (currency: string) => Promise<number>
/** Get ATM's available balance in sats (for limiting cash-in transactions) */
getAvailableBalance: () => Promise<number>
/** Generate noffer string for cash-out (static payment code) */ /** Generate noffer string for cash-out (static payment code) */
generateNoffer: () => Promise<string> generateNoffer: () => Promise<string>
/** /**