feat(machine): open a verified Bolt Card session at tap-to-enter
Entry now spends the tap's single-use SUN once, on the card server's new /session endpoint (aiolabs/boltcards feat/card-session-endpoint), instead of parsing the lnurlw locally and deferring every check to Complete. The server proves a genuine, non-replayed card and returns the wallet balance plus the hit-keyed LUD-03 withdraw and LUD-06 pay second steps — the same single-use bearer /scan and /pay hand out — so Complete still needs no second tap and the ATM holds no p/c for the visit. - electron/boltcard-session.ts: /scan → /session URL derivation, response parsing, 404 → 'card server does not support sessions'. - lnurl-withdraw / lnurl-pay: the second steps are now callable on their own (executeWithdrawCallback, resolveInvoiceFromPayStep); the tap paths are unchanged and reuse them. - IPC: lnurl:open-card-session, lnurl:withdraw-session, lnurl:pay-session. - store: handleBoltCardEntry opens the session then authorizes the server-returned external_id; the payment handlers take a source (raw tap or session); a withheld withdraw step declines with the server's reason. The boltcard AccessScan no longer carries the lnurlw. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
ce4b5a5dc6
commit
735132b032
12 changed files with 639 additions and 63 deletions
|
|
@ -59,6 +59,17 @@ interface CardPayTarget {
|
|||
lnurl?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The LUD-06 second step on its own: what a `payRequest` (or a Bolt Card
|
||||
* session, see boltcard-session.ts) hands us to fetch an invoice.
|
||||
*/
|
||||
export interface PayStep {
|
||||
callback: string
|
||||
minSendable?: number
|
||||
maxSendable?: number
|
||||
metadata?: string
|
||||
}
|
||||
|
||||
/** LUD-06 payRequest (subset) + error shape. */
|
||||
interface PayRequest {
|
||||
tag?: string
|
||||
|
|
@ -163,17 +174,18 @@ async function toPayRequest(
|
|||
}
|
||||
|
||||
async function requestInvoice(
|
||||
pr: PayRequest,
|
||||
pr: PayStep,
|
||||
amountMsat: number,
|
||||
ctx: Ctx
|
||||
): Promise<ResolveCardInvoiceResult> {
|
||||
if (!(amountMsat > 0)) return { ok: false, reason: 'no amount to send' }
|
||||
if (typeof pr.minSendable === 'number' && amountMsat < pr.minSendable) {
|
||||
return { ok: false, reason: 'amount is below the card wallet minimum' }
|
||||
}
|
||||
if (typeof pr.maxSendable === 'number' && amountMsat > pr.maxSendable) {
|
||||
return { ok: false, reason: 'amount is above the card wallet maximum' }
|
||||
}
|
||||
const cbUrl = appendQuery(pr.callback!, { amount: String(amountMsat) })
|
||||
const cbUrl = appendQuery(pr.callback, { amount: String(amountMsat) })
|
||||
let vals: PayValues
|
||||
try {
|
||||
const res = await ctx.doFetch(cbUrl, { signal: AbortSignal.timeout(ctx.timeoutMs) })
|
||||
|
|
@ -222,5 +234,19 @@ export async function resolveCardInvoice(
|
|||
if (!pr.ok) return pr
|
||||
|
||||
// 3) Ask for an invoice for the payout amount.
|
||||
return requestInvoice(pr.payRequest, amountMsat, ctx)
|
||||
return requestInvoice({ ...pr.payRequest, callback: pr.payRequest.callback! }, amountMsat, ctx)
|
||||
}
|
||||
|
||||
/**
|
||||
* The LUD-06 second step alone: fetch a BOLT11 for `amountMsat` from an
|
||||
* already-obtained pay step (from a Bolt Card session opened at tap-to-enter).
|
||||
* Never throws — every failure returns `{ ok: false, reason }`.
|
||||
*/
|
||||
export async function resolveInvoiceFromPayStep(
|
||||
step: PayStep,
|
||||
amountMsat: number,
|
||||
opts: ResolveCardInvoiceOptions = {}
|
||||
): Promise<ResolveCardInvoiceResult> {
|
||||
const ctx: Ctx = { doFetch: opts.fetchImpl ?? fetch, timeoutMs: opts.timeoutMs ?? 15_000 }
|
||||
return requestInvoice(step, amountMsat, ctx)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue