Address: deprecate fromString, replace with AddressParser

This change migrates from using `NetworkParameters` to `Network` for specifying the network and
also decouples from static methods in `Address` with an interface/implementation approach.

Note that there are 3 use cases for address parsing:

1. Any network is allowed - AddressParser.parseAddressAnyNetwork(String)
2. Parse for a specified network - AddressParser.parseAddress(String, Network)
3. Parse for a previously-specified (context dependent) network - AddressParser.Strict.parseAddress(String)

In most use cases, an AddressParser instance can be accessed through the Wallet,
which already knows the Network type and in this context validation for network makes
sense. This is why `Wallet` is implementing `AddressParser.Strict`

BitcoinURI allocates its own DefaultAddressParser for now, as do some other tests
and examples that don't have access to a Wallet

In the future DefaultAddressParser may be replaced by something loaded
via the ServiceLoader mechanism or other dynamically configured mechanism.
This commit is contained in:
Sean Gilligan 2022-08-03 09:05:47 -07:00 committed by Andreas Schildbach
parent 279b35b25f
commit 51f1d69e87
14 changed files with 161 additions and 38 deletions

View file

@ -30,10 +30,11 @@ import static com.google.common.base.Preconditions.checkNotNull;
/**
* Base class for addresses, e.g. native segwit addresses ({@link SegwitAddress}) or legacy addresses ({@link LegacyAddress}).
* <p>
* Use {@link #fromString(NetworkParameters, String)} to conveniently construct any kind of address from its textual
* Use an implementation of {@link AddressParser#parseAddress(String, BitcoinNetwork)} to conveniently construct any kind of address from its textual
* form.
*/
public abstract class Address implements Comparable<Address> {
protected static final AddressParser addressParser = new DefaultAddressParser();
protected final NetworkParameters params;
protected final byte[] bytes;
@ -60,22 +61,14 @@ public abstract class Address implements Comparable<Address> {
* if the given string doesn't parse or the checksum is invalid
* @throws AddressFormatException.WrongNetwork
* if the given string is valid but not for the expected network (eg testnet vs mainnet)
* @deprecated Use {@link org.bitcoinj.wallet.Wallet#parseAddress(String)} or {@link AddressParser#parseAddress(String, BitcoinNetwork)}
*/
@Deprecated
public static Address fromString(@Nullable NetworkParameters params, String str)
throws AddressFormatException {
try {
return LegacyAddress.fromBase58(params, str);
} catch (AddressFormatException.WrongNetwork x) {
throw x;
} catch (AddressFormatException x) {
try {
return SegwitAddress.fromBech32(params, str);
} catch (AddressFormatException.WrongNetwork x2) {
throw x;
} catch (AddressFormatException x2) {
throw new AddressFormatException(str);
}
}
return (params != null)
? addressParser.parseAddress(str, params.network())
: addressParser.parseAddressAnyNetwork(str);
}
/**

View file

@ -0,0 +1,55 @@
/*
* Copyright by the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.core;
import org.bitcoinj.base.BitcoinNetwork;
import org.bitcoinj.base.exceptions.AddressFormatException;
// TODO: Move this class to o.b.base
/**
* Interface for parsing and validating address strings.
*/
public interface AddressParser {
/**
* Parse an address that could be for any network
* @param addressString string representation of address
* @return A validated address object
* @throws AddressFormatException invalid address string
*/
Address parseAddressAnyNetwork(String addressString) throws AddressFormatException;
/**
* Parse an address and validate for specified network
* @param addressString string representation of address
* @param network the network the address string must represent
* @return A validated address object
* @throws AddressFormatException invalid address string or not valid for specified network
*/
Address parseAddress(String addressString, BitcoinNetwork network) throws AddressFormatException;
@FunctionalInterface
interface Strict {
/**
* Parse an address in a strict context (e.g. the network must be valid)
* @param addressString string representation of address
* @return A validated address object
* @throws AddressFormatException invalid address string or not valid for network (provided by context)
*/
Address parseAddress(String addressString) throws AddressFormatException;
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright by the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.core;
import org.bitcoinj.base.BitcoinNetwork;
import org.bitcoinj.base.exceptions.AddressFormatException;
/**
* Address Parser that knows about the address types supported by bitcoinj core.
*/
public class DefaultAddressParser implements AddressParser {
@Override
public Address parseAddressAnyNetwork(String addressString) throws AddressFormatException {
return parseAddress(addressString, null);
}
@Override
public Address parseAddress(String addressString, BitcoinNetwork network) throws AddressFormatException {
NetworkParameters params = (network != null) ? NetworkParameters.of(network) : null;
try {
return LegacyAddress.fromBase58(params, addressString);
} catch (AddressFormatException.WrongNetwork x) {
throw x;
} catch (AddressFormatException x) {
try {
return SegwitAddress.fromBech32(params, addressString);
} catch (AddressFormatException.WrongNetwork x2) {
throw x;
} catch (AddressFormatException x2) {
throw new AddressFormatException(addressString);
}
}
}
}

View file

@ -190,8 +190,7 @@ public class LegacyAddress extends Address {
*/
@Deprecated
public static NetworkParameters getParametersFromAddress(String address) throws AddressFormatException {
// TODO: Provide a `Network`-based mechanism for resolving "alt addresses"
return NetworkParameters.fromAddress(LegacyAddress.fromBase58(null, address));
return NetworkParameters.fromAddress(Address.addressParser.parseAddressAnyNetwork(address));
}
@Override

View file

@ -21,6 +21,8 @@ import org.bitcoinj.base.Network;
import org.bitcoinj.core.Address;
import org.bitcoinj.base.exceptions.AddressFormatException;
import org.bitcoinj.base.Coin;
import org.bitcoinj.core.AddressParser;
import org.bitcoinj.core.DefaultAddressParser;
import org.bitcoinj.core.NetworkParameters;
import javax.annotation.Nullable;
@ -78,6 +80,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
* @see <a href="https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki">BIP 0021</a>
*/
public class BitcoinURI {
private AddressParser addressParser = new DefaultAddressParser();
// Not worth turning into an enum
public static final String FIELD_MESSAGE = "message";
public static final String FIELD_LABEL = "label";
@ -177,7 +180,9 @@ public class BitcoinURI {
if (!addressToken.isEmpty()) {
// Attempt to parse the addressToken as a Bitcoin address for this network
try {
Address address = Address.fromString(params, addressToken);
Address address = (params != null)
? addressParser.parseAddress(addressToken, params.network())
: addressParser.parseAddressAnyNetwork(addressToken);
putWithValidation(FIELD_ADDRESS, address);
} catch (final AddressFormatException e) {
throw new BitcoinURIParseException("Bad address", e);

View file

@ -26,14 +26,17 @@ import com.google.protobuf.ByteString;
import net.jcip.annotations.GuardedBy;
import org.bitcoinj.base.BitcoinNetwork;
import org.bitcoinj.base.Network;
import org.bitcoinj.base.exceptions.AddressFormatException;
import org.bitcoinj.base.utils.StreamUtils;
import org.bitcoinj.core.AbstractBlockChain;
import org.bitcoinj.core.Address;
import org.bitcoinj.base.Base58;
import org.bitcoinj.core.AddressParser;
import org.bitcoinj.core.BlockChain;
import org.bitcoinj.core.BloomFilter;
import org.bitcoinj.base.Coin;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.DefaultAddressParser;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.FilteredBlock;
import org.bitcoinj.core.InsufficientMoneyException;
@ -174,8 +177,11 @@ import static com.google.common.base.Preconditions.checkState;
* for more information about this.</p>
*/
public class Wallet extends BaseTaggableObject
implements NewBestBlockListener, TransactionReceivedInBlockListener, PeerFilterProvider, KeyBag, TransactionBag, ReorganizeListener {
implements NewBestBlockListener, TransactionReceivedInBlockListener, PeerFilterProvider,
KeyBag, TransactionBag, ReorganizeListener, AddressParser.Strict {
private static final Logger log = LoggerFactory.getLogger(Wallet.class);
private static final AddressParser addressParser = new DefaultAddressParser();
// Ordering: lock > keyChainGroupLock. KeyChainGroup is protected separately to allow fast querying of current receive address
// even if the wallet itself is busy e.g. saving or processing a big reorg. Useful for reducing UI latency.
protected final ReentrantLock lock = Threading.lock(Wallet.class);
@ -494,6 +500,17 @@ public class Wallet extends BaseTaggableObject
return params;
}
/**
* Parse an address string using all formats this wallet knows about for the wallet's network type
* @param addressString Address string to parse
* @return A validated address
* @throws AddressFormatException if invalid string
*/
@Override
public Address parseAddress(String addressString) throws AddressFormatException {
return addressParser.parseAddress(addressString, params.network());
}
/**
* Gets the active keychains via {@link KeyChainGroup#getActiveKeyChains(long)}.
*/

View file

@ -31,6 +31,8 @@ import static org.junit.Assert.assertEquals;
* the default comparators.
*/
public class AddressComparatorSortTest {
private static final AddressParser addressParser = new DefaultAddressParser();
/**
* A manually sorted list of address for verifying sorting with our default comparator.
* See {@link Address#compareTo}.
@ -48,7 +50,7 @@ public class AddressComparatorSortTest {
// Test net, Segwit
"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
"tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx"
).map(s -> Address.fromString(null, s))
).map(addressParser::parseAddressAnyNetwork)
.collect(StreamUtils.toUnmodifiableList());
@Test

View file

@ -46,8 +46,9 @@ public class DoubleSpend {
System.out.println(kit.wallet());
kit.wallet().getBalanceFuture(COIN, Wallet.BalanceType.AVAILABLE).get();
Transaction tx1 = kit.wallet().createSend(Address.fromString(kit.params(), "bcrt1qsmf9envp5dphlu6my2tpwfmce0793jvpvlg5ez"), CENT);
Transaction tx2 = kit.wallet().createSend(Address.fromString(kit.params(), "bcrt1qsmf9envp5dphlu6my2tpwfmce0793jvpvlg5ez"), CENT.add(SATOSHI.multiply(10)));
Address destinationAddress = kit.wallet().parseAddress("bcrt1qsmf9envp5dphlu6my2tpwfmce0793jvpvlg5ez");
Transaction tx1 = kit.wallet().createSend(destinationAddress, CENT);
Transaction tx2 = kit.wallet().createSend(destinationAddress, CENT.add(SATOSHI.multiply(10)));
final Peer peer = kit.peerGroup().getConnectedPeers().get(0);
peer.addPreMessageReceivedEventListener(Threading.SAME_THREAD,
(peer1, m) -> {

View file

@ -21,9 +21,10 @@ import org.bitcoinj.base.ScriptType;
import org.bitcoinj.base.Sha256Hash;
import org.bitcoinj.core.Address;
import org.bitcoinj.base.Coin;
import org.bitcoinj.core.AddressParser;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.DefaultAddressParser;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionBroadcast;
import org.bitcoinj.core.TransactionConfidence;
@ -69,13 +70,14 @@ public class ForwardingService implements AutoCloseable {
// Figure out which network we should connect to. Each network gets its own set of files.
Address address;
BitcoinNetwork network;
AddressParser addressParser = new DefaultAddressParser();
if (args.length >= 2) {
// Verify address belongs to network
network = BitcoinNetwork.fromString(args[1]).orElseThrow();
address = Address.fromString(NetworkParameters.of(network), args[0]);
address = addressParser.parseAddress(args[0], network);
} else {
// Infer network from address
address = Address.fromString(null, args[0]);
address = addressParser.parseAddressAnyNetwork(args[0]);
network = address.network();
}

View file

@ -60,13 +60,14 @@ public class PrivateKeys {
key = ECKey.fromPrivate(privKey);
}
System.out.println("Address from private key is: " + SegwitAddress.fromKey(params, key).toString());
// And the address ...
Address destination = Address.fromString(params, args[1]);
// Import the private key to a fresh wallet.
Wallet wallet = Wallet.createDeterministic(params, ScriptType.P2PKH);
wallet.importKey(key);
// And the address ...
Address destination = wallet.parseAddress(args[1]);
// Find the transactions that involve those coins.
final MemoryBlockStore blockStore = new MemoryBlockStore(params);
BlockChain chain = new BlockChain(params, wallet, blockStore);

View file

@ -48,7 +48,7 @@ public class SendRequest {
// To which address you want to send the coins?
// The Address class represents a Bitcoin address.
Address to = Address.fromString(kit.params(), "bcrt1qspfueag7fvty7m8htuzare3xs898zvh30fttu2");
Address to = kit.wallet().parseAddress("bcrt1qspfueag7fvty7m8htuzare3xs898zvh30fttu2");
System.out.println("Send money to: " + to.toString());
// There are different ways to create and publish a SendRequest. This is probably the easiest one.

View file

@ -16,13 +16,12 @@
package org.bitcoinj.walletfx.controls;
import org.bitcoinj.base.BitcoinNetwork;
import org.bitcoinj.core.Address;
import org.bitcoinj.base.exceptions.AddressFormatException;
import org.bitcoinj.core.NetworkParameters;
import javafx.scene.Node;
import javafx.scene.control.TextField;
import org.bitcoinj.core.AddressParser;
import org.bitcoinj.walletfx.utils.TextFieldValidator;
/**
@ -30,11 +29,12 @@ import org.bitcoinj.walletfx.utils.TextFieldValidator;
* if the address is invalid for those params, and enable/disable the nodes.
*/
public class BitcoinAddressValidator {
private BitcoinNetwork network;
private final AddressParser.Strict parser;
private Node[] nodes;
public BitcoinAddressValidator(BitcoinNetwork network, TextField field, Node... nodes) {
this.network = network;
public BitcoinAddressValidator(AddressParser.Strict parser, TextField field, Node... nodes) {
this.parser = parser;
this.nodes = nodes;
// Handle the red highlighting, but don't highlight in red just when the field is empty because that makes
@ -52,7 +52,7 @@ public class BitcoinAddressValidator {
private boolean testAddr(String text) {
try {
Address.fromString(NetworkParameters.of(network), text);
parser.parseAddress(text);
return true;
} catch (AddressFormatException e) {
return false;

View file

@ -69,7 +69,7 @@ public class SendMoneyController implements OverlayController<SendMoneyControlle
app = WalletApplication.instance();
Coin balance = app.walletAppKit().wallet().getBalance();
checkState(!balance.isZero());
new BitcoinAddressValidator(app.network(), address, sendBtn);
new BitcoinAddressValidator(app.walletAppKit().wallet(), address, sendBtn);
new TextFieldValidator(amountEdit, text ->
!WTUtils.didThrow(() -> checkState(Coin.parseCoin(text).compareTo(balance) <= 0)));
amountEdit.setText(balance.toPlainString());
@ -84,7 +84,7 @@ public class SendMoneyController implements OverlayController<SendMoneyControlle
// Address exception cannot happen as we validated it beforehand.
try {
Coin amount = Coin.parseCoin(amountEdit.getText());
Address destination = Address.fromString(NetworkParameters.of(app.network()), address.getText());
Address destination = app.walletAppKit().wallet().parseAddress(address.getText());
SendRequest req;
if (amount.equals(app.walletAppKit().wallet().getBalance()))
req = SendRequest.emptyWallet(destination);

View file

@ -446,7 +446,7 @@ public class WalletTool implements Callable<Integer> {
if (selectAddrStr != null) {
Address selectAddr;
try {
selectAddr = Address.fromString(params, selectAddrStr);
selectAddr = wallet.parseAddress(selectAddrStr);
} catch (AddressFormatException x) {
System.err.println("Could not parse given address, or wrong network: " + selectAddrStr);
return 1;
@ -761,7 +761,7 @@ public class WalletTool implements Callable<Integer> {
addr = null;
} else {
// Treat as an address.
addr = Address.fromString(params, destination);
addr = wallet.parseAddress(destination);
key = null;
}
}
@ -1228,7 +1228,7 @@ public class WalletTool implements Callable<Integer> {
key = wallet.findKeyFromPubKey(HEX.decode(pubKeyStr));
} else {
try {
Address address = Address.fromString(wallet.getParams(), addrStr);
Address address = wallet.parseAddress(addrStr);
key = wallet.findKeyFromAddress(address);
} catch (AddressFormatException e) {
System.err.println(addrStr + " does not parse as a Bitcoin address of the right network parameters.");