Merge pull request 'fix(access): session Complete failed on IPC clone; keep rate lookup off the unlock path' (#97) from fix/boltcard-session-ipc-clone into dev
Reviewed-on: #97
This commit is contained in:
commit
da4969d510
2 changed files with 49 additions and 5 deletions
|
|
@ -306,6 +306,22 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
// screen (raw lnurlw, spent by this call) or the session opened at entry
|
||||
// (hit-keyed steps, no p/c).
|
||||
type BoltCardSource = { lnurlw: string } | { session: CardSession }
|
||||
// Electron IPC structured-clones its arguments and rejects Vue reactive
|
||||
// proxies with "An object could not be cloned". `loadedBoltCard` is a ref,
|
||||
// so anything reached through it is a proxy — copy the steps field by field
|
||||
// into plain objects before they cross the bridge.
|
||||
const plainWithdrawStep = (w: NonNullable<CardSession['withdraw']>) => ({
|
||||
callback: w.callback,
|
||||
k1: w.k1,
|
||||
minWithdrawable: w.minWithdrawable,
|
||||
maxWithdrawable: w.maxWithdrawable,
|
||||
})
|
||||
const plainPayStep = (p: CardSession['pay']) => ({
|
||||
callback: p.callback,
|
||||
minSendable: p.minSendable,
|
||||
maxSendable: p.maxSendable,
|
||||
metadata: p.metadata,
|
||||
})
|
||||
// Access-control gate config (ADR-003). Defaults disabled → the machine's
|
||||
// `locked` state bypasses straight to `idle` (behaviour identical to no gate).
|
||||
// Populated from RuntimeConfig.accessControl in initializeForProduction.
|
||||
|
|
@ -327,6 +343,10 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
// The card balance is hidden by default on the public screen; the holder
|
||||
// reveals it with the eye toggle. Resets on re-lock.
|
||||
const cardBalanceRevealed = ref(false)
|
||||
// When the card server names a currency but didn't price the balance (its
|
||||
// rate cache was cold — it never blocks the unlock on a rate lookup), price
|
||||
// it here from the ATM's own rate source, in that currency.
|
||||
const cardFiatRate = ref<{ currency: string; btcPrice: number } | null>(null)
|
||||
const fiatCode = ref('USD')
|
||||
// Defaults are 0 — the operator's fee config (received via Nostr
|
||||
// kind-30078 `bitspire-fees:<atm_pubkey>` envelope from satmachineadmin)
|
||||
|
|
@ -472,6 +492,10 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
const card = loadedBoltCard.value
|
||||
if (!card) return null
|
||||
if (card.currency && card.fiat !== null) return { amount: card.fiat, currency: card.currency }
|
||||
const rate = cardFiatRate.value
|
||||
if (card.currency && rate && rate.currency === card.currency) {
|
||||
return { amount: (card.balanceSats / 1e8) * rate.btcPrice, currency: card.currency }
|
||||
}
|
||||
if (btcPrice.value && btcPrice.value > 0) {
|
||||
return { amount: (card.balanceSats / 1e8) * btcPrice.value, currency: fiatCode.value }
|
||||
}
|
||||
|
|
@ -540,6 +564,7 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
if (state === 'locked' && loadedBoltCard.value) {
|
||||
loadedBoltCard.value = null
|
||||
cardBalanceRevealed.value = false
|
||||
cardFiatRate.value = null
|
||||
}
|
||||
|
||||
// Detect network from first invoice we see
|
||||
|
|
@ -673,7 +698,7 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
'session' in source
|
||||
? source.session.withdraw
|
||||
? await api.withdrawWithSession({
|
||||
withdraw: source.session.withdraw,
|
||||
withdraw: plainWithdrawStep(source.session.withdraw),
|
||||
bolt11: invoice,
|
||||
amountMsat,
|
||||
})
|
||||
|
|
@ -713,7 +738,7 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
const api = window.electronAPI!
|
||||
const res =
|
||||
'session' in source
|
||||
? await api.resolveSessionInvoice({ pay: source.session.pay, amountMsat })
|
||||
? await api.resolveSessionInvoice({ pay: plainPayStep(source.session.pay), amountMsat })
|
||||
: await api.resolveCardInvoice({ lnurlw: source.lnurlw, amountMsat })
|
||||
if (!res.ok || !res.bolt11) {
|
||||
boltCardProcessing.value = false
|
||||
|
|
@ -763,7 +788,14 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
boltCardProcessing.value = true
|
||||
nfcStatus.value = { state: 'processing', message: 'Verifying card…' }
|
||||
try {
|
||||
const t0 = Date.now()
|
||||
const opened = await window.electronAPI.openCardSession({ lnurlw })
|
||||
console.info(
|
||||
`[ATM] Card session ${opened.ok ? 'opened' : 'refused'} in ${Date.now() - t0} ms` +
|
||||
(opened.ok
|
||||
? ` (fiat ${opened.session.fiat === null ? 'not ' : ''}priced by card server)`
|
||||
: '')
|
||||
)
|
||||
if (!opened.ok) {
|
||||
nfcStatus.value = { state: 'declined', message: opened.reason }
|
||||
denyAccess(opened.reason)
|
||||
|
|
@ -777,8 +809,18 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
if (outcome.status === 'granted') {
|
||||
loadedBoltCard.value = opened.session
|
||||
cardBalanceRevealed.value = false
|
||||
cardFiatRate.value = null
|
||||
nfcStatus.value = { state: 'accepted', message: 'Card accepted' }
|
||||
grantAccess(outcome.role, outcome.credentialIdHash)
|
||||
// Price the balance in the card's currency off the unlock path.
|
||||
const { currency, fiat, externalId } = opened.session
|
||||
if (currency && fiat === null) {
|
||||
void fetchBtcPrice(currency).then((price) => {
|
||||
if (price && loadedBoltCard.value?.externalId === externalId) {
|
||||
cardFiatRate.value = { currency, btcPrice: price }
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// pin-required can't occur for card-only open-enrollment; treat as denied.
|
||||
const reason = outcome.status === 'denied' ? outcome.reason : 'card not authorized'
|
||||
|
|
|
|||
|
|
@ -63,9 +63,11 @@ one `hit` is recorded.
|
|||
- `balance_msat` — the card wallet's balance. Display only.
|
||||
- `currency` / `fiat` — the balance priced the way the LNbits wallet page does
|
||||
it: the wallet's own currency (per-wallet setting) first, else the instance's
|
||||
default accounting currency, at the server's rate. `null` when the server has
|
||||
no currency or the rate lookup failed; the ATM then prices the sats itself in
|
||||
its own fiat at its display rate. A rate failure never fails the session.
|
||||
default accounting currency. `fiat` is filled **only from the server's
|
||||
already-warm rate cache** — this response gates the unlock, and a cold rate
|
||||
lookup queries external exchanges (~1 s). On a cache miss it is `null` and
|
||||
the ATM prices the sats itself: in `currency` from its own rate source, else
|
||||
in its own fiat at its display rate. No rate lookup ever blocks the session.
|
||||
- `withdraw` — the LUD-03 second step. The ATM calls
|
||||
`callback?k1=<hit>&pr=<bolt11>` at cash-out Complete. `null` with
|
||||
`withdraw_blocked_reason` set when `/scan` would have refused (daily limit
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue