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

bernardladenthin / BitcoinAddressFinder / #327

06 Jun 2025 10:36PM UTC coverage: 65.407% (-0.7%) from 66.078%
#327

push

bernardladenthin
refactor: simplify Bech32 detection and fallback, add tests for invalid addresses

4 of 4 new or added lines in 1 file covered. (100.0%)

8 existing lines in 3 files now uncovered.

1246 of 1905 relevant lines covered (65.41%)

0.65 hits per line

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

89.61
/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
        try {
110
            // bitcoin Bech32 (P2WSH or P2WPKH) or P2TR
111
            // supported (20 bytes): https://privatekeys.pw/address/bitcoin/bc1qazcm763858nkj2dj986etajv6wquslv8uxwczt
112

113
            // do everything manual
114
            Bech32.Bech32Data bechData = Bech32.decode(address);
1✔
115
            // is protected: bechData.witnessProgram();
116
            Class<?> clazz = Bech32.Bech32Bytes.class;
1✔
117
            Method witnessProgramMethod;
118
            try {
119
                witnessProgramMethod = clazz.getDeclaredMethod("witnessProgram");
1✔
120
                witnessProgramMethod.setAccessible(true);
1✔
121
                try {
122
                    byte[] hash160AsByteArray = (byte[]) witnessProgramMethod.invoke(bechData);
1✔
123
                    if (hash160AsByteArray.length == SegwitAddress.WITNESS_PROGRAM_LENGTH_PKH) {
1✔
124
                        final ByteBuffer hash160 = keyUtility.byteBufferUtility.byteArrayToByteBuffer(hash160AsByteArray);
1✔
125
                        return new AddressToCoin(hash160, amount);
1✔
126
                    } else if (hash160AsByteArray.length == SegwitAddress.WITNESS_PROGRAM_LENGTH_SH) {
1✔
127
                        // P2WSH is unsupported
128
                        return null;
1✔
129
                    } else if (hash160AsByteArray.length == SegwitAddress.WITNESS_PROGRAM_LENGTH_TR) {
1✔
130
                        // P2WTR is unsupported
131
                        return null;
×
132
                    } else {
133
                        throw new AddressFormatException();
1✔
134
                    }
135
                } catch (IllegalAccessException ex) {
×
136
                    // skip and continue
137
                } catch (InvocationTargetException ex) {
×
138
                    // skip and continue
139
                }
×
140
            } catch (NoSuchMethodException ex) {
×
141
                // skip and continue
142
            } catch (SecurityException ex) {
×
143
                // skip and continue
UNCOV
144
            }
×
145
        } catch(AddressFormatException e) {
1✔
146
            // skip and continue
UNCOV
147
        }
×
148
        
149
        if (address.startsWith("t")) {
1✔
150
            // ZCash has two version bytes
151
            ByteBuffer hash160 = getHash160AsByteBufferFromBase58AddressUnchecked(address, keyUtility, VERSION_BYTES_ZCASH);
1✔
152
            return new AddressToCoin(hash160, amount);
1✔
153
        } else if (address.startsWith("p")) {
1✔
154
            // p: bitcoin cash / CashAddr (P2SH), this is a unique format and does not work
155
            // p: peercoin possible
156
            try {
157
                ByteBuffer hash160 = getHash160AsByteBufferFromBase58AddressUnchecked(address, keyUtility, VERSION_BYTES_ZCASH);
1✔
158
                return new AddressToCoin(hash160, amount);
1✔
159
            } catch (RuntimeException e) {
1✔
160
                // will be thrown for bitcoin cash P2SH
161
                return null;
1✔
162
            }
163
        } else {
164
            try {
165
                ByteBuffer hash160 = getHash160AsByteBufferFromBase58AddressUnchecked(address, keyUtility, VERSION_BYTES_REGULAR);
1✔
166
                return new AddressToCoin(hash160, amount);
1✔
167
            } catch (AddressFormatException e) {
1✔
168
                return null;
1✔
169
            }
170
        }
171
    }
172

173
    private ByteBuffer getHash160AsByteBufferFromBase58AddressUnchecked(String base58, KeyUtility keyUtility, int srcPos) {
174
        byte[] hash160 = getHash160fromBase58AddressUnchecked(base58, srcPos);
1✔
175
        ByteBuffer hash160AsByteBuffer = keyUtility.byteBufferUtility.byteArrayToByteBuffer(hash160);
1✔
176
        return hash160AsByteBuffer;
1✔
177
    }
178

179
    byte[] getHash160fromBase58AddressUnchecked(String base58, int srcPos) {
180
        byte[] decoded = Base58.decode(base58);
1✔
181
        byte[] hash160 = new byte[20];
1✔
182
        int toCopy = Math.min(decoded.length - srcPos, hash160.length);
1✔
183
        System.arraycopy(decoded, srcPos, hash160, 0, toCopy);
1✔
184
        return hash160;
1✔
185
    }
186

187
    @Nullable
188
    private Coin getCoinIfPossible(String[] lineSplitted, Coin defaultValue) throws NumberFormatException {
189
        if (lineSplitted.length > 1) {
1✔
190
            String amountString = lineSplitted[1];
1✔
191
            try {
192
                return Coin.valueOf(Long.valueOf(amountString));
1✔
193
            } catch (NumberFormatException e) {
1✔
194
                return defaultValue;
1✔
195
            }
196
        } else {
197
            return defaultValue;
1✔
198
        }
199
    }
200
}
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