diff --git a/pyircbot/modules/CryptoWallet.py b/pyircbot/modules/CryptoWallet.py index 5ccc50a..45550ea 100755 --- a/pyircbot/modules/CryptoWallet.py +++ b/pyircbot/modules/CryptoWallet.py @@ -33,7 +33,8 @@ class CryptoWallet(ModuleBase): self.bot.act_PRIVMSG(msg.args[0], ".setaddr: '{}' is not a supported currency. Supported currencies are: {}" .format(cmd.args[0], supportedStr)) return - if len(cmd.args[1]) < 16 or len(cmd.args[1]) > 42: + client = rpc.getRpc(cmd.args[0]) + if not client.validate_addr(cmd.args[1]): self.bot.act_PRIVMSG(msg.args[0], ".setaddr: '{}' appears to be an invalid address.".format(cmd.args[1])) return @@ -48,14 +49,9 @@ class CryptoWallet(ModuleBase): @info("getbal retrieve your balance ", cmds=["getbal"]) @command("getbal", require_args=1, allow_private=True) def handle_getbal(self, msg, cmd): - usage = ".getbal " if not self.check_login(msg.prefix, msg.args[0]): return attr, rpc = self.getMods() - # Check for args - if len(cmd.args) != 1: - self.bot.act_PRIVMSG(msg.args[0], ".getbal: usage: {}".format(usage)) - return # Check if currency is known if not rpc.isSupported(cmd.args[0]): supportedStr = ', '.join(rpc.getSupported()) @@ -76,7 +72,7 @@ class CryptoWallet(ModuleBase): self.bot.act_PRIVMSG(msg.args[0], "{}: your balance is: {} {}".format(msg.prefix.nick, amount, cmd.args[0].upper())) - @info("withdraw send coins to your withdraw address", cmds=["withdraw"]) + @info("withdraw send coins to your withdraw address", cmds=["withdraw"]) @command("withdraw", require_args=2, allow_private=True) def handle_withdraw(self, msg, cmd): if not self.check_login(msg.prefix, msg.args[0]): @@ -116,7 +112,7 @@ class CryptoWallet(ModuleBase): self.bot.act_PRIVMSG(msg.args[0], ".withdraw: Withdrawing that much would put you below the reserve " "({} {}).".format(client.reserve, cmd.args[0].upper())) self.bot.act_PRIVMSG(msg.args[0], ".withdraw: The reserve is to cover network transaction fees. To recover " - "it you must close your account. (Talk to an admin)") + "it you must close your account. (Talk to my owner)") return # Check if the precision is wrong @@ -128,8 +124,8 @@ class CryptoWallet(ModuleBase): # Create a transaction txn = client.send(walletname, withdrawaddr, withdrawamount) if txn: - self.bot.act_PRIVMSG(msg.args[0], "{}: .withdraw: {} {} sent to {}. " - .format(msg.prefix.nick, withdrawamount, client.name, withdrawaddr)) + self.bot.act_PRIVMSG(msg.args[0], "{}: .withdraw: {} {} sent to {}." + .format(msg.prefix.nick, withdrawamount, client.name.upper(), withdrawaddr)) self.bot.act_PRIVMSG(msg.prefix.nick, "Withdrawal: (You)->{}: Transaction ID: {}" .format(withdrawaddr, txn)) else: @@ -185,8 +181,8 @@ class CryptoWallet(ModuleBase): # Create a transaction txn = client.send(walletname, tx_dest, withdrawamount) if txn: - self.bot.act_PRIVMSG(msg.args[0], "{}: .send: {} {} sent to {}. " - .format(msg.prefix.nick, withdrawamount, client.name, tx_dest)) + self.bot.act_PRIVMSG(msg.args[0], "{}: .send: {} {} sent to {}." + .format(msg.prefix.nick, withdrawamount, client.name.upper(), tx_dest)) self.bot.act_PRIVMSG(msg.prefix.nick, "Send: (You)->{}: Transaction ID: {}".format(tx_dest, txn)) else: self.bot.act_PRIVMSG(msg.args[0], "{}: .send: Transaction create failed. Maybe the address is invalid " @@ -196,7 +192,7 @@ class CryptoWallet(ModuleBase): # Check if dest user has a password set destUserPassword = attr.getKey(tx_dest, "password") if destUserPassword is None: - self.bot.act_PRIVMSG(msg.args[0], "{} .send: {} doesn't have a password set." + self.bot.act_PRIVMSG(msg.args[0], "{}: .send: {} doesn't have a password set." .format(msg.prefix.nick, tx_dest)) return @@ -215,8 +211,8 @@ class CryptoWallet(ModuleBase): print(destWalletName) if client.canMove(srcWalletName, destWalletName, withdrawamount): if client.move(srcWalletName, destWalletName, withdrawamount): - self.bot.act_PRIVMSG(msg.args[0], "{} .send: {} {} sent to {}. " - .format(msg.prefix.nick, withdrawamount, client.name, tx_dest)) + self.bot.act_PRIVMSG(msg.args[0], "{}: .send: {} {} sent to {}." + .format(msg.prefix.nick, withdrawamount, client.name.upper(), tx_dest)) else: self.bot.act_PRIVMSG(msg.args[0], "{}: uh-oh, something went wrong doing that." .format(msg.prefix.nick)) @@ -247,7 +243,7 @@ class CryptoWallet(ModuleBase): attr, rpc = self.getMods() if not cmd.args: self.bot.act_PRIVMSG(msg.args[0], - ".curinfo: supported currencies: {}. Use '.curinfo BTC' to see details. " + ".curinfo: supported currencies: {}. Use '.curinfo BTC' to see details." .format(', '.join([x.upper() for x in rpc.getSupported()]))) else: if not rpc.isSupported(cmd.args[0]): diff --git a/pyircbot/modules/CryptoWalletRPC.py b/pyircbot/modules/CryptoWalletRPC.py index f8c15ad..7813e19 100755 --- a/pyircbot/modules/CryptoWalletRPC.py +++ b/pyircbot/modules/CryptoWalletRPC.py @@ -11,6 +11,7 @@ from pyircbot.modulebase import ModuleBase from bitcoinrpc.authproxy import AuthServiceProxy import re from threading import Thread +from decimal import Decimal class CryptoWalletRPC(ModuleBase): @@ -24,7 +25,7 @@ class CryptoWalletRPC(ModuleBase): # Create a dict of abbreviation=>BitcoinRPC objcet relation self.log.info("CryptoWalletRPC: loadrpcservices: connecting to RPCs") for abbr, coin in self.config["types"].items(): - self.rpcservices[abbr.lower()] = BitcoinRPC(self, + self.rpcservices[abbr.lower()] = BitcoinRPC(self.log, abbr.lower(), coin["name"], coin["host"], @@ -64,9 +65,8 @@ class CryptoWalletRPC(ModuleBase): class BitcoinRPC(object): - def __init__(self, parent, name, fullname, host, port, username, password, precision, reserve, addr_re): + def __init__(self, logger, name, fullname, host, port, username, password, precision, reserve, addr_re): # Store info and connect - self.master = parent self.name = name self.host = host self.port = port @@ -75,7 +75,7 @@ class BitcoinRPC(object): self.precision = precision self.reserve = reserve self.addr_re = addr_re - self.log = self.master.log + self.log = logger self.con = None # AuthServiceProxy (bitcoin json rpc client) stored here Thread(target=self.ping).start() # Initiate rpc connection @@ -89,11 +89,20 @@ class BitcoinRPC(object): return True if type(addr) is str and self.addr_re.match(addr) else False def getBal(self, acct): - # get a balance of an address or an account + """ + Return the balance of the passed account + :param acct: account name + :type acct: str + :return: decimal.Decimal + """ return self.getAcctBal(acct) def getAcctAddr(self, acct): - # returns the address for an account. creates if necessary + """ + Return the deposit address associated with the passed account + :param acct: account name + :type acct: str + """ self.ping() addrs = self.con.getaddressesbyaccount(acct) if len(addrs) == 0: @@ -103,7 +112,7 @@ class BitcoinRPC(object): def getAcctBal(self, acct): # returns an account's balance self.ping() - return float(self.con.getbalance(acct)) + return Decimal(self.con.getbalance(acct)) def canMove(self, fromAcct, toAcct, amount): # true or false if fromAcct can afford to give toAcct an amount of coins diff --git a/run-tests.sh b/run-tests.sh index 6a304de..a2f0da7 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -3,4 +3,4 @@ export PYTHONUNBUFFERED=1 export PYTHONPATH=. -py.test --fulltrace --cov=pyircbot --cov-report html -n 4 tests/ $@ +py.test --cov=pyircbot --cov-report html -n 4 tests/ $@ diff --git a/tests/lib.py b/tests/lib.py index 726f969..a14561b 100644 --- a/tests/lib.py +++ b/tests/lib.py @@ -168,3 +168,7 @@ def livebot(ircserver, tmpdir): yield port, server, bot, bot_t, channel, nick bot.kill(message="bye", forever=True) + + +def pm(bot, line, nick="chatter"): + bot.feed_line(line, args=['bot'], sender=(nick, "root", "cia.gov")) diff --git a/tests/modules/test_cryptowallet.py b/tests/modules/test_cryptowallet.py new file mode 100644 index 0000000..59c8373 --- /dev/null +++ b/tests/modules/test_cryptowallet.py @@ -0,0 +1,204 @@ +import pytest +from tests.lib import * # NOQA - fixtures +from unittest.mock import MagicMock, call +from tests.modules.test_nickuser import nickbot # NOQA - fixture +from decimal import Decimal +from tests.lib import pm +import re +from pyircbot.modules.CryptoWalletRPC import BitcoinRPC + + +class ReallyFakeBitcoinRPC(BitcoinRPC): + """ + Fake BitcoinRPC instance we mock into the test fakebot instances. We add the `balance` attribute which is used for + keeping track of the fake account's balance + """ + def __init__(self): + super().__init__(logger=MagicMock(), + name="fake", + fullname="Fakecoin", + host="127.0.0.1", + port=12345, + username="foo", + password="bar", + precision=4, + reserve=5, + addr_re=re.compile("^FAKE[a-f0-9A-F]{12}$")) + self.balance = Decimal("666.0067") + + def getAcctAddr(self, acct): + return "FOOADDRESS" + + def getAcctBal(self, acct): + return self.balance + + def send(self, fromAcct, toAddr, amount): + return "txidFOOBAR" + + def move(self, fromAcct, toAcct, amount): + return True + + +@pytest.fixture +def cryptobot(nickbot): + """ + Provide a bot loaded with the CryptoWallet modules +. """ + nickbot.botconfig["module_configs"]["CryptoWalletRPC"] = \ + {"types": { + "FAKE": { + "name": "Fakecoin", + "host": "127.0.0.1", + "username": "", + "password": "", + "port": 1234, + "precision": 4, + "reserve": 1.0, + "link": "http://fakecoin.com/", + "addrfmt": "^FAKE[a-f0-9A-F]{12}$"}}} + + nickbot.loadmodule("CryptoWalletRPC") + nickbot.loadmodule("CryptoWallet") + nickbot.moduleInstances['CryptoWalletRPC'].rpcservices['fake'] = ReallyFakeBitcoinRPC() + return nickbot + + +def test_getbal_authed(cryptobot): + cryptobot.feed_line(".getbal fake") + cryptobot.act_PRIVMSG.assert_called_once_with('#test', 'chatter: Please .login to use this command.') + + +def test_setup(cryptobot, mynick="chatter"): + pm(cryptobot, ".setpass foobar", nick=mynick) + cryptobot.act_PRIVMSG.assert_called_once_with(mynick, '.setpass: Your password has been set to "foobar".') + cryptobot.act_PRIVMSG.reset_mock() + pm(cryptobot, ".login foobar", nick=mynick) + cryptobot.act_PRIVMSG.assert_called_once_with(mynick, '.login: You have been logged in from: cia.gov') + cryptobot.act_PRIVMSG.reset_mock() + + +def test_getbal(cryptobot): + test_setup(cryptobot) + cryptobot.feed_line(".getbal fake") + cryptobot.act_PRIVMSG.assert_called_once_with('#test', 'chatter: your balance is: 666.0067 FAKE') + + +def test_setaddr(cryptobot): + # Must login + cryptobot.feed_line(".setaddr fake FAKE123456789012") + cryptobot.act_PRIVMSG.assert_called_once_with('#test', 'chatter: Please .login to use this command.') + cryptobot.act_PRIVMSG.reset_mock() + test_setup(cryptobot) + # Invalid currency + cryptobot.feed_line(".setaddr invalidcoin baz") + cryptobot.act_PRIVMSG.assert_called_once_with( + '#test', + ".setaddr: 'invalidcoin' is not a supported currency. Supported currencies are: fake") + cryptobot.act_PRIVMSG.reset_mock() + # Invalid address + cryptobot.feed_line(".setaddr fake baz") + cryptobot.act_PRIVMSG.assert_called_once_with('#test', ".setaddr: 'baz' appears to be an invalid address.") + cryptobot.act_PRIVMSG.reset_mock() + # OK + cryptobot.feed_line(".setaddr fake FAKE123456789012") + cryptobot.act_PRIVMSG.assert_called_once_with( + '#test', + '.setaddr: Your address has been saved as: FAKE123456789012. Please verify that this is correct or your coins ' + 'could be lost.') + cryptobot.act_PRIVMSG.reset_mock() + + +def test_withdraw(cryptobot): + test_setup(cryptobot) + # Must set withdraw addr + cryptobot.feed_line(".withdraw FAKE 400") + cryptobot.act_PRIVMSG.assert_called_once_with( + '#test', + '.withdraw: You need to set a withdraw address before withdrawing. Try .setaddr') + cryptobot.act_PRIVMSG.reset_mock() + # Set withdraw addr + cryptobot.feed_line(".setaddr FAKE FAKE123456789012") + cryptobot.act_PRIVMSG.assert_called_once_with( + '#test', + '.setaddr: Your address has been saved as: FAKE123456789012. Please verify that this is correct or ' + 'your coins could be lost.') + cryptobot.act_PRIVMSG.reset_mock() + # Withdraw with wrong decimal precision + cryptobot.feed_line(".withdraw FAKE 400.00001") + cryptobot.act_PRIVMSG.assert_called_once_with('#test', ".withdraw: FAKE has maximum 4 decimal places") + cryptobot.act_PRIVMSG.reset_mock() + # Withdraw too much + cryptobot.feed_line(".withdraw FAKE 800") + cryptobot.act_PRIVMSG.assert_called_once_with('#test', ".withdraw: You don't have enough FAKE to withdraw 800") + cryptobot.act_PRIVMSG.reset_mock() + # Withdraw below reserve + cryptobot.feed_line(".withdraw FAKE 666") + cryptobot.act_PRIVMSG.assert_has_calls( + [call('#test', '.withdraw: Withdrawing that much would put you below the reserve (5 FAKE).'), + call('#test', '.withdraw: The reserve is to cover network transaction fees. To recover it you must close your ' + 'account. (Talk to my owner)')]) + cryptobot.act_PRIVMSG.reset_mock() + # Withdraw + cryptobot.feed_line(".withdraw FAKE 400") + cryptobot.act_PRIVMSG.assert_has_calls( + [call('#test', 'chatter: .withdraw: 400 FAKE sent to FAKE123456789012.'), + call('chatter', 'Withdrawal: (You)->FAKE123456789012: Transaction ID: txidFOOBAR')]) + cryptobot.act_PRIVMSG.reset_mock() + + +def test_send(cryptobot): + test_setup(cryptobot) + # Send too much + cryptobot.feed_line(".send FAKE 800 FAKE123456789012") + cryptobot.act_PRIVMSG.assert_called_once_with('#test', "chatter: .send: You don't have enough FAKE to send 800") + cryptobot.act_PRIVMSG.reset_mock() + # Send below reserve + cryptobot.feed_line(".send FAKE 666 FAKE123456789012") + cryptobot.act_PRIVMSG.assert_has_calls( + [call('#test', '.send: Sending that much would put you below the reserve (5 FAKE).'), + call('#test', '.send: The reserve is to cover network transaction fees. To recover it you must close your ' + 'account. (Talk to my owner)')]) + cryptobot.act_PRIVMSG.reset_mock() + # Send with wrong decimal precision + cryptobot.feed_line(".send FAKE 400.00001 FAKE123456789012") + cryptobot.act_PRIVMSG.assert_called_once_with('#test', ".send: FAKE has maximum 4 decimal places") + cryptobot.act_PRIVMSG.reset_mock() + # Send + cryptobot.feed_line(".send FAKE 400 FAKE123456789012") + cryptobot.act_PRIVMSG.assert_has_calls( + [call('#test', 'chatter: .send: 400 FAKE sent to FAKE123456789012.'), + call('chatter', 'Send: (You)->FAKE123456789012: Transaction ID: txidFOOBAR')]) + cryptobot.act_PRIVMSG.reset_mock() + + +def test_getaddr(cryptobot): + test_setup(cryptobot) + cryptobot.feed_line(".getaddr FAKE") + cryptobot.act_PRIVMSG.assert_called_once_with('#test', 'chatter: your FAKE deposit address is: FOOADDRESS') + + +def test_send_local(cryptobot): + """ + Similar to test_send but we send to a mocked local account + """ + test_setup(cryptobot) + # Fails if chatter2 has password yet + cryptobot.feed_line(".send FAKE 400 chatter2") + cryptobot.act_PRIVMSG.assert_called_once_with('#test', "chatter: .send: chatter2 doesn't have a password set.") + cryptobot.act_PRIVMSG.reset_mock() + + test_setup(cryptobot, mynick="chatter2") + + cryptobot.feed_line(".send FAKE 400 chatter2") + cryptobot.act_PRIVMSG.assert_called_once_with('#test', 'chatter: .send: 400 FAKE sent to chatter2.') + cryptobot.act_PRIVMSG.reset_mock() + + +def test_curinfo(cryptobot): + cryptobot.feed_line(".curinfo") + cryptobot.act_PRIVMSG.assert_called_once_with('#test', ".curinfo: supported currencies: FAKE. Use " + "'.curinfo BTC' to see details.") + cryptobot.act_PRIVMSG.reset_mock() + cryptobot.feed_line(".curinfo fake") + cryptobot.act_PRIVMSG.assert_called_once_with('#test', + ".curinfo: fake - Fakecoin. More info: http://fakecoin.com/") diff --git a/tests/modules/test_nickuser.py b/tests/modules/test_nickuser.py index 833cbc8..9a54546 100644 --- a/tests/modules/test_nickuser.py +++ b/tests/modules/test_nickuser.py @@ -1,5 +1,6 @@ import pytest from contextlib import closing +from tests.lib import pm from tests.lib import * # NOQA - fixtures # TODO: @@ -26,10 +27,6 @@ def nickbot(fakebot): return fakebot -def pm(nickbot, line): - nickbot.feed_line(line, args=['bot']) - - def test_blind_login(nickbot): pm(nickbot, ".login foobar") nickbot.act_PRIVMSG.assert_called_once_with('chatter', '.login: You must first set a password with .setpass')