From 51f1d69e87e9fe2eab0b3af44a3b066e0fd24aeb Mon Sep 17 00:00:00 2001 From: Sean Gilligan Date: Wed, 3 Aug 2022 09:05:47 -0700 Subject: [PATCH] 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. --- .../main/java/org/bitcoinj/core/Address.java | 21 +++---- .../java/org/bitcoinj/core/AddressParser.java | 55 +++++++++++++++++++ .../bitcoinj/core/DefaultAddressParser.java | 48 ++++++++++++++++ .../java/org/bitcoinj/core/LegacyAddress.java | 3 +- .../java/org/bitcoinj/uri/BitcoinURI.java | 7 ++- .../main/java/org/bitcoinj/wallet/Wallet.java | 19 ++++++- .../core/AddressComparatorSortTest.java | 4 +- .../org/bitcoinj/examples/DoubleSpend.java | 5 +- .../bitcoinj/examples/ForwardingService.java | 8 ++- .../org/bitcoinj/examples/PrivateKeys.java | 5 +- .../org/bitcoinj/examples/SendRequest.java | 2 +- .../controls/BitcoinAddressValidator.java | 12 ++-- .../wallettemplate/SendMoneyController.java | 4 +- .../org/bitcoinj/wallettool/WalletTool.java | 6 +- 14 files changed, 161 insertions(+), 38 deletions(-) create mode 100644 core/src/main/java/org/bitcoinj/core/AddressParser.java create mode 100644 core/src/main/java/org/bitcoinj/core/DefaultAddressParser.java diff --git a/core/src/main/java/org/bitcoinj/core/Address.java b/core/src/main/java/org/bitcoinj/core/Address.java index 4799f4200..5adbc4956 100644 --- a/core/src/main/java/org/bitcoinj/core/Address.java +++ b/core/src/main/java/org/bitcoinj/core/Address.java @@ -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}). *

- * 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

{ + 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
{ * 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); } /** diff --git a/core/src/main/java/org/bitcoinj/core/AddressParser.java b/core/src/main/java/org/bitcoinj/core/AddressParser.java new file mode 100644 index 000000000..0d0febfec --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/AddressParser.java @@ -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; + } +} diff --git a/core/src/main/java/org/bitcoinj/core/DefaultAddressParser.java b/core/src/main/java/org/bitcoinj/core/DefaultAddressParser.java new file mode 100644 index 000000000..7dd37bf61 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/DefaultAddressParser.java @@ -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); + } + } + } +} diff --git a/core/src/main/java/org/bitcoinj/core/LegacyAddress.java b/core/src/main/java/org/bitcoinj/core/LegacyAddress.java index d472ed4b5..07fd2f534 100644 --- a/core/src/main/java/org/bitcoinj/core/LegacyAddress.java +++ b/core/src/main/java/org/bitcoinj/core/LegacyAddress.java @@ -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 diff --git a/core/src/main/java/org/bitcoinj/uri/BitcoinURI.java b/core/src/main/java/org/bitcoinj/uri/BitcoinURI.java index 7ce6ba80b..6f28df501 100644 --- a/core/src/main/java/org/bitcoinj/uri/BitcoinURI.java +++ b/core/src/main/java/org/bitcoinj/uri/BitcoinURI.java @@ -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 BIP 0021 */ 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); diff --git a/core/src/main/java/org/bitcoinj/wallet/Wallet.java b/core/src/main/java/org/bitcoinj/wallet/Wallet.java index 86003d17b..994c58426 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Wallet.java +++ b/core/src/main/java/org/bitcoinj/wallet/Wallet.java @@ -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.

*/ 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)}. */ diff --git a/core/src/test/java/org/bitcoinj/core/AddressComparatorSortTest.java b/core/src/test/java/org/bitcoinj/core/AddressComparatorSortTest.java index bc9295576..df93ae8cd 100644 --- a/core/src/test/java/org/bitcoinj/core/AddressComparatorSortTest.java +++ b/core/src/test/java/org/bitcoinj/core/AddressComparatorSortTest.java @@ -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 diff --git a/examples/src/main/java/org/bitcoinj/examples/DoubleSpend.java b/examples/src/main/java/org/bitcoinj/examples/DoubleSpend.java index 5dcb64b64..a6ced5c61 100644 --- a/examples/src/main/java/org/bitcoinj/examples/DoubleSpend.java +++ b/examples/src/main/java/org/bitcoinj/examples/DoubleSpend.java @@ -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) -> { diff --git a/examples/src/main/java/org/bitcoinj/examples/ForwardingService.java b/examples/src/main/java/org/bitcoinj/examples/ForwardingService.java index 7c36255bb..8ae214bf6 100644 --- a/examples/src/main/java/org/bitcoinj/examples/ForwardingService.java +++ b/examples/src/main/java/org/bitcoinj/examples/ForwardingService.java @@ -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(); } diff --git a/examples/src/main/java/org/bitcoinj/examples/PrivateKeys.java b/examples/src/main/java/org/bitcoinj/examples/PrivateKeys.java index 671d788f9..5d0e86af3 100644 --- a/examples/src/main/java/org/bitcoinj/examples/PrivateKeys.java +++ b/examples/src/main/java/org/bitcoinj/examples/PrivateKeys.java @@ -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); diff --git a/examples/src/main/java/org/bitcoinj/examples/SendRequest.java b/examples/src/main/java/org/bitcoinj/examples/SendRequest.java index 29acaa85d..6bdaeb518 100644 --- a/examples/src/main/java/org/bitcoinj/examples/SendRequest.java +++ b/examples/src/main/java/org/bitcoinj/examples/SendRequest.java @@ -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. diff --git a/wallettemplate/src/main/java/org/bitcoinj/walletfx/controls/BitcoinAddressValidator.java b/wallettemplate/src/main/java/org/bitcoinj/walletfx/controls/BitcoinAddressValidator.java index 619de55b3..3536b52cb 100644 --- a/wallettemplate/src/main/java/org/bitcoinj/walletfx/controls/BitcoinAddressValidator.java +++ b/wallettemplate/src/main/java/org/bitcoinj/walletfx/controls/BitcoinAddressValidator.java @@ -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; diff --git a/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java b/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java index f7c152483..e79dc15b2 100644 --- a/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java +++ b/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java @@ -69,7 +69,7 @@ public class SendMoneyController implements OverlayController !WTUtils.didThrow(() -> checkState(Coin.parseCoin(text).compareTo(balance) <= 0))); amountEdit.setText(balance.toPlainString()); @@ -84,7 +84,7 @@ public class SendMoneyController implements OverlayController { 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 { 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 { 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.");