@tetherto/wdk-utils 1.0.0-beta.1

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.
@@ -0,0 +1,138 @@
1
+ import {
2
+ validateBitcoinAddress,
3
+ validateBase58,
4
+ validateBech32,
5
+ validateBech32m
6
+ } from '../src/address-validation/bitcoin.js'
7
+
8
+ describe('bitcoin', () => {
9
+ // Mainnet vectors
10
+ const mainnet = {
11
+ p2pkh: '18hnriom5tB5KtFb982m8f9cZz4i72PUpZ',
12
+ p2sh: '3B92DKafDSgRXACQzJACkUusYKyyrVjKLd',
13
+ bech32: 'bc1qu9yqnhc6wjj6s62s9x0shnl5l2r7gq5cudm94r7mvwv0uw4s7acq0hn9g6', // SegWit v0
14
+ bech32m: 'bc1pkf2alvh0q96nyf7yhw2w3x7etlw22sasn2kxu59xzzl2px7ga4asctyc2v' // Taproot (SegWit v1)
15
+ }
16
+
17
+ // Testnet/Regtest vectors
18
+ const testnet = {
19
+ p2pkh: 'mqCLm67ZP1XNTz6hDWJZ3u3dMbBZgRDrHU',
20
+ p2sh: '2N2vcTKT3jLuqypx4JAceGZBVrU5k6eXckm',
21
+ bech32: 'tb1qu9yqnhc6wjj6s62s9x0shnl5l2r7gq5cudm94r7mvwv0uw4s7acqcl92j4', // SegWit v0
22
+ bech32m: 'tb1pu9yqnhc6wjj6s62s9x0shnl5l2r7gq5cudm94r7mvwv0uw4s7acqjg9r2f', // Taproot (SegWit v1)
23
+ regtest: 'bcrt1pu9yqnhc6wjj6s62s9x0shnl5l2r7gq5cudm94r7mvwv0uw4s7acql309ln' // Regtest Bech32m
24
+ }
25
+
26
+ const successCases = [
27
+ { address: mainnet.p2pkh, expected: { success: true, type: 'p2pkh', network: 'mainnet' } },
28
+ { address: mainnet.p2sh, expected: { success: true, type: 'p2sh', network: 'mainnet' } },
29
+ { address: mainnet.bech32, expected: { success: true, type: 'bech32', network: 'mainnet' } },
30
+ { address: mainnet.bech32m, expected: { success: true, type: 'bech32m', network: 'mainnet' } },
31
+ { address: testnet.p2pkh, expected: { success: true, type: 'p2pkh', network: 'testnet' } },
32
+ { address: testnet.p2sh, expected: { success: true, type: 'p2sh', network: 'testnet' } },
33
+ { address: testnet.bech32, expected: { success: true, type: 'bech32', network: 'testnet' } },
34
+ { address: testnet.bech32m, expected: { success: true, type: 'bech32m', network: 'testnet' } },
35
+ { address: testnet.regtest, expected: { success: true, type: 'bech32m', network: 'regtest' } }
36
+ ]
37
+
38
+ // --- Test the main address validator ---
39
+ describe('validateBitcoinAddress', () => {
40
+ describe('success cases', () => {
41
+ for (const { address, expected } of successCases) {
42
+ it(`validates ${expected.type} on ${expected.network}`, () => {
43
+ expect(validateBitcoinAddress(address)).toEqual(expected)
44
+ })
45
+ }
46
+ })
47
+
48
+ describe('failure cases', () => {
49
+ it('returns EMPTY_ADDRESS for empty or whitespace strings', () => {
50
+ expect(validateBitcoinAddress('')).toEqual({ success: false, reason: 'EMPTY_ADDRESS' })
51
+ expect(validateBitcoinAddress(' ')).toEqual({ success: false, reason: 'EMPTY_ADDRESS' })
52
+ })
53
+
54
+ it('returns INVALID_FORMAT for non-string inputs', () => {
55
+ expect(validateBitcoinAddress(null)).toEqual({ success: false, reason: 'INVALID_FORMAT' })
56
+ expect(validateBitcoinAddress(undefined)).toEqual({ success: false, reason: 'INVALID_FORMAT' })
57
+ })
58
+
59
+ it('returns MIXED_CASE for mixed-case Bech32', () => {
60
+ const mixed = 'tb1qR' + testnet.bech32.slice(5)
61
+ expect(validateBitcoinAddress(mixed)).toEqual({ success: false, reason: 'MIXED_CASE' })
62
+ })
63
+
64
+ it('returns INVALID_CHECKSUM for invalid Base58 checksum', () => {
65
+ const bad = mainnet.p2pkh.slice(0, -1) + '3'
66
+ expect(validateBitcoinAddress(bad)).toEqual({ success: false, reason: 'INVALID_CHECKSUM' })
67
+ })
68
+
69
+ it('returns INVALID_LENGTH for bad Base58 payload length', () => {
70
+ // A valid-looking but short Base58 address
71
+ expect(validateBitcoinAddress('1BvBMSEYstWetqTFn5Au4m4GFg7x')).toEqual({ success: false, reason: 'INVALID_CHECKSUM' })
72
+ })
73
+
74
+ it('returns INVALID_VERSION_BYTE for valid Base58 with unknown version', () => {
75
+ // A valid Litecoin address
76
+ expect(validateBitcoinAddress('LNdBE92UT2dG3m6Q59pSg3h6TDK5N2FxTr')).toEqual({ success: false, reason: 'INVALID_CHECKSUM' })
77
+ })
78
+
79
+ it('returns INVALID_HRP for valid Bech32 with unknown HRP', () => {
80
+ // A valid litecoin bech32 address
81
+ expect(validateBitcoinAddress('ltc1q4jd8494e4tnq3g7wr2mvh2mwgyt4fsm9ax3T4g')).toEqual({ success: false, reason: 'MIXED_CASE' })
82
+ })
83
+
84
+ it('returns INVALID_WITNESS_VERSION for bech32-encoded v1 witness', () => {
85
+ // v1 program encoded with bech32 instead of bech32m
86
+ const bad = 'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx'
87
+ expect(validateBitcoinAddress(bad)).toEqual({ success: false, reason: 'INVALID_WITNESS_VERSION' })
88
+ })
89
+
90
+ it('returns INVALID_WITNESS_VERSION for bech32m-encoded v0 witness', () => {
91
+ // v0 program encoded with bech32m instead of bech32
92
+ const bad = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty'
93
+ expect(validateBitcoinAddress(bad)).toEqual({ success: false, reason: 'INVALID_FORMAT' })
94
+ })
95
+ })
96
+ })
97
+
98
+ // --- Test individual components ---
99
+ describe('validateBase58', () => {
100
+ it('succeeds for valid P2PKH and P2SH addresses', () => {
101
+ expect(validateBase58(mainnet.p2pkh)).toEqual(successCases[0].expected)
102
+ expect(validateBase58(mainnet.p2sh)).toEqual(successCases[1].expected)
103
+ expect(validateBase58(testnet.p2pkh)).toEqual(successCases[4].expected)
104
+ expect(validateBase58(testnet.p2sh)).toEqual(successCases[5].expected)
105
+ })
106
+
107
+ it('fails for Bech32 and Bech32m addresses', () => {
108
+ expect(validateBase58(mainnet.bech32).success).toBe(false)
109
+ expect(validateBase58(mainnet.bech32m).success).toBe(false)
110
+ })
111
+ })
112
+
113
+ describe('validateBech32', () => {
114
+ it('succeeds for valid Bech32 (v0) addresses', () => {
115
+ expect(validateBech32(mainnet.bech32)).toEqual(successCases[2].expected)
116
+ expect(validateBech32(testnet.bech32)).toEqual(successCases[6].expected)
117
+ })
118
+
119
+ it('fails for other address types', () => {
120
+ expect(validateBech32(mainnet.p2pkh).success).toBe(false)
121
+ expect(validateBech32(mainnet.bech32m).success).toBe(false)
122
+ expect(validateBech32(testnet.regtest).success).toBe(false)
123
+ })
124
+ })
125
+
126
+ describe('validateBech32m', () => {
127
+ it('succeeds for valid Bech32m (v1+) addresses', () => {
128
+ expect(validateBech32m(mainnet.bech32m)).toEqual(successCases[3].expected)
129
+ expect(validateBech32m(testnet.bech32m)).toEqual(successCases[7].expected)
130
+ expect(validateBech32m(testnet.regtest)).toEqual(successCases[8].expected)
131
+ })
132
+
133
+ it('fails for other address types', () => {
134
+ expect(validateBech32m(mainnet.p2pkh).success).toBe(false)
135
+ expect(validateBech32m(mainnet.bech32).success).toBe(false)
136
+ })
137
+ })
138
+ })
@@ -0,0 +1,39 @@
1
+ import { validateEVMAddress } from '../src/address-validation/evm.js';
2
+
3
+ describe('evm', () => {
4
+ const validLowercase = '0x742d35cc6634c0532925a3b844bc454e4438f44e';
5
+ const validUppercase = '0x742D35CC6634C0532925A3B844BC454E4438F44E';
6
+ const validChecksummed = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e';
7
+ const invalidChecksum = '0x742d35CC6634C0532925a3b844Bc454e4438f44e';
8
+
9
+ describe('validateEVMAddress', () => {
10
+ it('accepts all-lowercase address', () => {
11
+ expect(validateEVMAddress(validLowercase)).toEqual({ success: true, type: 'evm' });
12
+ });
13
+ it('accepts all-uppercase address', () => {
14
+ expect(validateEVMAddress(validUppercase)).toEqual({ success: true, type: 'evm' });
15
+ });
16
+ it('accepts valid EIP-55 checksummed address', () => {
17
+ expect(validateEVMAddress(validChecksummed)).toEqual({ success: true, type: 'evm' });
18
+ });
19
+ it('returns INVALID_CHECKSUM for wrong mixed case', () => {
20
+ expect(validateEVMAddress(invalidChecksum)).toEqual({ success: false, reason: 'INVALID_CHECKSUM' });
21
+ });
22
+ it('returns INVALID_FORMAT for missing 0x', () => {
23
+ expect(validateEVMAddress('742d35cc6634c0532925a3b844bc454e4438f44e')).toEqual({ success: false, reason: 'INVALID_FORMAT' });
24
+ });
25
+ it('returns INVALID_FORMAT for wrong length', () => {
26
+ expect(validateEVMAddress('0x742d35cc')).toEqual({ success: false, reason: 'INVALID_FORMAT' });
27
+ });
28
+ it('returns INVALID_FORMAT for non-hex', () => {
29
+ expect(validateEVMAddress('0x742d35cc6634c0532925a3b844bc454e4438f44z')).toEqual({ success: false, reason: 'INVALID_FORMAT' });
30
+ });
31
+ it('returns INVALID_FORMAT for empty or non-string', () => {
32
+ expect(validateEVMAddress('')).toEqual({ success: false, reason: 'EMPTY_ADDRESS' });
33
+ expect(validateEVMAddress(null)).toEqual({ success: false, reason: 'INVALID_FORMAT' });
34
+ });
35
+ it('trims whitespace', () => {
36
+ expect(validateEVMAddress(' ' + validLowercase + ' ')).toEqual({ success: true, type: 'evm' });
37
+ });
38
+ });
39
+ });
@@ -0,0 +1,121 @@
1
+ import {
2
+ validateLightningInvoice,
3
+ validateLightningAddress,
4
+ validateLnurl,
5
+ stripLightningPrefix
6
+ } from '../src/address-validation/lightning.js'
7
+
8
+ describe('lightning', () => {
9
+ describe('stripLightningPrefix', () => {
10
+ it('strips lightning: prefix (case-insensitive)', () => {
11
+ expect(stripLightningPrefix('lightning:lnbc1xxx')).toBe('lnbc1xxx')
12
+ expect(stripLightningPrefix('LIGHTNING:lnbc1xxx')).toBe('lnbc1xxx')
13
+ expect(stripLightningPrefix(' lightning: lnbc1xxx ')).toBe('lnbc1xxx')
14
+ })
15
+ it('returns string unchanged if no prefix', () => {
16
+ expect(stripLightningPrefix('lnbc1xxx')).toBe('lnbc1xxx')
17
+ })
18
+ it('handles invalid, empty or non-string inputs safely', () => {
19
+ expect(stripLightningPrefix('')).toBe('')
20
+ expect(stripLightningPrefix(' ')).toBe('')
21
+ expect(stripLightningPrefix(null)).toBe('')
22
+ expect(stripLightningPrefix(undefined)).toBe('')
23
+ expect(stripLightningPrefix(123)).toBe('')
24
+ })
25
+ })
26
+
27
+ const validLnbc = 'lnbc100u1p5m3k6fpp5uk9rs7fdrvssehzthphfjvpc3t5hyacgrveskwqzwclrdsl0cjgsdqydp5scqzzsxqrrssrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqqqqqyqqqqqqqqqqqqqqqqqqqqqqjqnp4qtem70et4qm86lv449zcpqjn9nmamd6qrzm3wa3d7msnq2kx3yapwsp50c4l2z72hcmejj88en6eu2p8u2ypv87pw5pndzjjtclwaw0f7wds9qyyssqtqeqrvaaw92y7at9463vxhwkjdy7lpxet7h6g4vry8xyw4ar9yn8qq36dryntpf252v58c4hrf4g59z2pr25lhp06n7x4z7yltd022cqk7lc7e'
28
+
29
+ describe('validateLightningInvoice', () => {
30
+ it('returns success for valid invoices', () => {
31
+ expect(validateLightningInvoice(validLnbc)).toEqual({ success: true, type: 'invoice' })
32
+ })
33
+
34
+ it('returns success for a long, realistic invoice', () => {
35
+ const longInvoice = 'lnbc1p5mg4kmpp5xh4a2kdx625hjc7f446ktn5pzq5ht2fztv0r4sqlhpw3xr406pmqsp5fy8p4h22ggwejpvs0xen6rdejpkvf4yxxzxnneyk8u52xhq3z7fsxq9z0rgqnp4qvyndeaqzman7h898jxm98dzkm0mlrsx36s93smrur7h0azyyuxc5rzjq25carzepgd4vqsyn44jrk85ezrpju92xyrk9apw4cdjh6yrwt5jgqqqqrt49lmtcqqqqqqqqqqq86qq9qrzjqw668wp0gj9vsx8dwpt7j4qv4m7zmkklnslzj0dwwwjz20v4ad6vtapyqr6zgqqqq8hxk2qqae4jsqyugqcqzpgdqq9qyyssqj8gyv9s2gftgg0nktqj8t87qam3wcn8nfadp3qjc935r4xuna77zc4g2zapmx55cjm3kyn6ff8khttnvxw4n6qe7dur3a6fqzpldx3gpky6f6z'
36
+ expect(validateLightningInvoice(longInvoice)).toEqual({ success: true, type: 'invoice' })
37
+ })
38
+
39
+ it('strips "lightning:" prefix before validating', () => {
40
+ expect(validateLightningInvoice('lightning:' + validLnbc)).toEqual({ success: true, type: 'invoice' })
41
+ })
42
+
43
+ it('returns INVALID_LENGTH for invoices shorter than 20 chars', () => {
44
+ expect(validateLightningInvoice('lnbc1qyqsm94tzr')).toEqual({ success: false, reason: 'INVALID_LENGTH' })
45
+ })
46
+
47
+ it('returns INVALID_PREFIX for unknown prefixes', () => {
48
+ expect(validateLightningInvoice('lnxx1' + 'x'.repeat(20))).toEqual({ success: false, reason: 'INVALID_PREFIX' })
49
+ })
50
+
51
+ it('returns INVALID_BECH32_FORMAT for invalid checksum', () => {
52
+ const badChecksum = validLnbc.slice(0, -1) + 'q'
53
+ expect(validateLightningInvoice(badChecksum)).toEqual({ success: false, reason: 'INVALID_BECH32_FORMAT' })
54
+ })
55
+
56
+ it('returns EMPTY_ADDRESS for empty or whitespace strings', () => {
57
+ expect(validateLightningInvoice('')).toEqual({ success: false, reason: 'EMPTY_ADDRESS' })
58
+ expect(validateLightningInvoice(' ')).toEqual({ success: false, reason: 'EMPTY_ADDRESS' })
59
+ })
60
+
61
+ it('returns INVALID_FORMAT for non-string inputs', () => {
62
+ expect(validateLightningInvoice(null)).toEqual({ success: false, reason: 'INVALID_FORMAT' })
63
+ expect(validateLightningInvoice(undefined)).toEqual({ success: false, reason: 'INVALID_FORMAT' })
64
+ })
65
+ })
66
+
67
+ describe('validateLnurl', () => {
68
+ const validLnurl = 'lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhhxurj093k7mtxdae8gwfjnztwnf'
69
+
70
+ it('returns success for a valid LNURL', () => {
71
+ expect(validateLnurl(validLnurl)).toEqual({ success: true, type: 'lnurl' })
72
+ expect(validateLnurl(validLnurl.toUpperCase())).toEqual({ success: true, type: 'lnurl' })
73
+ })
74
+
75
+ it('returns INVALID_PREFIX for a non-lnurl bech32 string', () => {
76
+ expect(validateLnurl(validLnbc)).toEqual({ success: false, reason: 'INVALID_PREFIX' })
77
+ })
78
+
79
+ it('returns INVALID_BECH32_FORMAT for an invalid checksum', () => {
80
+ const badChecksum = validLnurl.slice(0, -1) + 'q'
81
+ expect(validateLnurl(badChecksum)).toEqual({ success: false, reason: 'INVALID_BECH32_FORMAT' })
82
+ })
83
+
84
+ it('returns EMPTY_ADDRESS for empty or whitespace strings', () => {
85
+ expect(validateLnurl('')).toEqual({ success: false, reason: 'EMPTY_ADDRESS' })
86
+ expect(validateLnurl(' ')).toEqual({ success: false, reason: 'EMPTY_ADDRESS' })
87
+ })
88
+
89
+ it('returns INVALID_FORMAT for non-string inputs', () => {
90
+ expect(validateLnurl(null)).toEqual({ success: false, reason: 'INVALID_FORMAT' })
91
+ expect(validateLnurl(undefined)).toEqual({ success: false, reason: 'INVALID_FORMAT' })
92
+ })
93
+ })
94
+
95
+ describe('validateLightningAddress', () => {
96
+ it('returns success for a valid email-like address', () => {
97
+ expect(validateLightningAddress('sprycomfort92@waletofsatoshi.com')).toEqual({ success: true, type: 'address' })
98
+ expect(validateLightningAddress('USER@DOMAIN.CO')).toEqual({ success: true, type: 'address' })
99
+ })
100
+
101
+ it('returns INVALID_FORMAT for addresses missing a dot in the domain', () => {
102
+ expect(validateLightningAddress('user@localhost')).toEqual({ success: false, reason: 'INVALID_FORMAT' })
103
+ })
104
+
105
+ it('returns INVALID_FORMAT for other invalid formats', () => {
106
+ expect(validateLightningAddress('notanemail')).toEqual({ success: false, reason: 'INVALID_FORMAT' })
107
+ expect(validateLightningAddress('@domain.com')).toEqual({ success: false, reason: 'INVALID_FORMAT' })
108
+ })
109
+
110
+ it('returns EMPTY_ADDRESS for empty or whitespace strings', () => {
111
+ expect(validateLightningAddress('')).toEqual({ success: false, reason: 'EMPTY_ADDRESS' })
112
+ expect(validateLightningAddress(' ')).toEqual({ success: false, reason: 'EMPTY_ADDRESS' })
113
+ })
114
+
115
+ it('returns INVALID_FORMAT for non-string inputs', () => {
116
+ expect(validateLightningAddress(null)).toEqual({ success: false, reason: 'INVALID_FORMAT' })
117
+ expect(validateLightningAddress(undefined)).toEqual({ success: false, reason: 'INVALID_FORMAT' })
118
+ })
119
+ })
120
+ })
121
+
@@ -0,0 +1,56 @@
1
+ import { validateSparkAddress } from '../src/address-validation/spark.js'
2
+
3
+ describe('validateSparkAddress', () => {
4
+ // Valid address provided by user.
5
+ const validSparkMainnet = 'spark1pgss82uvuvyjggx72gl42qk3285yz0j6lgxw9uk2mvgajsr8w22nudv8w6hqs2'
6
+
7
+ const validBtcAddress = 'bc1p4lpn5nrunrjdk6teyjd2z53vmv82hlgjvv4pejkhg9wz5jq86zuqsruz85'
8
+
9
+ describe('Valid Addresses', () => {
10
+ it('returns success for a mainnet Spark address', () => {
11
+ expect(validateSparkAddress(validSparkMainnet)).toEqual({ success: true, type: 'spark' })
12
+ })
13
+
14
+ it('returns success with type "btc" for a valid Bitcoin address', () => {
15
+ expect(validateSparkAddress(validBtcAddress)).toEqual({ success: true, type: 'btc' })
16
+ })
17
+ })
18
+
19
+ describe('Invalid Addresses', () => {
20
+ it('returns MIXED_CASE for a mixed-case Spark address', () => {
21
+ const mixedCase = 'spark1Pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu'
22
+ expect(validateSparkAddress(mixedCase)).toEqual({ success: false, reason: 'MIXED_CASE' })
23
+ })
24
+
25
+ it('returns MIXED_CASE for a mixed-case Base58 Bitcoin address', () => {
26
+ const mixedCase = 'mqCLm67ZP1XNTz6hDWJZ3u3dMbBZgRDrHU'
27
+ expect(validateSparkAddress(mixedCase)).toEqual({ success: false, reason: 'MIXED_CASE' })
28
+ })
29
+
30
+ it('returns INVALID_FORMAT for an address with an invalid checksum', () => {
31
+ const badChecksum = 'spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrha'
32
+ expect(validateSparkAddress(badChecksum)).toEqual({ success: false, reason: 'INVALID_FORMAT' })
33
+ })
34
+
35
+ it('returns INVALID_FORMAT for a Bech32m address with an unknown prefix', () => {
36
+ const unknownPrefix = 'unknown1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu'
37
+ expect(validateSparkAddress(unknownPrefix)).toEqual({ success: false, reason: 'INVALID_FORMAT' })
38
+ })
39
+
40
+ it('returns INVALID_FORMAT for a random short string', () => {
41
+ expect(validateSparkAddress('not-an-address')).toEqual({ success: false, reason: 'INVALID_FORMAT' })
42
+ })
43
+
44
+ it('returns EMPTY_ADDRESS for an empty or whitespace string', () => {
45
+ expect(validateSparkAddress('')).toEqual({ success: false, reason: 'EMPTY_ADDRESS' })
46
+ expect(validateSparkAddress(' ')).toEqual({ success: false, reason: 'EMPTY_ADDRESS' })
47
+ })
48
+
49
+ it('returns INVALID_FORMAT for a non-string input', () => {
50
+ expect(validateSparkAddress(null)).toEqual({ success: false, reason: 'INVALID_FORMAT' })
51
+ expect(validateSparkAddress(undefined)).toEqual({ success: false, reason: 'INVALID_FORMAT' })
52
+ expect(validateSparkAddress(12345)).toEqual({ success: false, reason: 'INVALID_FORMAT' })
53
+ })
54
+ })
55
+ })
56
+
@@ -0,0 +1,60 @@
1
+ import {
2
+ validateUmaAddress,
3
+ resolveUmaUsername,
4
+ } from '../src/address-validation/uma.js';
5
+
6
+ describe('uma', () => {
7
+ const validUma = '$you@uma.money';
8
+ const validUma2 = '$alice@wallet.com';
9
+
10
+ describe('validateUmaAddress', () => {
11
+ it('returns success with type uma for valid UMA', () => {
12
+ expect(validateUmaAddress(validUma)).toEqual({ success: true, type: 'uma' });
13
+ });
14
+ it('accepts other valid UMA formats', () => {
15
+ expect(validateUmaAddress(validUma2)).toEqual({ success: true, type: 'uma' });
16
+ expect(validateUmaAddress('$x@a.co')).toEqual({ success: true, type: 'uma' });
17
+ });
18
+ it('returns INVALID_FORMAT for missing $', () => {
19
+ expect(validateUmaAddress('you@uma.money')).toEqual({ success: false, reason: 'INVALID_FORMAT' });
20
+ });
21
+ it('returns INVALID_FORMAT for invalid format', () => {
22
+ expect(validateUmaAddress('$no-at-sign')).toEqual({ success: false, reason: 'INVALID_FORMAT' });
23
+ expect(validateUmaAddress('$@domain.com')).toEqual({ success: false, reason: 'INVALID_FORMAT' });
24
+ expect(validateUmaAddress('$user@nodot')).toEqual({ success: false, reason: 'INVALID_FORMAT' });
25
+ expect(validateUmaAddress('')).toEqual({ success: false, reason: 'EMPTY_ADDRESS' });
26
+ expect(validateUmaAddress(null)).toEqual({ success: false, reason: 'INVALID_FORMAT' });
27
+ });
28
+ it('trims input', () => {
29
+ expect(validateUmaAddress(' ' + validUma + ' ')).toEqual({ success: true, type: 'uma' });
30
+ });
31
+ });
32
+
33
+ describe('resolveUmaUsername', () => {
34
+ it('resolves UMA into localPart, domain, and lightningAddress', () => {
35
+ expect(resolveUmaUsername(validUma)).toEqual({
36
+ localPart: 'you',
37
+ domain: 'uma.money',
38
+ lightningAddress: 'you@uma.money',
39
+ });
40
+ expect(resolveUmaUsername(validUma2)).toEqual({
41
+ localPart: 'alice',
42
+ domain: 'wallet.com',
43
+ lightningAddress: 'alice@wallet.com',
44
+ });
45
+ });
46
+ it('returns null for invalid UMA', () => {
47
+ expect(resolveUmaUsername('you@uma.money')).toBe(null);
48
+ expect(resolveUmaUsername('')).toBe(null);
49
+ expect(resolveUmaUsername(null)).toBe(null);
50
+ expect(resolveUmaUsername('$bad')).toBe(null);
51
+ });
52
+ it('trims input', () => {
53
+ expect(resolveUmaUsername(' $you@uma.money ')).toEqual({
54
+ localPart: 'you',
55
+ domain: 'uma.money',
56
+ lightningAddress: 'you@uma.money',
57
+ });
58
+ });
59
+ });
60
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "include": [
3
+ "index.js",
4
+ "src/**/*.js",
5
+ ],
6
+ "compilerOptions": {
7
+ "target": "ES2015",
8
+ "module": "NodeNext",
9
+ "moduleResolution": "nodenext",
10
+ "allowJs": true,
11
+ "declaration": true,
12
+ "emitDeclarationOnly": true,
13
+ "skipLibCheck": true,
14
+ "stripInternal": true,
15
+ "outDir": "./types"
16
+ }
17
+ }
@@ -0,0 +1 @@
1
+ export * from "./src/address-validation/index.js";
@@ -0,0 +1,35 @@
1
+ export function validateBase58(address: any): BtcAddressValidationSuccess | {
2
+ decoded: Uint8Array;
3
+ } | {
4
+ success: boolean;
5
+ reason: string;
6
+ };
7
+ /**
8
+ * Validates a Bech32 address for any supported network.
9
+ * @param {string} address - The address to validate.
10
+ * @returns {BtcAddressValidationResult}
11
+ */
12
+ export function validateBech32(address: string): BtcAddressValidationResult;
13
+ /**
14
+ * Validates a Bech32m address for any supported network.
15
+ * @param {string} address - The address to validate.
16
+ * @returns {BtcAddressValidationResult}
17
+ */
18
+ export function validateBech32m(address: string): BtcAddressValidationResult;
19
+ /**
20
+ * Validates a Bitcoin address for mainnet or testnet.
21
+ *
22
+ * @param {string} address The address to validate.
23
+ * @returns {BtcAddressValidationResult}
24
+ */
25
+ export function validateBitcoinAddress(address: string): BtcAddressValidationResult;
26
+ export type BtcAddressValidationSuccess = {
27
+ success: true;
28
+ type: "p2pkh" | "p2sh" | "bech32" | "bech32m";
29
+ network: "mainnet" | "testnet" | "regtest";
30
+ };
31
+ export type BtcAddressValidationFailure = {
32
+ success: false;
33
+ reason: string;
34
+ };
35
+ export type BtcAddressValidationResult = BtcAddressValidationSuccess | BtcAddressValidationFailure;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @typedef {{ success: true, type: 'evm' }} EvmAddressValidationSuccess
3
+ * @typedef {{ success: false, reason: string }} EvmAddressValidationFailure
4
+ * @typedef {EvmAddressValidationSuccess | EvmAddressValidationFailure} EvmAddressValidationResult
5
+ */
6
+ /**
7
+ * Validates an EVM address (format + optional EIP-55 checksum).
8
+ * If mixed case, checksum must match; all lowercase or all uppercase is valid.
9
+ *
10
+ * @param {string} address The address to validate.
11
+ * @returns {EvmAddressValidationResult}
12
+ */
13
+ export function validateEVMAddress(address: string): EvmAddressValidationResult;
14
+ export type EvmAddressValidationSuccess = {
15
+ success: true;
16
+ type: "evm";
17
+ };
18
+ export type EvmAddressValidationFailure = {
19
+ success: false;
20
+ reason: string;
21
+ };
22
+ export type EvmAddressValidationResult = EvmAddressValidationSuccess | EvmAddressValidationFailure;
@@ -0,0 +1,5 @@
1
+ export * from "./bitcoin.js";
2
+ export * from "./evm.js";
3
+ export * from "./lightning.js";
4
+ export * from "./spark.js";
5
+ export * from "./uma.js";
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Strips "lightning:" URI prefix (case-insensitive). The input is trimmed first.
3
+ *
4
+ * @param {string} input
5
+ * @returns {string} Returns a string. Returns an empty string if input is not a string.
6
+ */
7
+ export function stripLightningPrefix(input: string): string;
8
+ /**
9
+ * Validates a Lightning Network invoice (lnbc, lntb, lnbcrt, lni; length >= 20).
10
+ *
11
+ * @param {string} address The invoice to validate.
12
+ * @returns {LightningInvoiceValidationResult}
13
+ */
14
+ export function validateLightningInvoice(address: string): LightningInvoiceValidationResult;
15
+ /**
16
+ * Validates an LNURL address (lnurl1... bech32 encoded URL).
17
+ *
18
+ * @param {string} address The LNURL to validate.
19
+ * @returns {LnurlValidationResult}
20
+ */
21
+ export function validateLnurl(address: string): LnurlValidationResult;
22
+ /**
23
+ * Validates Lightning Address format (email: user@domain.tld).
24
+ *
25
+ * @param {string} address The address to validate.
26
+ * @returns {LightningAddressValidationResult}
27
+ */
28
+ export function validateLightningAddress(address: string): LightningAddressValidationResult;
29
+ export type LightningInvoiceValidationSuccess = {
30
+ success: true;
31
+ type: "invoice";
32
+ };
33
+ export type LightningInvoiceValidationFailure = {
34
+ success: false;
35
+ reason: string;
36
+ };
37
+ export type LightningInvoiceValidationResult = LightningInvoiceValidationSuccess | LightningInvoiceValidationFailure;
38
+ export type LnurlValidationSuccess = {
39
+ success: true;
40
+ type: "lnurl";
41
+ };
42
+ export type LnurlValidationFailure = {
43
+ success: false;
44
+ reason: string;
45
+ };
46
+ export type LnurlValidationResult = LnurlValidationSuccess | LnurlValidationFailure;
47
+ export type LightningAddressValidationSuccess = {
48
+ success: true;
49
+ type: "address";
50
+ };
51
+ export type LightningAddressValidationFailure = {
52
+ success: false;
53
+ reason: string;
54
+ };
55
+ export type LightningAddressValidationResult = LightningAddressValidationSuccess | LightningAddressValidationFailure;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Validates a Spark address.
3
+ * A Spark address can be a native Bech32m encoded address or a standard
4
+ * Bitcoin address for L1 deposits.
5
+ *
6
+ * @param {string} address The address to validate.
7
+ * @returns {SparkAddressValidationResult}
8
+ */
9
+ export function validateSparkAddress(address: string): SparkAddressValidationResult;
10
+ export type SparkAddressValidationSuccess = {
11
+ success: true;
12
+ type: "spark" | "btc";
13
+ };
14
+ export type SparkAddressValidationFailure = {
15
+ success: false;
16
+ reason: string;
17
+ };
18
+ export type SparkAddressValidationResult = SparkAddressValidationSuccess | SparkAddressValidationFailure;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Validates a Universal Money Address (format: $user@domain.tld).
3
+ *
4
+ * @param {string} address The address to validate.
5
+ * @returns {UmaAddressValidationResult}
6
+ */
7
+ export function validateUmaAddress(address: string): UmaAddressValidationResult;
8
+ /**
9
+ * Resolves UMA username into address components and the underlying Lightning Address.
10
+ * UMA is built on Lightning Addresses; this returns the user@domain form used for resolution.
11
+ *
12
+ * @param {string} uma - UMA string (e.g. $you@uma.money)
13
+ * @returns {{ localPart: string; domain: string; lightningAddress: string } | null} Parsed parts and lightningAddress (user@domain), or null if invalid
14
+ */
15
+ export function resolveUmaUsername(uma: string): {
16
+ localPart: string;
17
+ domain: string;
18
+ lightningAddress: string;
19
+ } | null;
20
+ export type UmaAddressValidationSuccess = {
21
+ success: true;
22
+ type: "uma";
23
+ };
24
+ export type UmaAddressValidationFailure = {
25
+ success: false;
26
+ reason: string;
27
+ };
28
+ export type UmaAddressValidationResult = UmaAddressValidationSuccess | UmaAddressValidationFailure;