mirror of
https://github.com/tompro/sattle.git
synced 2026-08-27 07:15:59 +00:00
sattle: Quasar PWA scaffold with lnurlcash-kit protocol core
M1: Quasar 2 / Vue 3 / TS / Pinia / i18n / PWA app shell in the lnurl-wallet palette (#002222/#004444/#55ffcc, Noto Sans). Protocol layer is the external lnurlcash-kit library (git dependency on the fork until upstream merges the prepare fix / publishes to npm); keys.ts + receive.ts vendored from lnurl-wallet with the derivation domain renamed to 'sattle'. Nix dev shell, ci workflow (typecheck, vitest, pwa build).
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
resolveNoteInput,
|
||||
noteK1,
|
||||
noteDeclaredAmount,
|
||||
serverOf,
|
||||
fetchNoteInfo,
|
||||
rotateNote,
|
||||
withNewK1,
|
||||
NoteSpentError,
|
||||
NoteUnknownError
|
||||
} from 'lnurlcash-kit'
|
||||
import type {Bearer, NewBearer} from './types'
|
||||
|
||||
// shared by Scan and Paste: resolve whatever came in to a note URL, ask the
|
||||
// issuing service what it is worth (an informational GET - per spec this
|
||||
// always puts k1 on the wire, so receive.ts's caller should rotate right
|
||||
// after, see secureReceivedNote). Returns the note even when the info fetch
|
||||
// fails - a bearer is better stored unverified than dropped.
|
||||
export const receiveNote = async (
|
||||
input: string,
|
||||
existing: Bearer[]
|
||||
): Promise<NewBearer> => {
|
||||
const url = resolveNoteInput(input)
|
||||
if (!url) {
|
||||
throw new Error('Not an LNURLcash bearer note (needs a k1).')
|
||||
}
|
||||
const k1 = noteK1(url)
|
||||
if (
|
||||
existing.some(
|
||||
b => noteK1(b.url) === k1 && serverOf(b.url) === serverOf(url)
|
||||
)
|
||||
) {
|
||||
throw new Error('This note is already in your wallet.')
|
||||
}
|
||||
try {
|
||||
const info = await fetchNoteInfo(url)
|
||||
return {
|
||||
url,
|
||||
callback: info.callback,
|
||||
amount: info.maxWithdrawable,
|
||||
verified: true,
|
||||
mintPubkey: info.mintPubkey
|
||||
}
|
||||
} catch (err) {
|
||||
// the service positively told us this k1 is dead - that's worth more
|
||||
// than the sender's own claim, so don't paper over it with an
|
||||
// unverified fallback the way an unreachable/unknown-shaped error
|
||||
// below does. The caller (ReceiveDialog.tsx) surfaces this and never stores
|
||||
// the note.
|
||||
if (err instanceof NoteSpentError || err instanceof NoteUnknownError) {
|
||||
throw err
|
||||
}
|
||||
// service unreachable (or some other non-definitive failure) - fall
|
||||
// back to the sender's own (unverified) declared amount so the note
|
||||
// isn't shown as worth nothing
|
||||
return {
|
||||
url,
|
||||
callback: '',
|
||||
amount: noteDeclaredAmount(url) ?? 0,
|
||||
verified: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After receiving a note, rotate it: the previous holder (and anything that
|
||||
// logged the URL in transit, since the informational GET above already put
|
||||
// k1 on the wire) still knows the old secret - a rotate burns it and mints
|
||||
// a fresh one only this wallet knows. Returns the updated note URL. Throws
|
||||
// when the service refuses (e.g. a plain LUD-03 withdraw link that doesn't
|
||||
// speak lnurlcash) - the caller should warn, not fail the receive.
|
||||
export const secureReceivedNote = async (note: {
|
||||
url: string
|
||||
callback: string
|
||||
amount: number
|
||||
}): Promise<string> => {
|
||||
const k1 = noteK1(note.url)
|
||||
if (!k1 || !note.callback) {
|
||||
throw new Error('Note has no callback to rotate against yet.')
|
||||
}
|
||||
const result = await rotateNote(note.callback, k1)
|
||||
return withNewK1(note.url, result.k1, note.amount, result.signature)
|
||||
}
|
||||
Reference in New Issue
Block a user