• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bernardladenthin / BitcoinAddressFinder / #323

06 Jun 2025 08:54PM UTC coverage: 66.684% (-0.3%) from 66.972%
#323

push

bernardladenthin
Improve Bech32 and WKH address handling; add P2MS support and cleanup

- Refactored and extended Bech32 address decoding with reflection for witnessProgram()
- Added support for BitCore WKH addresses using Base36 decoding
- Ignored P2MS formats using d-/m-/s- prefixes (e.g. Blockchair)
- Added test coverage for valid Bech32 decoding and WKH hash extraction
- Cleaned up old unreachable code and improved formatting for readability
- Updated support matrix in README.md accordingly

62 of 70 new or added lines in 2 files covered. (88.57%)

1 existing line in 1 file now uncovered.

1311 of 1966 relevant lines covered (66.68%)

0.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.75
/src/main/java/net/ladenthin/bitcoinaddressfinder/AddressTxtLine.java
1
// @formatter:off
2
/**
3
 * Copyright 2020 Bernard Ladenthin bernard.ladenthin@gmail.com
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *    http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 */
18
// @formatter:on
19
package net.ladenthin.bitcoinaddressfinder;
20

21
import com.github.kiulian.converter.AddressConverter;
22
import java.lang.reflect.InvocationTargetException;
23
import java.lang.reflect.Method;
24
import java.math.BigInteger;
25
import java.nio.ByteBuffer;
26
import org.bitcoinj.base.Base58;
27
import org.bitcoinj.base.Bech32;
28
import org.bitcoinj.base.Coin;
29
import org.bitcoinj.base.SegwitAddress;
30
import org.bitcoinj.base.exceptions.AddressFormatException;
31
import org.jspecify.annotations.Nullable;
32

33

34
/**
35
 * Most txt files have a common format which uses Base58 address and separated
36
 * anmount.
37
 */
38
public class AddressTxtLine {
1✔
39

40
    /**
41
     * Should not be {@link Coin#ZERO} because it can't be written to LMDB.
42
     */
43
    public static final Coin DEFAULT_COIN = Coin.SATOSHI;
1✔
44

45
    public static final String IGNORE_LINE_PREFIX = "#";
46
    public static final String ADDRESS_HEADER = "address";
47
    
48
    
49
    private final static int VERSION_BYTES_REGULAR = 1;
50
    private final static int VERSION_BYTES_ZCASH = 2;
51

52
    /**
53
     * If no coins can be found in the line {@link #DEFAULT_COIN} is used.
54
     *
55
     * @param line The line to parse.
56
     * @param keyUtility The {@link KeyUtility}.
57
     * @return Returns an {@link AddressToCoin} instance.
58
     */
59
    @Nullable
60
    public AddressToCoin fromLine(String line, KeyUtility keyUtility) {
61
        String[] lineSplitted = SeparatorFormat.split(line);
1✔
62
        String address = lineSplitted[0];
1✔
63
        Coin amount = getCoinIfPossible(lineSplitted, DEFAULT_COIN);
1✔
64
        address = address.trim();
1✔
65
        if (address.isEmpty() || address.startsWith(IGNORE_LINE_PREFIX) || address.startsWith(ADDRESS_HEADER)) {
1✔
66
            return null;
1✔
67
        }
68
        
69
        // Riecoin
70
        {
71
            final String OP_DUP = "76";
1✔
72
            final String OP_HASH160 = "a9";
1✔
73
            final String OP_PUSH_20_BYTES = "14";
1✔
74
            final int length20Bytes = PublicKeyBytes.RIPEMD160_HASH_NUM_BYTES;
1✔
75
            final String riecoinP2SHPrefix = OP_DUP + OP_HASH160 + OP_PUSH_20_BYTES;
1✔
76
            final int riecoinScriptPubKeyLengthHex = length20Bytes * 2 + riecoinP2SHPrefix.length();
1✔
77
            if (address.length() >= riecoinScriptPubKeyLengthHex && address.startsWith(riecoinP2SHPrefix)) {
1✔
78
                final String hash160Hex = address.substring(riecoinP2SHPrefix.length(), length20Bytes*2+riecoinP2SHPrefix.length());
1✔
79
                final ByteBuffer hash160 = keyUtility.byteBufferUtility.getByteBufferFromHex(hash160Hex);
1✔
80
                return new AddressToCoin(hash160, amount);
1✔
81
            }
82
        }
83

84
        // blockchair Multisig format prefix (P2MS)
85
        if (
1✔
86
               address.startsWith("d-")
1✔
87
            || address.startsWith("m-")
1✔
88
            || address.startsWith("s-")
1✔
89
        ) {
90
            return null;
1✔
91
        }
92

93
        if (address.startsWith("wkh_")) {
1✔
94
            // BitCore (WKH) is base36 encoded hash160
95
            String addressWKH = address.substring("wkh_".length());
1✔
96
            
97
            byte[] hash160 = new Base36Decoder().decodeBase36ToFixedLengthBytes(addressWKH, PublicKeyBytes.RIPEMD160_HASH_NUM_BYTES);
1✔
98

99
            ByteBuffer hash160AsByteBuffer = keyUtility.byteBufferUtility.byteArrayToByteBuffer(hash160);
1✔
100
            return new AddressToCoin(hash160AsByteBuffer, amount);
1✔
101
        }
102
        
103
        if (address.startsWith("q")) {
1✔
104
            // q: bitcoin cash Base58 (P2PKH)
105
            // convert to legacy address
106
            address = AddressConverter.toLegacyAddress(address);
1✔
107
        }
108
        
109
        if (
1✔
110
            // bitcoin
111
               address.startsWith("bc1")
1✔
112
            // Bitcoin Oil
113
            || address.startsWith("btco1")
1✔
114
            // Canada-eCoin
115
            || address.startsWith("cdn1q")
1✔
116
            // Canada-eCoin
117
            || address.startsWith("btx1")
1✔
118
            // DeFiChain
119
            || address.startsWith("df1q")
1✔
120
            // digibyte
121
            || address.startsWith("dgb1")
1✔
122
            // Doichain
123
            || address.startsWith("dc1q")
1✔
124
            // Groestlcoin || Groestlcoin TestNet
125
            || address.startsWith("grs1") || address.startsWith("tgrs1")
1✔
126
            // feathercoin
127
            || address.startsWith("fc1")
1✔
128
            // litecoin cash
129
            || address.startsWith("lcc1")
1✔
130
            // litecoin
131
            || address.startsWith("ltc1")
1✔
132
            // Mooncoin
133
            || address.startsWith("moon1")
1✔
134
            // Myriad
135
            || address.startsWith("my1q")
1✔
136
            // namecoin
137
            || address.startsWith("nc1")
1✔
138
            // Riecoin
139
            || address.startsWith("ric1")
1✔
140
            // SpaceXpanse
141
            || address.startsWith("rod1q")
1✔
142
            // syscoin
143
            || address.startsWith("sys1")
1✔
144
            // TheHolyRogerCoin
145
            || address.startsWith("rog1q")
1✔
146
            // UFO
147
            || address.startsWith("uf1q")
1✔
148
            // vertcoin
149
            || address.startsWith("vtc1")
1✔
150
        ) {
151
            // bitcoin Bech32 (P2WSH or P2WPKH) or P2TR
152
            // supported (20 bytes): https://privatekeys.pw/address/bitcoin/bc1qazcm763858nkj2dj986etajv6wquslv8uxwczt
153
            try {
154
                //Bech32.Bech32Data bech32Data = Bech32.decode(address);
155
            } catch (AddressFormatException e) {
156
                throw new RuntimeException(e);
157
            } catch (RuntimeException e) {
158
                throw e;
159
            }
160
            // do everything manual
161
            Bech32.Bech32Data bechData = Bech32.decode(address);
1✔
162
            // is protected: bechData.witnessProgram();
163
            Class<?> clazz = Bech32.Bech32Bytes.class;
1✔
164
            Method witnessProgramMethod;
165
            try {
166
                witnessProgramMethod = clazz.getDeclaredMethod("witnessProgram");
1✔
167
                witnessProgramMethod.setAccessible(true);
1✔
168
                try {
169
                    byte[] hash160AsByteArray = (byte[]) witnessProgramMethod.invoke(bechData);
1✔
170
                    if (hash160AsByteArray.length == SegwitAddress.WITNESS_PROGRAM_LENGTH_PKH) {
1✔
171
                        final ByteBuffer hash160 = keyUtility.byteBufferUtility.byteArrayToByteBuffer(hash160AsByteArray);
1✔
172
                        return new AddressToCoin(hash160, amount);
1✔
173
                    } else if (hash160AsByteArray.length == SegwitAddress.WITNESS_PROGRAM_LENGTH_SH) {
1✔
174
                        return null;
1✔
175
                    } else if (hash160AsByteArray.length == SegwitAddress.WITNESS_PROGRAM_LENGTH_TR) {
1✔
NEW
176
                        return null;
×
177
                    } else {
178
                        throw new AddressFormatException();
1✔
179
                    }
NEW
180
                } catch (IllegalAccessException ex) {
×
NEW
181
                } catch (InvocationTargetException ex) {
×
NEW
182
                }
×
NEW
183
            } catch (NoSuchMethodException ex) {
×
NEW
184
            } catch (SecurityException ex) {
×
NEW
185
            }
×
NEW
186
            throw new AddressFormatException();
×
187
        } else if (address.startsWith("p")) {
1✔
188
            // p: bitcoin cash / CashAddr (P2SH), this is a unique format and does not work
189
            // p: peercoin possible
190
            try {
191
                ByteBuffer hash160 = getHash160AsByteBufferFromBase58AddressUnchecked(address, keyUtility, VERSION_BYTES_ZCASH);
1✔
192
                return new AddressToCoin(hash160, amount);
1✔
193
            } catch (RuntimeException e) {
1✔
194
                return null;
1✔
195
            }
196
        } else if (
1✔
197
                   address.startsWith("1")
1✔
198
                || address.startsWith("2")
1✔
199
                || address.startsWith("3")
1✔
200
                || address.startsWith("4")
1✔
201
                || address.startsWith("7")
1✔
202
                || address.startsWith("8")
1✔
203
                || address.startsWith("9")
1✔
204
                || address.startsWith("a")
1✔
205
                || address.startsWith("A")
1✔
206
                || address.startsWith("B")
1✔
207
                || address.startsWith("b")
1✔
208
                || address.startsWith("C")
1✔
209
                || address.startsWith("d")
1✔
210
                || address.startsWith("D")
1✔
211
                || address.startsWith("E")
1✔
212
                || address.startsWith("F")
1✔
213
                || address.startsWith("G")
1✔
214
                || address.startsWith("H")
1✔
215
                || address.startsWith("i")
1✔
216
                || address.startsWith("J")
1✔
217
                || address.startsWith("K")
1✔
218
                || address.startsWith("L")
1✔
219
                || address.startsWith("M")
1✔
220
                || address.startsWith("N")
1✔
221
                || address.startsWith("p")
1✔
222
                || address.startsWith("P")
1✔
223
                || address.startsWith("Q")
1✔
224
                || address.startsWith("R")
1✔
225
                || address.startsWith("s")
1✔
226
                || address.startsWith("S")
1✔
227
                || address.startsWith("t")
1✔
228
                || address.startsWith("T")
1✔
229
                || address.startsWith("u")
1✔
230
                || address.startsWith("V")
1✔
231
                || address.startsWith("W")
1✔
232
                || address.startsWith("x")
1✔
233
                || address.startsWith("X")
1✔
234
        ) {
235
            // prefix clashes for signs: 2, 7, 8, 9, A, C, M
236
            //
237
            // Base58 P2SH
238
            // 2: Mooncoin / Particl
239
            // 3: litecoin deprecated / bitcoin / Particl
240
            // 7: dash / Riecoin
241
            // 8: DeFiChain / BYTZ
242
            // 9: dogecoin multisig / 42-coin
243
            // A: dogecoin / SaluS
244
            // B: CloakCoin
245
            // C: UFO
246
            // M: litecoin / Myriad
247
            // t: Zcash
248
            //
249
            // Base58 P2PKH
250
            // 1: Terracoin
251
            // 2: BitCore / Pinkcoin
252
            // 4: novacoin / Myriad / 42-coin
253
            // 7: feathercoin / Dimecoin
254
            // 8: Vanillacash
255
            // 9: Catcoin
256
            // a: Firo
257
            // A: AuroraCoin / Primecoin
258
            // B: curecoin / BitBlocks / blackcoin / UFO / Smileycoin / Blocknet / BitcoinPlus
259
            // b: Bitmark / BolivarCoin
260
            // C: CloakCoin / CROWN / ChessCoin / Artbyte / Canada-eCoin
261
            // d: LiteDoge / Diamond / DeFiChain
262
            // D: dogecoin / digibyte / PIVX / DigitalCoin / Divicoin / ColossusXT
263
            // e: Electron
264
            // E: Emerald / InfiniLooP
265
            // F: Groestlcoin
266
            // G: bitcoin gold / Goldcash
267
            // H: Herencia
268
            // i: Innova / I/O Coin / Infinitecoin
269
            // J: MasterNoder2
270
            // K: Lynx
271
            // L: litecoin / Luckycoin / Lanacoin / e-Gulden / Elite
272
            // M: Mooncoin
273
            // N: namecoin / Deutsche eMark / Doichain
274
            // p: Element
275
            // P: Peercoin / PotCoin / PAC Protocol / PutinCoin v2 / Particl / PandaCoin / PakCoin
276
            // Q: Quark
277
            // R: reddcoin / Komodo / NewYorkCoin / Particl / Raptoreum / SpaceXpanse
278
            // s: BYTZ
279
            // S: Sterlingcoin / Syscoin / SaluS / Alias
280
            // t: Zcash
281
            // T: Trezarcoin
282
            // u: Unobtanium
283
            // U: Coino
284
            // V: vertcoin / VeriCoin / Versacoin / TheHolyRogerCoin
285
            // W: WorldCoin
286
            // x: Clam / iXcoin
287
            // X: dash / Validity
288
            
289
            if (address.startsWith("t")) {
1✔
290
                // ZCash has two version bytes
291
                ByteBuffer hash160 = getHash160AsByteBufferFromBase58AddressUnchecked(address, keyUtility, VERSION_BYTES_ZCASH);
1✔
292
                return new AddressToCoin(hash160, amount);
1✔
293
            } else {
294
                ByteBuffer hash160 = getHash160AsByteBufferFromBase58AddressUnchecked(address, keyUtility, VERSION_BYTES_REGULAR);
1✔
295
                return new AddressToCoin(hash160, amount);
1✔
296
            }
297
        } else {
298
            // bitcoin Base58 (P2PKH)
299
            ByteBuffer hash160;
300
            try {
301
                hash160 = keyUtility.getHash160ByteBufferFromBase58String(address);
×
302
            } catch (
1✔
303
                    AddressFormatException.InvalidChecksum   // InvalidChecksum
304
                  | AddressFormatException.WrongNetwork      // e.g. bitcoin testnet
305
                  | AddressFormatException.InvalidDataLength // e.g. too short address
306
                  e
307
            ) {
308
                hash160 = getHash160AsByteBufferFromBase58AddressUnchecked(address, keyUtility, VERSION_BYTES_REGULAR);
1✔
UNCOV
309
            }
×
310
            return new AddressToCoin(hash160, amount);
1✔
311
        }
312
    }
313

314
    private ByteBuffer getHash160AsByteBufferFromBase58AddressUnchecked(String base58, KeyUtility keyUtility, int srcPos) {
315
        byte[] hash160 = getHash160fromBase58AddressUnchecked(base58, srcPos);
1✔
316
        ByteBuffer hash160AsByteBuffer = keyUtility.byteBufferUtility.byteArrayToByteBuffer(hash160);
1✔
317
        return hash160AsByteBuffer;
1✔
318
    }
319

320
    byte[] getHash160fromBase58AddressUnchecked(String base58, int srcPos) {
321
        byte[] decoded = Base58.decode(base58);
1✔
322
        byte[] hash160 = new byte[20];
1✔
323
        int toCopy = Math.min(decoded.length - srcPos, hash160.length);
1✔
324
        System.arraycopy(decoded, srcPos, hash160, 0, toCopy);
1✔
325
        return hash160;
1✔
326
    }
327

328
    @Nullable
329
    private Coin getCoinIfPossible(String[] lineSplitted, Coin defaultValue) throws NumberFormatException {
330
        if (lineSplitted.length > 1) {
1✔
331
            String amountString = lineSplitted[1];
1✔
332
            try {
333
                return Coin.valueOf(Long.valueOf(amountString));
1✔
334
            } catch (NumberFormatException e) {
1✔
335
                return defaultValue;
1✔
336
            }
337
        } else {
338
            return defaultValue;
1✔
339
        }
340
    }
341
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc