-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathbip21.ts
More file actions
96 lines (79 loc) · 2.61 KB
/
Copy pathbip21.ts
File metadata and controls
96 lines (79 loc) · 2.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
// bitcoin:<address>[?amount=<amount>][?label=<label>][?message=<message>]
import { fromSatoshis, prettyNumber, toSatoshis } from './format'
import { isArkAddress } from './address'
export interface Bip21Decoded {
address?: string
arkAddress?: string
satoshis?: number
/** Raw amount in asset units when assetId is present (not converted to satoshis) */
assetAmount?: number
invoice?: string
lnurl?: string
assetId?: string
}
/** decode a bip21 uri */
export const decodeBip21 = (uri: string): Bip21Decoded => {
const result: Bip21Decoded = {
address: undefined,
satoshis: undefined,
invoice: undefined,
lnurl: undefined,
}
// use lowercase for consistency
const bip21Url = uri.trim()
if (!bip21Url.toLowerCase().startsWith('bitcoin:')) {
throw new Error('Invalid BIP21 URI')
}
// remove 'bitcoin:' prefix
const urlWithoutPrefix = bip21Url.slice(8)
// split address and query parameters
const [address, queryString] = urlWithoutPrefix.split('?')
result.address = address
if (queryString) {
const params = new URLSearchParams(queryString)
if (params.has('ark')) {
const arkAddress = params.get('ark') ?? ''
if (isArkAddress(arkAddress)) result.arkAddress = arkAddress
}
if (params.has('assetid')) {
result.assetId = params.get('assetid')!
}
if (params.has('amount')) {
const amount = parseFloat(params.get('amount')!)
if (isNaN(amount) || amount < 0 || !isFinite(amount)) throw new Error('Invalid amount')
if (result.assetId != null) {
result.assetAmount = amount
} else {
result.satoshis = toSatoshis(amount)
}
}
if (params.has('lightning')) {
if (params.get('lightning')?.startsWith('lnurl')) {
result.lnurl = params.get('lightning')!
} else if (params.get('lightning')?.startsWith('ln')) {
result.invoice = params.get('lightning')!
}
}
}
return result
}
export const encodeBip21 = (address: string, arkAddress: string, invoice: string, sats: number, lnurl?: string) => {
return (
`bitcoin:${address}` +
`?ark=${arkAddress}` +
(invoice ? `&lightning=${invoice}` : lnurl ? `&lightning=${lnurl}` : '') +
(sats ? `&amount=${prettyNumber(fromSatoshis(sats))}` : '')
)
}
export const encodeBip21Asset = (arkAddress: string, assetId: string, amount: number) => {
return `bitcoin:?ark=${arkAddress}&assetid=${assetId}&amount=${amount}`
}
export const isBip21 = (data: string): boolean => {
try {
decodeBip21(data)
return true
} catch {
return false
}
}