diff --git a/README.md b/README.md index 7150a3f..49e1002 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Simple CLI PeerAssets client. Implemented using `pypeerassets` Python library, this command line program is useful as companion utility during PeerAssets development and testing. It is built for headless (CLI) usage via intuitive and easy to learn set of commands. All deck id's are shortened by taking only 20 first characters of full sha256 deck id, this is to allow easier user interaction -and use less screen space. You can always get full deck id by calling `pacli --info` command as shown bellow. +and use less screen space. You can always get full deck id by calling `pacli deck --info` command as shown bellow. When querying for deck you can use short deck id, full deck id and deck name. Using short or full deck id is advised as decks can have a same name. @@ -15,23 +15,23 @@ Examples: show all commands -> pacli --status +> pacli status show current network, all subscribed decks and their card balances -> pacli --newaddress +> pacli new_address generate a new address and load it into wallet -> pacli --addressbalance
+> pacli address_balance
show card balance of the address -> pacli deck --search "My little pony" +> pacli deck search "My little pony" search for deck called "My little pony" -> pacli deck --list all +> pacli deck list all list all decks on the network @@ -52,11 +52,11 @@ Complex operations take JSON-like sturucture as argument, mimicking peercoind JS * amount variable is always a list * receiver variable is always a list -> pacli deck --new '{"name": "My own asset", "number_of_decimals": 1, "issue_mode": "ONCE"}' +> pacli deck new '{"name": "My own asset", "number_of_decimals": 1, "issue_mode": "ONCE"}' issue a new asset named "My own asset". -> pacli card --list *deck_id* +> pacli card list *deck_id* list all card transfers related to this deck @@ -65,25 +65,25 @@ list all card transfers related to this deck verify deck checksum, checksum is difference between issued cards and balances of all the addresses. If it is not zero, something is wrong with deck balances. This function will return True if all is fine. -> pacli card --burn '{"deck": "d460651e1d9147770ec9d4c254bcc68ff5d203a86b97c09d00955fb3f714cab3", "amounts": [11]}' +> pacli card burn '{"deck": "d460651e1d9147770ec9d4c254bcc68ff5d203a86b97c09d00955fb3f714cab3", "amounts": [11]}' burn 11 of card on this deck, this transaction will be denied if you have no cards on this deck. -> pacli card --issue '{"deck": "hopium_v2", "receiver": ["n29g3XjvxqWLKgEkyg4Z1BmgrJLccqiH3x"], "amount": [110]}' +> pacli card issue '{"deck": "hopium_v2", "receiver": ["n29g3XjvxqWLKgEkyg4Z1BmgrJLccqiH3x"], "amount": [110]}' issue 110 cards to n29g3XjvxqWLKgEkyg4Z1BmgrJLccqiH3x, this transaction will be declined if you do not own deck issuing address. -> pacli card --transfer '{"deck": "08c1928ce84d9066f120", "receiver": ["n1GqTk2NFvSCX3h78rkEA3DoiJW8QxT3Mm", "mv8J47BV8ahpKq7dNXut3kXPgQQCQea5FR", +> pacli card transfer '{"deck": "08c1928ce84d9066f120", "receiver": ["n1GqTk2NFvSCX3h78rkEA3DoiJW8QxT3Mm", "mv8J47BV8ahpKq7dNXut3kXPgQQCQea5FR", "myeFFDLXvpGUh8gBPZdCNEsLQ7ZPZkH7d8"], "amount": [1, 9.98, 200.1]}' transfer cards of "08c1928ce84d9066f120" deck (queried by short id, it is clementines deck) to three different addresses. This transaction will be denied if you have no address which holds cards of this deck or if your balance is not sufficient. -> pacli vote --new '{"deck": "hopium_v2", "choices": ["y", "n"], "count_mode": "SIMPLE", "description": "yes or no?", "start_block": 27306, "end_block": 27310}' +> pacli vote new '{"deck": "hopium_v2", "choices": ["y", "n"], "count_mode": "SIMPLE", "description": "yes or no?", "start_block": 27306, "end_block": 27310}' create new vote on the "hopium_v2" deck with choices "y" and "no", starting from block 27306 and lasting until block 27310. -> pacli vote --list "hopium_v2" +> pacli vote list "hopium_v2" Shows all the votes on this deck. @@ -96,7 +96,7 @@ Shows all the votes on this deck. +------------------------------------------------------------------+------------------------------------+------------------+-------------+-----------+ ``` -> pacli vote --cast '{"vote": "7459c9f4738001e3c50653d6066e3d41a9ffb2a1f3d786721bc472bcb04f17fa", choice: "yes"}' +> pacli vote cast '{"vote": "7459c9f4738001e3c50653d6066e3d41a9ffb2a1f3d786721bc472bcb04f17fa", choice: "yes"}' cast "yes" vote to vote_id 7459c9f4738001e3c50653d6066e3d41a9ffb2a1f3d786721bc472bcb04f17fa diff --git a/pacli/__main__.py b/pacli/__main__.py index b9fd929..7590c8d 100644 --- a/pacli/__main__.py +++ b/pacli/__main__.py @@ -1,904 +1,15 @@ -from datetime import datetime -from terminaltables import AsciiTable -from binascii import hexlify -from appdirs import user_config_dir -from pacli.config import write_default_config, read_conf -from pacli.export import export_to_csv -import os, argparse -import pypeerassets as pa -from pypeerassets.pautils import amount_to_exponent, exponent_to_amount -import json -import logging - -from pacli.keystore import GpgKeystore, as_local_key_provider - -conf_dir = user_config_dir("pacli") -conf_file = os.path.join(conf_dir, "pacli.conf") -logfile = os.path.join(conf_dir, "pacli.log") -keyfile = os.path.join(conf_dir, "pacli.gpg") - -class Settings: - pass - - -def load_conf(): - '''load user configuration''' - - settings = read_conf(conf_file) - - for key in settings: - setattr(Settings, key, settings[key]) - - logging.basicConfig(filename=logfile, level=logging.getLevelName(Settings.loglevel)) - logging.basicConfig(level=logging.getLevelName(Settings.loglevel), - format="%(asctime)s %(levelname)s %(message)s") - - logging.debug("logging initialized") - - -def first_run(): - '''if first run, setup local configuration directory.''' - - if not os.path.exists(conf_dir): - os.mkdir(conf_dir) - if not os.path.exists(conf_file): - write_default_config(conf_file) - if not os.path.exists(keyfile): - open(keyfile, 'a').close() - -def set_up(provider): - '''setup''' - - # if provider is local node, check if PA P2TH is loaded in local node - # this handles indexing of transaction - if Settings.provider == "rpcnode": - if Settings.production: - if not provider.listtransactions("PAPROD"): - pa.pautils.load_p2th_privkeys_into_local_node(provider) - if not Settings.production: - if not provider.listtransactions("PATEST"): - pa.pautils.load_p2th_privkeys_into_local_node(provider, prod=False) - #elif Settings.provider != 'holy': - # pa.pautils.load_p2th_privkeys_into_local_node(provider, keyfile) - -def default_account_utxo(provider, amount): - '''set default address to be used with pacli''' - - if "PACLI" not in provider.listaccounts().keys(): - addr = provider.getaddressesbyaccount("PACLI") - print("\n", "Please fund this address: {addr}".format(addr=addr)) - return - - for i in provider.getaddressesbyaccount("PACLI"): - try: - return provider.select_inputs(amount, i) - except ValueError: - pass - - print("\n", "Please fund one of the following addresses: {addrs}".format( - addrs=provider.getaddressesbyaccount("PACLI"))) - return - - -def change(utxo): - '''decide what will be change address - * default - pay back to largest utxo - * standard - behave as wallet does - pay to new address - ''' - - if Settings.change == "default": - m = max([i["amount"] for i in utxo["utxos"]]) - return [i["address"] for i in utxo["utxos"] if i["amount"] == m][0] - - if Settings.change == "standard": - return provider.getnewaddress() - -def tstamp_to_iso(tstamp): - '''make iso timestamp from unix timestamp''' - - return datetime.fromtimestamp(tstamp).isoformat() - - -def find_deck(provider, key: str) -> list: - '''find deck by ''' - - decks = list(pa.find_all_valid_decks(provider, deck_version=Settings.deck_version, prod=Settings.production)) - for i in decks: - i.short_id = i.asset_id[:20] - - return [d for d in decks if key in d.__dict__.values()] - - -class ListDecks: - - @classmethod - def __init__(cls, decks): - cls.decks = list(decks) - - ## Deck table header - deck_table = [ - ## add subscribed column - ("asset ID", "asset name", "issuer", "mode") - ] - - table = AsciiTable(deck_table, title="Decks") - - @classmethod - def dtl(cls, deck): - '''deck-to-list deck to table-printable list''' - - l = [] - l.append(deck["asset_id"][:20]) - l.append(deck["name"]) - l.append(deck["issuer"]) - l.append(deck["issue_mode"]) - - return l - - @classmethod - def pack_decks_for_printing(cls): - - assert cls.decks, {"error": "No decks found!"} - - for i in cls.decks: - cls.deck_table.append( - cls.dtl(i.__dict__) - ) - -class ListCards: - - @classmethod - def __init__(cls, provider, cards): - cls.provider = provider - cls.cards = list(cards) - - ## Deck table header - cls.card_table = [ - ## add subscribed column - ("txid", "sender", "receiver", "amount", "type", "confirms") - ] - - cls.table = AsciiTable(cls.card_table, title="Card transfers of this deck:") - - @classmethod - def dtl(cls, card, subscribed=False): - '''cards-to-list cards to table-printable list''' - - l = [] - l.append(card["txid"][:20]) - l.append(card["sender"]) - l.append(card["receiver"][0]) - l.append(exponent_to_amount(card["amount"][0], - card["number_of_decimals"])) - l.append(card["type"]) - if card["blockhash"] != 0: - l.append(cls.provider.getrawtransaction(card["txid"], 1)["confirmations"]) - else: - l.append(0) - - return l - - @classmethod - def pack_cards_for_printing(cls): - - #assert len(cls.cards) > 0, {"error": "No cards found!"} - - for i in cls.cards: - cls.card_table.append( - cls.dtl(i.__dict__) - ) - - -class DeckInfo: - - @classmethod - def __init__(cls, deck): - assert isinstance(deck, pa.Deck) - cls.deck = deck - - ## Deck table header - cls.deck_table = [ - ## add subscribed column - ("asset name", "issuer", "issue mode", "decimals", "issue time") - ] - - cls.table = AsciiTable(cls.deck_table, title="Deck id: " + cls.deck.asset_id + " ") - - @staticmethod - def dtl(deck, subscribed=False): - '''deck-to-list deck to table-printable list''' - - l = [] - l.append(deck["name"]) - l.append(deck["issuer"]) - l.append(deck["issue_mode"]) - l.append(deck["number_of_decimals"]) - l.append(tstamp_to_iso(deck["issue_time"])) - - return l - - @classmethod - def pack_decks_for_printing(cls): - - cls.deck_table.append(cls.dtl(cls.deck.__dict__)) - - -class DeckBalances: - '''Show balances of address tied with this deck.''' - - @classmethod - def __init__(cls, deck, balances): - assert isinstance(deck, pa.Deck) - cls.balances = dict(balances) - cls.deck = deck - - ## Deck table header - cls.deck_table = [ - ## add subscribed column - ("address", "balance") - ] - - cls.table = AsciiTable(cls.deck_table, title="Deck id: " + cls.deck.asset_id + " ") - - @classmethod - def dtl(cls, addr, balance): - '''deck-to-list deck to table-printable list''' - - l = [] - l.append(addr) - l.append(exponent_to_amount(balance, - cls.deck.number_of_decimals)) - - return l - - @classmethod - def pack_for_printing(cls): - - assert len(cls.balances) > 0, {"error": "No balances found!"} - - for k in cls.balances: - cls.deck_table.append( - cls.dtl(k, cls.balances[k]) - ) - - -def deck_list(provider): - '''list command''' - - decks = pa.find_all_valid_decks(provider=provider, deck_version=Settings.deck_version, - prod=Settings.production) - d = ListDecks(decks) - d.pack_decks_for_printing() - print(d.table.table) - - -def deck_subscribe(provider, deck_id): - '''subscribe command, load deck p2th into local node, pass ''' - - try: - deck = find_deck(provider, deck_id)[0] - except IndexError: - print({"error": "Deck not found!"}) - return - pa.load_deck_p2th_into_local_node(provider, deck) - - -def deck_search(provider, key): - '''search commands, query decks by ''' - - decks = find_deck(provider, key) - d = ListDecks(provider, decks) - d.pack_decks_for_printing() - print(d.table.table) - - -def deck_info(provider, deck_id): - '''info commands, show full deck details''' - - try: - deck = find_deck(provider, deck_id)[0] - except IndexError: - print("\n", {"error": "Deck not found!"}) - return - info = DeckInfo(deck) - info.pack_decks_for_printing() - print(info.table.table) - - -def deck_balances(provider, deck_id): - '''show deck balances''' - - try: - deck = find_deck(provider, deck_id)[0] - except IndexError: - print("\n", {"error": "Deck not found!"}) - return - balances = get_state(provider, deck).balances - b = DeckBalances(deck, balances) - b.pack_for_printing() - print(b.table.table) - - -def deck_checksum(provider, deck_id): - '''info commands, show full deck details''' - - try: - deck = find_deck(provider, deck_id)[0] - except IndexError: - print("\n", {"error": "Deck not found!"}) - return - deck_state = get_state(provider, deck) - if deck_state.checksum: - print("\n", "Deck checksum is correct.") - else: - print("\n", "Deck checksum is incorrect.") - - -def new_deck(provider, deck, broadcast): - ''' - Spawn a new PeerAssets deck. - - pacli deck --new '{"name": "test", "number_of_decimals": 1, "issue_mode": "ONCE"}' - - Will return deck span txid. - ''' - - deck = json.loads(deck) - deck["network"] = Settings.network - deck["production"] = Settings.production - #utxo = provider.select_inputs(0.02) # we need 0.02 PPC - utxo = default_account_utxo(provider, 0.02) - if utxo: - change_address = change(utxo) - else: - return - raw_deck = pa.deck_spawn(pa.Deck(**deck), - inputs=utxo, - change_address=change_address - ) - raw_deck_spawn = hexlify(raw_deck).decode() - signed = provider.signrawtransaction(raw_deck_spawn) - - if broadcast: - txid = provider.sendrawtransaction(signed["hex"]) - print("\n", txid, "\n") - - deck["asset_id"] = txid - d = pa.Deck(**deck) - pa.load_deck_p2th_into_local_node(provider, d) # subscribe to deck - else: - print("\nraw transaction:\n", signed["hex"], "\n") - - -def list_cards(provider, args): - ''' - List cards of this .abs - - pacli card -list - ''' - - try: - deck = find_deck(provider, args)[0] - except IndexError: - print("\n", {"error": "Deck not found!"}) - return - if isinstance(provider, pa.RpcNode): - if not provider.getaddressesbyaccount(deck.name): - print("\n", {"error": "You must subscribe to deck to be able to list transactions."}) - return - - all_cards = pa.find_card_transfers(provider, deck) - cards = pa.validate_card_issue_modes(deck, all_cards) - c = ListCards(provider, cards) - c.pack_cards_for_printing() - print(c.table.table) - - -def export_cards(provider, args): - ''' - export cards to csv - - pacli card -export filename - ''' - - try: - deck = find_deck(provider, args[0])[0] - except IndexError: - print("\n", {"error": "Deck not found!"}) - return - if not provider.getaddressesbyaccount(deck.name): - print("\n", {"error": "You must subscribe to deck to be able to list transactions."}) - return - - all_cards = pa.find_card_transfers(provider, deck) - cards = pa.validate_card_issue_modes(deck, all_cards) - - export_to_csv(cards, args[1]) - - -def card_issue(provider, args, broadcast): - ''' - Issue new cards of this deck. - - pacli card -issue '{"deck": "deck_id", - "receivers": [list of receiver addresses], - "amounts": [list of amounts] - } - ''' - - issue = json.loads(args) - try: - deck = find_deck(provider, issue["deck"])[0] - except IndexError: - print("\n", {"error": "Deck not found."}) - return - - if not provider.gettransaction(deck.asset_id)["confirmations"] > 0: - print("\n", "You are trying to issue cards on a deck which has not been confirmed yet.") - - if provider.validateaddress(deck.issuer)["ismine"]: - try: - utxo = provider.select_inputs(0.02, deck.issuer) - except ValueError: - print("\n", {"error": "Please send funds to the deck issuing address: {0}".format(deck.issuer)}) - return - else: - print("\n", {"error": "You are not the owner of this deck."}) - return - - issue["amount"] = [amount_to_exponent(float(i), deck.number_of_decimals) for i in issue["amount"]] - change_address = change(utxo) - ct = pa.CardTransfer(deck=deck, receiver=issue["receiver"], - amount=issue["amount"]) - raw_ct = hexlify(pa.card_issue(deck, ct, utxo, - change_address - ) - ).decode() - - signed = provider.signrawtransaction(raw_ct) - - if broadcast: - txid = provider.sendrawtransaction(signed["hex"]) # send the tx - print("\n", txid, "\n") - else: - print("\nraw transaction:\n", signed["hex"], "\n") - - -def card_burn(provider, args, broadcast): - ''' - Burn cards of this deck. - - pacli card -burn '{"deck": "deck_id", "amount": ["amount"]}' - ''' - - args = json.loads(args) - try: - deck = find_deck(provider, args["deck"])[0] - except IndexError: - print("\n", {"error": "Deck not found!"}) - return - if not provider.getaddressesbyaccount(deck.name): - print("\n", {"error": "You are not even subscribed to this deck, how can you burn cards?"}) - return - try: - my_balance = get_my_balance(provider, deck.asset_id) - except ValueError: - print("\n", {"error": "You have no cards on this deck."}) - return - - args["amount"] = [amount_to_exponent(float(i), deck.number_of_decimals) for i in args["amount"]] - assert sum(args["amount"]) <= sum(my_balance.values()), {"error": "You don't have enough cards on this deck."} - - utxo = provider.select_inputs(0.02) - change_address = change(utxo) - cb = pa.CardTransfer(deck=deck, receiver=[deck.issuer], amount=args["amount"]) - raw_cb = hexlify(pa.card_burn(deck, cb, utxo, - change_address - ) - ).decode() - - signed = provider.signrawtransaction(raw_cb) - - if broadcast: - txid = provider.sendrawtransaction(signed["hex"]) # send the tx - print("\n", txid, "\n") - else: - print("\nraw transaction:\n", signed["hex"], "\n") - - -def card_transfer(provider, args, broadcast): - ''' - Transfer cards to - - pacli card -transfer '{"deck": "deck_id", - "receivers": [list of receiver addresses], "amounts": [list of amounts] - } - ''' - - args = json.loads(args) - try: - deck = find_deck(provider, args["deck"])[0] - except IndexError: - print({"error": "Deck not found!"}) - return - if not provider.getaddressesbyaccount(deck.name): - print("\n", {"error": "You are not even subscribed to this deck, how can you transfer cards?"}) - try: - my_balance = get_my_balance(provider, deck.asset_id) - except ValueError: - print("\n", {"error": "You have no cards on this deck."}) - return - - args["amount"] = [amount_to_exponent(float(i), deck.number_of_decimals) for i in args["amount"]] - assert sum(args["amount"]) <= sum(my_balance.values()), {"error": "You don't have enough cards on this deck."} - - utxo = provider.select_inputs(0.02) - change_address = change(utxo) - ct = pa.CardTransfer(deck=deck, receiver=args["receiver"], amount=args["amount"]) - raw_ct = hexlify(pa.card_transfer(deck, ct, utxo, - change_address - ) - ).decode() - - signed = provider.signrawtransaction(raw_ct) - - if broadcast: - txid = provider.sendrawtransaction(signed["hex"]) # send the tx - print("\n", txid, "\n") - else: - print("\nraw transaction:\n", signed["hex"], "\n") - - -def get_state(provider, deck): - '''return balances of this deck''' - - cards = pa.find_card_transfers(provider, deck) - if cards: - return pa.DeckState(cards) - else: - raise ValueError("No cards on this deck.") - - -def get_my_balance(provider, deck_id): - '''get balances on the deck owned by me''' - - try: - deck = find_deck(provider, deck_id)[0] - except IndexError: - print("\n", {"error": "Deck not found!"}) - - my_addresses = provider.getaddressesbyaccount() - deck_balances = get_state(provider, deck).balances - matches = list(set(my_addresses).intersection(deck_balances)) - - return {i: deck_balances[i] for i in matches if i in deck_balances.keys()} - - -def address_balance(provider, deck_id, address): - '''show deck balances''' - - try: - deck = find_deck(provider, deck_id)[0] - except IndexError: - print("\n", {"error": "Deck not found!"}) - return - balances = get_state(provider, deck).balances - try: - b = exponent_to_amount(balances[address], deck.number_of_decimals) - except: - print("\n", {"error": "This address has no card balance."}) - return - - print("\n", "Card balance: {balance}".format(balance=b), "\n") - - -def subscribed_decks(provider): - '''find subscribed-to decks''' - - decks = pa.find_all_valid_decks(provider) - for i in decks: - if provider.getaddressesbyaccount(i.name): - yield i - - -def status(provider): - '''show status of this pacli instance''' - - report = {} - report["network"] = provider.network - report["subscribed_decks"] = [] - for i in list(subscribed_decks(provider)): - report["subscribed_decks"].append({ - "deck_name": i.name, - "deck_id": i.asset_id, - "number_of_decimals": i.number_of_decimals - }) - for deck in report["subscribed_decks"]: - try: - my_balances = get_my_balance(provider, deck["deck_id"]) - deck["balance"] = exponent_to_amount(sum(my_balances.values()), - deck["number_of_decimals"]) - deck["address_handle"] = list(my_balances.keys())[0] # show address which handles this deck, first one only though - deck.pop("number_of_decimals") # this should not go into report - except: - deck.pop("number_of_decimals") # this should not go into report - deck["balance"] = 0 - - return report - - -def new_address(provider): - '''generate new address and import into wallet.''' - - key = pa.Kutil(network=provider.network) - provider.importprivkey(key.wif, "PACLI") - return key.address - -## Voting # - -def new_vote(provider, args, broadcast): - ''' - Initialize new vote on the - - pacli vote --new '{"choices": ["y", "n"], "count_mode": "SIMPLE", - "description": "test", "start_block": int, "end_block": int}' - ''' - - try: - deck = find_deck(provider, args[0])[0] - except IndexError: - print("\n", {"error": "Deck not found!"}) - return - - args = json.loads(args[1]) - inputs = provider.select_inputs(0.02) - change_address = change(inputs) - vote = pa.Vote(version=1, deck=deck, **args) - - raw_vote_init = hexlify(pa.vote_init(vote, inputs, change_address)).decode() - signed = provider.signrawtransaction(raw_vote_init)["hex"] - - if broadcast: - txid = provider.sendrawtransaction(signed) # send the tx - print("\n", txid, "\n") - else: - print("\nraw transaction:\n", signed, "\n") - - -def vote_cast(provider, args, broadcast): - ''' - cast a vote - args = deck, vote_id, choice - ''' - - try: - deck = find_deck(provider, args[0])[0] - except IndexError: - print("\n", {"error": "Deck not found!"}) - return - - for i in pa.find_vote_inits(provider, deck): - if i.vote_id == args[1]: - vote = i - - if not vote: - return {"error": "Vote not found."} - if isinstance(args[2], int): - choice = args[2] - else: - choice = list(vote.choices).index(args[2]) - inputs = provider.select_inputs(0.02) - change_address = change(inputs) - cast = pa.vote_cast(vote, choice, inputs, change_address) - - raw_vote_cast = hexlify(cast).decode() - signed = provider.signrawtransaction(raw_vote_cast)["hex"] - - if broadcast: - txid = provider.sendrawtransaction(signed) # send the tx - print("\n", txid, "\n") - else: - print("\nraw transaction:\n", signed, "\n") - - -class ListVotes: - - @classmethod - def __init__(cls, provider, votes): - cls.provider = provider - cls.votes = list(votes) - - ## Vote table header - cls.vote_table = [ - ## add subscribed column - ("vote_id", "sender", "description", "start_block", "end_block") - ] - - cls.table = AsciiTable(cls.vote_table, title="Votes on this deck:") - - @classmethod - def dtl(cls, vote, subscribed=False): - '''votes-to-list: votes to table-printable list''' - - l = [] - l.append(vote["vote_id"]) - l.append(vote["sender"]) - l.append(vote["description"]) - l.append(vote["start_block"]) - l.append(vote["end_block"]) - - return l - - @classmethod - def pack_for_printing(cls): - - for i in cls.votes: - cls.vote_table.append( - cls.dtl(i.__dict__) - ) - - -def list_votes(provider, args): - '''list all votes on the ''' - - try: - deck = find_deck(provider, args)[0] - except IndexError: - print("\n", {"error": "Deck not found!"}) - return - vote_inits = list(pa.find_vote_inits(provider, deck)) - - c = ListVotes(provider, vote_inits) - c.pack_for_printing() - print(c.table.table) - - -def vote_info(provider, args): - '''show detail information about on the ''' - - try: - deck = find_deck(provider, args[0])[0] - except IndexError: - print("\n", {"error": "Deck not found!"}) - return - - for i in pa.find_vote_inits(provider, deck): - if i.vote_id == args[1]: - vote = i - - vote.deck = vote.deck.asset_id - print("\n", vote.__dict__) - return - - -def cli(): - '''CLI arguments parser''' - - parser = argparse.ArgumentParser(description='Simple CLI PeerAssets client.') - subparsers = parser.add_subparsers(title="Commands", - dest="command", - description='valid subcommands') - - parser.add_argument("--broadcast", action="store_true", help="broadcast resulting transactions") - parser.add_argument("--newaddress", action="store_true", - help="generate a new address and import to wallet") - parser.add_argument("--status", action="store_true", help="show pacli status") - parser.add_argument("--addressbalance", action="store", - metavar=('DECK_ID', 'ADDRESS'), nargs=2, help="check card balance of the address") - - deck = subparsers.add_parser('deck', help='Deck manipulation.') - deck.add_argument("--list", action="store_true", help="list decks") - deck.add_argument("--info", action="store", help="show details of ") - deck.add_argument("--subscribe", action="store", help="subscribe to ") - deck.add_argument("--search", action="store", help='''search for decks by name, id, - issue mode, issuer or number of decimals''') - deck.add_argument("--new", action="store", help="spawn new deck") - deck.add_argument("--checksum", action="store", help="verify deck card balance checksum") - deck.add_argument("--balances", action="store", help="show balances of this deck") - - card = subparsers.add_parser('card', help='Card manipulation.') - card.add_argument("--list", action="store", help="list all card transactions of this deck") - card.add_argument("--export", action="store", nargs=2, help="export all cards of this deck to csv file") - card.add_argument("--issue", action="store", help="issue cards for this deck") - card.add_argument("--transfer", action="store", help="send cards to receivers") - card.add_argument("--burn", action="store", help="burn cards") - - vote = subparsers.add_parser('vote', help='Vote manipulation.') - vote.add_argument("--list", action="store", help="List all vote transactions of this deck") - vote.add_argument("--new", action="store", nargs=2, help="Initiate new vote or poll.") - vote.add_argument("--cast", action="store", help="Cast a vote.") - vote.add_argument("--info", action="store", nargs=2, help="Information about vote.") - - return parser.parse_args() - - -def configured_provider(Settings): - " resolve settings into configured provider " - - if Settings.provider.lower() == "rpcnode": - Provider = pa.RpcNode - kwargs = dict(testnet=Settings.testnet) - - elif Settings.provider.lower() == "holy": - Provider = pa.Holy - kwargs = dict(network=Settings.network) - - elif Settings.provider.lower() == "cryptoid": - Provider = pa.Cryptoid - kwargs = dict(network=Settings.network) - - else: - raise Exception('invalid provider') - - if Settings.keystore.lower() == "gnupg": - Provider = as_local_key_provider(Provider) - kwargs['keystore'] = keystore = GpgKeystore(Settings, keyfile) - - provider = Provider(**kwargs) - set_up(provider) - - return provider +import click +from pacli.deck import deck +from pacli.card import card +from pacli.vote import vote +from pacli.top_level_commands import top_level +top_level.add_command(deck) +top_level.add_command(card) +top_level.add_command(vote) def main(): + top_level() - first_run() - load_conf() - - provider = configured_provider(Settings) - - args = cli() - - if args.status: - print(json.dumps(status(provider), indent=4)) - - if args.newaddress: - print("\n", new_address(provider)) - - if args.addressbalance: - address_balance(provider, args.addressbalance[0], args.addressbalance[1]) - - if args.command == "deck": - if args.list: - deck_list(provider) - if args.subscribe: - deck_subscribe(provider, args.subscribe) - if args.search: - deck_search(provider, args.search) - if args.info: - deck_info(provider, args.info) - if args.new: - new_deck(provider, args.new, args.broadcast) - if args.checksum: - deck_checksum(provider, args.checksum) - if args.balances: - deck_balances(provider, args.balances) - - if args.command == "card": - if args.issue: - card_issue(provider, args.issue, args.broadcast) - if args.burn: - card_burn(provider, args.burn, args.broadcast) - if args.transfer: - card_transfer(provider, args.transfer, args.broadcast) - if args.list: - list_cards(provider, args.list) - if args.export: - export_cards(provider, args.export) - - if args.command == "vote": - if args.new: - new_vote(provider, args.new, args.broadcast) - if args.list: - list_votes(provider, args.list) - if args.cast: - vote_cast(provider, args.cast) - if args.info: - vote_info(provider, args.info) - - if (hasattr(provider, 'keystore')): - # could possibly make this direct behavior of dumprivkeys - provider.keystore.write(provider.dumpprivkeys()) - if __name__ == "__main__": main() diff --git a/pacli/card.py b/pacli/card.py new file mode 100644 index 0000000..711c525 --- /dev/null +++ b/pacli/card.py @@ -0,0 +1,159 @@ +import click +from pacli.export import export_to_csv +from binascii import hexlify +import pypeerassets as pa +from pypeerassets.pautils import amount_to_exponent, exponent_to_amount +from pacli.deck import find_deck +import json +from pacli.provider import provider, change +from .utils import print_table, throw, handle_transaction + + +def validate_transfer(deck, amounts): + if not provider.getaddressesbyaccount(deck.asset_id): + throw("You are not subscribed to this deck") + try: + my_balance = get_my_balance(deck.asset_id) + if not sum(amounts) <= sum(my_balance.values()): + throw("You don't have enough cards on this deck.") + except ValueError: + throw("You have no cards on this deck.") + +def transfer_cards(deck, receivers, amounts, broadcast): + validate_transfer(deck, amounts) + utxo = provider.select_inputs(0.02) + + change_address = change(utxo) + ct = pa.CardTransfer(deck=deck, receiver=receivers, amount=amounts) + handle_transaction(pa.card_transfer(deck, ct, utxo, change_address), broadcast) + + +def card_line_item(card): + c = card.__dict__ + return [c["txid"][:20], + c["sender"], + c["receiver"][0], + exponent_to_amount(c["amount"][0], c["number_of_decimals"]), + c["type"], + provider.getrawtransaction(c["txid"], 1)["confirmations"] if c["blockhash"] != 0 else 0 ] + +def print_card_list(cards): + ## TODO: add subscribed column + print_table( + title="Card transfers of this deck:", + heading=("txid", "sender", "receiver", "amount", "type", "confirms"), + data=map(card_line_item, cards)) + + +@click.group() +def card(): + pass + + +@card.command() +@click.argument('deck_id') +def list(deck_id): + '''List cards of this ''' + deck = find_deck(deck_id) + + if isinstance(provider, pa.RpcNode): + if not provider.getaddressesbyaccount(deck.asset_id): + print("\n", {"error": "You must subscribe to deck to be able to list transactions."}) + return + all_cards = pa.find_card_transfers(provider, deck) + + if not all_cards: + print("\n", "No cards have been issued for deck %s" % deck.asset_id) + return + + cards = pa.validate_card_issue_modes(deck, all_cards) + print_card_list(cards) + + +@card.command() +@click.argument('deck_id') +@click.argument('filename') +def export(deck_id, filename): + ''' export cards to csv ''' + + deck = find_deck(deck_id) + + if not provider.getaddressesbyaccount(deck.asset_id): + throw("You must subscribe to deck to be able to list transactions.") + + all_cards = pa.find_card_transfers(provider, deck) + cards = pa.validate_card_issue_modes(deck, all_cards) + + export_to_csv(cards, filename) + + +def parse_transfer_json(context, param, transfer_json: str) -> dict: + return json.loads(transfer_json) + +@card.command() +@click.argument('issuence', callback=parse_transfer_json) +@click.option('--broadcast/--no-broadcast', default=False) +def issue(issuence, broadcast): + ''' + Issue new cards of this deck. + + pacli card issue '{"deck": "deck_id", + "receivers": [list of receiver addresses], + "amounts": [list of amounts] + } + ''' + + deck = find_deck(issuence["deck"]) + + if not provider.gettransaction(deck.asset_id)["confirmations"] > 0: + print("\n", "You are trying to issue cards on a deck which has not been confirmed yet.") + + if provider.validateaddress(deck.issuer)["ismine"]: + try: + utxo = provider.select_inputs(0.02, deck.issuer) + except ValueError: + throw("Please send funds to the deck issuing address: {0}".format(deck.issuer)) + else: + raise throw("You are not the owner of this deck.") + + receivers = issuence["receivers"] + amounts = [amount_to_exponent(float(i), deck.number_of_decimals) for i in issuence["amounts"]] + + change_address = change(utxo) + ct = pa.CardTransfer(deck=deck, receiver=receivers, amount=amounts) + handle_transaction(pa.card_issue(deck, ct, utxo, change_address), broadcast) + + +@card.command() +@click.argument('deck_id') +@click.argument('burn_order', callback=parse_transfer_json) +@click.option('--broadcast/--no-broadcast', default=False) +def burn(burn_order, broadcast): + ''' + Burn cards of this deck. + + pacli card burn amount_one, amount_two... + ''' + + deck = find_deck(burn_order['deck']) + amounts = [amount_to_exponent(float(i), deck.number_of_decimals) for i in burn_order['amounts']] + transfer_cards(deck, [deck.issuer], amounts, broadcast) + + +@card.command() +@click.argument('transfer_order', callback=parse_transfer_json) +@click.option('--broadcast/--no-broadcast', default=False) +def transfer(transfer_order, broadcast): + ''' + Transfer cards to + + pacli card -transfer '{"deck": "deck_id", + "receivers": [list of receiver addresses], "amounts": [list of amounts] + } + ''' + + deck = find_deck(transfer_order["deck"]) + transfer_order["amount"] = [amount_to_exponent(float(i), deck.number_of_decimals) for i in transfer_order["amount"]] + transfer_cards(deck, transfer_order["receiver"], transfer_order["amount"], broadcast) + + diff --git a/pacli/config.py b/pacli/config.py index 2dac2fd..1918ec3 100644 --- a/pacli/config.py +++ b/pacli/config.py @@ -1,5 +1,7 @@ +from appdirs import user_config_dir +import logging import configparser -import sys +import os, sys from .default_conf import default_conf def write_default_config(conf_file=None): @@ -19,7 +21,7 @@ def write_default_config(conf_file=None): "gnupgkey": "" } -required = { "network", "production", "loglevel", "change" } +required = { "network", "deck_version", "production", "loglevel", "change" } def read_conf(conf_file): config = configparser.ConfigParser() @@ -39,3 +41,46 @@ def read_conf(conf_file): settings["testnet"] = True return settings + + +conf_dir = user_config_dir("pacli") +conf_file = os.path.join(conf_dir, "pacli.conf") +logfile = os.path.join(conf_dir, "pacli.log") +keyfile = os.path.join(conf_dir, "pacli.gpg") + + +def init_config(): + '''if first run, setup local configuration directory.''' + if not os.path.exists(conf_dir): + os.mkdir(conf_dir) + if not os.path.exists(conf_file): + write_default_config(conf_file) + + +def load_conf(): + '''load user configuration''' + + init_config() + + class Settings: + pass + + settings = read_conf(conf_file) + + for key in settings: + setattr(Settings, key, settings[key]) + + setattr(Settings, 'keyfile', keyfile) + setattr(Settings, 'deck_version', int(Settings.deck_version)) + + logging.basicConfig(filename=logfile, level=logging.getLevelName(Settings.loglevel)) + logging.basicConfig(level=logging.getLevelName(Settings.loglevel), + format="%(asctime)s %(levelname)s %(message)s") + + logging.debug("logging initialized") + + return Settings + + +Settings = load_conf() + diff --git a/pacli/deck.py b/pacli/deck.py new file mode 100644 index 0000000..f8cf67a --- /dev/null +++ b/pacli/deck.py @@ -0,0 +1,200 @@ +import click +from click_default_group import DefaultGroup +from binascii import hexlify +#from pypeerassets import find_card_transfers, find_all_valid_decks, DeckState, Deck, load_deck_p2th_into_local_node +import pypeerassets as pa +import json +from pacli.config import Settings +from pacli.provider import provider, change +from pacli.utils import tstamp_to_iso, print_table + + +def default_account_utxo(amount): + '''set default address to be used with pacli''' + + if "PACLI" not in provider.listaccounts().keys(): + addr = provider.getaddressesbyaccount("PACLI") + print("\n", "Please fund this address: {addr}".format(addr=addr)) + return + + for i in provider.getaddressesbyaccount("PACLI"): + try: + return provider.select_inputs(amount, i) + except ValueError: + pass + + print("\n", "Please fund one of the following addresses: {addrs}".format( + addrs=provider.getaddressesbyaccount("PACLI"))) + return + + +def get_state(deck): + '''return balances of this deck''' + + cards = pa.find_card_transfers(provider, deck) + if cards: + return pa.DeckState(cards) + else: + raise ValueError("No cards on this deck.") + + +def deck_title(deck): + return "Deck id: " + deck.asset_id + " " + + +def print_deck_info(deck: pa.Deck): + ## TODO add subscribed column + print_table( + title=deck_title(deck), + heading=("asset name", "issuer", "issue mode", "decimals", "issue time"), + data=[[ + getattr(deck, attr) for attr in + ["name", "issuer", "issue_mode", "number_of_decimals", "issue_time"] ]]) + + +def print_deck_balances(deck, balances={}): + '''Show balances of address tied with this deck.''' + assert isinstance(deck, pa.Deck) + precision = deck.number_of_decimals + ## TODO add subscribed column + print_table( + title=deck_title(deck), + heading=("address", "balance"), + data=[[address, exponent_to_amount(balance, precision)] for address, balance in balances]) + + +def deck_summary_line_item(deck): + d = deck.__dict__ + return [d["asset_id"][:20], + d["name"], + d["issuer"], + d["issue_mode"] ] + + +def print_deck_list(decks): + '''Show summary of every deck''' + ## TODO add subscribed column + print_table( + title="Decks", + heading=("asset ID", "asset name", "issuer", "mode"), + data=map(deck_summary_line_item, decks)) + +def add_short_id(deck): + deck.short_id = deck.asset_id[:20] + return deck + +def search_decks(key: str) -> list: + '''search decks by ''' + + decks = pa.find_all_valid_decks(provider, deck_version=Settings.deck_version, prod=Settings.production) + decks = map(add_short_id, decks) + + return [d for d in decks if key in d.asset_id or (key in d.__dict__.values())] + + +def find_deck(key: str): + '''find single deck by ''' + try: + return search_decks(key)[0] + except IndexError: + raise Exception({"error": "Deck not found!"}) + + +class SingleDeck: + + def __init__(self, deck_id): + self.deck = find_deck(deck_id) + + def info(self): + '''info commands, show full deck details''' + print_deck_info(self.deck) + + def balances(self): + '''show deck balances''' + print_deck_balances(self.deck, get_state(self.deck).balances) + + def subscribe(self): + '''subscribe command, load deck p2th into local node''' + print('wtf') + pa.load_deck_p2th_into_local_node(provider, self.deck) + print('subsribed to deck %s', self.deck.asset_id) + + def checksum(self): + ''' verify checksum ''' + if get_state(self.deck).checksum: + print("\n", "Deck checksum is correct.") + else: + print("\n", "Deck checksum is incorrect.") + + @classmethod + def options(cls, func): + for option in ['info', 'balances', 'subscribe', 'checksum']: + func = click.option('--' + option, is_flag=True, help=getattr(cls, option).__doc__)(func) + return func + + + +@click.group(cls=DefaultGroup, default='find', default_if_no_args=True) +def deck(): + pass + + +@deck.command() +@click.argument('deck_id') +@SingleDeck.options +def find(deck_id, **options): + deck = SingleDeck(deck_id) + for option in [ opt for opt, selected in options.items() if selected ] or ['info']: + getattr(deck, option)() + + +@deck.command() +@click.argument('deck_id') +def search(deck_id): + '''search decks by ''' + print_deck_list(search_decks(deck_id)) + + +@deck.command() +def list(): + '''list decks''' + decks = pa.find_all_valid_decks(provider=provider, deck_version=Settings.deck_version, + prod=Settings.production) + print_deck_list(decks) + + +@deck.command() +@click.argument('deck') +@click.option('--broadcast/--no-broadcast', default=False, help='broadcast resulting transactions') +def new(deck, broadcast): + ''' Spawn a new PeerAssets deck. Returns the deck span txid. + [deck] is deck description json. I.E. '{"name": "test", "number_of_decimals": 1, "issue_mode": "ONCE"}' + ''' + + deck = json.loads(deck) + deck["network"] = Settings.network + deck["production"] = Settings.production + deck["version"] = Settings.deck_version + #utxo = provider.select_inputs(0.02) # we need 0.02 PPC + utxo = default_account_utxo(0.02) + if utxo: + change_address = change(utxo) + else: + return + raw_deck = pa.deck_spawn(pa.Deck(**deck), + inputs=utxo, + change_address=change_address) + raw_deck_spawn = hexlify(raw_deck).decode() + signed = provider.signrawtransaction(raw_deck_spawn) + + if broadcast: + txid = provider.sendrawtransaction(signed["hex"]) + print("\n", txid, "\n") + + deck["asset_id"] = txid + d = pa.Deck(**deck) + pa.load_deck_p2th_into_local_node(provider, d) # subscribe to deck + else: + print("\nraw transaction:\n", signed["hex"], "\n") + + diff --git a/pacli/keystore.py b/pacli/keystore.py index 8515906..aa71780 100644 --- a/pacli/keystore.py +++ b/pacli/keystore.py @@ -1,7 +1,8 @@ -import sys, pickle +import sys, os, pickle, atexit from binascii import hexlify, unhexlify import gnupg, getpass from pypeerassets.kutil import Kutil +from pypeerassets import RpcNode class GpgKeystore: """ @@ -10,11 +11,14 @@ class GpgKeystore: Uses pickle because private keys are binary """ - def __init__(self, Settings, keyfile): + def __init__(self, Settings): assert Settings.keystore == "gnupg" + if not os.path.exists(Settings.keyfile): + open(keyfile, 'a').close() + self._key = Settings.gnupgkey - self._keyfile = keyfile + self._keyfile = Settings.keyfile self._init_settings = dict( homedir=Settings.gnupgdir, @@ -23,6 +27,12 @@ def __init__(self, Settings, keyfile): secring='secring.gpg') self.gpg = gnupg.GPG(**self._init_settings) + def unpickle(self, decrypted: bytes) -> dict: + return pickle.loads(unhexlify(str(decrypted).encode())) + + def pickle(self, data: dict) -> str: + return hexlify(pickle.dumps(data)).decode() + def read(self) -> dict: password = getpass.getpass("Input gpg key password:") contents = open(self._keyfile, 'rb').read() @@ -33,10 +43,10 @@ def read(self) -> dict: decrypted = self.gpg.decrypt(contents, passphrase=password) assert decrypted.ok, decrypted.status - return pickle.loads(unhexlify(str(decrypted).encode())) + return self.unpickle(decrypted) def write(self, data: dict) -> str: - encrypted = self.gpg.encrypt(hexlify(pickle.dumps(data)).decode(), self._key) + encrypted = self.gpg.encrypt(self.pickle(data), self._key) assert encrypted.ok, encrypted.status keyfile = open(self._keyfile, "w") keyfile.write(str(encrypted)) @@ -64,7 +74,16 @@ def __init__(self, keystore: GpgKeystore, **kwargs): self.__init__hack__ = Provider.__init__ self.__init__hack__(**kwargs) self.keystore = keystore - self.privkeys = keystore.read() + + #TODO only do this and cleanup if needed + self.load_privkeys() + + @atexit.register + def _cleanup(): + self.keystore.write(self.dumpprivkeys()) + + def load_privkeys(self): + self.privkeys = self.keystore.read() def importprivkey(self, privkey: str, label: str) -> int: """import with