remove bolt11, bech32 and js-lnurl dependencies.

replace with much faster invoice amount parsing.
fix lnurl encoding/decoding.
This commit is contained in:
fiatjaf 2023-03-03 11:36:50 -03:00
parent 17afc35d1c
commit 0fb38749ac
No known key found for this signature in database
GPG Key ID: BAD43C4BE5C1A3A1
7 changed files with 77 additions and 30 deletions

BIN
package-lock.json generated

Binary file not shown.

View File

@ -29,15 +29,13 @@
"@bugsnag/js": "^7.18.0", "@bugsnag/js": "^7.18.0",
"@fortawesome/fontawesome-free": "^6.2.1", "@fortawesome/fontawesome-free": "^6.2.1",
"@noble/secp256k1": "^1.7.0", "@noble/secp256k1": "^1.7.0",
"@scure/base": "^1.1.1",
"@tsconfig/svelte": "^3.0.0", "@tsconfig/svelte": "^3.0.0",
"bech32": "^2.0.0",
"bolt11": "^1.4.0",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"compressorjs": "^1.1.1", "compressorjs": "^1.1.1",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"hurdak": "github:ConsignCloud/hurdak", "hurdak": "github:ConsignCloud/hurdak",
"husky": "^8.0.3", "husky": "^8.0.3",
"js-lnurl": "^0.5.1",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"localforage-memoryStorageDriver": "^0.9.2", "localforage-memoryStorageDriver": "^0.9.2",
"nostr-tools": "^1.7.4", "nostr-tools": "^1.7.4",

View File

@ -200,4 +200,3 @@ const applyContext = (notes, context) => {
export default { export default {
listen, load, loadPeople, personKinds, loadParents, streamContext, applyContext, listen, load, loadPeople, personKinds, loadParents, streamContext, applyContext,
} }

View File

@ -1,9 +1,8 @@
import {uniq, pick, identity, isEmpty} from 'ramda' import {uniq, pick, identity, isEmpty} from 'ramda'
import {nip05} from 'nostr-tools' import {nip05} from 'nostr-tools'
import {getParams} from 'js-lnurl'
import {noop, createMap, ensurePlural, chunk, switcherFn} from 'hurdak/lib/hurdak' import {noop, createMap, ensurePlural, chunk, switcherFn} from 'hurdak/lib/hurdak'
import {log} from 'src/util/logger' import {log} from 'src/util/logger'
import {hexToBech32, tryFetch, now, sleep, tryJson, timedelta, shuffle, hash} from 'src/util/misc' import {lnurlEncode, lnurlDecode, tryFetch, now, sleep, tryJson, timedelta, shuffle, hash} from 'src/util/misc'
import {Tags, roomAttrs, personKinds, isRelay, isShareableRelay, normalizeRelayUrl} from 'src/util/nostr' import {Tags, roomAttrs, personKinds, isRelay, isShareableRelay, normalizeRelayUrl} from 'src/util/nostr'
import database from 'src/agent/database' import database from 'src/agent/database'
@ -308,25 +307,31 @@ const verifyNip05 = (pubkey, as) =>
}, noop) }, noop)
const verifyZapper = async (pubkey, address) => { const verifyZapper = async (pubkey, address) => {
let url
if (address.startsWith('lnurl1')) {
// Try to parse it as a lud06 LNURL // Try to parse it as a lud06 LNURL
let zapper = await getParams(address) as any url = lnurlDecode(address)
let lnurl = address const lnurl = address
} else if (address.includes('@')) {
// If that failed, try to parse it as a lud16 address // Otherwise try to parse it as a lud16 address
if (zapper.status === 'ERROR' && address.includes('@')) {
const [name, domain] = address.split('@') const [name, domain] = address.split('@')
if (!domain || !name) { if (!domain || !name) {
return return
} }
const url = `https://${domain}/.well-known/lnurlp/${name}` url = `https://${domain}/.well-known/lnurlp/${name}`
} else {
// Otherwise this is not valid
return
}
const res = await tryFetch(() => fetch(url)) const res = await tryFetch(() => fetch(url))
let zapper
if (res) { if (res) {
zapper = await res.json() zapper = await res.json()
lnurl = hexToBech32('lnurl', url) lnurl = lnurlEncode('lnurl', url)
}
} }
if (zapper?.allowsNostr && zapper?.nostrPubkey) { if (zapper?.allowsNostr && zapper?.nostrPubkey) {

46
src/util/lightning.ts Normal file
View File

@ -0,0 +1,46 @@
const DIVISORS = {
m: BigInt(1e3),
u: BigInt(1e6),
n: BigInt(1e9),
p: BigInt(1e12)
}
const MAX_MILLISATS = BigInt('2100000000000000000')
const MILLISATS_PER_BTC = BigInt(1e11)
function hrpToMillisat(hrpString: string) {
let divisor, value
if (hrpString.slice(-1).match(/^[munp]$/)) {
divisor = hrpString.slice(-1)
value = hrpString.slice(0, -1)
} else if (hrpString.slice(-1).match(/^[^munp0-9]$/)) {
throw new Error('Not a valid multiplier for the amount')
} else {
value = hrpString
}
if (!value.match(/^\d+$/))
throw new Error('Not a valid human readable amount')
const valueBN = BigInt(value)
const millisatoshisBN = divisor
? (valueBN * MILLISATS_PER_BTC) / DIVISORS[divisor]
: valueBN * MILLISATS_PER_BTC
if (
(divisor === 'p' && !(valueBN % BigInt(10) === BigInt(0))) ||
millisatoshisBN > MAX_MILLISATS
) {
throw new Error('Amount is outside of valid range')
}
return millisatoshisBN
}
export function invoiceAmount(bolt11: string): number {
const hrp = bolt11.match(/lnbc(\d+\w)/)
const bn = hrpToMillisat(hrp[1])
return Number(bn)
}

View File

@ -1,5 +1,4 @@
import {bech32} from 'bech32' import {bech32, utf8} from '@scure/base'
import {Buffer} from 'buffer'
import {debounce, throttle} from 'throttle-debounce' import {debounce, throttle} from 'throttle-debounce'
import {aperture, path as getPath, allPass, pipe, isNil, complement, equals, is, pluck, sum, identity, sortBy} from "ramda" import {aperture, path as getPath, allPass, pipe, isNil, complement, equals, is, pluck, sum, identity, sortBy} from "ramda"
import Fuse from "fuse.js/dist/fuse.min.js" import Fuse from "fuse.js/dist/fuse.min.js"
@ -340,11 +339,11 @@ export const uploadFile = (url, fileObj) => {
return fetchJson(url, {method: 'POST', body}) return fetchJson(url, {method: 'POST', body})
} }
export const hexToBech32 = (prefix, hex) => export const lnurlEncode = (prefix, url) =>
bech32.encode(prefix, bech32.toWords(Buffer.from(hex, 'hex'))) bech32.encode(prefix, bech32.toWords(utf8.decode(url)))
export const bech32ToHex = b32 => export const lnurlDecode = b32 =>
Buffer.from(bech32.fromWords(bech32.decode(b32).words)).toString('hex') utf8.encode(bech32.fromWords(bech32.decode(b32).words))
export const formatSats = sats => { export const formatSats = sats => {
const formatter = new Intl.NumberFormat() const formatter = new Intl.NumberFormat()

View File

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import cx from 'classnames' import cx from 'classnames'
import bolt11 from 'bolt11'
import QRCode from 'qrcode' import QRCode from 'qrcode'
import {nip19} from 'nostr-tools' import {nip19} from 'nostr-tools'
import {find, sum, last, whereEq, without, uniq, pluck, reject, propEq} from 'ramda' import {find, sum, last, whereEq, without, uniq, pluck, reject, propEq} from 'ramda'
@ -9,9 +8,10 @@
import {slide} from 'svelte/transition' import {slide} from 'svelte/transition'
import {navigate} from 'svelte-routing' import {navigate} from 'svelte-routing'
import {quantify} from 'hurdak/lib/hurdak' import {quantify} from 'hurdak/lib/hurdak'
import {Tags, findRootId, findReplyId, displayPerson, isLike} from "src/util/nostr" import {Tags, findRootId, findReplyId, displayPerson, isLike} from 'src/util/nostr'
import {formatTimestamp, now, tryJson, stringToColor, formatSats, fetchJson} from 'src/util/misc' import {formatTimestamp, now, tryJson, stringToColor, formatSats, fetchJson} from 'src/util/misc'
import {extractUrls, copyToClipboard} from "src/util/html" import {extractUrls, copyToClipboard} from "src/util/html"
import {invoiceAmount} from 'src/util/lightning'
import ImageCircle from 'src/partials/ImageCircle.svelte' import ImageCircle from 'src/partials/ImageCircle.svelte'
import Input from 'src/partials/Input.svelte' import Input from 'src/partials/Input.svelte'
import Textarea from 'src/partials/Textarea.svelte' import Textarea from 'src/partials/Textarea.svelte'
@ -74,7 +74,7 @@
return tryJson(() => ({ return tryJson(() => ({
...zap, ...zap,
invoice: bolt11.decode(zapMeta.bolt11), invoiceAmount: invoiceAmount(zapMeta.bolt11),
request: JSON.parse(zapMeta.description), request: JSON.parse(zapMeta.description),
})) }))
}) })
@ -88,11 +88,11 @@
return false return false
} }
const {invoice, request} = zap const {invoiceAmount, request} = zap
const reqMeta = Tags.from(request).asMeta() const reqMeta = Tags.from(request).asMeta()
// Verify that the zapper actually sent the requested amount (if it was supplied) // Verify that the zapper actually sent the requested amount (if it was supplied)
if (reqMeta.amount && reqMeta.amount !== parseInt(invoice.millisatoshis)) { if (reqMeta.amount && parseInt(reqMeta.amount) !== invoiceAmount) {
return false return false
} }
@ -114,7 +114,7 @@
$: zapped = find(z => z.request.pubkey === $profile?.pubkey, zaps) $: zapped = find(z => z.request.pubkey === $profile?.pubkey, zaps)
$: $likesCount = likes.length $: $likesCount = likes.length
$: $flagsCount = flags.length $: $flagsCount = flags.length
$: $zapsTotal = sum(zaps.map(zap => zap.invoice.satoshis)) $: $zapsTotal = sum(zaps.map(zap => zap.invoiceAmount)) / 1000
$: $repliesCount = note.replies.length $: $repliesCount = note.replies.length
$: visibleNotes = note.replies.filter(r => showContext ? true : !r.isContext) $: visibleNotes = note.replies.filter(r => showContext ? true : !r.isContext)
$: canZap = $person?.zapper && user.canZap() $: canZap = $person?.zapper && user.canZap()