Skip to content

Commit

Permalink
Merge pull request #2 from PiRK/guess_prefix
Browse files Browse the repository at this point in the history
Add a guess_prefix function and guessprefix command line tool
  • Loading branch information
PiRK authored Jul 1, 2021
2 parents 0081c59 + b171810 commit b8c7a44
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 31 deletions.
31 changes: 24 additions & 7 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,19 @@ Then you can convert your address via:

.. code:: python
address = Address.from_string("155fzsEBHy9Ri2bMQ8uuuR3tv1YzcDywd4").cash_address()
address = Address.from_string("155fzsEBHy9Ri2bMQ8uuuR3tv1YzcDywd4").to_cash_address()
or

.. code:: python
address = Address.from_string("ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435ugq9umvq").legacy_address()
address = Address.from_string("ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435ugq9umvq").to_legacy_address()
You can convert between different *CashAddr* prefixes:

.. code:: python
address = Address.from_string("ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435ugq9umvq").cash_address(prefix="foobar")
address = Address.from_string("ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435ugq9umvq").to_cash_address(prefix="foobar")
Validating address
~~~~~~~~~~~~~~~~~~
Expand All @@ -69,6 +69,16 @@ or
convert.is_valid('ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435ugq9umvq')
Guessing a prefix
~~~~~~~~~~~~~~~~~

You can guess the prefix for a cash address. This only works for a short list of
commonly used prefixes, such as "ecash", "bitcoincash", "simpleledger" or "etoken".

.. code:: python
convert.guess_prefix('qqkv9wr69ry2p9l53lxp635va4h86wv435ugq9umvq')
As a command line tool
----------------------

Expand All @@ -79,7 +89,7 @@ console:

::

ecashconvert --help
ecashaddress --help

If this is not the case, an alternative is to run the library the
following way:
Expand All @@ -95,14 +105,21 @@ prefix.

::

ecashconvert bitcoincash:qq3dmep4sj4u5nt8v2qaa3ea7kh7km8j05dhde02hg
ecashaddress convert bitcoincash:qq3dmep4sj4u5nt8v2qaa3ea7kh7km8j05dhde02hg

To output a *CashAddr* with a different prefix, use the ``--prefix``
option:

::

ecashconvert bchtest:qq3dmep4sj4u5nt8v2qaa3ea7kh7km8j05f9f7das5 --prefix ectest
ecashaddress convert bchtest:qq3dmep4sj4u5nt8v2qaa3ea7kh7km8j05f9f7das5 --prefix ectest

The tool also lets you guess the prefix from an address without prefix, if the
prefix is in a short list of commonly used prefixes:

::

ecashaddress guessprefix qr4pqy6q4cy2d50zpaek57nnrja7289fksp38mkrxf

Development
===========
Expand Down Expand Up @@ -130,7 +147,7 @@ Development

git checkout -b my_dev_branch
# do your stuff
python ecashaddress.tests.test
python -m ecashaddress.tests.test
git commit

6. Push you branch to your fork of the repository.
Expand Down
64 changes: 57 additions & 7 deletions ecashaddress/__main__.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,75 @@
import argparse
from typing import Sequence
import warnings

from .convert import Address
from .convert import Address, guess_prefix, KNOWN_PREFIXES
from . import version


def main():
def convert():
"""This function is the entry point defined in setup.py for the command
line tools ecashconvert.
line ecashconvert.
"""
warnings.warn(
"`ecashconvert` is deprecated, use `ecashaddress convert` instead",
DeprecationWarning
)
parser = argparse.ArgumentParser(description='Convert eCash address formats.')
parser.add_argument("input_addresses", help="Input addresses to be converted.", nargs="+")
group = parser.add_mutually_exclusive_group()
group.add_argument("--prefix", help="Output cashaddr prefix.", default="ecash")
group.add_argument("--legacy", help="Convert to legacy BTC address.", action="store_true")

args = parser.parse_args()
_convert(args.input_addresses, args.prefix, args.legacy)


for addr in args.input_addresses:
if args.legacy:
print(Address.from_string(addr).legacy_address())
def _convert(input_addresses: Sequence[str], prefix: str, is_legacy: bool):
for addr in input_addresses:
if is_legacy:
print(Address.from_string(addr).to_legacy_address())
else:
print(Address.from_string(addr).cash_address(args.prefix))
print(Address.from_string(addr).to_cash_address(prefix))


def main():
"""This function is the entry point defined in setup.py for the command
line ecashaddress.
"""
parser = argparse.ArgumentParser(
description='Tools for working with cash addresses')
parser.add_argument("-v", "--version", help="Print the version number",
action="store_true")
subparsers = parser.add_subparsers(dest='command')

convert_parser = subparsers.add_parser(
'convert', help = "Convert eCash address formats")
convert_parser.add_argument(
"input_addresses", help="Input addresses to be converted.", nargs="+")
group = convert_parser.add_mutually_exclusive_group()
group.add_argument("--prefix", help="Output CashAddr prefix.",
default="ecash")
group.add_argument("--legacy", help="Convert to legacy BTC address.",
action="store_true")

guessprefix_parser = subparsers.add_parser(
'guessprefix',
help=f"Guess the prefix from a CashAddr address, by trying a list of"
f" commonly used prefixes: {', '.join(KNOWN_PREFIXES)}.")
guessprefix_parser.add_argument(
"address", help="Input cash address without prefix.")

args = parser.parse_args()
if args.version:
print(version)
return

if args.command == "convert":
_convert(args.input_addresses, args.prefix, args.legacy)
elif args.command == "guessprefix":
print(guess_prefix(args.address))
elif args.command is None:
parser.print_help()


if __name__ == '__main__':
Expand Down
74 changes: 66 additions & 8 deletions ecashaddress/convert.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
from __future__ import annotations

from typing import Optional
import sys
import warnings

from ecashaddress.crypto import *
from ecashaddress.base58 import b58decode_check, b58encode_check
import sys


KNOWN_PREFIXES = ["ecash", "bitcoincash", "etoken", "simpleledger",
"ectest", "bchtest", "ecregtest", "bchreg"]


class InvalidAddress(Exception):
Expand Down Expand Up @@ -41,10 +50,24 @@ def __str__(self):
return 'version: {}\npayload: {}\nprefix: {}'.format(self.version, self.payload, self.prefix)

def legacy_address(self):
warnings.warn(
"legacy_address is deprecated, use to_legacy_address instead",
DeprecationWarning
)
return self.to_legacy_address()

def to_legacy_address(self) -> str:
version_int = Address._address_type('legacy', self.version)[1]
return b58encode_check(Address.code_list_to_string([version_int] + self.payload))

def cash_address(self, prefix=None):
warnings.warn(
"cash_address is deprecated, use to_cash_address instead",
DeprecationWarning
)
return self.to_cash_address(prefix)

def to_cash_address(self, prefix: Optional[str] = None) -> str:
prefix = prefix if prefix is not None else self.prefix
self._check_case(prefix)
is_uppercase = prefix == prefix.upper()
Expand Down Expand Up @@ -77,18 +100,18 @@ def _address_type(address_type, version):
raise InvalidAddress('Could not determine address version')

@staticmethod
def from_string(address_string):
def from_string(address_string: str) -> Address:
try:
address_string = str(address_string)
except Exception:
raise InvalidAddress('Expected string as input')
if ':' not in address_string:
return Address._legacy_string(address_string)
return Address.from_legacy_string(address_string)
else:
return Address._cash_string(address_string)
return Address.from_cash_string(address_string)

@staticmethod
def _legacy_string(address_string):
def from_legacy_string(address_string: str) -> Address:
try:
decoded = bytearray(b58decode_check(address_string))
except ValueError:
Expand All @@ -100,7 +123,7 @@ def _legacy_string(address_string):
return Address(version, payload)

@staticmethod
def _cash_string(address_string):
def from_cash_string(address_string: str) -> Address:
Address._check_case(address_string)
address_string = address_string.lower()
colon_count = address_string.count(':')
Expand All @@ -126,11 +149,11 @@ def _check_case(text):


def to_cash_address(address):
return Address.from_string(address).cash_address()
return Address.from_string(address).to_cash_address()


def to_legacy_address(address):
return Address.from_string(address).legacy_address()
return Address.from_string(address).to_legacy_address()


def is_valid(address):
Expand All @@ -139,3 +162,38 @@ def is_valid(address):
return True
except InvalidAddress:
return False


def guess_prefix(cashaddress: str) -> str:
f"""Return the lower-case prefix.
If the prefix is not specified in the input address, a list of usual
prefixes is tried and this function returns the first one that matches.
The following prefixes are tried, in this order: {KNOWN_PREFIXES}.
If prefix is specified but does not match the checksum, an InvalidAddress
error is raised.
If the prefix is omitted and no known prefix matches the checksum, an
empty string is returned.
"""
if ':' in cashaddress:
try:
addr = Address.from_cash_string(cashaddress)
except InvalidAddress:
raise
else:
return addr.prefix

known_prefixes = []
for prefix in KNOWN_PREFIXES:
known_prefixes.append(prefix.lower())
known_prefixes.append(prefix.upper())

for prefix in known_prefixes:
try:
Address.from_cash_string(prefix + ":" + cashaddress)
except InvalidAddress:
pass
else:
return prefix
return ""
55 changes: 47 additions & 8 deletions ecashaddress/tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,21 @@ def test_prefixes(self):
default_prefix = addr.prefix
self.assertEqual(default_prefix, Address.MAINNET_PREFIX)

self.assertEqual(addr.cash_address(prefix='ecash'),
self.assertEqual(addr.to_cash_address(prefix='ecash'),
'ecash:qr4pqy6q4cy2d50zpaek57nnrja7289fks00weqyz7')
self.assertEqual(addr.cash_address(prefix='bitcoincash'),
self.assertEqual(addr.to_cash_address(prefix='bitcoincash'),
'bitcoincash:qr4pqy6q4cy2d50zpaek57nnrja7289fkskz6jm7yf')
self.assertEqual(addr.cash_address(prefix='abc'),
self.assertEqual(addr.to_cash_address(prefix='abc'),
'abc:qr4pqy6q4cy2d50zpaek57nnrja7289fksqt4c50w9')
self.assertEqual(addr.cash_address(prefix='simpleledger'),
self.assertEqual(addr.to_cash_address(prefix='simpleledger'),
'simpleledger:qr4pqy6q4cy2d50zpaek57nnrja7289fks6e3fw76h')

regtest_address = 'regtest:qr4pqy6q4cy2d50zpaek57nnrja7289fksjm6es9se'
addr2 = Address.from_string(regtest_address)
self.assertEqual(addr2.legacy_address(), legacy_address)
self.assertEqual(addr2.to_legacy_address(), legacy_address)
# The prefix defaults to the one in the input string.
self.assertEqual(addr2.prefix, 'regtest')
self.assertEqual(addr2.cash_address(), regtest_address)
self.assertEqual(addr2.to_cash_address(), regtest_address)

def test_prefix_case(self):
with self.assertRaises(InvalidAddress):
Expand All @@ -95,11 +95,50 @@ def test_prefix_case(self):

addr = Address.from_string('regtest:qr4pqy6q4cy2d50zpaek57nnrja7289fksjm6es9se')
# The address should take the same case as the specified prefix
self.assertEqual(addr.cash_address(prefix="SLP"),
self.assertEqual(addr.to_cash_address(prefix="SLP"),
'SLP:QR4PQY6Q4CY2D50ZPAEK57NNRJA7289FKSWF89PY2G')
# Do not allow mixed-case prefixes
with self.assertRaises(InvalidAddress):
addr.cash_address(prefix="sLp")
addr.to_cash_address(prefix="sLp")


class TestGuessPrefix(unittest.TestCase):
def _test(self, addr, expected_prefix):
self.assertEqual(convert.guess_prefix(addr), expected_prefix)

def test_explicit_prefixes(self):
self._test("ecash:qr4pqy6q4cy2d50zpaek57nnrja7289fks00weqyz7", "ecash")
# The way address works, we always store a lower case prefix
self._test("ECASH:QR4PQY6Q4CY2D50ZPAEK57NNRJA7289FKS00WEQYZ7", "ecash")
self._test("foobar:qr4pqy6q4cy2d50zpaek57nnrja7289fksyz309rn7", "foobar")

def test_fail_to_guess(self):
# foobar:
self._test("qr4pqy6q4cy2d50zpaek57nnrja7289fksyz309rn7", "")
# abc:
self._test("qr4pqy6q4cy2d50zpaek57nnrja7289fksqt4c50w9", "")

def test_successful_guess(self):
self._test("qr4pqy6q4cy2d50zpaek57nnrja7289fks00weqyz7", "ecash")
self._test("QR4PQY6Q4CY2D50ZPAEK57NNRJA7289FKS00WEQYZ7", "ECASH")
self._test("qr4pqy6q4cy2d50zpaek57nnrja7289fkskz6jm7yf", "bitcoincash")
self._test("qr4pqy6q4cy2d50zpaek57nnrja7289fksp38mkrxf", "etoken")
self._test("qr4pqy6q4cy2d50zpaek57nnrja7289fks6e3fw76h", "simpleledger")
self._test("pqc3tyspqwn95retv5k3c5w4fdq0cxvv95895yhkd4", "ectest")
self._test("pqc3tyspqwn95retv5k3c5w4fdq0cxvv95u36gfk00", "bchtest")


def test_invalid_checksum(self):
with self.assertRaises(InvalidAddress):
convert.guess_prefix("ecash:qr4pqy6q4cy2d50zpaek57nnrja7289fks00000000")

def test_mixed_case(self):
with self.assertRaises(InvalidAddress):
convert.guess_prefix("ECASH:qr4pqy6q4cy2d50zpaek57nnrja7289fks00weqyz7")
with self.assertRaises(InvalidAddress):
convert.guess_prefix("ecash:QR4PQY6Q4CY2D50ZPAEK57NNRJA7289FKS00WEQYZ7")
with self.assertRaises(InvalidAddress):
convert.guess_prefix("ecash:Qr4pqy6q4cy2d50zpaek57nnrja7289fks00weqyz7")


if __name__ == '__main__':
Expand Down
8 changes: 7 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ def get_version():
setup(name=PROJECT,
version=get_version(),
packages=find_packages(),
entry_points={'console_scripts': ['ecashconvert=ecashaddress.__main__:main',]},
entry_points={
'console_scripts': [
'ecashconvert=ecashaddress.__main__:convert',
'ecashaddress=ecashaddress.__main__:main'
]

},
description='Python library and command line tool for converting cashaddr',
url='https://github.com/PiRK/ecashaddress/',
python_requires='>=3.7',
Expand Down

0 comments on commit b8c7a44

Please sign in to comment.