diff --git a/neo/Prompt/Commands/Invoke.py b/neo/Prompt/Commands/Invoke.py index 771ae2f66..017bae072 100644 --- a/neo/Prompt/Commands/Invoke.py +++ b/neo/Prompt/Commands/Invoke.py @@ -317,7 +317,7 @@ def test_invoke(script, wallet, outputs, withdrawal_tx=None, if context.Completed: wallet_tx.scripts = context.GetScripts() else: - logger.warn("Not gathering signatures for test build. For a non-test invoke that would occur here.") + logger.warning("Not gathering signatures for test build. For a non-test invoke that would occur here.") # if not gather_signatures(context, wallet_tx, owners): # return None, [], 0, None diff --git a/neo/Prompt/Commands/Tokens.py b/neo/Prompt/Commands/Tokens.py index 0e2fb647e..7785364d9 100644 --- a/neo/Prompt/Commands/Tokens.py +++ b/neo/Prompt/Commands/Tokens.py @@ -5,12 +5,14 @@ from neocore.UInt160 import UInt160 from prompt_toolkit import prompt from decimal import Decimal -from neo.Core.TX.TransactionAttribute import TransactionAttribute, TransactionAttributeUsage +from neo.Core.TX.TransactionAttribute import TransactionAttribute import binascii from neo.Prompt.CommandBase import CommandBase, CommandDesc, ParameterDesc from neo.Prompt.PromptData import PromptData from neo.Prompt.Utils import get_arg from neo.Implementations.Wallets.peewee.Models import NEP5Token as ModelNEP5Token +from neo.Core.TX.TransactionAttribute import TransactionAttributeUsage +from neocore.Utils import isValidPublicAddress import peewee @@ -18,6 +20,7 @@ class CommandWalletToken(CommandBase): def __init__(self): super().__init__() self.register_sub_command(CommandTokenDelete()) + self.register_sub_command(CommandTokenSend()) def command_desc(self): return CommandDesc('token', 'various token operations') @@ -77,23 +80,103 @@ def command_desc(self): return CommandDesc('delete', 'remove a token from the wallet', [p1]) -def token_send(wallet, args, prompt_passwd=True): - if len(args) < 4: - print("please provide a token symbol, from address, to address, and amount") - return False +class CommandTokenSend(CommandBase): + + def __init__(self): + super().__init__() - user_tx_attributes = None - if len(args) > 4: - args, user_tx_attributes = get_tx_attr_from_args(args) + def execute(self, arguments): + wallet = PromptData.Wallet + + if len(arguments) < 4: + print("Please specify the required parameters") + return False + + if len(arguments) > 5: + # the 5th argument is the optional attributes, + print("Too many parameters supplied. Please check your command") + return False + + _, user_tx_attributes = get_tx_attr_from_args(arguments) + + token = arguments[0] + send_from = arguments[1] + send_to = arguments[2] + try: + amount = float(arguments[3]) + except ValueError: + print(f"{arguments[3]} is not a valid amount") + return False + + try: + success = token_send(wallet, token, send_from, send_to, amount, user_tx_attributes) + except ValueError as e: + # occurs if arguments are invalid + print(str(e)) + success = False + + return success + + def command_desc(self): + p1 = ParameterDesc('token', 'token symbol name or script_hash') + p2 = ParameterDesc('from_addr', 'address to send token from') + p3 = ParameterDesc('to_addr', 'address to send token to') + p4 = ParameterDesc('amount', 'number of tokens to send') + p5 = ParameterDesc('--tx-attr', f"a list of transaction attributes to attach to the transaction\n\n" + f"{' ':>17} See: http://docs.neo.org/en-us/network/network-protocol.html section 4 for a description of possible attributes\n\n" # noqa: E128 ignore indentation + f"{' ':>17} Example:\n" + f"{' ':>20} --tx-attr=[{{\"usage\": ,\"data\":\"\"}}, ...]\n" + f"{' ':>20} --tx-attr=[{{\"usage\": 0x90,\"data\":\"my brief description\"}}]\n", optional=True) + + return CommandDesc('send', 'send a token from the wallet', [p1, p2, p3, p4, p5]) + + +def token_send(wallet, token_str, send_from, send_to, amount, prompt_passwd=True, user_tx_attributes=None): + """ + + Args: + wallet (Wallet): a UserWallet instance + token_str (str): symbol name or script_hash + send_from (str): a wallet address + send_to (str): a wallet address + amount (float): the number of tokens to send + prompt_passwd (bool): (optional) whether to prompt for a password before sending it to the network + user_tx_attributes (list): a list of ``TransactionAttribute``s. + + Returns: + a Transaction object if successful, False otherwise. + """ + if not user_tx_attributes: + user_tx_attributes = [] + + token = None + for t in wallet.GetTokens().values(): + if token_str == t.symbol: + token = t + break + elif token_str == t.ScriptHash.ToString(): + token = t + break - token = get_asset_id(wallet, args[0]) if not isinstance(token, NEP5Token): - print("The given symbol does not represent a loaded NEP5 token") - return False + raise ValueError("The given token argument does not represent a known NEP5 token") - send_from = args[1] - send_to = args[2] - amount = amount_from_string(token, args[3]) + if not isValidPublicAddress(send_from): + raise ValueError("send_from is not a valid address") + + if not isValidPublicAddress(send_to): + raise ValueError("send_to is not a valid address") + + try: + # internally this function uses the `Decimal` class which will parse the float amount to its required format. + # the name is a bit misleading /shrug + amount = amount_from_string(token, amount) + except Exception: + raise ValueError(f"{amount} is not a valid amount") + + for attr in user_tx_attributes: + if not isinstance(attr, TransactionAttribute): + raise ValueError(f"{attr} is not a valid transaction attribute") return do_token_transfer(token, wallet, send_from, send_to, amount, prompt_passwd=prompt_passwd, tx_attributes=user_tx_attributes) diff --git a/neo/Prompt/Commands/tests/test_token_commands.py b/neo/Prompt/Commands/tests/test_token_commands.py index db83fa6f3..6a48032f0 100644 --- a/neo/Prompt/Commands/tests/test_token_commands.py +++ b/neo/Prompt/Commands/tests/test_token_commands.py @@ -1,6 +1,6 @@ from neo.Utils.WalletFixtureTestCase import WalletFixtureTestCase from neo.Wallets.utils import to_aes_key -from neo.Prompt.Utils import get_asset_id +from neo.Prompt.Utils import get_asset_id, get_tx_attr_from_args from neo.Implementations.Wallets.peewee.UserWallet import UserWallet from neo.Implementations.Notifications.LevelDB.NotificationDB import NotificationDB from neo.Core.Blockchain import Blockchain @@ -115,8 +115,7 @@ def test_token_send_good(self): addr_from = wallet.GetDefaultContract().Address addr_to = self.watch_addr_str - args = [token.symbol, addr_from, addr_to, '1300'] - send = token_send(wallet, args, prompt_passwd=True) + send = token_send(wallet, token.symbol, addr_from, addr_to, 1300, prompt_passwd=True) self.assertTrue(send) res = send.ToJson() @@ -129,9 +128,9 @@ def test_token_send_with_user_attributes(self): token = self.get_token(wallet) addr_from = wallet.GetDefaultContract().Address addr_to = self.watch_addr_str + _, attributes = get_tx_attr_from_args(['--tx-attr=[{"usage":241,"data":"This is a remark"},{"usage":242,"data":"This is a remark 2"}]']) - args = [token.symbol, addr_from, addr_to, '1300', '--tx-attr=[{"usage":241,"data":"This is a remark"},{"usage":242,"data":"This is a remark 2"}]'] - send = token_send(wallet, args, prompt_passwd=True) + send = token_send(wallet, token.symbol, addr_from, addr_to, 1300, user_tx_attributes=attributes, prompt_passwd=True) self.assertTrue(send) res = send.ToJson() @@ -146,8 +145,8 @@ def test_token_send_bad_user_attributes(self): addr_from = wallet.GetDefaultContract().Address addr_to = self.watch_addr_str - args = [token.symbol, addr_from, addr_to, '100', '--tx-attr=[{"usa:241,"data":"This is a remark"}]'] - send = token_send(wallet, args, prompt_passwd=True) + _, attributes = get_tx_attr_from_args(['--tx-attr=[{"usa:241,"data":"This is a remark"}]']) + send = token_send(wallet, token.symbol, addr_from, addr_to, 100, user_tx_attributes=attributes, prompt_passwd=True) self.assertTrue(send) res = send.ToJson() @@ -160,20 +159,20 @@ def test_token_send_bad_args(self): # too few args addr_from = wallet.GetDefaultContract().Address addr_to = self.watch_addr_str - args = [token.symbol, addr_from, addr_to] - send = token_send(wallet, args, prompt_passwd=False) + with self.assertRaises(ValueError) as context: + token_send(wallet, token.symbol, addr_from, addr_to, None, prompt_passwd=False) - self.assertFalse(send) + self.assertIn("not a valid amount", str(context.exception)) def test_token_send_bad_token(self): wallet = self.GetWallet1(recreate=True) addr_from = wallet.GetDefaultContract().Address addr_to = self.watch_addr_str - args = ["Blah", addr_from, addr_to, '1300'] - send = token_send(wallet, args, prompt_passwd=False) + with self.assertRaises(ValueError) as context: + token_send(wallet, "Blah", addr_from, addr_to, 1300, prompt_passwd=False) - self.assertFalse(send) + self.assertIn("does not represent a known NEP5 token", str(context.exception)) def test_token_send_no_tx(self): with patch('neo.Wallets.NEP5Token.NEP5Token.Transfer', return_value=(None, 0, None)): @@ -182,8 +181,7 @@ def test_token_send_no_tx(self): addr_from = wallet.GetDefaultContract().Address addr_to = self.watch_addr_str - args = [token.symbol, addr_from, addr_to, '1300'] - send = token_send(wallet, args, prompt_passwd=False) + send = token_send(wallet, token.symbol, addr_from, addr_to, 1300, prompt_passwd=False) self.assertFalse(send) @@ -194,8 +192,7 @@ def test_token_send_bad_password(self): addr_from = wallet.GetDefaultContract().Address addr_to = self.watch_addr_str - args = [token.symbol, addr_from, addr_to, '1300'] - send = token_send(wallet, args) + send = token_send(wallet, token.symbol, addr_from, addr_to, 1300) self.assertFalse(send) @@ -617,6 +614,71 @@ def test_wallet_token_delete(self): self.assertTrue(res) self.assertIn("deleted", mock_print.getvalue()) + def test_wallet_token_send(self): + + with self.OpenWallet1(): + # test with no parameters + with patch('sys.stdout', new=StringIO()) as mock_print: + args = ['token', 'send'] + res = CommandWallet().execute(args) + self.assertFalse(res) + self.assertIn("specify the required parameter", mock_print.getvalue()) + + # test with insufficient parameters + with patch('sys.stdout', new=StringIO()) as mock_print: + args = ['token', 'send', 'arg1'] + res = CommandWallet().execute(args) + self.assertFalse(res) + self.assertIn("specify the required parameter", mock_print.getvalue()) + + # test with too many parameters (max is 4 mandatory + 1 optional) + with patch('sys.stdout', new=StringIO()) as mock_print: + args = ['token', 'send', 'arg1', 'arg2', 'arg3', 'arg4', 'arg5', 'arg6'] + res = CommandWallet().execute(args) + self.assertFalse(res) + self.assertIn("Too many parameters supplied", mock_print.getvalue()) + + # test with invalid token argument + with patch('sys.stdout', new=StringIO()) as mock_print: + args = ['token', 'send', 'invalid_token_name', 'arg2', 'arg3', '10'] + res = CommandWallet().execute(args) + self.assertFalse(res) + self.assertIn("does not represent a known NEP5 token", mock_print.getvalue()) + + # test with valid token arg, but invalid from_addr + with patch('sys.stdout', new=StringIO()) as mock_print: + args = ['token', 'send', 'NXT4', 'invalid_from_addr', 'arg3', '10'] + res = CommandWallet().execute(args) + self.assertFalse(res) + self.assertIn("not a valid address", mock_print.getvalue()) + + # test with valid token and from_addr, but invalid to_addr + with patch('sys.stdout', new=StringIO()) as mock_print: + args = ['token', 'send', 'NXT4', 'AZfFBeBqtJvaTK9JqG8uk6N7FppQY6byEg', 'invalid_to_addr', '10'] + res = CommandWallet().execute(args) + self.assertFalse(res) + self.assertIn("not a valid address", mock_print.getvalue()) + + # test with invalid amount + with patch('sys.stdout', new=StringIO()) as mock_print: + args = ['token', 'send', 'NXT4', 'AZfFBeBqtJvaTK9JqG8uk6N7FppQY6byEg', 'AZfFBeBqtJvaTK9JqG8uk6N7FppQY6byEg', 'invalid_amount'] + res = CommandWallet().execute(args) + self.assertFalse(res) + self.assertIn("not a valid amount", mock_print.getvalue()) + + # Note that there is no test for invalid tx-attributes. Invalid attributes result in an empty attribute list being + # passed to the underlying function thus having no effect. + + # test with a good transfer + # we don't really send anything. Testing `do_token_transfer` already happens in `test_token_send_good()` + with patch('neo.Prompt.Commands.Tokens.do_token_transfer', side_effect=[object()]): + token = self.get_token(PromptData.Wallet) + addr_from = PromptData.Wallet.GetDefaultContract().Address + addr_to = self.watch_addr_str + + send = token_send(PromptData.Wallet, token.symbol, addr_from, addr_to, 13, prompt_passwd=False) + self.assertTrue(send) + # utility function def Approve_Allowance(self): wallet = self.GetWallet1(recreate=True) diff --git a/requirements.txt b/requirements.txt index 776149dac..47292811a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,10 +33,10 @@ logzero==1.5.0 memory-profiler==0.54.0 mmh3==2.5.1 mock==2.0.0 -mpmath==1.0.0 +mpmath==1.1.0 neo-boa==0.5.6 neo-python-rpc==0.2.1 -neocore==0.5.4 +neocore==0.5.5 pbr==4.2.0 peewee==3.6.4 pexpect==4.6.0