diff --git a/beem/account.py b/beem/account.py index f8fac1d2dbc5f781f8468c3df796e2525bd12c58..3e15c87d2ae2e474efed14061837f9f73defbec0 100644 --- a/beem/account.py +++ b/beem/account.py @@ -614,13 +614,19 @@ class Account(BlockchainObject): return last_item else: try: + op_count = 0 + self.steem.rpc.set_next_node_on_empty_reply(True) if self.steem.rpc.get_use_appbase(): try: - return self.steem.rpc.get_account_history({'account': self["name"], 'start': -1, 'limit': 0}, api="account_history")['history'][0][0] + op_count = self.steem.rpc.get_account_history({'account': self["name"], 'start': -1, 'limit': 0}, api="account_history")['history'] except: - return self.steem.rpc.get_account_history(self["name"], -1, 0)[0][0] + op_count = self.steem.rpc.get_account_history(self["name"], -1, 0) else: - return self.steem.rpc.get_account_history(self["name"], -1, 0, api="database")[0][0] + op_count = self.steem.rpc.get_account_history(self["name"], -1, 0, api="database") + if isinstance(op_count, list) and len(op_count) > 0 and len(op_count[0]) > 0: + return op_count[0][0] + else: + return 0 except IndexError: return 0 diff --git a/beem/cli.py b/beem/cli.py index 9e24c1beb931afbcdb3219c4c008052f92765475..6ff14e88d64f12d4fdcde4cddd401fdbe1e8ae4f 100644 --- a/beem/cli.py +++ b/beem/cli.py @@ -9,11 +9,15 @@ from beem.amount import Amount from beem.account import Account from beem.steem import Steem from beem.comment import Comment -from beem.storage import configStorage +from beem.block import Block +from beem.witness import Witness +from beem.blockchain import Blockchain +from beem import exceptions from beem.version import version as __version__ from datetime import datetime, timedelta import pytz from beembase import operations + from beemgraphenebase.account import PrivateKey, PublicKey import json from prettytable import PrettyTable @@ -21,6 +25,7 @@ import math import random import logging import click +import re click.disable_unicode_literals_warning = True log = logging.getLogger(__name__) @@ -32,15 +37,30 @@ availableConfigurationKeys = [ ] +def prompt_callback(ctx, param, value): + if value in ["yes", "y", "ye"]: + value = True + else: + print("Please write yes, ye or y to confirm!") + ctx.abort() + + +def prompt_flag_callback(ctx, param, value): + if not value: + ctx.abort() + + @click.group(chain=True) @click.option( - '--node', '-n', default="", help="URL for public Steem API") + '--node', '-n', default="", help="URL for public Steem API (e.g. https://api.steemit.com)") @click.option( - '--offline', is_flag=True, default=False, help="Prevent connecting to network") + '--offline', '-o', is_flag=True, default=False, help="Prevent connecting to network") @click.option( - '--nobroadcast', '-d', is_flag=True, default=False, help="Do not broadcast") + '--no-broadcast', '-d', is_flag=True, default=False, help="Do not broadcast") @click.option( - '--unsigned', is_flag=True, default=False, help="Nothing will be signed") + '--no-wallet', '-p', is_flag=True, default=False, help="Do not load the wallet") +@click.option( + '--unsigned', '-x', is_flag=True, default=False, help="Nothing will be signed") @click.option( '--blocking', is_flag=True, default=False, help="Wait for broadcasted transactions to be included in a block and return full transaction") @@ -48,45 +68,67 @@ availableConfigurationKeys = [ '--bundle', is_flag=True, default=False, help="Do not broadcast transactions right away, but allow to bundle operations ") @click.option( - '--expiration', '-e', default=30, + '--expires', '-e', default=30, help='Delay in seconds until transactions are supposed to expire(defaults to 60)') @click.option( - '--debug', is_flag=True, default=False, help='Enables Debug output') + '--verbose', '-v', default=3, help='Verbosity') @click.version_option(version=__version__) -def cli(node, offline, nobroadcast, unsigned, blocking, bundle, expiration, debug): +def cli(node, offline, no_broadcast, no_wallet, unsigned, blocking, bundle, expires, verbose): + + # Logging + log = logging.getLogger(__name__) + verbosity = ["critical", "error", "warn", "info", "debug"][int( + min(verbose, 4))] + log.setLevel(getattr(logging, verbosity.upper())) + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s') + ch = logging.StreamHandler() + ch.setLevel(getattr(logging, verbosity.upper())) + ch.setFormatter(formatter) + log.addHandler(ch) + + debug = verbose > 0 stm = Steem( node=node, - nobroadcast=nobroadcast, + nobroadcast=no_broadcast, + offline=offline, + nowallet=no_wallet, unsigned=unsigned, blocking=blocking, bundle=bundle, - expiration=expiration, + expiration=expires, debug=debug ) set_shared_steem_instance(stm) + pass @cli.command() -@click.option( - '--account', '-a', multiple=False, help="Set default account") -@click.option( - '--weight', '-w', multiple=False, help="Set your default vote weight") -@click.option( - '--node', '-n', multiple=True, help="Set nodes") -def set(account, weight, node): - """ Set configuration +@click.argument('key') +@click.argument('value') +def set(key, value): + """ Set default_account, default_vote_weight or nodes + + set [key] [value] + + Examples: + + Set the default vote weight to 50 %: + set default_vote_weight 50 """ stm = shared_steem_instance() - if account: - stm.set_default_account(account) - if weight: - configStorage["default_vote_weight"] = weight - if len(node) > 0: - nodes = [] - for n in node: - nodes.append(n) - configStorage["node"] = nodes + if key == "default_account": + stm.set_default_account(value) + elif key == "default_vote_weight": + stm.set_default_vote_weight(value) + elif key == "nodes" or key == "node": + if bool(value) or value != "default": + stm.set_default_nodes(value) + else: + stm.set_default_nodes("") + else: + print("wrong key") @cli.command() @@ -100,23 +142,28 @@ def config(): # hide internal config data if key in availableConfigurationKeys: t.add_row([key, stm.config[key]]) - for node in stm.config.nodes: - t.add_row(["node", node]) + if "default_nodes" in stm.config and bool(stm.config["default_nodes"]): + node = stm.config["default_nodes"] + elif "node" in stm.config: + node = stm.config["node"] + nodes = json.dumps(node, indent=4) + t.add_row(["nodes", nodes]) t.add_row(["data_dir", stm.config.data_dir]) print(t) @cli.command() -@click.option('--password', prompt=True, hide_input=True, - confirmation_prompt=True) @click.option( '--wipe', is_flag=True, default=False, + prompt='Do you want to wipe your wallet? Are your sure? This is IRREVERSIBLE! If you dont have a backup you may lose access to your account!', help="Delete old wallet. All wallet data are deleted!") -def createwallet(password, purge): +@click.option('--password', prompt=True, hide_input=True, + confirmation_prompt=True) +def createwallet(wipe, password): """ Create new wallet with password """ stm = shared_steem_instance() - if purge and click.confirm('Do you want to continue to wipe your Wallet?'): + if wipe and click.confirm('Do you want to continue to wipe your Wallet?'): stm.wallet.wipe(True) stm.wallet.create(password) set_shared_steem_instance(stm) @@ -138,10 +185,14 @@ def walletinfo(): @cli.command() @click.option('--password', prompt=True, hide_input=True, - confirmation_prompt=False) -@click.option('--key') -def addkey(password, key): + confirmation_prompt=False, help='Password to unlock wallet') +@click.option('--unsafe-import-key', prompt='Enter private key', hide_input=False, + confirmation_prompt=False, help='Private key to import to wallet (unsafe, unless shell history is deleted afterwards)') +def addkey(password, unsafe_import_key): """ Add key to wallet + + When no [OPTION] is given, a password prompt for unlocking the wallet + and a prompt for entering the private key are shown. """ stm = shared_steem_instance() if not stm.wallet.locked(): @@ -151,7 +202,34 @@ def addkey(password, key): print("Could not be unlocked!") else: print("Unlocked!") - stm.wallet.addPrivateKey(key) + stm.wallet.addPrivateKey(unsafe_import_key) + set_shared_steem_instance(stm) + + +@cli.command() +@click.option('--confirm', + prompt='Are your sure? This is IRREVERSIBLE! If you dont have a backup you may lose access to your account!', + hide_input=False, callback=prompt_flag_callback, is_flag=True, + confirmation_prompt=False, help='Please confirm!') +@click.option('--password', prompt=True, hide_input=True, + confirmation_prompt=False, help='Password to unlock wallet') +@click.argument('pub') +def delkey(confirm, password, pub): + """ Delete key from the wallet + + PUB is the public key from the private key + which will be deleted from the wallet + """ + stm = shared_steem_instance() + if not stm.wallet.locked(): + return + stm.wallet.unlock(password) + if stm.wallet.locked(): + print("Could not be unlocked!") + else: + print("Unlocked!") + if click.confirm('Do you want to continue to wipe the private key?'): + stm.wallet.removePrivateKeyFromPublicKey(pub) set_shared_steem_instance(stm) @@ -160,20 +238,10 @@ def listkeys(): """ Show stored keys """ stm = shared_steem_instance() - t = PrettyTable(["Available Key", "Account", "Type"]) + t = PrettyTable(["Available Key"]) t.align = "l" for key in stm.wallet.getPublicKeys(): - if key[0:2] != stm.prefix: - continue - account = stm.wallet.getAccount(key) - if account["name"] is None: - accountName = "" - keyType = "Memo" - else: - accountName = account["name"] - keyType = account["type"] - # keyType = stm.wallet.getKeyType(account, key) - t.add_row([key, accountName, keyType]) + t.add_row([key]) print(t) @@ -193,18 +261,48 @@ def listaccounts(): @cli.command() @click.argument('post', nargs=1) -@click.option('--account', '-a') +@click.option('--account', '-a', help='Voter account name') @click.option('--weight', '-w', default=100.0, help='Vote weight (from 0.1 to 100.0)') def upvote(post, account, weight): - """Upvote a post/comment""" + """Upvote a post/comment + + POST is @author/permlink + """ + stm = shared_steem_instance() if not weight: - weight = configStorage["default_vote_weight"] + weight = stm.config["default_vote_weight"] if not account: - account = configStorage["default_account"] - post = Comment(post) + account = stm.config["default_account"] + try: + post = Comment(post) + except Exception as e: + log.error(str(e)) + return post.upvote(weight, voter=account) +@cli.command() +@click.argument('post', nargs=1) +@click.option('--account', '-a', help='Voter account name') +@click.option('--weight', '-w', default=100.0, help='Vote weight (from 0.1 to 100.0)') +def downvote(post, account, weight): + """Downvote a post/comment + + POST is @author/permlink + """ + stm = shared_steem_instance() + if not weight: + weight = stm.config["default_vote_weight"] + if not account: + account = stm.config["default_account"] + try: + post = Comment(post) + except Exception as e: + log.error(str(e)) + return + post.downvote(weight, voter=account) + + @cli.command() @click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True) @@ -260,7 +358,10 @@ def balance(account): @cli.command() @click.argument('objects', nargs=-1) def info(objects): - """ Show info + """ Show basic blockchain info + + General information about the blockchain, a block, an account, + a post/comment and a public key """ stm = shared_steem_instance() if not objects: @@ -276,10 +377,109 @@ def info(objects): t.add_row(["steem per mvest", steem_per_mvest]) t.add_row(["internal price", price]) print(t.get_string(sortby="Key")) - for o in objects: - a = Account(o, steem_instance=stm) - a.print_info() - print("\n") + # Block + for obj in objects: + if re.match("^[0-9-]*$", obj) or re.match("^-[0-9]*$", obj) or re.match("^[0-9-]*:[0-9]", obj) or re.match("^[0-9-]*:-[0-9]", obj): + tran_nr = '' + if re.match("^[0-9-]*:[0-9-]", obj): + obj, tran_nr = obj.split(":") + if int(obj) < 1: + b = Blockchain() + block_number = b.get_current_block_num() + int(obj) - 1 + else: + block_number = obj + block = Block(block_number) + if block: + t = PrettyTable(["Key", "Value"]) + t.align = "l" + for key in sorted(block): + value = block[key] + if key == "transactions" and not bool(tran_nr): + t.add_row(["Nr. of transactions", len(value)]) + elif key == "transactions" and bool(tran_nr): + if int(tran_nr) < 0: + tran_nr = len(value) + int(tran_nr) + else: + tran_nr = int(tran_nr) + if len(value) > tran_nr and tran_nr > -1: + t_value = json.dumps(value[tran_nr], indent=4) + t.add_row(["transaction %d/%d" % (tran_nr, len(value)), t_value]) + elif key == "transaction_ids" and bool(tran_nr): + if int(tran_nr) < 0: + tran_nr = len(value) + int(tran_nr) + else: + tran_nr = int(tran_nr) + if len(value) > int(tran_nr) and int(tran_nr) > -1: + t.add_row(["transaction_id %d/%d" % (int(tran_nr), len(value)), value[int(tran_nr)]]) + else: + t.add_row([key, value]) + print(t) + else: + print("Block number %s unknown" % obj) + elif re.match("^[a-zA-Z0-9\-\._]{2,16}$", obj): + account = Account(obj, steem_instance=stm) + t = PrettyTable(["Key", "Value"]) + t.align = "l" + account_json = account.json() + for key in sorted(account_json): + value = account_json[key] + if key == "json_metadata": + value = json.dumps(json.loads(value or "{}"), indent=4) + elif key in ["posting", "witness_votes", "active", "owner"]: + value = json.dumps(value, indent=4) + elif key == "reputation" and int(value) > 0: + value = int(value) + rep = account.rep + value = "{:.2f} ({:d})".format(rep, value) + elif isinstance(value, dict) and "asset" in value: + value = str(account[key]) + t.add_row([key, value]) + print(t) + + # witness available? + try: + witness = Witness(obj) + witness_json = witness.json() + t = PrettyTable(["Key", "Value"]) + t.align = "l" + for key in sorted(witness_json): + value = witness_json[key] + if key in ["props", "sbd_exchange_rate"]: + value = json.dumps(value, indent=4) + t.add_row([key, value]) + print(t) + except exceptions.WitnessDoesNotExistsException as e: + print(str(e)) + # Public Key + elif re.match("^STM.{48,55}$", obj): + account = stm.wallet.getAccountFromPublicKey(obj) + if account: + t = PrettyTable(["Account"]) + t.align = "l" + t.add_row([account]) + print(t) + else: + print("Public Key not known" % obj) + # Post identifier + elif re.match(".*@.{3,16}/.*$", obj): + post = Comment(obj) + post_json = post.json() + if post_json: + t = PrettyTable(["Key", "Value"]) + t.align = "l" + for key in sorted(post_json): + value = post_json[key] + if (key in ["json_metadata"]): + value = json.loads(value) + value = json.dumps(value, indent=4) + elif (key in ["tags", "active_votes"]): + value = json.dumps(value, indent=4) + t.add_row([key, value]) + print(t) + else: + print("Post now known" % obj) + else: + print("Couldn't identify object to read") if __name__ == "__main__": diff --git a/beem/steem.py b/beem/steem.py index 09ef7c9b883d8e50b52c0e20dc81ff12f9080c97..7a568080af761cee9e58f82f6581e900aed58841 100644 --- a/beem/steem.py +++ b/beem/steem.py @@ -622,6 +622,19 @@ class Steem(object): Account(account, steem_instance=self) config["default_account"] = account + def set_default_nodes(self, nodes): + """ Set the default nodes to be used + """ + if bool(nodes): + config["node"] = nodes + else: + config.delete("node") + + def set_default_vote_weight(self, vote_weight): + """ Set the default vote weight to be used + """ + config["default_vote_weight"] = vote_weight + def finalizeOp(self, ops, account, permission, **kwargs): """ This method obtains the required private keys if present in the wallet, finalizes the transaction, signs it and diff --git a/tests/beem/test_cli.py b/tests/beem/test_cli.py index 4e2ed9e44624abdf9ad46e1c857b88c174b2da52..ad2872800ac67e41bca998c594d7e121bf57eaa2 100644 --- a/tests/beem/test_cli.py +++ b/tests/beem/test_cli.py @@ -12,29 +12,24 @@ from pprint import pprint from beem import Steem, exceptions from beem.account import Account from beem.amount import Amount +from beemgraphenebase.account import PrivateKey from beem.cli import cli, balance from beem.instance import set_shared_steem_instance from beembase.operationids import getOperationNameForId from beem.utils import get_node_list -wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" +wif = "5Jt2wTfhUt5GkZHV1HYVfkEaJ6XnY8D2iA4qjtK9nnGXAhThM3w" class Testcases(unittest.TestCase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - - self.bts = Steem( - node=get_node_list(appbase=False), - nobroadcast=True, - bundle=False, - # Overwrite wallet to use this list of wifs only - wif={"active": wif}, - num_retries=10 - ) - self.bts.set_default_account("test") - set_shared_steem_instance(self.bts) + runner = CliRunner() + runner.invoke(cli, ['set', 'default_vote_weight', '100']) + runner.invoke(cli, ['set', 'default_account', 'beem']) + runner.invoke(cli, ['set', 'nodes', 'wss://testnet.steem.vc']) + runner.invoke(cli, ['createwallet', '--wipe True'], input='test\ntest\n') def test_balance(self): runner = CliRunner() @@ -46,12 +41,17 @@ class Testcases(unittest.TestCase): result = runner.invoke(cli, ['config']) self.assertEqual(result.exit_code, 0) - def test_listkeys(self): + def test_addkey(self): runner = CliRunner() - result = runner.invoke(cli, ['addkey', '--password test', '--key ' + wif]) + result = runner.invoke(cli, ['addkey', '--password test', '--unsafe-import-key ' + wif]) self.assertEqual(result.exit_code, 2) - def test_addkeys(self): + def test_delkey(self): + runner = CliRunner() + result = runner.invoke(cli, ['delkey', '--password test', wif]) + self.assertEqual(result.exit_code, 2) + + def test_listkeys(self): runner = CliRunner() result = runner.invoke(cli, ['listkeys']) self.assertEqual(result.exit_code, 0) @@ -65,7 +65,11 @@ class Testcases(unittest.TestCase): runner = CliRunner() result = runner.invoke(cli, ['info']) self.assertEqual(result.exit_code, 0) - result = runner.invoke(cli, ['info', 'test']) + result = runner.invoke(cli, ['info', 'beem']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['info', '100']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['info', '--', '-1']) self.assertEqual(result.exit_code, 0) def test_changepassword(self): @@ -85,5 +89,5 @@ class Testcases(unittest.TestCase): def test_set(self): runner = CliRunner() - result = runner.invoke(cli, ['set', '-w 100']) + result = runner.invoke(cli, ['set', 'set_default_vote_weight', '100']) self.assertEqual(result.exit_code, 0) diff --git a/tests/beem/test_testnet.py b/tests/beem/test_testnet.py index a19caa2bb7e745716a370752af3cb3244edb52a2..6009e25a3af12aaf785b13c03678ff8ed8a20d5d 100644 --- a/tests/beem/test_testnet.py +++ b/tests/beem/test_testnet.py @@ -182,13 +182,16 @@ class Testcases(unittest.TestCase): "memo": '2 of 2 serialized/deserialized transaction'})) tx.appendSigner("elf", "active") + tx.addSigningInformation("elf", "active") tx.sign() tx.clearWifs() self.assertEqual(len(tx['signatures']), 1) - new_tx = TransactionBuilder(tx=tx.json(), steem_instance=steem) + steem.wallet.removeAccount("elf") + tx_json = tx.json() + del tx + new_tx = TransactionBuilder(tx=tx_json, steem_instance=steem) self.assertEqual(len(new_tx['signatures']), 1) steem.wallet.addPrivateKey(self.active_private_key_of_steemfiles) - new_tx.addSigningInformation("steemfiles", "active", reconstruct_tx=False) new_tx.appendMissingSignatures() new_tx.sign(reconstruct_tx=False) self.assertEqual(len(new_tx['signatures']), 2) @@ -224,6 +227,42 @@ class Testcases(unittest.TestCase): steem.nobroadcast = True steem.wallet.addPrivateKey(self.active_private_key_of_elf) + def test_transfer_2of2_wif(self): + # Send a 2 of 2 transaction from elf which needs steemfiles's cosign to send + # funds but sign the transaction with elf's key and then serialize the transaction + # and deserialize the transaction. After that, sign with steemfiles's key. + steem = Steem( + node=["wss://testnet.steem.vc"], + num_retries=10, + keys=[self.active_private_key_of_elf] + ) + + tx = TransactionBuilder(steem_instance=steem) + tx.appendOps(Transfer(**{"from": 'elf', + "to": 'leprechaun', + "amount": '0.01 SBD', + "memo": '2 of 2 serialized/deserialized transaction'})) + + tx.appendSigner("elf", "active") + tx.addSigningInformation("elf", "active") + tx.sign() + tx.clearWifs() + self.assertEqual(len(tx['signatures']), 1) + tx_json = tx.json() + del steem + del tx + + steem = Steem( + node=["wss://testnet.steem.vc"], + num_retries=10, + keys=[self.active_private_key_of_steemfiles] + ) + new_tx = TransactionBuilder(tx=tx_json, steem_instance=steem) + new_tx.appendMissingSignatures() + new_tx.sign(reconstruct_tx=False) + self.assertEqual(len(new_tx['signatures']), 2) + new_tx.broadcast() + def test_verifyAuthority(self): stm = self.bts stm.wallet.unlock("123")