diff --git a/src/main/scala/org/scalacoin/protocol/Address.scala b/src/main/scala/org/scalacoin/protocol/Address.scala new file mode 100644 index 0000000000..833b6a9b1e --- /dev/null +++ b/src/main/scala/org/scalacoin/protocol/Address.scala @@ -0,0 +1,96 @@ +package org.scalacoin.protocol + +import org.bitcoinj.core.{VersionedChecksummedBytes, Base58, Utils} + +case class AddressInfo(bitcoinAddress: BitcoinAddress, n_tx: Long, total_received: Long, total_sent: Long, + final_balance: Long) + +sealed abstract class Address(val address : String) +sealed case class BitcoinAddress(bitcoinAddress: String) extends Address(bitcoinAddress) { + require(BitcoinAddress.validate(bitcoinAddress), "Bitcoin address was invalid " + bitcoinAddress) +} + +sealed case class AssetAddress(assetAddress : String) extends Address(assetAddress) { + require(AssetAddress.validate(assetAddress), "The provided asset was was invalid: " + assetAddress) +} + +object BitcoinAddress { + def validate(bitcoinAddress: String): Boolean = { + val illegalChars = List('O', 'I', 'l', '0') + bitcoinAddress.length >= 26 && bitcoinAddress.length <= 35 && + (p2pkh(bitcoinAddress) || p2shAddress(bitcoinAddress)) && + bitcoinAddress.filter(c => illegalChars.contains(c)).size == 0 + } + + /** + * Converts a bitcoin address to an asset address + * @param address + * @return + */ + def convertToAssetAddress(address : BitcoinAddress) : AssetAddress = { + val underlying : String = address.bitcoinAddress + val base58decodeChecked : Array[Byte] = Base58.decodeChecked(underlying) + require ( + base58decodeChecked.size == 21 + ) + AssetAddress(new VersionedChecksummedBytes(0x13, base58decodeChecked){}.toString()) + } + + /** + * Checks if a address is a valid p2sh address + * @param address + * @return + */ + def p2shAddress(address : String) : Boolean = { + address.charAt(0) == '3' || address.charAt(0) == '2' + } + + /** + * Checks if a address is a valid p2sh address + * @param address + * @return + */ + def p2shAddress(address : BitcoinAddress) : Boolean = p2shAddress(address.bitcoinAddress) + + /** + * Checks if an address is a valid p2pkh address + * @param address + * @return + */ + def p2pkh(address : String) : Boolean = { + val firstChar = address.charAt(0) + firstChar == '1' || firstChar == 'm' || firstChar == 'n' + } + + /** + * Checks if an address is a valid p2pkh address + * @param address + * @return + */ + def p2pkh(address : BitcoinAddress) : Boolean = p2pkh(address.bitcoinAddress) +} + +object AssetAddress { + def validate(assetAddress : String) = { + //asset addresses must have the one byte namespace equivalent to 19 + //which ends up being 'a' in the ascii character set + val base58decodechecked : Array[Byte] = Base58.decodeChecked(assetAddress) + require(base58decodechecked != null) + base58decodechecked.size == 22 && base58decodechecked(0) == 0x13 + } + + /** + * Converts an asset address into a bitcoin address + * @param assetAddress + * @return + */ + def convertToBitcoinAddress(assetAddress : AssetAddress) = { + val underlying : String = assetAddress.assetAddress + val base58decodeChecked : Array[Byte] = Base58.decodeChecked(underlying) + + require(base58decodeChecked.size == 22) + + val slice = base58decodeChecked.slice(2, base58decodeChecked.length) + BitcoinAddress(new VersionedChecksummedBytes(base58decodeChecked(1), slice){}.toString()) + } +} diff --git a/src/test/scala/org/scalacoin/protocol/AddressTest.scala b/src/test/scala/org/scalacoin/protocol/AddressTest.scala new file mode 100644 index 0000000000..7a9ae01ef3 --- /dev/null +++ b/src/test/scala/org/scalacoin/protocol/AddressTest.scala @@ -0,0 +1,27 @@ +package org.scalacoin.protocol + +import org.scalacoin.util.TestUtil +import org.scalatest.{FlatSpec, MustMatchers} + +/** + * Created by chris on 3/23/15. + */ +class AddressTest extends FlatSpec with MustMatchers { + val assetAddress = TestUtil.assetAddress + "Addresses" must "be able to convert back and forth between a Bitcoin Address & an asset address" in { + val convertedOnce = BitcoinAddress.convertToAssetAddress(TestUtil.bitcoinAddress) + val actual : BitcoinAddress = AssetAddress.convertToBitcoinAddress(convertedOnce) + actual must be (TestUtil.bitcoinAddress) + val bitcoinAddress = AssetAddress.convertToBitcoinAddress(assetAddress) + val actualAssetAddress = BitcoinAddress.convertToAssetAddress(bitcoinAddress) + actualAssetAddress must be (assetAddress) + } + + it must "allow type encapsulation for addresses" in { + + val bitcoinAddress : Address = TestUtil.bitcoinAddress + val assetAddress : Address = TestUtil.assetAddress + assetAddress must be (TestUtil.assetAddress) + bitcoinAddress must be (TestUtil.bitcoinAddress) + } +} diff --git a/src/test/scala/org/scalacoin/protocol/BitcoinAddressTest.scala b/src/test/scala/org/scalacoin/protocol/BitcoinAddressTest.scala new file mode 100644 index 0000000000..f7923d7a2b --- /dev/null +++ b/src/test/scala/org/scalacoin/protocol/BitcoinAddressTest.scala @@ -0,0 +1,68 @@ +package org.scalacoin.protocol + +import org.scalatest.{FlatSpec, MustMatchers} + +class BitcoinAddressTest extends FlatSpec with MustMatchers { + + "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy" must "be a valid bitcoin address" in { + val address = "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy" + BitcoinAddress(address).bitcoinAddress must be(address) + + } + + "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy" must "be a valid p2sh address and not a valid p2pkh" in { + val address = "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy" + BitcoinAddress.p2shAddress(address) must be (true) + BitcoinAddress.p2pkh(address) must be (false) + } + + "17WN1kFw8D6w1eHzqvkh49xwjE3iPN925b" must "be a valid p2pkh" in { + val address = "17WN1kFw8D6w1eHzqvkh49xwjE3iPN925b" + BitcoinAddress.p2pkh(address) must be (true) + BitcoinAddress.p2shAddress(address) must be (false) + } + + "The empty string" must "not be a valid bitcoin address" in { + intercept[IllegalArgumentException] { + BitcoinAddress("") + } + } + "A string that is 25 characters long" must "not be a valid bitcoin address" in { + val address = "3J98t1WpEZ73CNmQviecrnyiW" + intercept[IllegalArgumentException] { + BitcoinAddress(address) + } + } + + "A string that is 36 characters long" must "not be a valid bitcoin address" in { + val address = "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLyyy" + intercept[IllegalArgumentException] { + BitcoinAddress(address) + } + } + + "3J98t1WpEZ73CNmQviecrnyiWr (26 characters) " must "be a valid bitcoin address" in { + val address = "3J98t1WpEZ73CNmQviecrnyiWr" + BitcoinAddress(address).bitcoinAddress must be(address) + } + + "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLyy (35 characters)" must "be a valid bitcoin address" in { + val address = "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLyy" + BitcoinAddress(address).bitcoinAddress must be(address) + } + + + + + "akJsoCcyh34FGPotxfEoSXGwFPCNAkyCgTA" must "be a valid asset address" in { + val assetAddress = AssetAddress("akJsoCcyh34FGPotxfEoSXGwFPCNAkyCgTA") + assetAddress.assetAddress must be ("akJsoCcyh34FGPotxfEoSXGwFPCNAkyCgTA") + } + + "An asset address with the first character replaced" must "not be a valid asset address" in { + //3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLyy + intercept[org.bitcoinj.core.AddressFormatException] { + val assetAddress = AssetAddress("aJ98t1WpEZ73CNmQviecrnyiWrnqRhWNLyy") + } + } +} \ No newline at end of file diff --git a/src/test/scala/org/scalacoin/util/TestUtil.scala b/src/test/scala/org/scalacoin/util/TestUtil.scala new file mode 100644 index 0000000000..85fe11f10c --- /dev/null +++ b/src/test/scala/org/scalacoin/util/TestUtil.scala @@ -0,0 +1,15 @@ +package org.scalacoin.util + +import org.scalacoin.protocol.{AssetAddress, BitcoinAddress} + +/** + * Created by chris on 12/2/15. + */ +object TestUtil { + + val testBitcoinAddress = BitcoinAddress("n3p1ct69ao3qxWvEvzLhLtWG2zJGTjN3EV") + val testP2SHAddress = BitcoinAddress("2MzYbQdkSVp5wVyMRp6A5PHPuQNHpiaTbCj") + val bitcoinAddress = BitcoinAddress("1C4kYhyLftmkn48YarSoLupxHfYFo8kp64") + val multiSigAddress = BitcoinAddress("342ftSRCvFHfCeFFBuz4xwbeqnDw6BGUey") + val assetAddress = AssetAddress("akJsoCcyh34FGPotxfEoSXGwFPCNAkyCgTA") +}