bitcoin-decoder 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/README.md +19 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +162 -0
- package/dist/types/index.d.ts +69 -0
- package/dist/types/index.js +7 -0
- package/dist/utils/ark.d.ts +4 -0
- package/dist/utils/ark.js +43 -0
- package/dist/utils/bip-321.d.ts +3 -0
- package/dist/utils/bip-321.js +56 -0
- package/dist/utils/bitcoin.d.ts +4 -0
- package/dist/utils/bitcoin.js +124 -0
- package/dist/utils/bolt11.d.ts +3 -0
- package/dist/utils/bolt11.js +23 -0
- package/dist/utils/bolt12.d.ts +3 -0
- package/dist/utils/bolt12.js +170 -0
- package/dist/utils/lightning-address.d.ts +5 -0
- package/dist/utils/lightning-address.js +65 -0
- package/dist/utils/lnurl.d.ts +4 -0
- package/dist/utils/lnurl.js +32 -0
- package/dist/utils/network.d.ts +3 -0
- package/dist/utils/network.js +48 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Psycarlo
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install bitcoin-decoder
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { decode } from 'bitcoin-decoder'
|
|
13
|
+
|
|
14
|
+
const result = decode('1111111111111111111114oLvT2')
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## License
|
|
18
|
+
|
|
19
|
+
Released under the **MIT** license.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DecodedData, Input } from './types';
|
|
2
|
+
declare function decode(input: Input): DecodedData;
|
|
3
|
+
export { decode };
|
|
4
|
+
export type { DecodedData, Destination, Metadata, Network, ParsedLNAddress, WellKnown } from './types';
|
|
5
|
+
export { wellKnown } from './utils/lightning-address';
|
|
6
|
+
declare const _default: {
|
|
7
|
+
decode: typeof decode;
|
|
8
|
+
};
|
|
9
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { DecodeError } from './types';
|
|
2
|
+
import { ARK_PREFIXES, ark } from './utils/ark';
|
|
3
|
+
import { bip321 } from './utils/bip-321';
|
|
4
|
+
import { BITCOIN_ADDRESS_PREFIXES, bitcoin } from './utils/bitcoin';
|
|
5
|
+
import { bolt11 } from './utils/bolt11';
|
|
6
|
+
import { bolt12 } from './utils/bolt12';
|
|
7
|
+
import { lightningAddress } from './utils/lightning-address';
|
|
8
|
+
import { LNURL_PREFIX, lnurl } from './utils/lnurl';
|
|
9
|
+
import { getNetwork } from './utils/network';
|
|
10
|
+
const BIP321_PREFIX = 'bitcoin:';
|
|
11
|
+
const LIGHTNING_PREFIX = 'lightning:';
|
|
12
|
+
const BOLT11_PREFIXES = ['lnbcrt', 'lntbs', 'lnbc', 'lntb'];
|
|
13
|
+
const BOLT12_PREFIXES = ['lnbcrto1', 'lnto1', 'lno1'];
|
|
14
|
+
function toDestination(pd) {
|
|
15
|
+
if (pd.type === 'bitcoin-address') {
|
|
16
|
+
return {
|
|
17
|
+
destination: pd.value,
|
|
18
|
+
type: pd.type,
|
|
19
|
+
protocol: pd.protocol,
|
|
20
|
+
addressType: pd.addressType
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
destination: pd.value,
|
|
25
|
+
type: pd.type,
|
|
26
|
+
protocol: pd.protocol
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function decodeBolt11(input, invoice) {
|
|
30
|
+
const parsedDestination = bolt11(invoice);
|
|
31
|
+
const destination = toDestination(parsedDestination.destination);
|
|
32
|
+
return {
|
|
33
|
+
input,
|
|
34
|
+
destination,
|
|
35
|
+
destinations: [destination],
|
|
36
|
+
network: getNetwork(invoice) || 'unknown',
|
|
37
|
+
metadata: parsedDestination.metadata
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function decodeBolt12(input, offer) {
|
|
41
|
+
const parsedDestination = bolt12(offer);
|
|
42
|
+
const destination = toDestination(parsedDestination.destination);
|
|
43
|
+
return {
|
|
44
|
+
input,
|
|
45
|
+
destination,
|
|
46
|
+
destinations: [destination],
|
|
47
|
+
network: getNetwork(offer) || 'unknown',
|
|
48
|
+
metadata: parsedDestination.metadata
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function decodeLightningAddress(input) {
|
|
52
|
+
const parsedDestination = lightningAddress(input);
|
|
53
|
+
const destination = toDestination(parsedDestination.destination);
|
|
54
|
+
return {
|
|
55
|
+
input,
|
|
56
|
+
destination,
|
|
57
|
+
destinations: [destination],
|
|
58
|
+
network: 'unknown'
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function decodeLnurl(input) {
|
|
62
|
+
const parsedDestination = lnurl(input);
|
|
63
|
+
const destination = toDestination(parsedDestination.destination);
|
|
64
|
+
return {
|
|
65
|
+
input,
|
|
66
|
+
destination,
|
|
67
|
+
destinations: [destination],
|
|
68
|
+
network: 'unknown'
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function decodeArk(input) {
|
|
72
|
+
const parsedDestination = ark(input);
|
|
73
|
+
const destination = toDestination(parsedDestination.destination);
|
|
74
|
+
return {
|
|
75
|
+
input,
|
|
76
|
+
destination,
|
|
77
|
+
destinations: [destination],
|
|
78
|
+
network: getNetwork(input) || 'unknown'
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function decodeBip321(input) {
|
|
82
|
+
const parsedDestinations = bip321(input);
|
|
83
|
+
const first = parsedDestinations[0];
|
|
84
|
+
if (!first) {
|
|
85
|
+
throw new DecodeError(`No supported payment methods in BIP-321 URI: ${input}`, 'NO_PAYMENT_METHODS');
|
|
86
|
+
}
|
|
87
|
+
const destination = toDestination(first.destination);
|
|
88
|
+
const destinations = parsedDestinations.map((pd) => toDestination(pd.destination));
|
|
89
|
+
return {
|
|
90
|
+
input,
|
|
91
|
+
destination,
|
|
92
|
+
destinations,
|
|
93
|
+
network: getNetwork(destination.destination) || 'unknown',
|
|
94
|
+
metadata: first.metadata
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function decodeBitcoin(input) {
|
|
98
|
+
const parsedDestination = bitcoin(input);
|
|
99
|
+
const destination = toDestination(parsedDestination.destination);
|
|
100
|
+
return {
|
|
101
|
+
input,
|
|
102
|
+
destination,
|
|
103
|
+
destinations: [destination],
|
|
104
|
+
network: getNetwork(input) || 'unknown'
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function decodeInput(input) {
|
|
108
|
+
const lowerInput = input.toLowerCase();
|
|
109
|
+
if (lowerInput.startsWith(BIP321_PREFIX)) {
|
|
110
|
+
return decodeBip321(input);
|
|
111
|
+
}
|
|
112
|
+
if (lowerInput.startsWith(LIGHTNING_PREFIX)) {
|
|
113
|
+
return decodeBolt11(input, input.slice(LIGHTNING_PREFIX.length));
|
|
114
|
+
}
|
|
115
|
+
if (BOLT11_PREFIXES.some((prefix) => lowerInput.startsWith(prefix))) {
|
|
116
|
+
return decodeBolt11(input, input);
|
|
117
|
+
}
|
|
118
|
+
if (BOLT12_PREFIXES.some((prefix) => lowerInput.startsWith(prefix))) {
|
|
119
|
+
return decodeBolt12(input, input);
|
|
120
|
+
}
|
|
121
|
+
if (lowerInput.startsWith(LNURL_PREFIX)) {
|
|
122
|
+
return decodeLnurl(input);
|
|
123
|
+
}
|
|
124
|
+
if (ARK_PREFIXES.some((prefix) => lowerInput.startsWith(prefix))) {
|
|
125
|
+
return decodeArk(input);
|
|
126
|
+
}
|
|
127
|
+
if (input.includes('@')) {
|
|
128
|
+
return decodeLightningAddress(input);
|
|
129
|
+
}
|
|
130
|
+
if (BITCOIN_ADDRESS_PREFIXES.some((prefix) => lowerInput.startsWith(prefix))) {
|
|
131
|
+
return decodeBitcoin(input);
|
|
132
|
+
}
|
|
133
|
+
throw new DecodeError(`Unknown input format: ${input}`, 'UNKNOWN_FORMAT');
|
|
134
|
+
}
|
|
135
|
+
function getErrorCode(error) {
|
|
136
|
+
if (error instanceof DecodeError) {
|
|
137
|
+
return {
|
|
138
|
+
valid: false,
|
|
139
|
+
input: '',
|
|
140
|
+
errorMessage: error.message,
|
|
141
|
+
errorCode: error.code
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
145
|
+
return {
|
|
146
|
+
valid: false,
|
|
147
|
+
input: '',
|
|
148
|
+
errorMessage: message,
|
|
149
|
+
errorCode: 'UNKNOWN_FORMAT'
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function decode(input) {
|
|
153
|
+
try {
|
|
154
|
+
return { valid: true, ...decodeInput(input) };
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
return { ...getErrorCode(error), input };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export { decode };
|
|
161
|
+
export { wellKnown } from './utils/lightning-address';
|
|
162
|
+
export default { decode };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type Input = string;
|
|
2
|
+
/** Bitcoin network
|
|
3
|
+
*
|
|
4
|
+
* `testnet` here includes bitcoin testnet, signet, and regtest.
|
|
5
|
+
*/
|
|
6
|
+
export type Network = 'mainnet' | 'testnet' | 'unknown';
|
|
7
|
+
export type Protocol = 'on-chain' | 'lightning' | 'ark';
|
|
8
|
+
export type Metadata = {
|
|
9
|
+
/** Amount in sats */
|
|
10
|
+
amount?: number;
|
|
11
|
+
description?: string;
|
|
12
|
+
};
|
|
13
|
+
export type DestinationType = 'bitcoin-address' | 'bolt11' | 'bolt12' | 'lnurl' | 'lnaddress' | 'ark-address';
|
|
14
|
+
export type BitcoinAddressType = 'p2pkh' | 'p2sh' | 'p2wpkh' | 'p2wsh' | 'p2tr';
|
|
15
|
+
export type Destination = {
|
|
16
|
+
destination: string;
|
|
17
|
+
type: 'bitcoin-address';
|
|
18
|
+
protocol: Protocol;
|
|
19
|
+
addressType: BitcoinAddressType;
|
|
20
|
+
} | {
|
|
21
|
+
destination: string;
|
|
22
|
+
type: Exclude<DestinationType, 'bitcoin-address'>;
|
|
23
|
+
protocol: Protocol;
|
|
24
|
+
};
|
|
25
|
+
export type ErrorCode = 'UNKNOWN_FORMAT' | 'INVALID_HRP' | 'INVALID_VERSION' | 'EMPTY_ADDRESS' | 'PAYLOAD_TOO_SHORT' | 'INVALID_CHECKSUM' | 'INVALID_BOLT12' | 'INVALID_BIP321' | 'INVALID_LNADDRESS' | 'INVALID_LNURL' | 'NO_PAYMENT_METHODS';
|
|
26
|
+
export type ParsedLNAddress = {
|
|
27
|
+
username: string;
|
|
28
|
+
domain: string;
|
|
29
|
+
};
|
|
30
|
+
export type WellKnown = {
|
|
31
|
+
callback: string;
|
|
32
|
+
minSendable: number;
|
|
33
|
+
maxSendable: number;
|
|
34
|
+
commentAllowed?: number;
|
|
35
|
+
metadata?: string;
|
|
36
|
+
};
|
|
37
|
+
type DecodedSuccess = {
|
|
38
|
+
valid: true;
|
|
39
|
+
/** Raw text passed by the user */
|
|
40
|
+
input: Input;
|
|
41
|
+
/** All parsed destinations */
|
|
42
|
+
destinations: Destination[];
|
|
43
|
+
/** Shortcut primary destination for convenience. Lightning > Ark > On-chain */
|
|
44
|
+
readonly destination: Destination;
|
|
45
|
+
/** Bitcoin network associated with the destination(s) */
|
|
46
|
+
network: Network;
|
|
47
|
+
/** Most relevant metadata */
|
|
48
|
+
metadata?: Metadata;
|
|
49
|
+
};
|
|
50
|
+
type DecodedError = {
|
|
51
|
+
valid: false;
|
|
52
|
+
/** Raw text passed by the user */
|
|
53
|
+
input: Input;
|
|
54
|
+
errorMessage: string;
|
|
55
|
+
errorCode: ErrorCode;
|
|
56
|
+
};
|
|
57
|
+
export type DecodedData = DecodedSuccess | DecodedError;
|
|
58
|
+
export declare class DecodeError extends Error {
|
|
59
|
+
code: ErrorCode;
|
|
60
|
+
constructor(message: string, code: ErrorCode);
|
|
61
|
+
}
|
|
62
|
+
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
63
|
+
export type ParsedDestination = {
|
|
64
|
+
destination: DistributiveOmit<Destination, 'destination'> & {
|
|
65
|
+
value: string;
|
|
66
|
+
};
|
|
67
|
+
metadata?: Metadata;
|
|
68
|
+
};
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { bech32m } from '@scure/base';
|
|
2
|
+
import { DecodeError } from '../types';
|
|
3
|
+
const ARK_PREFIXES = ['ark', 'tark'];
|
|
4
|
+
const VALID_VERSIONS = [0, 1];
|
|
5
|
+
const MIN_PAYLOAD_LENGTH = 4;
|
|
6
|
+
function validate(input) {
|
|
7
|
+
let prefix;
|
|
8
|
+
let words;
|
|
9
|
+
try {
|
|
10
|
+
const decoded = bech32m.decode(input, 1023);
|
|
11
|
+
prefix = decoded.prefix;
|
|
12
|
+
words = decoded.words;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new DecodeError('Invalid bech32m checksum', 'INVALID_CHECKSUM');
|
|
16
|
+
}
|
|
17
|
+
if (!ARK_PREFIXES.includes(prefix)) {
|
|
18
|
+
throw new DecodeError(`Invalid HRP: ${prefix}`, 'INVALID_HRP');
|
|
19
|
+
}
|
|
20
|
+
const [version] = words;
|
|
21
|
+
if (version === undefined) {
|
|
22
|
+
throw new DecodeError('Empty address', 'EMPTY_ADDRESS');
|
|
23
|
+
}
|
|
24
|
+
if (!VALID_VERSIONS.includes(version)) {
|
|
25
|
+
throw new DecodeError(`Unknown version: ${version}`, 'INVALID_VERSION');
|
|
26
|
+
}
|
|
27
|
+
const payloadBits = (words.length - 1) * 5;
|
|
28
|
+
const payloadBytes = Math.floor(payloadBits / 8);
|
|
29
|
+
if (payloadBytes < MIN_PAYLOAD_LENGTH) {
|
|
30
|
+
throw new DecodeError('Payload too short', 'PAYLOAD_TOO_SHORT');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function ark(input) {
|
|
34
|
+
validate(input);
|
|
35
|
+
return {
|
|
36
|
+
destination: {
|
|
37
|
+
value: input,
|
|
38
|
+
protocol: 'ark',
|
|
39
|
+
type: 'ark-address'
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export { ark, ARK_PREFIXES };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { parseBIP321 } from 'bip-321';
|
|
2
|
+
import { ark } from './ark';
|
|
3
|
+
import { bitcoin } from './bitcoin';
|
|
4
|
+
import { bolt11 } from './bolt11';
|
|
5
|
+
import { bolt12 } from './bolt12';
|
|
6
|
+
const SATS_PER_BTC = 100_000_000;
|
|
7
|
+
/** Priority order: Lightning (0) > Ark (1) > On-chain (2) */
|
|
8
|
+
const PROTOCOL_PRIORITY = {
|
|
9
|
+
lightning: 0,
|
|
10
|
+
offer: 0,
|
|
11
|
+
ark: 1,
|
|
12
|
+
onchain: 2
|
|
13
|
+
};
|
|
14
|
+
const SUPPORTED_TYPES = new Set(Object.keys(PROTOCOL_PRIORITY));
|
|
15
|
+
function isSupportedType(type) {
|
|
16
|
+
return SUPPORTED_TYPES.has(type);
|
|
17
|
+
}
|
|
18
|
+
function getMetadata(result) {
|
|
19
|
+
const amount = result.amount
|
|
20
|
+
? Math.round(result.amount * SATS_PER_BTC)
|
|
21
|
+
: undefined;
|
|
22
|
+
const description = result.message ?? result.label;
|
|
23
|
+
if (amount === undefined && description === undefined) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return { amount, description };
|
|
27
|
+
}
|
|
28
|
+
function mapPaymentMethod(paymentMethod, metadata) {
|
|
29
|
+
if (paymentMethod.type === 'lightning') {
|
|
30
|
+
return bolt11(paymentMethod.value);
|
|
31
|
+
}
|
|
32
|
+
if (paymentMethod.type === 'offer') {
|
|
33
|
+
return bolt12(paymentMethod.value);
|
|
34
|
+
}
|
|
35
|
+
if (paymentMethod.type === 'ark') {
|
|
36
|
+
return ark(paymentMethod.value);
|
|
37
|
+
}
|
|
38
|
+
const parsed = bitcoin(paymentMethod.value);
|
|
39
|
+
return { ...parsed, metadata };
|
|
40
|
+
}
|
|
41
|
+
function parse(result) {
|
|
42
|
+
const metadata = getMetadata(result);
|
|
43
|
+
return result.paymentMethods
|
|
44
|
+
.filter((method) => (method.valid || method.type === 'offer') &&
|
|
45
|
+
isSupportedType(method.type))
|
|
46
|
+
.sort((a, b) => PROTOCOL_PRIORITY[a.type] - PROTOCOL_PRIORITY[b.type])
|
|
47
|
+
.map((method) => mapPaymentMethod(method, metadata));
|
|
48
|
+
}
|
|
49
|
+
function bip321(input) {
|
|
50
|
+
const result = parseBIP321(input);
|
|
51
|
+
if (!result.valid) {
|
|
52
|
+
throw new Error(`Invalid BIP-321 URI: ${result.errors.join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
return parse(result);
|
|
55
|
+
}
|
|
56
|
+
export { bip321 };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
2
|
+
import { bech32, bech32m, createBase58check } from '@scure/base';
|
|
3
|
+
import { DecodeError } from '../types';
|
|
4
|
+
const b58c = createBase58check(sha256);
|
|
5
|
+
const SEGWIT_HRPS = ['bc', 'tb', 'bcrt'];
|
|
6
|
+
const BITCOIN_ADDRESS_PREFIXES = [
|
|
7
|
+
'bc1',
|
|
8
|
+
'tb1',
|
|
9
|
+
'bcrt1',
|
|
10
|
+
'1',
|
|
11
|
+
'3',
|
|
12
|
+
'm',
|
|
13
|
+
'n',
|
|
14
|
+
'2'
|
|
15
|
+
];
|
|
16
|
+
const P2PKH_VERSIONS = new Set([0x00, 0x6f]);
|
|
17
|
+
const P2SH_VERSIONS = new Set([0x05, 0xc4]);
|
|
18
|
+
function getAddressType(version) {
|
|
19
|
+
if (P2PKH_VERSIONS.has(version)) {
|
|
20
|
+
return 'p2pkh';
|
|
21
|
+
}
|
|
22
|
+
if (P2SH_VERSIONS.has(version)) {
|
|
23
|
+
return 'p2sh';
|
|
24
|
+
}
|
|
25
|
+
throw new DecodeError(`Unknown version byte: ${version}`, 'INVALID_VERSION');
|
|
26
|
+
}
|
|
27
|
+
function validateLegacy(input) {
|
|
28
|
+
let decoded;
|
|
29
|
+
try {
|
|
30
|
+
decoded = b58c.decode(input);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new DecodeError('Invalid base58check checksum', 'INVALID_CHECKSUM');
|
|
34
|
+
}
|
|
35
|
+
if (decoded.length !== 21) {
|
|
36
|
+
throw new DecodeError('Invalid address length', 'PAYLOAD_TOO_SHORT');
|
|
37
|
+
}
|
|
38
|
+
const version = decoded[0];
|
|
39
|
+
if (version === undefined) {
|
|
40
|
+
throw new DecodeError('Empty address', 'EMPTY_ADDRESS');
|
|
41
|
+
}
|
|
42
|
+
return getAddressType(version);
|
|
43
|
+
}
|
|
44
|
+
function isValidHrp(prefix) {
|
|
45
|
+
return SEGWIT_HRPS.includes(prefix);
|
|
46
|
+
}
|
|
47
|
+
function getSegwitAddressType(version, programLength) {
|
|
48
|
+
if (version === 0) {
|
|
49
|
+
return programLength === 20 ? 'p2wpkh' : 'p2wsh';
|
|
50
|
+
}
|
|
51
|
+
return 'p2tr';
|
|
52
|
+
}
|
|
53
|
+
function validateSegwit(input) {
|
|
54
|
+
const lowerInput = input.toLowerCase();
|
|
55
|
+
// Try bech32 (witness version 0)
|
|
56
|
+
try {
|
|
57
|
+
const decoded = bech32.decode(lowerInput, 90);
|
|
58
|
+
if (!isValidHrp(decoded.prefix)) {
|
|
59
|
+
throw new DecodeError(`Invalid HRP: ${decoded.prefix}`, 'INVALID_HRP');
|
|
60
|
+
}
|
|
61
|
+
const [version] = decoded.words;
|
|
62
|
+
if (version !== 0) {
|
|
63
|
+
throw new DecodeError('Witness v0 must use bech32 encoding', 'INVALID_VERSION');
|
|
64
|
+
}
|
|
65
|
+
const data = bech32.fromWords(decoded.words.slice(1));
|
|
66
|
+
if (data.length !== 20 && data.length !== 32) {
|
|
67
|
+
throw new DecodeError(`Invalid witness program length: ${data.length}`, 'PAYLOAD_TOO_SHORT');
|
|
68
|
+
}
|
|
69
|
+
return getSegwitAddressType(version, data.length);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof DecodeError) {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Try bech32m (witness version 1+)
|
|
77
|
+
try {
|
|
78
|
+
const decoded = bech32m.decode(lowerInput, 90);
|
|
79
|
+
if (!isValidHrp(decoded.prefix)) {
|
|
80
|
+
throw new DecodeError(`Invalid HRP: ${decoded.prefix}`, 'INVALID_HRP');
|
|
81
|
+
}
|
|
82
|
+
const [version] = decoded.words;
|
|
83
|
+
if (version === undefined || version < 1 || version > 16) {
|
|
84
|
+
throw new DecodeError(`Invalid witness version: ${version}`, 'INVALID_VERSION');
|
|
85
|
+
}
|
|
86
|
+
const data = bech32m.fromWords(decoded.words.slice(1));
|
|
87
|
+
if (version === 1) {
|
|
88
|
+
if (data.length !== 32) {
|
|
89
|
+
throw new DecodeError(`Invalid witness program length: ${data.length}`, 'PAYLOAD_TOO_SHORT');
|
|
90
|
+
}
|
|
91
|
+
return getSegwitAddressType(version, data.length);
|
|
92
|
+
}
|
|
93
|
+
// Future witness versions (2-16): program must be 2-40 bytes
|
|
94
|
+
if (data.length < 2 || data.length > 40) {
|
|
95
|
+
throw new DecodeError(`Invalid witness program length: ${data.length}`, 'PAYLOAD_TOO_SHORT');
|
|
96
|
+
}
|
|
97
|
+
return getSegwitAddressType(version, data.length);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof DecodeError) {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
throw new DecodeError('Invalid bech32/bech32m checksum', 'INVALID_CHECKSUM');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function validate(input) {
|
|
107
|
+
const lowerInput = input.toLowerCase();
|
|
108
|
+
if (SEGWIT_HRPS.some((hrp) => lowerInput.startsWith(`${hrp}1`))) {
|
|
109
|
+
return validateSegwit(input);
|
|
110
|
+
}
|
|
111
|
+
return validateLegacy(input);
|
|
112
|
+
}
|
|
113
|
+
function bitcoin(input) {
|
|
114
|
+
const addressType = validate(input);
|
|
115
|
+
return {
|
|
116
|
+
destination: {
|
|
117
|
+
value: input,
|
|
118
|
+
protocol: 'on-chain',
|
|
119
|
+
type: 'bitcoin-address',
|
|
120
|
+
addressType
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export { bitcoin, BITCOIN_ADDRESS_PREFIXES };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { decode } from 'light-bolt11-decoder';
|
|
2
|
+
function getSection(decodedInvoice, name) {
|
|
3
|
+
return decodedInvoice.sections.find((section) => section.name === name);
|
|
4
|
+
}
|
|
5
|
+
function parse(decodedInvoice) {
|
|
6
|
+
const amount = getSection(decodedInvoice, 'amount');
|
|
7
|
+
const description = getSection(decodedInvoice, 'description');
|
|
8
|
+
return {
|
|
9
|
+
destination: {
|
|
10
|
+
value: decodedInvoice.paymentRequest,
|
|
11
|
+
protocol: 'lightning',
|
|
12
|
+
type: 'bolt11'
|
|
13
|
+
},
|
|
14
|
+
metadata: {
|
|
15
|
+
amount: amount ? Number(amount.value) / 1000 : undefined,
|
|
16
|
+
description: description?.value
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function bolt11(input) {
|
|
21
|
+
return parse(decode(input));
|
|
22
|
+
}
|
|
23
|
+
export { bolt11 };
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { bech32, hex, utf8 } from '@scure/base';
|
|
2
|
+
const BECH32_ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
|
3
|
+
// BOLT12 TLV types from the spec
|
|
4
|
+
const TLV_TYPES = {
|
|
5
|
+
2: 'offer_chains',
|
|
6
|
+
4: 'offer_metadata',
|
|
7
|
+
6: 'offer_currency',
|
|
8
|
+
8: 'offer_amount',
|
|
9
|
+
10: 'description',
|
|
10
|
+
12: 'offer_features',
|
|
11
|
+
14: 'offer_absolute_expiry',
|
|
12
|
+
16: 'offer_paths',
|
|
13
|
+
18: 'issuer',
|
|
14
|
+
20: 'offer_quantity_max',
|
|
15
|
+
22: 'node_id',
|
|
16
|
+
80: 'invreq_metadata',
|
|
17
|
+
82: 'invreq_payer_id',
|
|
18
|
+
160: 'invoice_paths',
|
|
19
|
+
162: 'invoice_blindedpay',
|
|
20
|
+
164: 'invoice_created_at',
|
|
21
|
+
166: 'invoice_relative_expiry',
|
|
22
|
+
168: 'invoice_payment_hash',
|
|
23
|
+
170: 'invoice_amount',
|
|
24
|
+
172: 'invoice_fallbacks',
|
|
25
|
+
174: 'invoice_features',
|
|
26
|
+
176: 'invoice_node_id',
|
|
27
|
+
240: 'signature'
|
|
28
|
+
};
|
|
29
|
+
/** Read a BigSize integer (variable-length encoding from the Lightning spec). */
|
|
30
|
+
function readBigSize(data, offset) {
|
|
31
|
+
const first = data[offset];
|
|
32
|
+
if (first === undefined) {
|
|
33
|
+
throw new Error('Unexpected end of data');
|
|
34
|
+
}
|
|
35
|
+
if (first < 0xfd) {
|
|
36
|
+
return [first, offset + 1];
|
|
37
|
+
}
|
|
38
|
+
if (first === 0xfd) {
|
|
39
|
+
return [(data[offset + 1] ?? 0) * 256 + (data[offset + 2] ?? 0), offset + 3];
|
|
40
|
+
}
|
|
41
|
+
if (first === 0xfe) {
|
|
42
|
+
return [
|
|
43
|
+
(data[offset + 1] ?? 0) * 16_777_216 +
|
|
44
|
+
(data[offset + 2] ?? 0) * 65_536 +
|
|
45
|
+
(data[offset + 3] ?? 0) * 256 +
|
|
46
|
+
(data[offset + 4] ?? 0),
|
|
47
|
+
offset + 5
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
throw new Error('BigSize value too large');
|
|
51
|
+
}
|
|
52
|
+
function parseValue(type, value) {
|
|
53
|
+
const name = TLV_TYPES[type];
|
|
54
|
+
switch (name) {
|
|
55
|
+
case 'description':
|
|
56
|
+
case 'issuer':
|
|
57
|
+
case 'offer_currency':
|
|
58
|
+
return utf8.encode(value);
|
|
59
|
+
case 'node_id':
|
|
60
|
+
case 'offer_metadata':
|
|
61
|
+
case 'invreq_metadata':
|
|
62
|
+
case 'invreq_payer_id':
|
|
63
|
+
case 'invoice_payment_hash':
|
|
64
|
+
case 'invoice_node_id':
|
|
65
|
+
case 'signature':
|
|
66
|
+
return hex.encode(value);
|
|
67
|
+
case 'offer_amount':
|
|
68
|
+
case 'offer_quantity_max':
|
|
69
|
+
case 'offer_absolute_expiry':
|
|
70
|
+
case 'invoice_created_at':
|
|
71
|
+
case 'invoice_relative_expiry':
|
|
72
|
+
case 'invoice_amount': {
|
|
73
|
+
let n = 0;
|
|
74
|
+
for (const byte of value) {
|
|
75
|
+
n = n * 256 + byte;
|
|
76
|
+
}
|
|
77
|
+
return n;
|
|
78
|
+
}
|
|
79
|
+
case 'offer_chains': {
|
|
80
|
+
const chains = [];
|
|
81
|
+
for (let i = 0; i < value.length; i += 32) {
|
|
82
|
+
chains.push(hex.encode(value.slice(i, i + 32)));
|
|
83
|
+
}
|
|
84
|
+
return chains;
|
|
85
|
+
}
|
|
86
|
+
default:
|
|
87
|
+
return hex.encode(value);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Decode bech32 data without checksum verification.
|
|
92
|
+
* BOLT12 offers use bech32 encoding but omit the checksum per spec,
|
|
93
|
+
* which causes standard bech32 decoders to reject them.
|
|
94
|
+
*/
|
|
95
|
+
function decodeBech32NoChecksum(str) {
|
|
96
|
+
const lower = str.toLowerCase();
|
|
97
|
+
const sepIndex = lower.lastIndexOf('1');
|
|
98
|
+
if (sepIndex < 1) {
|
|
99
|
+
throw new Error('Invalid bech32 string');
|
|
100
|
+
}
|
|
101
|
+
const dataStr = lower.slice(sepIndex + 1);
|
|
102
|
+
const words = [];
|
|
103
|
+
for (const char of dataStr) {
|
|
104
|
+
const idx = BECH32_ALPHABET.indexOf(char);
|
|
105
|
+
if (idx === -1) {
|
|
106
|
+
throw new Error(`Invalid bech32 character: ${char}`);
|
|
107
|
+
}
|
|
108
|
+
words.push(idx);
|
|
109
|
+
}
|
|
110
|
+
return words;
|
|
111
|
+
}
|
|
112
|
+
function decode(offerRequest) {
|
|
113
|
+
if (typeof offerRequest !== 'string') {
|
|
114
|
+
throw new Error('Offer request must be a string');
|
|
115
|
+
}
|
|
116
|
+
const lower = offerRequest.toLowerCase();
|
|
117
|
+
if (!lower.startsWith('ln')) {
|
|
118
|
+
throw new Error('Invalid BOLT12 offer request');
|
|
119
|
+
}
|
|
120
|
+
// Try standard bech32 decode first (handles offers with valid checksum),
|
|
121
|
+
// then fall back to no-checksum decode (e.g. Phoenix wallet offers)
|
|
122
|
+
let words;
|
|
123
|
+
const decoded = bech32.decodeUnsafe(offerRequest, Number.MAX_SAFE_INTEGER);
|
|
124
|
+
if (decoded) {
|
|
125
|
+
words = Array.from(decoded.words);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
words = decodeBech32NoChecksum(offerRequest);
|
|
129
|
+
}
|
|
130
|
+
// Convert 5-bit words to bytes
|
|
131
|
+
const data = bech32.fromWordsUnsafe(words);
|
|
132
|
+
if (!data) {
|
|
133
|
+
throw new Error('Failed to convert bech32 words to bytes');
|
|
134
|
+
}
|
|
135
|
+
// Parse byte-level TLV records (BigSize type + BigSize length + value)
|
|
136
|
+
const sections = [];
|
|
137
|
+
let offset = 0;
|
|
138
|
+
while (offset < data.length) {
|
|
139
|
+
const [type, afterType] = readBigSize(data, offset);
|
|
140
|
+
const [length, afterLength] = readBigSize(data, afterType);
|
|
141
|
+
const value = data.slice(afterLength, afterLength + length);
|
|
142
|
+
offset = afterLength + length;
|
|
143
|
+
const name = TLV_TYPES[type] ?? `unknown_${type}`;
|
|
144
|
+
sections.push({
|
|
145
|
+
name,
|
|
146
|
+
value: parseValue(type, value)
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return { offerRequest, sections };
|
|
150
|
+
}
|
|
151
|
+
function getSection(decodedOffer, name) {
|
|
152
|
+
return decodedOffer.sections.find((section) => section.name === name);
|
|
153
|
+
}
|
|
154
|
+
function parse(decodedOffer) {
|
|
155
|
+
const description = getSection(decodedOffer, 'description');
|
|
156
|
+
return {
|
|
157
|
+
destination: {
|
|
158
|
+
value: decodedOffer.offerRequest,
|
|
159
|
+
protocol: 'lightning',
|
|
160
|
+
type: 'bolt12'
|
|
161
|
+
},
|
|
162
|
+
metadata: {
|
|
163
|
+
description: typeof description?.value === 'string' ? description.value : undefined
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function bolt12(input) {
|
|
168
|
+
return parse(decode(input));
|
|
169
|
+
}
|
|
170
|
+
export { bolt12 };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Input, ParsedDestination, ParsedLNAddress, WellKnown } from '../types';
|
|
2
|
+
declare function parse(input: Input): ParsedLNAddress;
|
|
3
|
+
declare function lightningAddress(input: Input): ParsedDestination;
|
|
4
|
+
declare function wellKnown(lnaddress: string, needsParse?: boolean): Promise<WellKnown | false>;
|
|
5
|
+
export { lightningAddress, parse, wellKnown };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { DecodeError } from '../types';
|
|
2
|
+
const BASE_URL = 'https://';
|
|
3
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
4
|
+
function parse(input) {
|
|
5
|
+
const atIndex = input.indexOf('@');
|
|
6
|
+
if (atIndex < 1) {
|
|
7
|
+
throw new DecodeError('Invalid lightning address format', 'INVALID_LNADDRESS');
|
|
8
|
+
}
|
|
9
|
+
const username = input.slice(0, atIndex);
|
|
10
|
+
const domain = input.slice(atIndex + 1);
|
|
11
|
+
if (!domain.includes('.')) {
|
|
12
|
+
throw new DecodeError('Invalid lightning address format', 'INVALID_LNADDRESS');
|
|
13
|
+
}
|
|
14
|
+
return { username, domain };
|
|
15
|
+
}
|
|
16
|
+
function lightningAddress(input) {
|
|
17
|
+
parse(input);
|
|
18
|
+
return {
|
|
19
|
+
destination: {
|
|
20
|
+
value: input,
|
|
21
|
+
protocol: 'lightning',
|
|
22
|
+
type: 'lnaddress'
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function wellKnown(lnaddress, needsParse = true) {
|
|
27
|
+
let url = lnaddress;
|
|
28
|
+
if (needsParse) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = parse(lnaddress);
|
|
31
|
+
url = `${BASE_URL}${parsed.domain}/.well-known/lnurlp/${parsed.username}`;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(url, {
|
|
39
|
+
headers,
|
|
40
|
+
method: 'GET'
|
|
41
|
+
});
|
|
42
|
+
const json = (await response.json());
|
|
43
|
+
const { callback, minSendable, maxSendable, commentAllowed, metadata } = json;
|
|
44
|
+
if (!callback) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (!minSendable) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (!maxSendable) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
callback,
|
|
55
|
+
minSendable,
|
|
56
|
+
maxSendable,
|
|
57
|
+
commentAllowed,
|
|
58
|
+
metadata
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export { lightningAddress, parse, wellKnown };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { bech32 } from '@scure/base';
|
|
2
|
+
import { DecodeError } from '../types';
|
|
3
|
+
const LNURL_PREFIX = 'lnurl';
|
|
4
|
+
function lnurl(input) {
|
|
5
|
+
let words;
|
|
6
|
+
try {
|
|
7
|
+
const decoded = bech32.decode(input, 2000);
|
|
8
|
+
if (decoded.prefix !== LNURL_PREFIX) {
|
|
9
|
+
throw new DecodeError(`Invalid HRP: expected "${LNURL_PREFIX}", got "${decoded.prefix}"`, 'INVALID_LNURL');
|
|
10
|
+
}
|
|
11
|
+
words = decoded.words;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error instanceof DecodeError) {
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
throw new DecodeError('Invalid LNURL encoding', 'INVALID_LNURL');
|
|
18
|
+
}
|
|
19
|
+
const bytes = bech32.fromWords(words);
|
|
20
|
+
const url = new TextDecoder().decode(new Uint8Array(bytes));
|
|
21
|
+
if (!url.startsWith('https://')) {
|
|
22
|
+
throw new DecodeError('LNURL does not contain a valid URL', 'INVALID_LNURL');
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
destination: {
|
|
26
|
+
value: input,
|
|
27
|
+
protocol: 'lightning',
|
|
28
|
+
type: 'lnurl'
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export { lnurl, LNURL_PREFIX };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const MAINNET_ADDRESS_PREFIXES = ['bc1p', 'bc1q', 'bc1', '1', '3'];
|
|
2
|
+
const TESTNET_ADDRESS_PREFIXES = [
|
|
3
|
+
'bcrt1q',
|
|
4
|
+
'bcrt1p',
|
|
5
|
+
'tb1q',
|
|
6
|
+
'tb1p',
|
|
7
|
+
'm',
|
|
8
|
+
'n',
|
|
9
|
+
'2'
|
|
10
|
+
];
|
|
11
|
+
const MAINNET_BOLT11_PREFIXES = ['lnbc'];
|
|
12
|
+
const TESTNET_BOLT11_PREFIXES = ['lnbcrt', 'lntbs', 'lntb'];
|
|
13
|
+
const MAINNET_BOLT12_PREFIXES = ['lno1'];
|
|
14
|
+
const TESTNET_BOLT12_PREFIXES = ['lnbcrto1', 'lnto1'];
|
|
15
|
+
const MAINNET_ARK_PREFIXES = ['ark'];
|
|
16
|
+
const TESTNET_ARK_PREFIXES = ['tark'];
|
|
17
|
+
function startsWithAny(input, prefixes) {
|
|
18
|
+
return prefixes.some((prefix) => input.startsWith(prefix));
|
|
19
|
+
}
|
|
20
|
+
function getNetwork(input) {
|
|
21
|
+
const lowerInput = input.toLowerCase();
|
|
22
|
+
if (startsWithAny(lowerInput, TESTNET_ADDRESS_PREFIXES)) {
|
|
23
|
+
return 'testnet';
|
|
24
|
+
}
|
|
25
|
+
if (startsWithAny(lowerInput, MAINNET_ADDRESS_PREFIXES)) {
|
|
26
|
+
return 'mainnet';
|
|
27
|
+
}
|
|
28
|
+
if (startsWithAny(lowerInput, TESTNET_BOLT11_PREFIXES)) {
|
|
29
|
+
return 'testnet';
|
|
30
|
+
}
|
|
31
|
+
if (startsWithAny(lowerInput, MAINNET_BOLT11_PREFIXES)) {
|
|
32
|
+
return 'mainnet';
|
|
33
|
+
}
|
|
34
|
+
if (startsWithAny(lowerInput, TESTNET_BOLT12_PREFIXES)) {
|
|
35
|
+
return 'testnet';
|
|
36
|
+
}
|
|
37
|
+
if (startsWithAny(lowerInput, MAINNET_BOLT12_PREFIXES)) {
|
|
38
|
+
return 'mainnet';
|
|
39
|
+
}
|
|
40
|
+
if (startsWithAny(lowerInput, TESTNET_ARK_PREFIXES)) {
|
|
41
|
+
return 'testnet';
|
|
42
|
+
}
|
|
43
|
+
if (startsWithAny(lowerInput, MAINNET_ARK_PREFIXES)) {
|
|
44
|
+
return 'mainnet';
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
export { getNetwork };
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bitcoin-decoder",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Decode bitcoin QR codes, URIs, and raw strings",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"source": "./src/index.ts",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "psycarlo",
|
|
19
|
+
"email": "psycarlo1@gmail.com",
|
|
20
|
+
"url": "https://psycarlo.com"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.build.json",
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"type-check": "tsc --noEmit",
|
|
26
|
+
"check": "ultracite check",
|
|
27
|
+
"fix": "ultracite fix",
|
|
28
|
+
"prepublishOnly": "bun run build"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@noble/hashes": "^2.0.1",
|
|
32
|
+
"@scure/base": "^2.0.0",
|
|
33
|
+
"bip-321": "^0.0.10",
|
|
34
|
+
"light-bolt11-decoder": "^3.2.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@biomejs/biome": "2.4.0",
|
|
38
|
+
"@types/bun": "^1.3.9",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"ultracite": "7.2.3"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"bitcoin",
|
|
44
|
+
"decode",
|
|
45
|
+
"on-chain",
|
|
46
|
+
"lightning",
|
|
47
|
+
"ark",
|
|
48
|
+
"bip-21",
|
|
49
|
+
"bip-321",
|
|
50
|
+
"uri"
|
|
51
|
+
],
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/Psycarlo/bitcoin-decode"
|
|
56
|
+
},
|
|
57
|
+
"bugs": {
|
|
58
|
+
"email": "psycarlo1@gmail.com",
|
|
59
|
+
"url": "https://github.com/Psycarlo/bitcoin-decode/issues"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://github.com/Psycarlo/bitcoin-decode#readme",
|
|
62
|
+
"workspaces": [
|
|
63
|
+
"website"
|
|
64
|
+
]
|
|
65
|
+
}
|