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

bernardladenthin / BitcoinAddressFinder / #335

02 Jul 2025 05:54PM UTC coverage: 66.224% (+0.3%) from 65.91%
#335

push

bernardladenthin
Fix usage in test.

1298 of 1960 relevant lines covered (66.22%)

0.66 hits per line

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

91.47
/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.google.common.hash.Hashing;
22
import java.lang.reflect.Method;
23
import java.util.Arrays;
24
import java.nio.ByteBuffer;
25
import org.bitcoinj.base.Base58;
26
import org.bitcoinj.base.Bech32;
27
import org.bitcoinj.base.Coin;
28
import org.bitcoinj.base.SegwitAddress;
29
import org.bitcoinj.base.exceptions.AddressFormatException;
30
import org.bouncycastle.util.encoders.DecoderException;
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
    public static final String BITCOIN_CASH_PREFIX = "bitcoincash:";
48
    
49
    
50
    public final static int VERSION_BYTES_REGULAR = 1;
51
    public final static int VERSION_BYTES_ZCASH = 2;
52
    public final static int CHECKSUM_BYTES_REGULAR = 4;
53
    
54
    /**
55
    * Witness version 0, used for SegWit v0 addresses such as:
56
    * <ul>
57
    *   <li><b>P2WPKH</b> – Pay to Witness Public Key Hash</li>
58
    *   <li><b>P2WSH</b> – Pay to Witness Script Hash</li>
59
    * </ul>
60
    * Defined in <a href="https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki">BIP-173</a>.
61
    */
62
   private static final int WITNESS_VERSION_0 = 0;
63

64
   /**
65
    * Witness version 1, introduced with Taproot (SegWit v1):
66
    * <ul>
67
    *   <li><b>P2TR</b> – Pay to Taproot</li>
68
    * </ul>
69
    * Defined in <a href="https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki">BIP-341</a> and
70
    * encoded using Bech32m as specified in <a href="https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki">BIP-350</a>.
71
    */
72
   private static final int WITNESS_VERSION_1 = 1;
73
    
74
    /**
75
     * Parses a line containing an address and optional amount.
76
     * Returns {@code null} if the address is unsupported, malformed, or marked as ignored.
77
     * <p>
78
     * If no coin amount is specified in the line, {@link #DEFAULT_COIN} is used as a fallback.
79
     *
80
     * @param line the line to parse
81
     * @param keyUtility the {@link KeyUtility} used for conversions
82
     * @return an {@link AddressToCoin} instance, or {@code null} if the address is invalid or unsupported
83
     */
84
    @Nullable
85
    public AddressToCoin fromLine(String line, KeyUtility keyUtility) {
86
        // Remove the Bitcoin Cash prefix (which includes a colon) to avoid incorrect splitting.
87
        // This ensures the address is recognized properly and not misinterpreted during parsing.
88
        if(line.contains(BITCOIN_CASH_PREFIX)) {
1✔
89
            line = line.replace(BITCOIN_CASH_PREFIX, "");
1✔
90
        }
91
        String[] lineSplitted = SeparatorFormat.split(line);
1✔
92
        String address = lineSplitted[0];
1✔
93
        Coin amount = getCoinIfPossible(lineSplitted, DEFAULT_COIN);
1✔
94
        address = address.trim();
1✔
95
        if (address.isEmpty() || address.startsWith(IGNORE_LINE_PREFIX) || address.startsWith(ADDRESS_HEADER)) {
1✔
96
            return null;
1✔
97
        }
98
        
99
        // Riecoin: ScriptPubKey-style encoded address (hex with OP codes)
100
        {
101
            final String OP_DUP = "76";
1✔
102
            final String OP_HASH160 = "a9";
1✔
103
            final String OP_PUSH_20_BYTES = "14";
1✔
104
            final int length20Bytes = PublicKeyBytes.RIPEMD160_HASH_NUM_BYTES;
1✔
105
            final String riecoinP2SHPrefix = OP_DUP + OP_HASH160 + OP_PUSH_20_BYTES;
1✔
106
            final int riecoinScriptPubKeyLengthHex = length20Bytes * 2 + riecoinP2SHPrefix.length();
1✔
107
            if (address.length() >= riecoinScriptPubKeyLengthHex && address.startsWith(riecoinP2SHPrefix)) {
1✔
108
                final String hash160Hex = address.substring(riecoinP2SHPrefix.length(), length20Bytes*2+riecoinP2SHPrefix.length());
1✔
109
                final ByteBuffer hash160 = keyUtility.byteBufferUtility.getByteBufferFromHex(hash160Hex);
1✔
110
                return new AddressToCoin(hash160, amount, AddressType.P2PKH_OR_P2SH);
1✔
111
            }
112
        }
113

114
        // Blockchair Multisig (P2MS) format is not supported
115
        if (
1✔
116
               address.startsWith("d-")
1✔
117
            || address.startsWith("m-")
1✔
118
            || address.startsWith("s-")
1✔
119
        ) {
120
            return null;
1✔
121
        }
122
        
123
        // BitCore WKH format (Base36-encoded hash160)
124
        if (address.startsWith("wkh_")) {
1✔
125
            // BitCore (WKH) is base36 encoded hash160
126
            String addressWKH = address.substring("wkh_".length());
1✔
127
            byte[] hash160 = new Base36Decoder().decodeBase36ToFixedLengthBytes(addressWKH, PublicKeyBytes.RIPEMD160_HASH_NUM_BYTES);
1✔
128
            ByteBuffer hash160AsByteBuffer = keyUtility.byteBufferUtility.byteArrayToByteBuffer(hash160);
1✔
129
            return new AddressToCoin(hash160AsByteBuffer, amount, AddressType.P2WPKH);
1✔
130
        }
131
        
132
        // Bech32 decoding (P2WPKH, P2WSH, P2TR)
133
        try {
134
            Bech32.Bech32Data bechData = Bech32.decode(address);
1✔
135
            
136
            // protected: bechData.witnessVersion();
137
            short witnessVersion = invokeProtectedMethod(bechData, "witnessVersion", Short.class);
1✔
138
            // protected: bechData.witnessProgram();
139
            byte[] witnessProgram = invokeProtectedMethod(bechData, "witnessProgram", byte[].class);
1✔
140
            
141
            switch (witnessVersion) {
1✔
142
                case WITNESS_VERSION_0:
143
                    if (witnessProgram.length == SegwitAddress.WITNESS_PROGRAM_LENGTH_PKH) {
1✔
144
                        ByteBuffer hash160 = keyUtility.byteBufferUtility.byteArrayToByteBuffer(witnessProgram);
1✔
145
                        return new AddressToCoin(hash160, amount, AddressType.P2WPKH); // P2WPKH supported
1✔
146
                    } else if (witnessProgram.length == SegwitAddress.WITNESS_PROGRAM_LENGTH_SH) {
1✔
147
                        byte[] scriptHash = witnessProgram;
1✔
148
                        return null; // P2WSH not supported
1✔
149
                    }
150
                    break;
151
                case WITNESS_VERSION_1:
152
                    if (witnessProgram.length == SegwitAddress.WITNESS_PROGRAM_LENGTH_TR) {
1✔
153
                        byte[] tweakedPublicKey = witnessProgram;
1✔
154
                        return null; // P2TR not supported
1✔
155
                    }
156
                    break;
157
                default:
158
                    // not supported
159
                    return null;
1✔
160
            }
161
        } catch (AddressFormatException | ReflectiveOperationException  e) {
1✔
162
            // Bech32 parsing or reflection failed; continue to next format
163
        }
×
164
        
165
        // ZCash or Peercoin with 2-byte version
166
        if (address.startsWith("p") || address.startsWith("t")) {
1✔
167
            // p: bitcoin cash / CashAddr (P2SH), this is a unique format and does not work
168
            // p: peercoin possible
169
            // t: ZCash has two version bytes
170
            try {
171
                AddressToCoin addressToCoin = parseBase58Address(address, VERSION_BYTES_ZCASH, CHECKSUM_BYTES_REGULAR, keyUtility);
1✔
172
                return new AddressToCoin(addressToCoin.hash160(), amount, addressToCoin.type());
1✔
173
            } catch (RuntimeException e) {
1✔
174
                // Fall through to other format checks
175
            }
176
        }
177
        
178
        try {
179
            // Bitcoin Cash 'q' prefix: convert to legacy address
180
            if (address.startsWith("q")) {
1✔
181
                byte[] payload = extractPKHFromBitcoinCashAddress(address);
1✔
182
                ByteBuffer hash160 = keyUtility.byteBufferUtility.byteArrayToByteBuffer(payload);
1✔
183
                return new AddressToCoin(hash160, amount, AddressType.P2PKH_OR_P2SH);
1✔
184
            }
185
        } catch (DecoderException e) {
×
186
            throw e;
×
187
        } catch (RuntimeException | ReflectiveOperationException e) {
1✔
188
            return null;
1✔
189
        }
1✔
190
        
191
        // Fallback: assume Base58 with 1-byte version prefix
192
        try {
193
            AddressToCoin addressToCoin = parseBase58Address(address, VERSION_BYTES_REGULAR, CHECKSUM_BYTES_REGULAR, keyUtility);
1✔
194
            return new AddressToCoin(addressToCoin.hash160(), amount, addressToCoin.type());
1✔
195
        } catch (AddressFormatException e) {
1✔
196
            return null;
1✔
197
        }
198
    }
199

200
    public static byte[] extractPKHFromBitcoinCashAddress(String address) throws ReflectiveOperationException {
201
        if (address.startsWith(BITCOIN_CASH_PREFIX)) {
1✔
202
            address = address.substring(BITCOIN_CASH_PREFIX.length());
1✔
203
        }
204
        byte[] decoded5 = decodeBech32CharsetToValues(address);
1✔
205
        byte[] decoded8 = decode5to8WithPadding(decoded5);
1✔
206
        // Extracts the payload portion from the decoded Bech32 data.
207
        // Skips the first byte (address type/version) and removes the last 6 bytes (checksum).
208
        // The result is the raw payload encoded in 5-bit format.
209
        byte[] payload = Arrays.copyOfRange(decoded8, 1, decoded8.length - 6);
1✔
210
        return payload;
1✔
211
    }
212
    
213
    @SuppressWarnings("unchecked")
214
    private <T> T invokeProtectedMethod(Bech32.Bech32Bytes bech32Bytes, String methodName, Class<T> returnType) throws ReflectiveOperationException  {
215
        Class<?> clazz = Bech32.Bech32Bytes.class;
1✔
216
        Method method = clazz.getDeclaredMethod(methodName);
1✔
217
        method.setAccessible(true);
1✔
218
        return (T) method.invoke(bech32Bytes);
1✔
219
    }
220
    
221
    public static byte[] decodeBech32CharsetToValues(String base32String) {
222
        // Bech32 character set as defined in BIP-0173
223
        final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
1✔
224

225
        // Prepare lookup table for fast character-to-value resolution
226
        int[] lookup = new int[128];
1✔
227
        Arrays.fill(lookup, -1);
1✔
228
        for (int i = 0; i < CHARSET.length(); i++) {
1✔
229
            lookup[CHARSET.charAt(i)] = i;
1✔
230
        }
231

232
        // Decode characters to 5-bit values
233
        int len = base32String.length();
1✔
234
        byte[] result = new byte[len];
1✔
235
        for (int i = 0; i < len; i++) {
1✔
236
            char c = base32String.charAt(i);
1✔
237
            if (c >= 128 || lookup[c] == -1) {
1✔
238
                throw new IllegalArgumentException("Invalid character in Bech32 string: " + c);
1✔
239
            }
240
            result[i] = (byte) lookup[c];
1✔
241
        }
242

243
        return result;
1✔
244
    }
245
    
246
    /**
247
     * Return the data, fully-decoded with 8-bits per byte.
248
     * @return The data, fully-decoded as a byte array.
249
     */
250
    private static byte[] decode5to8(byte[] bytes) throws ReflectiveOperationException {
251
        return invokeConvertBitsStatic(bytes, 0, bytes.length, 5, 8, false);
×
252
    }
253
    
254
    private static byte[] decode5to8WithPadding(byte[] bytes) throws ReflectiveOperationException {
255
        return invokeConvertBitsStatic(bytes, 0, bytes.length, 5, 8, true);
1✔
256
    }
257
    
258
    private static byte[] encode8to5(byte[] data) throws ReflectiveOperationException {
259
        return invokeConvertBitsStatic(data, 0, data.length, 8, 5, true);
×
260
    }
261
    
262
    @SuppressWarnings("unchecked")
263
    private static byte[] invokeConvertBitsStatic(byte[] in, int inStart, int inLen, int fromBits, int toBits, boolean pad) throws ReflectiveOperationException {
264
        Method method = Bech32.class.getDeclaredMethod("convertBits", byte[].class, int.class, int.class, int.class, int.class, boolean.class);
1✔
265
        method.setAccessible(true);
1✔
266
        try {
267
            return (byte[]) method.invoke(null, in, inStart, inLen, fromBits, toBits, pad);
1✔
268
        } catch (ReflectiveOperationException e) {
×
269
            // rethrow AddressFormatException if it's the underlying cause
270
            Throwable cause = e.getCause();
×
271
            if (cause instanceof AddressFormatException) {
×
272
                throw (AddressFormatException) cause;
×
273
            }
274
            throw e;
×
275
        }
276
    }
277
    
278
    AddressToCoin parseBase58Address(String base58, int versionBytes, int checksumBytes, KeyUtility keyUtility) {
279
        byte[] decoded = Base58.decode(base58);
1✔
280
        
281
        final byte[] version;
282
        if (versionBytes > 0) {
1✔
283
            // copy version bytes
284
            version = new byte[versionBytes];
1✔
285
            System.arraycopy(decoded, 0, version, 0, version.length);
1✔
286
        } else {
287
            version = null;
×
288
        }
289
        
290
        byte[] hash160 = new byte[20];
1✔
291
        int storedBytes = Math.min(decoded.length - versionBytes, hash160.length);
1✔
292
        {
293
            // copy hash160
294
            System.arraycopy(decoded, versionBytes, hash160, 0, storedBytes);
1✔
295
        }
296
        
297
        final byte[] checksum;
298
        if (decoded.length >= versionBytes + hash160.length + checksumBytes) {
1✔
299
            checksum = new byte[checksumBytes];
1✔
300
            // copy cheksum
301
            System.arraycopy(decoded, versionBytes + storedBytes, checksum, 0, checksum.length);
1✔
302
            //String checksumAsHex = org.apache.commons.codec.binary.Hex.encodeHexString(checksum);
303
        } else {
304
            checksum = null;
1✔
305
        }
306
        
307
        boolean checksumMatches = false;
1✔
308
        if (version != null && checksum != null) {
1✔
309
            byte[] payload = new byte[version.length + hash160.length];
1✔
310
            System.arraycopy(version, 0, payload, 0, version.length);
1✔
311
            System.arraycopy(hash160, 0, payload, version.length, hash160.length);
1✔
312

313
            byte[] firstHash = Hashing.sha256().hashBytes(payload).asBytes();
1✔
314
            byte[] secondHash = Hashing.sha256().hashBytes(firstHash).asBytes();
1✔
315
            byte[] calculatedChecksum = Arrays.copyOfRange(secondHash, 0, checksumBytes);
1✔
316

317
            checksumMatches = Arrays.equals(calculatedChecksum, checksum);
1✔
318
        }
319
        
320
        //String decodedAsHex = org.apache.commons.codec.binary.Hex.encodeHexString(decoded);
321
        //String hash160AsHex = org.apache.commons.codec.binary.Hex.encodeHexString(hash160);
322

323
        ByteBuffer hash160AsByteBuffer = keyUtility.byteBufferUtility.byteArrayToByteBuffer(hash160);
1✔
324
        
325
        // fallback
326
        AddressType addressType = AddressType.P2PKH_OR_P2SH;
1✔
327
        
328
        String versionAsHex = org.apache.commons.codec.binary.Hex.encodeHexString(version);
1✔
329
        AddressToCoin addressToCoin = new AddressToCoin(hash160AsByteBuffer, DEFAULT_COIN, addressType);
1✔
330
        return addressToCoin;
1✔
331
    }
332

333
    @Nullable
334
    private Coin getCoinIfPossible(String[] lineSplitted, Coin defaultValue) throws NumberFormatException {
335
        if (lineSplitted.length > 1) {
1✔
336
            String amountString = lineSplitted[1];
1✔
337
            try {
338
                return Coin.valueOf(Long.valueOf(amountString));
1✔
339
            } catch (NumberFormatException e) {
1✔
340
                return defaultValue;
1✔
341
            }
342
        } else {
343
            return defaultValue;
1✔
344
        }
345
    }
346
}
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