feat(machine): Bolt Card (NFC) tap-to-receive on cash-in
Tap-to-receive for the buy flow, the receive counterpart to #83's cash-out tap-to-pay. A Bolt Card only emits an lnurlw withdraw voucher (wrong direction to deposit into it), so the tap is used as an authenticated identity (external_id + SUN p/c) to resolve the card wallet's lnurlp/Lightning Address; the ATM then pays an invoice for the payout over its existing nostr transport. - electron/lnurl-pay.ts: resolveCardInvoice() — resolve card -> pay target -> LUD-16/LUD-06 -> BOLT11. scanUrlToResolver() is the single HTTPS-today / nostr-tomorrow transport seam. 13 tests. - IPC lnurl:pay-card (main-process HTTPS to dodge renderer CORS) + preload/electron.d.ts surface. - stores/atm.ts: handleBoltCardReceive() settles via the existing payInvoice -> PAYMENT_RECEIVED path; the one NFC listener now routes the same tap by flow (cash-out pulls, cash-in receives). - CashInView.vue: NFC status + dev tap input. - docs/boltcard-receive-resolver.md: spec for the custom LNbits /boltcards/api/v1/pay/<id> resolver endpoint (omni-private side). Card issuance is unchanged — same NDEF/keys/external_id; receive is a server-side reading of the same tap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
73376a6c68
commit
64582e7fe6
8 changed files with 605 additions and 31 deletions
|
|
@ -43,6 +43,7 @@ import {
|
|||
} from './state-store.js'
|
||||
import { initializeHal, type HalInstance } from './hal-service.js'
|
||||
import { executeLnurlWithdraw } from './lnurl-withdraw.js'
|
||||
import { resolveCardInvoice } from './lnurl-pay.js'
|
||||
import { startNfcReader, type NfcStatus } from './nfc-service.js'
|
||||
|
||||
// ESM equivalent of __dirname
|
||||
|
|
@ -120,9 +121,7 @@ function loadBranding(): BrandingConfig | null {
|
|||
if (Object.keys(colors).length > 0) customColors = colors
|
||||
if (dark && typeof dark === 'object') {
|
||||
const darkColors = Object.fromEntries(
|
||||
Object.entries(dark as Record<string, unknown>).filter(
|
||||
([, v]) => typeof v === 'string'
|
||||
)
|
||||
Object.entries(dark as Record<string, unknown>).filter(([, v]) => typeof v === 'string')
|
||||
) as Record<string, string>
|
||||
if (Object.keys(darkColors).length > 0) customColorsDark = darkColors
|
||||
}
|
||||
|
|
@ -431,6 +430,20 @@ ipcMain.handle(
|
|||
}
|
||||
)
|
||||
|
||||
// Bolt Card cash-in (receive): resolve a tapped card + payout amount to a
|
||||
// BOLT11 on the card wallet, which the renderer then pays over the nostr
|
||||
// transport (stores/atm.ts payInvoice). HTTPS to the card host runs here in the
|
||||
// main process to dodge renderer CORS. See lnurl-pay.ts.
|
||||
ipcMain.handle(
|
||||
'lnurl:pay-card',
|
||||
async (
|
||||
_event,
|
||||
args: { lnurlw: string; amountMsat: number }
|
||||
): Promise<{ ok: boolean; bolt11?: string; reason?: string }> => {
|
||||
return resolveCardInvoice(args.lnurlw, args.amountMsat)
|
||||
}
|
||||
)
|
||||
|
||||
// State persistence IPC handlers
|
||||
ipcMain.handle('state:load-cassettes', () => loadCassettes())
|
||||
ipcMain.handle('state:set-cassettes', (_event, cassettes) => setCassettes(cassettes))
|
||||
|
|
@ -447,9 +460,7 @@ ipcMain.handle('state:remediate-transaction', (_event, txid: string, remediatedB
|
|||
ipcMain.handle('state:get-last-known-config-created-at', (): number =>
|
||||
getLastKnownConfigCreatedAt()
|
||||
)
|
||||
ipcMain.handle('state:get-bootstrap-published-at', (): number | null =>
|
||||
getBootstrapPublishedAt()
|
||||
)
|
||||
ipcMain.handle('state:get-bootstrap-published-at', (): number | null => getBootstrapPublishedAt())
|
||||
ipcMain.handle('state:mark-bootstrap-published', (_event, unixTimestamp: number): void => {
|
||||
markBootstrapPublished(unixTimestamp)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue