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

bernardladenthin / BitcoinAddressFinder / #326

06 Jun 2025 09:53PM UTC coverage: 66.078% (-0.6%) from 66.684%
#326

push

bernardladenthin
Refactor Bech32 handling and simplify Base58 fallback in AddressTxtLine

6 of 7 new or added lines in 1 file covered. (85.71%)

1 existing line in 1 file now uncovered.

1272 of 1925 relevant lines covered (66.08%)

0.66 hits per line

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

91.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
        ) try {
152
        //try {
153
            // bitcoin Bech32 (P2WSH or P2WPKH) or P2TR
154
            // supported (20 bytes): https://privatekeys.pw/address/bitcoin/bc1qazcm763858nkj2dj986etajv6wquslv8uxwczt
155

156
            // do everything manual
157
            Bech32.Bech32Data bechData = Bech32.decode(address);
1✔
158
            // is protected: bechData.witnessProgram();
159
            Class<?> clazz = Bech32.Bech32Bytes.class;
1✔
160
            Method witnessProgramMethod;
161
            try {
162
                witnessProgramMethod = clazz.getDeclaredMethod("witnessProgram");
1✔
163
                witnessProgramMethod.setAccessible(true);
1✔
164
                try {
165
                    byte[] hash160AsByteArray = (byte[]) witnessProgramMethod.invoke(bechData);
1✔
166
                    if (hash160AsByteArray.length == SegwitAddress.WITNESS_PROGRAM_LENGTH_PKH) {
1✔
167
                        final ByteBuffer hash160 = keyUtility.byteBufferUtility.byteArrayToByteBuffer(hash160AsByteArray);
1✔
168
                        return new AddressToCoin(hash160, amount);
1✔
169
                    } else if (hash160AsByteArray.length == SegwitAddress.WITNESS_PROGRAM_LENGTH_SH) {
1✔
170
                        // P2WSH is unsupported
171
                        return null;
1✔
172
                    } else if (hash160AsByteArray.length == SegwitAddress.WITNESS_PROGRAM_LENGTH_TR) {
1✔
173
                        // P2WTR is unsupported
UNCOV
174
                        return null;
×
175
                    } else {
176
                        throw new AddressFormatException();
1✔
177
                    }
178
                } catch (IllegalAccessException ex) {
×
179
                } catch (InvocationTargetException ex) {
×
180
                }
×
181
            } catch (NoSuchMethodException ex) {
×
182
            } catch (SecurityException ex) {
×
183
            }
×
184
        } catch(AddressFormatException e) {
1✔
185
            // throw this exception if its sure it was an bitcoin bech32 address, otherwise keep ahead
186
            //if(address.startsWith("bc1")) {
187
                throw e;
1✔
188
            //}
NEW
189
        }
×
190
        
191
        if (address.startsWith("t")) {
1✔
192
            // ZCash has two version bytes
193
            ByteBuffer hash160 = getHash160AsByteBufferFromBase58AddressUnchecked(address, keyUtility, VERSION_BYTES_ZCASH);
1✔
194
            return new AddressToCoin(hash160, amount);
1✔
195
        } else if (address.startsWith("p")) {
1✔
196
            // p: bitcoin cash / CashAddr (P2SH), this is a unique format and does not work
197
            // p: peercoin possible
198
            try {
199
                ByteBuffer hash160 = getHash160AsByteBufferFromBase58AddressUnchecked(address, keyUtility, VERSION_BYTES_ZCASH);
1✔
200
                return new AddressToCoin(hash160, amount);
1✔
201
            } catch (RuntimeException e) {
1✔
202
                // will be thrown for bitcoin cash P2SH
203
                return null;
1✔
204
            }
205
        } else {
206
            ByteBuffer hash160 = getHash160AsByteBufferFromBase58AddressUnchecked(address, keyUtility, VERSION_BYTES_REGULAR);
1✔
207
            return new AddressToCoin(hash160, amount);
1✔
208
        }
209
    }
210

211
    private ByteBuffer getHash160AsByteBufferFromBase58AddressUnchecked(String base58, KeyUtility keyUtility, int srcPos) {
212
        byte[] hash160 = getHash160fromBase58AddressUnchecked(base58, srcPos);
1✔
213
        ByteBuffer hash160AsByteBuffer = keyUtility.byteBufferUtility.byteArrayToByteBuffer(hash160);
1✔
214
        return hash160AsByteBuffer;
1✔
215
    }
216

217
    byte[] getHash160fromBase58AddressUnchecked(String base58, int srcPos) {
218
        byte[] decoded = Base58.decode(base58);
1✔
219
        byte[] hash160 = new byte[20];
1✔
220
        int toCopy = Math.min(decoded.length - srcPos, hash160.length);
1✔
221
        System.arraycopy(decoded, srcPos, hash160, 0, toCopy);
1✔
222
        return hash160;
1✔
223
    }
224

225
    @Nullable
226
    private Coin getCoinIfPossible(String[] lineSplitted, Coin defaultValue) throws NumberFormatException {
227
        if (lineSplitted.length > 1) {
1✔
228
            String amountString = lineSplitted[1];
1✔
229
            try {
230
                return Coin.valueOf(Long.valueOf(amountString));
1✔
231
            } catch (NumberFormatException e) {
1✔
232
                return defaultValue;
1✔
233
            }
234
        } else {
235
            return defaultValue;
1✔
236
        }
237
    }
238
}
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