Browse Source

Lots of reorganization and some new features. Now connects to an ORBIT node.

Shawn Wilson 10 months ago
parent
commit
1971838ef6

+ 2 - 0
CHANGELOG

@@ -1,6 +1,8 @@
 Copyright (C) 2018 Alpha Griffin
 @%@~LICENSE~@%@
 
+saw_081018_1 - Lots of reorganization and some new features. Now connects to an ORBIT node.
+
 saw_080118_1 - Add wallet rename and token transfer commands.
 
 saw_072818_1 - Clean up wallet management using the new API routines. Add wallet `balance` command.

+ 4 - 3
Makefile

@@ -7,7 +7,7 @@
 
 # You can set these variables from the command line.
 SPHINXOPTS    = -c etc/sphinx
-SPHINXBUILD   = sphinx-build
+SPHINXBUILD   = python3 -m sphinx
 PAPER         =
 DOCDIR        = doc
 
@@ -70,7 +70,6 @@ run:
 
 install:
 	./setup.py install
-	install -pv orbit-cli /usr/local/bin
 	if [ -d "doc/man" ]; then \
 		install -d /usr/local/share/man/man1; \
 		cp -r doc/man/*.1 /usr/local/share/man/man1/; \
@@ -81,7 +80,7 @@ install:
 
 
 apidoc:
-	sphinx-apidoc ag -o api
+	sphinx-apidoc --module-first ag -o api
 
 apidoc_clean:
 	rm -rf api
@@ -99,6 +98,8 @@ docs_clean:
 
 html:
 	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(DOCDIR)/html
+	cp CHANGELOG $(DOCDIR)/html
+	cp LICENSE $(DOCDIR)/html
 	@echo
 	@echo "Build finished. The HTML pages are in $(DOCDIR)/html."
 

+ 64 - 18
README.rst

@@ -1,18 +1,23 @@
-===========================================================================
+###########################################################################
 ORBIT CLI - Command-Line Interface for Op_Return Bitcoin-Implemented Tokens
-===========================================================================
+###########################################################################
+
+.. image:: https://badges.gitter.im/AlphaGriffin/orbit.svg
+   :alt: Join the chat at https://gitter.im/AlphaGriffin/orbit
+   :target: https://gitter.im/AlphaGriffin/orbit?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
 
 **A command-line interface for interacting with tokens on Bitcoin Cash implementing the ORBIT standard.**
 
 The official website for ORBIT is http://orbit.cash.
 
 .. contents:: Table of Contents
-
-*"Orbit the moon"*
+   :depth: 2
+   :local:
 
 
+************
 Introduction
-------------
+************
 
 The ORBIT CLI is a program that allows interaction with and management of tokens on Bitcoin Cash implementing the ORBIT standard.
 
@@ -20,7 +25,7 @@ ORBIT CLI is open source and licensed under the MIT license. See the `LICENSE <L
 
 
 The ORBIT Ecosystem
-~~~~~~~~~~~~~~~~~~~
+===================
 
 ORBIT is a specification for simple, fungible tokens implemented by utilizing OP_RETURN for the storage of token events on the Bitcoin Cash blockchain. No changes to the Bitcoin Cash protocol or nodes are required. However, wallets may wish to incorporate this token standard in order to allow the user to easily take account of and interact with tokens that adhere to this ORBIT standard.
 
@@ -32,35 +37,60 @@ The following projects, when used in conjunction with ORBIT CLI, complete a full
 - ORBIT Web: https://github.com/AlphaGriffin/orbit-web
 
 
+*************
 Specification
--------------
+*************
 
 The ORBIT repository at https://github.com/AlphaGriffin/orbit defines the official and complete specification for ORBIT. 
 
 *The current specification version is: 0 (beta testing). Version 0 is reserved and should be used for all testing.*
 
 
+
+************
+Contributing
+************
+
+Your help is appreciated! Alpha Griffin is a small team focused on developing new technology projects. If you have questions or comments or would like to   contribute to the ORBIT node or ecosystem in any way, please feel free to contact us. You may submit issues or pull requests directly on GitHub or          communicate with the team members at the following locations:
+
+- https://gitter.im/AlphaGriffin
+- https://alphagriffintrade.slack.com
+
+Have a suggestion or request? Let us know!
+
+
+To-Do List
+==========
+
+There are a number of tasks already identified on the `To-Do list <TODO>`_ that could use your help (included here in generated documentation).
+
+.. include:: TODO
+   :literal:
+
+
+
+*********
 ORBIT CLI
----------
+*********
 
 This ORBIT CLI is written in Python.
 
 
 Dependencies
-~~~~~~~~~~~~
+============
 
 - Python 3
-- ORBIT API: https://github.com/AlphaGriffin/orbit
-- appdirs: https://github.com/ActiveState/appdirs (`pip install appdirs`)
-- BitCash >= 0.5.2.4: https://github.com/sporestack/bitcash (`pip install bitcash\>=0.5.2.4`)
-- PyCrypto: https://github.com/dlitz/pycrypto (`pip install pycrypto`)
-- *For building documentation (optional):* sphinx and sphinx_rtd_theme (`pip install sphinx sphinx_rtd_theme`)
+- ORBIT API: https://github.com/AlphaGriffin/orbit (``pip install git+https://github.com/AlphaGriffin/orbit``)
+- appdirs: https://github.com/ActiveState/appdirs (``pip install appdirs``)
+- BitCash >= 0.5.2.4: https://github.com/sporestack/bitcash (``pip install bitcash\>=0.5.2.4``)
+- PyCrypto: https://github.com/dlitz/pycrypto (``pip install pycrypto``)
+- *For building documentation (optional):* sphinx and sphinx_rtd_theme (``pip install sphinx sphinx_rtd_theme``)
 
 In addition to the above, ORBIT CLI may require RPC access to a local or remote ORBIT node for some operations, such as the one provided by Alpha Griffin (http://orbit.alphagriffin.com).
 
 
 Build Overview
-~~~~~~~~~~~~~~
+==============
 
 Both a Makefile and setup.py are provided and used. The setup.py uses Python's standard setuptools package and you can call this script directly to do the basic Python tasks such as creating a wheel, etc.
 
@@ -89,7 +119,7 @@ To clean up all the common generated files from your project folder::
 
 
 Installing
-~~~~~~~~~~
+==========
 
 To install this project to the local system::
 
@@ -99,14 +129,30 @@ Note that you may need superuser permissions to perform the above step.
 
 
 Using
-~~~~~
+=====
 
 **FIXME**
 
 
+**********
 Python API
-----------
+**********
 
 .. toctree::
    API Documentation <api/modules>
 
+
+*******
+History
+*******
+
+All changes are tracked in the `CHANGELOG <CHANGELOG>`_ file.
+
+.. include:: CHANGELOG
+   :literal:
+
+
+----
+
+*"Orbit the moon"*
+

+ 7 - 0
TODO

@@ -0,0 +1,7 @@
+To-do list for ORBIT CLI:
+
+- finish API documentation
+- generated docs on website (doc.orbit.cash)
+- more cli functions (connect to node): token list, etc.
+- test cases
+

+ 20 - 8
ag/orbit/cli/__init__.py

@@ -22,19 +22,31 @@ from sys import stdin
 from getpass import getpass
 
 
-def arg(args, index, name, optional=False):
+def arg(args, index, name, optional=False, description=None, hints=None, none=None):
     value = None
 
-    if args is None:
-        value = input("    {}: ".format(name))
-
-    elif len(args) > index:
+    if args and len(args) > index:
         value = args[index]
 
-    elif not optional:
-        value = input("    {}: ".format(name))
+    elif args is None or not optional:
+        if description or hints:
+            print("    {}:".format(name))
+            if description:
+                print("        {}".format(description))
+            if hints:
+                for hint in hints:
+                    print("          * {}".format(hint))
+            value = input("      --> ")
+        else:
+            value = input("    {}: ".format(name))
+
+    if not optional and not value:
+        raise ValueError("{} is required".format(name))
 
-    return value if value else None
+    if none and value == none:
+        return None
+    else:
+        return value if value else None
 
 
 def password_handler(password=None, create=False):

+ 13 - 3
ag/orbit/cli/__main__.py

@@ -13,10 +13,14 @@ def usage():
     print()
     print("Usage: {} <module>".format(CALL))
     print()
+    print("    ORBIT command-line interface for tokens on Bitcoin Cash.")
+    print()
     print("Where <module> is:")
     print("    help     - Display this usage screen")
-    print("    token    - Token commands")
+    print("    config   - Configuration")
     print("    wallet   - Wallet commands")
+    print("    token    - Token user commands")
+    print("    admin    - Token administration commands")
     #print("    network  - Network commands")
     print()
 
@@ -35,12 +39,18 @@ with suppress(KeyboardInterrupt):
     if module == 'help':
         usage()
 
-    elif module == 'token':
-        from .token import __main__
+    elif module == 'config':
+        from .config import __main__
 
     elif module == 'wallet':
         from .wallet import __main__
 
+    elif module == 'token':
+        from .token import __main__
+
+    elif module == 'admin':
+        from .admin import __main__
+
     #elif module == 'network':
     #    from .network import __main__
 

+ 1 - 1
ag/orbit/cli/__version__.py

@@ -1,2 +1,2 @@
-__version__ = '0.0.3'
+__version__ = '0.0.4'
 

+ 9 - 0
ag/orbit/cli/admin/__init__.py

@@ -0,0 +1,9 @@
+# Copyright (C) 2018 Alpha Griffin
+# @%@~LICENSE~@%@
+
+"""ORBIT Token Administration Commands
+
+.. module:: ag.orbit.cli.admin
+   :synopsis: ORBIT Token Administration Commands
+"""
+

+ 109 - 0
ag/orbit/cli/admin/__main__.py

@@ -0,0 +1,109 @@
+# Copyright (C) 2018 Alpha Griffin
+# @%@~LICENSE~@%@
+
+#import ag.logging as log
+
+from ...command import invoke
+
+from sys import argv, exit
+from contextlib import suppress
+
+
+CALL = 'orbit-cli admin'
+
+def usage():
+    print()
+    print("Usage: {} <command>".format(CALL))
+    print()
+    print("    Token administration.")
+    print()
+    print("Where <command> is:")
+    print("    help")
+    print("        Display this usage screen")
+    print()
+    print("    create [<supply> <decimals> <symbol> [<name> [<main_uri> [<image_uri>]]]]")
+    print("        Create new token")
+    print("            - <supply> is initial token supply (number of indivisible units)")
+    print("            - <decimals> is number of decimal points to divide up the supply")
+    print("            - <symbol> is ticker symbol")
+    print("            - <name> is optional name")
+    print("            - <main_uri> is optional link to a web page, etc.")
+    print("            - <image_uri> is optional link or data for embedded image")
+    print()
+    print("    transfer [<to> <units|ALL>]")
+    print("        Transfer tokens")
+    print("            - <to> is the address to transfer to")
+    print("            - <units> is the number of indivisible units (not normalized) to transfer;")
+    print("                the text \"ALL\" may be used to transfer all available tokens")
+    print()
+    print("    advertise [<exchange_rate> <units_avail> <units_min> <units_max> <block_begin>")
+    print("              <block_end> <block_deliver> [<preregister>]]")
+    print("        Set up an automated crowd-sale or faucet")
+    print("            - <exchange_rate> is the price in satoshi for a single indivisible token unit;")
+    print("                a negative number indicates fractional value (e.g. -5 = 1/5 i.e. 5 units per satoshi);")
+    print("                the text \"NONE\" indicates free exchange (faucet)")
+    print("            - <units_avail> is the number of units to make available (this supply will be locked);")
+    print("                use \"ALL\" to make all available;")
+    #print("                use \"ANY\" to make all available without dedicating them (i.e. no supply locking)")
+    print("            - <units_min> is the minimum units a single user (address) shall receive;")
+    print("                the text \"NONE\" indicates no minimum")
+    print("            - <units_max> is the maximum units a singe user (address) shall receive;")
+    print("                the text \"NONE\" indicates no maximum")
+    print("            - <block_begin> is the block height when the crowd-sale or faucet becomes active;")
+    print("                use \"NOW\" to begin as soon as soon as the transaction is confirmed (next block)")
+    print("            - <block_end> is the block height when the crowd-sale or faucet closes;")
+    print("                use \"FOREVER\" to remain active until supply runs out")
+    print("            - <block_deliver> is the first block when tokens will be delivered;")
+    print("                use \"ANY\" for immediate delivery")
+    print("            - <preregister>, if present, indicates that users are allowed to register before <block_begin>;")
+    print("                only valid when using both <exchange_rate> and <block_begin>;")
+    print("                crowd-sale payments are still only accepted no sooner than <block_begin>;")
+    print("                payments sent with an early registration are simply ignored;")
+    print("                must be an affirmative boolean value: \"TRUE\", \"YES\", or \"Y\"")
+    print()
+    print("All commands may be called without arguments for full interactive mode.")
+    print()
+    print("Most commands require a private key for signing messages. You will be prompted")
+    print("to open a saved wallet or enter a key when required, and to confirm the transaction.")
+    print()
+    print("However, a private key may be piped in from stdin for non-interactive usage.")
+    print("Supplying a key in this manner will auto-calculate a fee and not ask for confirmation;")
+    print("USE WITH CAUTION.")
+    print()
+
+with suppress(KeyboardInterrupt):
+    if len(argv) > 1 and argv[1] is None:
+        # we were called from the parent module
+        args = argv[2:]
+    else:
+        args = argv[1:]
+
+    if len(args) < 1:
+        usage()
+        exit(401)
+
+    cmd = args[0]
+    args = args[1:] if len(args) > 1 else None
+        
+    if cmd == 'help':
+        usage()
+
+    elif cmd == 'create':
+        from .create import run
+        invoke(CALL, cmd, 402, run, args, 3, 6, optional=True)
+
+    elif cmd == 'transfer':
+        from .transfer import run
+        invoke(CALL, cmd, 403, run, args, 2, 2, optional=True)
+
+    elif cmd == 'advertise':
+        from .advertise import run
+        invoke(CALL, cmd, 404, run, args, 7, 8, optional=True)
+
+    else:
+        #log.error("unknown command", command=cmd)
+        print()
+        print("{}: unknown command: {}".format(CALL, cmd))
+        usage()
+        exit(499)
+

+ 103 - 0
ag/orbit/cli/admin/advertise.py

@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 Alpha Griffin
+# @%@~LICENSE~@%@
+
+from ag.orbit.command import main
+from ag.orbit.ops.advertisement import Advertise
+from ag.orbit.cli import arg
+from ag.orbit.cli.network import broadcast
+
+
+def run(args):
+    args = args if args else None
+
+    print()
+    print("Advertise token crowd-sale/faucet...")
+
+    if args:
+        if len(args) < 7 or len(args) > 8:
+            print()
+            raise ValueError("Expecting no less than 7 and no more than 8 arguments")
+
+    exchange_rate = arg(args, 0, "Exchange rate",
+            description="Price in satoshi for a single indivisible token unit.", hints=[
+                "A negative number indicates fractional value (e.g. -5 = 1/5 i.e. 5 units per satoshi).",
+                "The text \"NONE\" indicates free exchange (faucet)."],
+            none="NONE")
+    if exchange_rate:
+        exchange_rate = int(exchange_rate)
+
+    units_avail = arg(args, 1, "Available units",
+            description="The number of indivisible token units to make available.", hints=[
+                "Use \"ALL\" to dedicate the entire available supply.",
+                #"Use \"ANY\" to make the entire supply available without dedicating them.",
+                #"Note that unless \"ANY\" is used, units are set aside made unavailable for any other use."],
+                "Note that the units are set aside for this purpose and made unavailable for any other use."],
+            none="ALL")
+    if units_avail:
+        units_avail = int(units_avail)
+
+    units_min = arg(args, 2, "Minimum units",
+            description="The minimum number of units each user is required to receive.", hints=[
+                "Use \"NONE\" to have no per-user minimum."],
+            none="NONE")
+    if units_min:
+        units_min = int(units_min)
+
+    units_max = arg(args, 3, "Maximum units",
+            description="The maximum number of units each user is allowed to receive.", hints=[
+                "Use \"NONE\" to have no per-user maximum."],
+            none="NONE")
+    if units_max:
+        units_max = int(units_max)
+
+    block_begin = arg(args, 4, "Beginning block",
+            description="The block number (height) at which the crowd-sale or faucet begins.", hints=[
+                "Any payments sent before the beginning block will be ignored.",
+                "Use \"NOW\" to begin in the block immediately following the confirmation of this transaction."],
+            none="NOW")
+    if block_begin:
+        block_begin = int(block_begin)
+
+    block_end = arg(args, 5, "Ending block",
+            description="The block number (height) at which the crowd-sale or faucet will terminate.", hints=[
+                "Use \"FOREVER\" to remain active until supply runs out."],
+            none="FOREVER")
+    if block_end:
+        block_end = int(block_end)
+
+    block_deliver = arg(args, 6, "Delivery block",
+            description="The block number (height) at which tokens are first delivered to the users.", hints=[
+                "Use \"ANY\" for immediate delivery."],
+            none="ANY")
+    if block_deliver:
+        block_deliver = int(block_deliver)
+
+    if exchange_rate and block_begin:
+        preregister = arg(args, 7, "Preregister",
+                description="If true, indicates that users are allowed to register before the beginning block.", hints=[
+                    "Crowd-sale payments are still only accepted no sooner than the beginning block.",
+                    "Any payments sent along with an early registration will be ignored.",
+                    "Leave blank to not allow preregistration (the default). Otherwise, use \"TRUE\", \"YES\", or \"Y\""],
+                optional=True)
+        if preregister:
+            if preregister == "TRUE" or preregister == "YES" or preregister == "Y":
+                preregister = True
+            else:
+                raise ValueError("Preregister flag, if used, must be \"TRUE\", \"YES\", or \"Y\"")
+    elif args and len(args) > 7:
+        raise ValueError("Preregister flag may only be used when exchange rate and beginning block are used")
+    else:
+        preregister = False
+
+    advertise(exchange_rate, units_avail, units_min, units_max, block_begin, block_end, block_deliver, preregister)
+
+def advertise(exchange_rate, units_avail, units_min, units_max, block_begin, block_end, block_deliver, preregister=False):
+    op = Advertise(exchange_rate, units_avail, units_min, units_max, block_begin, block_end, block_deliver, preregister)
+    broadcast(op)
+
+
+if __name__ == '__main__':
+    main(run)
+

+ 4 - 4
ag/orbit/cli/token/create.py

@@ -4,7 +4,7 @@
 # @%@~LICENSE~@%@
 
 from ag.orbit.command import main
-from ag.orbit.ops.create import Create
+from ag.orbit.ops.allocation import Create
 from ag.orbit.cli import arg
 from ag.orbit.cli.network import broadcast
 
@@ -23,9 +23,9 @@ def run(args):
     supply = int(arg(args, 0, "Supply"))
     decimals = int(arg(args, 1, "Decimals"))
     symbol = arg(args, 2, "Symbol")
-    name = arg(args, 3, "Name", True)
-    main_uri = arg(args, 4, "Main URI", True)
-    image_uri = arg(args, 5, "Image URI", True)
+    name = arg(args, 3, "Name", optional=True)
+    main_uri = arg(args, 4, "Main URI", optional=True)
+    image_uri = arg(args, 5, "Image URI", optional=True)
 
     create(supply, decimals, symbol, name, main_uri, image_uri)
 

+ 41 - 0
ag/orbit/cli/admin/transfer.py

@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 Alpha Griffin
+# @%@~LICENSE~@%@
+
+from ag.orbit.command import main
+from ag.orbit.ops.allocation import Transfer
+from ag.orbit.cli import arg
+from ag.orbit.cli.network import broadcast
+
+
+def run(args):
+    args = args if args else None
+
+    print()
+    print("Transfer tokens...")
+
+    if args:
+        if len(args) != 2:
+            print()
+            raise ValueError("Expecting exactly 2 arguments")
+
+    to = arg(args, 0, "Destination address")
+
+    units = arg(args, 1, "Number of indivisible units (leave blank to transfer ALL tokens)", optional=True)
+    if units and units != "ALL":
+        units = int(units)
+
+    transfer(to, units)
+
+def transfer(to, units):
+    if units == "ALL":
+        units = None
+
+    op = Transfer(to, units)
+    broadcast(op)
+
+
+if __name__ == '__main__':
+    main(run)
+

+ 63 - 0
ag/orbit/cli/config/__init__.py

@@ -0,0 +1,63 @@
+# Copyright (C) 2018 Alpha Griffin
+# @%@~LICENSE~@%@
+
+#import ag.logging as log
+
+from os import makedirs, path
+
+from appdirs import AppDirs
+dirs = AppDirs("orbit-cli", "Alpha Griffin")
+
+dir = dirs.user_config_dir
+#log.debug("Starting up", configdir=dir)
+
+if not path.exists(dir):
+    #log.info("Running first-time setup for configuration...")
+
+    #log.debug("Creating user config directory")
+    makedirs(dir, exist_ok=True)
+
+if not path.isdir(dir):
+    #log.fatal("Expected a directory for configdir", configdir=dir)
+    raise Exception("Not a directory: " + dir)
+
+
+def get_orbit_host():
+    orbit = path.join(dir, 'orbit_host')
+
+    if not path.exists(orbit):
+        #raise ValueError('ORBIT node hostname / IP address not set. You must set a host first with: `orbit-cli config host`')
+        return 'localhost'
+
+    with open(orbit, 'r') as orbitin:
+        return orbitin.readline()
+
+def set_orbit_host(host):
+    orbit = path.join(dir, 'orbit_host')
+    with open(orbit, 'w') as out:
+        out.write(host)
+
+    return orbit
+
+
+def get_orbit_port():
+    orbit = path.join(dir, 'orbit_port')
+
+    if not path.exists(orbit):
+        #raise ValueError('ORBIT node port number not set. You must set a port first with: `orbit-cli config port`')
+        from ag.orbit.webapi import DEFAULT_PORT
+        return DEFAULT_PORT
+
+    with open(orbit, 'r') as orbitin:
+        return int(orbitin.readline())
+
+def set_orbit_port(port):
+    if int(port) < 1:
+        raise ValueError("Port number must be a positive integer.")
+
+    orbit = path.join(dir, 'orbit_port')
+    with open(orbit, 'w') as out:
+        out.write(port)
+
+    return orbit
+

+ 55 - 0
ag/orbit/cli/config/__main__.py

@@ -0,0 +1,55 @@
+# Copyright (C) 2018 Alpha Griffin
+# @%@~LICENSE~@%@
+
+from ...command import invoke
+
+from sys import argv, exit
+from contextlib import suppress
+
+
+CALL = 'orbit-cli config'
+
+def usage():
+    print()
+    print("Usage: {} <command>".format(CALL))
+    print()
+    print("    Configuration options.")
+    print()
+    print("Where <command> is:")
+    print("    help             - Display this usage screen")
+    print("    host [<host>]    - Set or display the ORBIT node hostname / IP address")
+    print("    port [<port>]    - Set or display the ORBIT node port number")
+    print()
+
+
+with suppress(KeyboardInterrupt):
+    if len(argv) > 1 and argv[1] is None:
+        # we were called from the parent module
+        args = argv[2:]
+    else:
+        args = argv[1:]
+
+    if len(args) < 1:
+        usage()
+        exit(101)
+
+    cmd = args[0]
+    args = args[1:] if len(args) > 1 else None
+
+    if cmd == 'help':
+        usage()
+
+    elif cmd == 'host':
+        from .host import run
+        invoke(CALL, cmd, 102, run, args, 1, 1, optional=True)
+
+    elif cmd == 'port':
+        from .port import run
+        invoke(CALL, cmd, 103, run, args, 1, 1, optional=True)
+
+    else:
+        print()
+        print("{}: unknown command: {}".format(CALL, cmd))
+        usage()
+        exit(199)
+

+ 34 - 0
ag/orbit/cli/config/host.py

@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 Alpha Griffin
+# @%@~LICENSE~@%@
+
+from ag.orbit.command import main
+from ag.orbit.cli.config import get_orbit_host, set_orbit_host
+
+
+def run(args):
+    if args and len(args) != 1:
+        raise ValueError("Expecting exactly 1 argument")
+
+    if args:
+        host = args[0]
+
+        print()
+        print("    Setting ORBIT node hostname / IP address to: {}".format(host))
+
+        orbit = set_orbit_host(host)
+
+        print()
+        print("ORBIT host saved to: {}".format(orbit))
+
+    else:
+        host = get_orbit_host()
+
+        print()
+        print("    Hostname / IP address for ORBIT node: {}".format(host))
+
+
+if __name__ == '__main__':
+    main(run)
+

+ 34 - 0
ag/orbit/cli/config/port.py

@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 Alpha Griffin
+# @%@~LICENSE~@%@
+
+from ag.orbit.command import main
+from ag.orbit.cli.config import get_orbit_port, set_orbit_port
+
+
+def run(args):
+    if args and len(args) != 1:
+        raise ValueError("Expecting exactly 1 argument")
+
+    if args:
+        port = args[0]
+
+        print()
+        print("    Setting ORBIT node port number to: {}".format(port))
+
+        orbit = set_orbit_port(port)
+
+        print()
+        print("ORBIT port saved to: {}".format(orbit))
+
+    else:
+        port = get_orbit_port()
+
+        print()
+        print("    Port number for ORBIT node: {}".format(port))
+
+
+if __name__ == '__main__':
+    main(run)
+

+ 1 - 1
ag/orbit/cli/network.py

@@ -99,7 +99,7 @@ def broadcast(op, token_address=None):
     #print(message)
 
     if len(message) > MESSAGE_LIMIT:
-        raise AssertionError("The data is too large")
+        raise ValueError("The data is too large; try reducing some text or removing optional data")
 
     # sanity check
     parsed = orbit.parse(message)

+ 2 - 2
ag/orbit/cli/token/__init__.py

@@ -1,9 +1,9 @@
 # Copyright (C) 2018 Alpha Griffin
 # @%@~LICENSE~@%@
 
-"""ORBIT Token Commands
+"""ORBIT Token User Commands
 
 .. module:: ag.orbit.cli.token
-   :synopsis: ORBIT Token Commands
+   :synopsis: ORBIT Token User Commands
 """
 

+ 21 - 16
ag/orbit/cli/token/__main__.py

@@ -15,19 +15,12 @@ def usage():
     print()
     print("Usage: {} <command>".format(CALL))
     print()
+    print("    User token management.")
+    print()
     print("Where <command> is:")
     print("    help")
     print("        Display this usage screen")
     print()
-    print("    create [<supply> <decimals> <symbol> [<name> [<main_uri> [<image_uri>]]]]")
-    print("        Create new token")
-    print("            - <supply> is initial token supply (number of indivisible units)")
-    print("            - <decimals> is number of decimal points to divide up the supply")
-    print("            - <symbol> is ticker symbol")
-    print("            - <name> is optional name")
-    print("            - <main_uri> is optional link to a web page, etc.")
-    print("            - <image_uri> is optional link or data for embedded image")
-    print()
     print("    transfer [<token> <to> <units|ALL>]")
     print("        Transfer tokens")
     print("            - <token> is the token address")
@@ -35,6 +28,14 @@ def usage():
     print("            - <units> is the number of indivisible units (not normalized) to transfer;")
     print("                the text \"ALL\" may be used to transfer all available tokens")
     print()
+    print("    register [<token>]")
+    print("        Register interest in crowd-sale or faucet")
+    print("            - <token> is the token address")
+    print()
+    print("    unregister [<token>]")
+    print("        Remove interest in crowd-sale or faucet")
+    print("            - <token> is the token address")
+    print()
     print("All commands may be called without arguments for full interactive mode.")
     print()
     print("Most commands require a private key for signing messages. You will be prompted")
@@ -54,7 +55,7 @@ with suppress(KeyboardInterrupt):
 
     if len(args) < 1:
         usage()
-        exit(201)
+        exit(301)
 
     cmd = args[0]
     args = args[1:] if len(args) > 1 else None
@@ -62,18 +63,22 @@ with suppress(KeyboardInterrupt):
     if cmd == 'help':
         usage()
 
-    elif cmd == 'create':
-        from .create import run
-        invoke(CALL, cmd, 202, run, args, 3, 6, optional=True)
-
     elif cmd == 'transfer':
         from .transfer import run
-        invoke(CALL, cmd, 203, run, args, 3, 3, optional=True)
+        invoke(CALL, cmd, 302, run, args, 3, 6, optional=True)
+
+    elif cmd == 'register':
+        from .register import run
+        invoke(CALL, cmd, 303, run, args, 1, 1, optional=True)
+
+    elif cmd == 'unregister':
+        from .unregister import run
+        invoke(CALL, cmd, 304, run, args, 1, 1, optional=True)
 
     else:
         #log.error("unknown command", command=cmd)
         print()
         print("{}: unknown command: {}".format(CALL, cmd))
         usage()
-        exit(299)
+        exit(399)
 

+ 3 - 3
ag/orbit/cli/token/transfer.py

@@ -4,7 +4,7 @@
 # @%@~LICENSE~@%@
 
 from ag.orbit.command import main
-from ag.orbit.ops.transfer import Transfer
+from ag.orbit.ops.allocation import Transfer
 from ag.orbit.cli import arg
 from ag.orbit.cli.network import broadcast
 
@@ -20,10 +20,10 @@ def run(args):
             print()
             raise ValueError("Expecting exactly 3 arguments")
 
-    token = arg(args, 0, "Token address (leave blank to use your wallet address)")
+    token = arg(args, 0, "Token address")
     to = arg(args, 1, "Destination address")
 
-    units = arg(args, 2, "Number of indivisible units (leave blank to transfer ALL tokens)")
+    units = arg(args, 2, "Number of indivisible units (leave blank to transfer ALL tokens)", optional=True)
     if units and units != "ALL":
         units = int(units)
 

+ 25 - 14
ag/orbit/cli/wallet/__main__.py

@@ -15,6 +15,8 @@ def usage():
     print()
     print("Usage: {} <command>".format(CALL))
     print()
+    print("    Wallet commands.")
+    print()
     print("Where <command> is:")
     print("    help")
     print("        Display this usage screen")
@@ -37,6 +39,11 @@ def usage():
     print("            - <name> is the current name of the wallet")
     print("            - <newname> is a new name for the wallet")
     print()
+    print("    key <name> [<password>]")
+    print("        Print the private key stored in a wallet")
+    print("            - <name> is the name of the wallet to read")
+    print("            - <password> is the encryption password used during creation")
+    print()
     print("    address <name> [<password>]")
     print("        Print the public address for a wallet")
     print("            - <name> is the name of the wallet to read")
@@ -47,8 +54,8 @@ def usage():
     print("            - <name> is the name of the wallet to read")
     print("            - <password> is the encryption password used during creation")
     print()
-    print("    key <name> [<password>]")
-    print("        Print the private key stored in a wallet")
+    print("    tokens <name> [<password>]")
+    print("        List the tokens and balances associated to this wallet (connects to ORBIT node)")
     print("            - <name> is the name of the wallet to read")
     print("            - <password> is the encryption password used during creation")
     #print()
@@ -71,7 +78,7 @@ with suppress(KeyboardInterrupt):
 
     if len(args) < 1:
         usage()
-        exit(301)
+        exit(201)
         
     cmd = args[0]
     args = args[1:] if len(args) > 1 else None
@@ -81,40 +88,44 @@ with suppress(KeyboardInterrupt):
 
     elif cmd == 'create':
         from .create import run
-        invoke(CALL, cmd, 302, run, args, 0, 2)
+        invoke(CALL, cmd, 202, run, args, 0, 2)
 
     elif cmd == 'import':
         from .import_key import run
-        invoke(CALL, cmd, 303, run, args, 1, 3)
+        invoke(CALL, cmd, 203, run, args, 1, 3)
 
     elif cmd == 'list':
         from .list import run
-        invoke(CALL, cmd, 304, run, args)
+        invoke(CALL, cmd, 204, run, args)
 
     elif cmd == 'rename':
         from .rename import run
-        invoke(CALL, cmd, 305, run, args, 2, 2)
+        invoke(CALL, cmd, 205, run, args, 2, 2)
+
+    elif cmd == 'key':
+        from .key import run
+        invoke(CALL, cmd, 206, run, args, 1, 2)
 
     elif cmd == 'address':
         from .address import run
-        invoke(CALL, cmd, 306, run, args, 1, 2)
+        invoke(CALL, cmd, 207, run, args, 1, 2)
 
     elif cmd == 'balance':
         from .balance import run
-        invoke(CALL, cmd, 307, run, args, 1, 2)
+        invoke(CALL, cmd, 208, run, args, 1, 2)
 
-    elif cmd == 'key':
-        from .key import run
-        invoke(CALL, cmd, 308, run, args, 1, 2)
+    elif cmd == 'tokens':
+        from .tokens import run
+        invoke(CALL, cmd, 209, run, args, 1, 2)
 
     #elif cmd == 'sign':
     #    from .sign import run
-    #    invoke(CALL, cmd, 308, run, args, 1, 3)
+    #    invoke(CALL, cmd, 208, run, args, 1, 3)
 
     else:
         #log.error("unknown command", command=cmd)
         print()
         print("{}: unknown command: {}".format(CALL, cmd))
         usage()
-        exit(399)
+        exit(299)
 

+ 68 - 0
ag/orbit/cli/wallet/tokens.py

@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 Alpha Griffin
+# @%@~LICENSE~@%@
+
+from ag.orbit.command import main
+from ag.orbit.cli import password_handler
+from ag.orbit.cli.config import get_orbit_host, get_orbit_port
+from ag.orbit.wallet import path, access
+from ag.orbit.webapi import Client, Endpoints
+
+from decimal import Decimal
+
+
+def run(args):
+    if args is None or len(args) < 1 or len(args) > 2:
+        raise ValueError("Expecting no less than 1 and no more than 2 arguments")
+
+    name = args[0].strip()
+    password = args[1] if len(args) > 1 else None
+
+    print()
+    tokens(name, password)
+
+def tokens(name, password=None):
+    print("Opening wallet...")
+
+    print("    Name: {}".format(name))
+    wpath = path(name)
+    print("    File: {}".format(wpath))
+
+    wallet = access(wpath, password_handler(password))
+
+    print()
+    print("Connecting to ORBIT node to retrieve token information...")
+
+    host = get_orbit_host()
+    port = get_orbit_port()
+
+    print("    Host: {}".format(host))
+    print("    Port: {}".format(port))
+
+    client = Client(host=host, port=port)
+    tokens = client.get_user_tokens(wallet.address)[Endpoints.USER_TOKENS]
+
+    if tokens:
+        for token in tokens:
+            print()
+            print("    Token @ {}".format(token['address']))
+            print("        Name: {}".format(token['name']))
+            print("        Symbol: {}".format(token['symbol']))
+            decimals = token['decimals']
+            print("        Decimals: {}".format(decimals))
+            print("      Balance")
+            total = token['units']
+            print("          Total: {} ({} unit{})".format(
+                Decimal(total).scaleb(-1 * decimals), total, "" if total == 1 else "s"))
+            available = token['available']
+            print("          Available: {} ({} unit{})".format(
+                Decimal(available).scaleb(-1 * decimals), available, "" if available == 1 else "s"))
+    else:
+        print()
+        print("    No tokens")
+
+
+if __name__ == '__main__':
+    main(run)
+

+ 36 - 0
api/ag.orbit.cli.admin.rst

@@ -0,0 +1,36 @@
+ag\.orbit\.cli\.admin package
+=============================
+
+.. automodule:: ag.orbit.cli.admin
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+Submodules
+----------
+
+ag\.orbit\.cli\.admin\.advertise module
+---------------------------------------
+
+.. automodule:: ag.orbit.cli.admin.advertise
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+ag\.orbit\.cli\.admin\.create module
+------------------------------------
+
+.. automodule:: ag.orbit.cli.admin.create
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+ag\.orbit\.cli\.admin\.transfer module
+--------------------------------------
+
+.. automodule:: ag.orbit.cli.admin.transfer
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+

+ 28 - 0
api/ag.orbit.cli.config.rst

@@ -0,0 +1,28 @@
+ag\.orbit\.cli\.config package
+==============================
+
+.. automodule:: ag.orbit.cli.config
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+Submodules
+----------
+
+ag\.orbit\.cli\.config\.host module
+-----------------------------------
+
+.. automodule:: ag.orbit.cli.config.host
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+ag\.orbit\.cli\.config\.port module
+-----------------------------------
+
+.. automodule:: ag.orbit.cli.config.port
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+

+ 7 - 7
api/ag.orbit.cli.rst

@@ -1,11 +1,18 @@
 ag\.orbit\.cli package
 ======================
 
+.. automodule:: ag.orbit.cli
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
 Subpackages
 -----------
 
 .. toctree::
 
+    ag.orbit.cli.admin
+    ag.orbit.cli.config
     ag.orbit.cli.token
     ag.orbit.cli.wallet
 
@@ -21,10 +28,3 @@ ag\.orbit\.cli\.network module
     :show-inheritance:
 
 
-Module contents
----------------
-
-.. automodule:: ag.orbit.cli
-    :members:
-    :undoc-members:
-    :show-inheritance:

+ 4 - 14
api/ag.orbit.cli.token.rst

@@ -1,17 +1,14 @@
 ag\.orbit\.cli\.token package
 =============================
 
-Submodules
-----------
-
-ag\.orbit\.cli\.token\.create module
-------------------------------------
-
-.. automodule:: ag.orbit.cli.token.create
+.. automodule:: ag.orbit.cli.token
     :members:
     :undoc-members:
     :show-inheritance:
 
+Submodules
+----------
+
 ag\.orbit\.cli\.token\.transfer module
 --------------------------------------
 
@@ -21,10 +18,3 @@ ag\.orbit\.cli\.token\.transfer module
     :show-inheritance:
 
 
-Module contents
----------------
-
-.. automodule:: ag.orbit.cli.token
-    :members:
-    :undoc-members:
-    :show-inheritance:

+ 10 - 4
api/ag.orbit.cli.wallet.rst

@@ -1,6 +1,11 @@
 ag\.orbit\.cli\.wallet package
 ==============================
 
+.. automodule:: ag.orbit.cli.wallet
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
 Submodules
 ----------
 
@@ -60,11 +65,12 @@ ag\.orbit\.cli\.wallet\.rename module
     :undoc-members:
     :show-inheritance:
 
+ag\.orbit\.cli\.wallet\.tokens module
+-------------------------------------
 
-Module contents
----------------
-
-.. automodule:: ag.orbit.cli.wallet
+.. automodule:: ag.orbit.cli.wallet.tokens
     :members:
     :undoc-members:
     :show-inheritance:
+
+

+ 0 - 2
api/modules.rst

@@ -2,6 +2,4 @@ ORBIT CLI
 =========
 
 .. toctree::
-   :maxdepth: 4
-
    ag.orbit.cli

BIN
etc/sphinx/_static/favicon-196.png


BIN
etc/sphinx/_static/favicon.ico


+ 11 - 9
etc/sphinx/conf.py

@@ -46,7 +46,7 @@ extensions = [
     'sphinx.ext.intersphinx',
     'sphinx.ext.todo',
     'sphinx.ext.coverage',
-    'sphinx.ext.pngmath',
+    'sphinx.ext.imgmath',
     'sphinx.ext.ifconfig',
     'sphinx.ext.viewcode',
 ]
@@ -134,8 +134,8 @@ html_theme = 'sphinx_rtd_theme'
 # further.  For a list of options available for each theme, see the
 # documentation.
 html_theme_options = {
-        #'canonical_url': 'http://doc.alphagriffin.com/' + NAME,
-        'analytics_id': 'UA-36092231-27',
+        'canonical_url': 'https://doc.orbit.cash/' + NAME,
+        'analytics_id': 'UA-36092231-29',
         'display_version': True,
         'sticky_navigation': True,
         'collapse_navigation': False,
@@ -280,12 +280,14 @@ latex_documents = [
 
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
-man_pages = [
-    (master_doc, COMMAND, 'Command for ' + NS + '.' + NAME + ' API',
-     [author], 1),
-    (master_doc, NS + '.' + NAME, 'API Documentation',
-     [author], 3)
-]
+man_pages = []
+if COMMAND:
+    man_pages.append(
+            (master_doc, COMMAND, 'Command for ' + NS + '.' + NAME + ' API',
+                [author], 1))
+man_pages.append(
+        ('api/modules', NS + '.' + NAME, 'API Documentation',
+            [author], 3))
 
 # If true, show URL addresses after external links.
 #man_show_urls = False

+ 2 - 1
setup.py

@@ -31,7 +31,7 @@ NAME    = 'cli'                         # should match source package name in NS
 COMMAND = 'orbit-cli'                   # command name may be different than package name
 REQUIRE = [                             # package dependencies
             #'ag.logging',
-            'ag.orbit(>=0.4,<1)',
+            'ag.orbit(>=0.7,<1)',
             'appdirs',
             'bitcash(>=0.5.2.4)',
             'pycrypto'
@@ -123,6 +123,7 @@ if __name__ == '__main__':
         url=URL,
         classifiers=CLASS,
         keywords=TAGS,
+        scripts=([ COMMAND ] if COMMAND else None),
 
         # run-time dependencies
         install_requires=REQUIRE,