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
|
|
@ -36,6 +36,17 @@ interface WithdrawRequest {
|
|||
|
||||
type FetchLike = typeof fetch
|
||||
|
||||
/**
|
||||
* The LUD-03 second step on its own: what a `withdrawRequest` (or a Bolt Card
|
||||
* session, see boltcard-session.ts) hands us to actually pull a payment.
|
||||
*/
|
||||
export interface WithdrawStep {
|
||||
callback: string
|
||||
k1: string
|
||||
minWithdrawable?: number
|
||||
maxWithdrawable?: number
|
||||
}
|
||||
|
||||
export interface ExecuteLnurlWithdrawOptions {
|
||||
/** Injected for tests; defaults to global fetch. */
|
||||
fetchImpl?: FetchLike
|
||||
|
|
@ -73,7 +84,8 @@ function appendQuery(url: string, params: Record<string, string>): string {
|
|||
}
|
||||
|
||||
function errMsg(e: unknown): string {
|
||||
if (e instanceof Error) return e.name === 'TimeoutError' || e.name === 'AbortError' ? 'timed out' : e.message
|
||||
if (e instanceof Error)
|
||||
return e.name === 'TimeoutError' || e.name === 'AbortError' ? 'timed out' : e.message
|
||||
return String(e)
|
||||
}
|
||||
|
||||
|
|
@ -105,16 +117,46 @@ export async function executeLnurlWithdraw(
|
|||
if (params.tag !== 'withdrawRequest' || !params.callback || !params.k1) {
|
||||
return { ok: false, reason: 'card did not return a withdraw voucher' }
|
||||
}
|
||||
|
||||
// 2) Hand our invoice to the callback — the card's wallet pays it.
|
||||
return executeWithdrawCallback(
|
||||
{
|
||||
callback: params.callback,
|
||||
k1: params.k1,
|
||||
minWithdrawable: params.minWithdrawable,
|
||||
maxWithdrawable: params.maxWithdrawable,
|
||||
},
|
||||
bolt11,
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The LUD-03 second step alone: hand our invoice to an already-obtained
|
||||
* withdraw step (from a `/scan` withdrawRequest, or from a Bolt Card session
|
||||
* opened at tap-to-enter) — the card's wallet pays it. `{ ok: true }` means the
|
||||
* card accepted the pull; settlement is observed by the invoice watcher.
|
||||
*/
|
||||
export async function executeWithdrawCallback(
|
||||
step: WithdrawStep,
|
||||
bolt11: string,
|
||||
opts: ExecuteLnurlWithdrawOptions = {}
|
||||
): Promise<LnurlWithdrawResult> {
|
||||
const doFetch = opts.fetchImpl ?? fetch
|
||||
const timeoutMs = opts.timeoutMs ?? 15_000
|
||||
|
||||
if (!bolt11 || !/^ln[a-z0-9]/i.test(bolt11.trim())) {
|
||||
return { ok: false, reason: 'no invoice to charge' }
|
||||
}
|
||||
if (
|
||||
opts.amountMsat != null &&
|
||||
typeof params.maxWithdrawable === 'number' &&
|
||||
opts.amountMsat > params.maxWithdrawable
|
||||
typeof step.maxWithdrawable === 'number' &&
|
||||
opts.amountMsat > step.maxWithdrawable
|
||||
) {
|
||||
return { ok: false, reason: 'card limit is below this amount' }
|
||||
}
|
||||
|
||||
// 2) Hand our invoice to the callback — the card's wallet pays it.
|
||||
const cbUrl = appendQuery(params.callback, { k1: params.k1, pr: bolt11.trim() })
|
||||
const cbUrl = appendQuery(step.callback, { k1: step.k1, pr: bolt11.trim() })
|
||||
let cb: { status?: string; reason?: string }
|
||||
try {
|
||||
const res = await doFetch(cbUrl, { signal: AbortSignal.timeout(timeoutMs) })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue