From c4dd1cb9dd40a7a16829a00f45acbd55f63d9895 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 30 Jun 2020 09:22:23 +0200 Subject: [PATCH] address parsing support --- .../sparrowwallet/drongo/address/Address.java | 54 ++++++++++++++ .../address/InvalidAddressException.java | 19 +++++ .../sparrowwallet/drongo/protocol/Bech32.java | 2 +- .../drongo/wallet/UtxoSelector.java | 7 ++ .../drongo/address/AddressTest.java | 70 +++++++++++++++++++ 5 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/sparrowwallet/drongo/address/InvalidAddressException.java create mode 100644 src/main/java/com/sparrowwallet/drongo/wallet/UtxoSelector.java create mode 100644 src/test/java/com/sparrowwallet/drongo/address/AddressTest.java diff --git a/src/main/java/com/sparrowwallet/drongo/address/Address.java b/src/main/java/com/sparrowwallet/drongo/address/Address.java index 6117b08..6ea15a1 100644 --- a/src/main/java/com/sparrowwallet/drongo/address/Address.java +++ b/src/main/java/com/sparrowwallet/drongo/address/Address.java @@ -1,8 +1,13 @@ package com.sparrowwallet.drongo.address; import com.sparrowwallet.drongo.protocol.Base58; +import com.sparrowwallet.drongo.protocol.Bech32; import com.sparrowwallet.drongo.protocol.ScriptType; +import java.util.Arrays; + +import static com.sparrowwallet.drongo.address.P2WPKHAddress.HRP; + public abstract class Address { protected final byte[] hash; @@ -42,4 +47,53 @@ public abstract class Address { public int hashCode() { return getAddress().hashCode(); } + + public static Address fromString(String address) throws InvalidAddressException { + Exception nested = null; + + if(address != null && (address.startsWith("1") || address.startsWith("3"))) { + try { + byte[] decodedBytes = Base58.decodeChecked(address); + if(decodedBytes.length == 21) { + int version = decodedBytes[0]; + byte[] hash = Arrays.copyOfRange(decodedBytes, 1, 21); + if(version == 0) { + return new P2PKHAddress(hash); + } + if(version == 5) { + return new P2SHAddress(hash); + } + } + } catch (Exception e) { + nested = e; + } + } + + if(address != null && address.startsWith(HRP)) { + try { + Bech32.Bech32Data data = Bech32.decode(address); + if (data.hrp.equals(HRP)) { + int witnessVersion = data.data[0]; + if (witnessVersion == 0) { + byte[] convertedProgram = Arrays.copyOfRange(data.data, 1, data.data.length); + byte[] witnessProgram = Bech32.convertBits(convertedProgram, 0, convertedProgram.length, 5, 8, false); + if (witnessProgram.length == 20) { + return new P2WPKHAddress(witnessProgram); + } + if (witnessProgram.length == 32) { + return new P2WSHAddress(witnessProgram); + } + } + } + } catch (Exception e) { + nested = e; + } + } + + if(nested != null) { + throw new InvalidAddressException("Could not parse invalid address " + address, nested); + } + + throw new InvalidAddressException("Could not parse invalid address " + address); + } } diff --git a/src/main/java/com/sparrowwallet/drongo/address/InvalidAddressException.java b/src/main/java/com/sparrowwallet/drongo/address/InvalidAddressException.java new file mode 100644 index 0000000..aaff0d2 --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/address/InvalidAddressException.java @@ -0,0 +1,19 @@ +package com.sparrowwallet.drongo.address; + +public class InvalidAddressException extends Exception { + public InvalidAddressException() { + super(); + } + + public InvalidAddressException(String msg) { + super(msg); + } + + public InvalidAddressException(Throwable cause) { + super(cause); + } + + public InvalidAddressException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/Bech32.java b/src/main/java/com/sparrowwallet/drongo/protocol/Bech32.java index 213ab1a..da5ad0a 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/Bech32.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/Bech32.java @@ -178,7 +178,7 @@ public class Bech32 { /** * Helper for re-arranging bits into groups. */ - private static byte[] convertBits(final byte[] in, final int inStart, final int inLen, final int fromBits, + public static byte[] convertBits(final byte[] in, final int inStart, final int inLen, final int fromBits, final int toBits, final boolean pad) { int acc = 0; int bits = 0; diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/UtxoSelector.java b/src/main/java/com/sparrowwallet/drongo/wallet/UtxoSelector.java new file mode 100644 index 0000000..6f713b8 --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/wallet/UtxoSelector.java @@ -0,0 +1,7 @@ +package com.sparrowwallet.drongo.wallet; + +import java.util.Collection; + +public interface UtxoSelector { + Collection select(long targetValue, Collection candidates); +} diff --git a/src/test/java/com/sparrowwallet/drongo/address/AddressTest.java b/src/test/java/com/sparrowwallet/drongo/address/AddressTest.java new file mode 100644 index 0000000..7b7959c --- /dev/null +++ b/src/test/java/com/sparrowwallet/drongo/address/AddressTest.java @@ -0,0 +1,70 @@ +package com.sparrowwallet.drongo.address; + +import org.junit.Assert; +import org.junit.Test; + +import java.security.SecureRandom; + +public class AddressTest { + @Test + public void validAddressTest() throws InvalidAddressException { + Address address1 = Address.fromString("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"); + Assert.assertTrue(address1 instanceof P2WPKHAddress); + Assert.assertEquals("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", address1.toString()); + + Address address2 = Address.fromString("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"); + Assert.assertTrue(address2 instanceof P2WSHAddress); + Assert.assertEquals("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", address2.toString()); + + Address address3 = Address.fromString("19Sp9dLinHy3dKo2Xxj53ouuZWAoVGGhg8"); + Assert.assertTrue(address3 instanceof P2PKHAddress); + Assert.assertEquals("19Sp9dLinHy3dKo2Xxj53ouuZWAoVGGhg8", address3.toString()); + + Address address4 = Address.fromString("34jnjFM4SbaB7Q8aMtNDG849RQ1gUYgpgo"); + Assert.assertTrue(address4 instanceof P2SHAddress); + Assert.assertEquals("34jnjFM4SbaB7Q8aMtNDG849RQ1gUYgpgo", address4.toString()); + } + + @Test + public void validRandomAddressTest() throws InvalidAddressException { + SecureRandom random = new SecureRandom(); + byte[] values = new byte[20]; + + for(int i = 0; i < 100; i++) { + random.nextBytes(values); + Address address = (i % 2 == 0 ? new P2PKHAddress(values) : new P2WPKHAddress(values)); + String strAddress = address.toString(); + Address checkAddress = Address.fromString(strAddress); + Assert.assertArrayEquals(values, checkAddress.getHash()); + } + + byte[] values32 = new byte[32]; + for(int i = 0; i < 100; i++) { + random.nextBytes(values32); + Address address = new P2WSHAddress(values32); + String strAddress = address.toString(); + Address checkAddress = Address.fromString(strAddress); + Assert.assertArrayEquals(values32, checkAddress.getHash()); + } + } + + @Test(expected = InvalidAddressException.class) + public void invalidCharacterAddressTest() throws InvalidAddressException { + Address address1 = Address.fromString("bc1qw508d6qejxtdg4y5R3zarvary0c5xw7kv8f3t4"); + } + + @Test(expected = InvalidAddressException.class) + public void invalidVersionAddressTest() throws InvalidAddressException { + Address address1 = Address.fromString("44jnjFM4SbaB7Q8aMtNDG849RQ1gUYgpgo"); + } + + @Test(expected = InvalidAddressException.class) + public void invalidChecksumAddressTest() throws InvalidAddressException { + Address address1 = Address.fromString("34jnjFM4SbaB7Q7aMtNDG849RQ1gUYgpgo"); + } + + @Test(expected = InvalidAddressException.class) + public void invalidChecksumAddressTest2() throws InvalidAddressException { + Address address1 = Address.fromString("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmb3"); + } +} \ No newline at end of file