diff --git a/beem/account.py b/beem/account.py index 0e11a14a12a90c0c473d6260aa929e13925bea98..f8a9fe74150db469eef5ee9c2875ce6164875df8 100644 --- a/beem/account.py +++ b/beem/account.py @@ -1362,6 +1362,38 @@ class Account(BlockchainObject): return self.steem.finalizeOp(op, account, "active") + def set_withdraw_vesting_route(self, + to, + percentage=100, + account=None, + auto_vest=False): + """ Set up a vesting withdraw route. When vesting shares are + withdrawn, they will be routed to these accounts based on the + specified weights. + :param str to: Recipient of the vesting withdrawal + :param float percentage: The percent of the withdraw to go + to the 'to' account. + :param str account: (optional) the vesting account + :param bool auto_vest: Set to true if the from account + should receive the VESTS as VESTS, or false if it should + receive them as STEEM. (defaults to ``False``) + """ + if not account: + account = self + if not account: + raise ValueError("You need to provide an account") + STEEMIT_100_PERCENT = 10000 + STEEMIT_1_PERCENT = (STEEMIT_100_PERCENT / 100) + op = operations.Set_withdraw_vesting_route( + **{ + "from_account": account["name"], + "to_account": to, + "percent": int(percentage * STEEMIT_1_PERCENT), + "auto_vest": auto_vest + }) + + return self.steem.finalizeOp(op, account, "active") + def allow( self, foreign, weight=None, permission="posting", account=None, threshold=None, **kwargs diff --git a/beem/cli.py b/beem/cli.py index 2effd3df38eb6ab56e0d43bda711012e64cc2dcd..eacc7ad8b0d54cdcf65103202ff081aaef436658 100644 --- a/beem/cli.py +++ b/beem/cli.py @@ -395,6 +395,49 @@ def powerdown(amount, password, account): print(tx) +@cli.command() +@click.argument('to', nargs=1) +@click.option('--percentage', default=100, help='The percent of the withdraw to go to the "to" account') +@click.option('--password', prompt=True, hide_input=True, + confirmation_prompt=False, help='Password to unlock wallet') +@click.option('--account', '-a', help='Powerup from this account') +@click.option('--auto_vest', help='Set to true if the from account should receive the VESTS as' + 'VESTS, or false if it should receive them as STEEM.', is_flag=True) +def powerdownroute(to, percentage, password, account, auto_vest): + """Setup a powerdown route""" + stm = shared_steem_instance() + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm, password): + return + acc = Account(account, steem_instance=stm) + tx = acc.set_withdraw_vesting_route(to, percentage, auto_vest=auto_vest) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('amount', nargs=1) +@click.option('--password', prompt=True, hide_input=True, + confirmation_prompt=False, help='Password to unlock wallet') +@click.option('--account', '-a', help='Powerup from this account') +def convert(amount, password, account): + """Convert STEEMDollars to Steem (takes a week to settle)""" + stm = shared_steem_instance() + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm, password): + return + acc = Account(account, steem_instance=stm) + try: + amount = float(amount) + except: + amount = str(amount) + tx = acc.convert(amount) + tx = json.dumps(tx, indent=4) + print(tx) + + @cli.command() @click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True) @@ -406,8 +449,7 @@ def changewalletpassphrase(password): @cli.command() -@click.option( - '--account', '-a', multiple=True) +@click.argument('account', nargs=-1) def balance(account): """ Shows balance """ @@ -447,6 +489,34 @@ def balance(account): print(t) +@cli.command() +@click.argument('account', nargs=-1, required=False) +def interest(account): + """ Get information about interest payment + """ + stm = shared_steem_instance() + if not account: + if "default_account" in stm.config: + account = [stm.config["default_account"]] + + t = PrettyTable([ + "Account", "Last Interest Payment", "Next Payment", + "Interest rate", "Interest" + ]) + t.align = "r" + for a in account: + a = Account(a, steem_instance=stm) + i = a.interest() + t.add_row([ + a["name"], + i["last_payment"], + "in %s" % (i["next_payment_duration"]), + "%.1f%%" % i["interest_rate"], + "%.3f %s" % (i["interest"], "SBD"), + ]) + print(t) + + @cli.command() @click.argument('objects', nargs=-1) def info(objects): diff --git a/beemapi/exceptions.py b/beemapi/exceptions.py index 1cfcf39ef654e6973238262cddd0bfb5cbaa53c2..0ab9981a62c78affefcf10cd3d5197f682c14d27 100644 --- a/beemapi/exceptions.py +++ b/beemapi/exceptions.py @@ -41,6 +41,10 @@ class NoApiWithName(RPCError): pass +class ApiNotSupported(RPCError): + pass + + class UnhandledRPCError(RPCError): pass diff --git a/beemapi/steemnoderpc.py b/beemapi/steemnoderpc.py index 815de5c14eff4b7d85e1309a8fb7a26966ff07c3..0a6e67cc31320658463727c09e996e4fcafe265a 100644 --- a/beemapi/steemnoderpc.py +++ b/beemapi/steemnoderpc.py @@ -57,7 +57,7 @@ class SteemNodeRPC(GrapheneRPC): self.error_cnt_call = cnt reply = super(SteemNodeRPC, self).rpcexec(payload) if self.next_node_on_empty_reply and not bool(reply) and self.n_urls > 1: - sleep_and_check_retries(self.num_retries_call, cnt, self.url, str("Empty reply"), sleep=False) + sleep_and_check_retries(self.num_retries_call, cnt, self.url, str("Empty reply"), sleep=False, call_retry=True) self.error_cnt[self.url] += 1 self.next() cnt = 0 @@ -69,7 +69,7 @@ class SteemNodeRPC(GrapheneRPC): return reply except exceptions.RPCErrorDoRetry as e: msg = exceptions.decodeRPCErrorMsg(e).strip() - sleep_and_check_retries(self.num_retries_call, cnt, self.url, str(msg)) + sleep_and_check_retries(self.num_retries_call, cnt, self.url, str(msg), call_retry=True) doRetry = True except exceptions.RPCError as e: doRetry = self._check_error_message(e, cnt) @@ -94,19 +94,25 @@ class SteemNodeRPC(GrapheneRPC): elif re.search("Could not find method", msg): raise exceptions.NoMethodWithName(msg) elif re.search("Could not find API", msg): - raise exceptions.NoApiWithName(msg) + if self._check_api_name(msg): + raise exceptions.ApiNotSupported(msg) + else: + raise exceptions.NoApiWithName(msg) elif re.search("irrelevant signature included", msg): raise exceptions.UnnecessarySignatureDetected(msg) elif re.search("WinError", msg): raise exceptions.RPCError(msg) elif re.search("Unable to acquire database lock", msg): - sleep_and_check_retries(self.num_retries_call, cnt, self.url, str(msg)) + sleep_and_check_retries(self.num_retries_call, cnt, self.url, str(msg), call_retry=True) doRetry = True elif re.search("Internal Error", msg): - sleep_and_check_retries(self.num_retries_call, cnt, self.url, str(msg)) + sleep_and_check_retries(self.num_retries_call, cnt, self.url, str(msg), call_retry=True) doRetry = True elif re.search("!check_max_block_age", str(e)): - sleep_and_check_retries(self.num_retries_call, cnt, self.url, str(msg), sleep=False) + if self.n_urls == 1: + raise exceptions.UnhandledRPCError(msg) + self.error_cnt[self.url] += 1 + sleep_and_check_retries(self.num_retries, self.error_cnt[self.url], self.url, str(msg), sleep=False) self.next() doRetry = True elif re.search("out_of_rangeEEEE: unknown key", msg) or re.search("unknown key:unknown key", msg): @@ -117,6 +123,37 @@ class SteemNodeRPC(GrapheneRPC): raise e return doRetry + def _check_api_name(self, msg): + error_start = "Could not find API " + if re.search(error_start + "account_history_api", msg): + return True + elif re.search(error_start + "tags_api", msg): + return True + elif re.search(error_start + "database_api", msg): + return True + elif re.search(error_start + "market_history_api", msg): + return True + elif re.search(error_start + "block_api", msg): + return True + elif re.search(error_start + "account_by_key_api", msg): + return True + elif re.search(error_start + "chain_api", msg): + return True + elif re.search(error_start + "follow_api", msg): + return True + elif re.search(error_start + "condenser_api", msg): + return True + elif re.search(error_start + "debug_node_api", msg): + return True + elif re.search(error_start + "witness_api", msg): + return True + elif re.search(error_start + "test_api", msg): + return True + elif re.search(error_start + "network_broadcast_api", msg): + return True + else: + return False + def get_account(self, name, **kwargs): """ Get full account details from account name diff --git a/beemgrapheneapi/graphenerpc.py b/beemgrapheneapi/graphenerpc.py index e40982ff9107090550357f2f30883695994ed4b1..4dbda5fccdeb48085e77abad54715501aec4f591 100644 --- a/beemgrapheneapi/graphenerpc.py +++ b/beemgrapheneapi/graphenerpc.py @@ -254,7 +254,6 @@ class GrapheneRPC(object): reply = {} while True: self.error_cnt_call += 1 - try: if self.current_rpc == 0 or self.current_rpc == 2: reply = self.ws_send(json.dumps(payload, ensure_ascii=False).encode('utf8')) @@ -262,7 +261,7 @@ class GrapheneRPC(object): reply = self.request_send(json.dumps(payload, ensure_ascii=False).encode('utf8')) if not bool(reply): self.error_cnt[self.url] += 1 - sleep_and_check_retries(self.num_retries_call, self.error_cnt_call, self.url, "Empty Reply") + sleep_and_check_retries(self.num_retries_call, self.error_cnt_call, self.url, "Empty Reply", call_retry=True) self.rpcconnect() else: break @@ -273,10 +272,10 @@ class GrapheneRPC(object): self.rpcconnect(next_url=False) except ConnectionError as e: self.error_cnt[self.url] += 1 - sleep_and_check_retries(self.num_retries_call, self.error_cnt_call, self.url, str(e)) + sleep_and_check_retries(self.num_retries_call, self.error_cnt_call, self.url, str(e), call_retry=True) except Exception as e: self.error_cnt[self.url] += 1 - sleep_and_check_retries(self.num_retries_call, self.error_cnt_call, self.url, str(e)) + sleep_and_check_retries(self.num_retries_call, self.error_cnt_call, self.url, str(e), call_retry=True) # retry self.rpcconnect() diff --git a/beemgrapheneapi/rpcutils.py b/beemgrapheneapi/rpcutils.py index 840824049589e46e00ff57a3276a7b558623c500..6f97a91b6cd8c927553f5947e3edbbb39cad43c9 100644 --- a/beemgrapheneapi/rpcutils.py +++ b/beemgrapheneapi/rpcutils.py @@ -83,13 +83,16 @@ def get_api_name(appbase, *args, **kwargs): return api_name -def sleep_and_check_retries(num_retries, cnt, url, errorMsg=None, sleep=True): +def sleep_and_check_retries(num_retries, cnt, url, errorMsg=None, sleep=True, call_retry=False): """Sleep and check if num_retries is reached""" if errorMsg: - log.warning("Error: {}\n".format(errorMsg)) + log.warning("Error: {}".format(errorMsg)) + if call_retry: + log.warning("Retry RPC Call on node: %s (%d/%d) \n" % (url, cnt, num_retries)) + else: + log.warning("Lost connection or internal error on node: %s (%d/%d) \n" % (url, cnt, num_retries)) if (num_retries >= 0 and cnt > num_retries): raise NumRetriesReached() - log.warning("\nLost connection or internal error on node: %s (%d/%d) " % (url, cnt, num_retries)) if not sleep: return if cnt < 1: diff --git a/tests/beem/test_cli.py b/tests/beem/test_cli.py index 2acc490aa8f614393e2ea1723514428539709d1f..a51d28ef47334043fc339502a1f5a19da2de4d2b 100644 --- a/tests/beem/test_cli.py +++ b/tests/beem/test_cli.py @@ -35,7 +35,12 @@ class Testcases(unittest.TestCase): def test_balance(self): runner = CliRunner() - result = runner.invoke(cli, ['balance', '-atest']) + result = runner.invoke(cli, ['balance', 'beem', 'beem1']) + self.assertEqual(result.exit_code, 0) + + def test_interest(self): + runner = CliRunner() + result = runner.invoke(cli, ['interest', 'beem', 'beem1']) self.assertEqual(result.exit_code, 0) def test_config(self): @@ -95,15 +100,25 @@ class Testcases(unittest.TestCase): def test_upvote(self): runner = CliRunner() - result = runner.invoke(cli, ['upvote', '@test/abcd', '--weight 100'], input='test\n') + result = runner.invoke(cli, ['upvote', '@test/abcd', '--weight 100' '--password test'], input='test\n') self.assertEqual(result.exit_code, 0) def test_downvote(self): runner = CliRunner() - result = runner.invoke(cli, ['downvote', '@test/abcd', '--weight 100'], input='test\n') + result = runner.invoke(cli, ['downvote', '@test/abcd', '--weight 100' '--password test'], input='test\n') self.assertEqual(result.exit_code, 0) def test_transfer(self): runner = CliRunner() - result = runner.invoke(cli, ['transfer', 'beem1', '1', 'SBD', 'test'], input='test\n') + result = runner.invoke(cli, ['transfer', 'beem1', '1', 'SBD', 'test' '--password test'], input='test\n') + self.assertEqual(result.exit_code, 0) + + def test_powerdownroute(self): + runner = CliRunner() + result = runner.invoke(cli, ['powerdownroute', 'beem1', '--password test'], input='test\n') + self.assertEqual(result.exit_code, 0) + + def test_convert(self): + runner = CliRunner() + result = runner.invoke(cli, ['convert', '1', '--password test'], input='test\n') self.assertEqual(result.exit_code, 0)