Repository: bitcoin-core/HWI
Pull request #116 (2019-02-01 01:16:56)

Handle all exceptions so that none are shown to the user
This PR adds `except` statements around the main command function to catch any and all exceptions and output the correct error code and message. Additional exceptions and error codes are added for new types of errors. Any exception thrown by a library will be caught and converted into the correct type of exception so that the correct message and code can be returned. This change makes it so that users using the command line tool should never see a backtrace.

This removes from the main `commands.py` file handling of exceptions. Instead those functions (which are intended for use as a library in other software) will throw the exceptions and the caller can handle them.


Patch:
diff --git a/hwilib/cli.py b/hwilib/cli.py
index bca802d..d01e1b6 100644
--- a/hwilib/cli.py
+++ b/hwilib/cli.py
@@ -2,8 +2,15 @@
 
 from .commands import backup_device, displayaddress, enumerate, find_device, \
     get_client, getmasterxpub, getxpub, getkeypool, prompt_pin, restore_device, send_pin, setup_device, \
-    signmessage, signtx, wipe_device, NO_DEVICE_PATH, DEVICE_CONN_ERROR, NO_PASSWORD, \
-    UNKNWON_DEVICE_TYPE
+    signmessage, signtx, wipe_device
+from .errors import (
+    HWWError,
+    NO_DEVICE_PATH,
+    DEVICE_CONN_ERROR,
+    NO_PASSWORD,
+    UNKNWON_DEVICE_TYPE,
+    UNKNOWN_ERROR
+)
 
 import argparse
 import getpass
@@ -151,10 +158,8 @@ def process_commands(args):
     elif args.device_type and args.device_path:
         try:
             client = get_client(device_type, device_path, password)
-        except NoPasswordError as e:
-            return {'error':str(e),'code':NO_PASSWORD}
-        except UnknownDeviceError as e:
-            return {'error':str(e),'code':UNKNWON_DEVICE_TYPE}
+        except HWWError as e:
+            return {'error': e.get_msg(), 'code': e.get_code()}
         except Exception as e:
             return {'error':str(e),'code':DEVICE_CONN_ERROR}
     else:
@@ -163,10 +168,26 @@ def process_commands(args):
     client.is_testnet = args.testnet
 
     # Do the commands
-    result = args.func(args, client)
+    try:
+        result = args.func(args, client)
+    except HWWError as e:
+        result = {'error': e.get_msg(), 'code': e.get_code()}
+    except Exception as e:
+        if args.debug:
+            import traceback
+            traceback.print_exc()
+        result = {'error': str(e), 'code': UNKNOWN_ERROR}
 
     # Close the device
-    client.close()
+    try:
+        client.close()
+    except HWWError as e:
+        result = {'error': e.get_msg(), 'code': e.get_code()}
+    except Exception as e:
+        if args.debug:
+            import traceback
+            traceback.print_exc()
+        result = {'error': str(e), 'code': UNKNOWN_ERROR}
 
     return result
 
diff --git a/hwilib/commands.py b/hwilib/commands.py
index 53ce649..f6d4792 100644
--- a/hwilib/commands.py
+++ b/hwilib/commands.py
@@ -8,24 +8,7 @@ import importlib
 from .serializations import PSBT, Base64ToHex, HexToBase64, hash160
 from .base58 import xpub_to_address, xpub_to_pub_hex, get_xpub_fingerprint_as_id, get_xpub_fingerprint_hex
 from os.path import dirname, basename, isfile
-from .hwwclient import NoPasswordError, UnavailableActionError, DeviceAlreadyInitError, DeviceAlreadyUnlockedError
-
-# Error codes
-NO_DEVICE_PATH = -1
-NO_DEVICE_TYPE = -2
-DEVICE_CONN_ERROR = -3
-UNKNWON_DEVICE_TYPE = -4
-INVALID_TX = -5
-NO_PASSWORD = -6
-BAD_ARGUMENT = -7
-NOT_IMPLEMENTED = -8
-UNAVAILABLE_ACTION = -9
-DEVICE_ALREADY_INIT = -10
-DEVICE_ALREADY_UNLOCKED = -11
-
-class UnknownDeviceError(Exception):
-    def __init__(self,*args,**kwargs):
-        Exception.__init__(self,*args,**kwargs)
+from .errors import NoPasswordError, UnavailableActionError, DeviceAlreadyInitError, DeviceAlreadyUnlockedError, UnknownDeviceError, BAD_ARGUMENT, NOT_IMPLEMENTED
 
 # Get the client for the device
 def get_client(device_type, device_path, password=''):
@@ -83,37 +66,19 @@ def find_device(device_path, password='', device_type=None, fingerprint=None):
     return None
 
 def getmasterxpub(client):
-    try:
-        return client.get_master_xpub()
-    except NotImplementedError as e:
-        return {'error': str(e), 'code': NOT_IMPLEMENTED}
+    return client.get_master_xpub()
 
 def signtx(client, psbt):
     # Deserialize the transaction
-    try:
-        tx = PSBT()
-        tx.deserialize(psbt)
-        return client.sign_tx(tx)
-    except NotImplementedError as e:
-        return {'error': str(e), 'code': NOT_IMPLEMENTED}
-    except IOError as e:
-        return {'error':'You must provide a PSBT','code':INVALID_TX}
-    except Exception as e:
-        return {'error': str(e), 'code': BAD_ARGUMENT}
+    tx = PSBT()
+    tx.deserialize(psbt)
+    return client.sign_tx(tx)
 
 def getxpub(client, path):
-    try:
-        return client.get_pubkey_at_path(path)
-    except NotImplementedError as e:
-        return {'error': str(e), 'code': NOT_IMPLEMENTED}
+    return client.get_pubkey_at_path(path)
 
 def signmessage(client, message, path):
-    try:
-        return client.sign_message(message, path)
-    except NotImplementedError as e:
-        return {'error': str(e), 'code': NOT_IMPLEMENTED}
-    except ValueError as e:
-        return {'error': str(e), 'code': BAD_ARGUMENT}
+    return client.sign_message(message, path)
 
 def getkeypool(client, path, start, end, internal=False, keypool=False, account=0, sh_wpkh=False, wpkh=True):
     if sh_wpkh == True and wpkh == True:
@@ -195,49 +160,19 @@ def displayaddress(client, path, sh_wpkh=False, wpkh=False):
     return client.display_address(path, sh_wpkh, wpkh)
 
 def setup_device(client, label='', backup_passphrase=''):
-    try:
-        return client.setup_device(label, backup_passphrase)
-    except UnavailableActionError as e:
-        return {'error': str(e), 'code': UNAVAILABLE_ACTION}
-    except DeviceAlreadyInitError as e:
-        return {'error': str(e), 'code': DEVICE_ALREADY_INIT}
-    except ValueError as e:
-        return {'error': str(e), 'code': BAD_ARGUMENT}
+    return client.setup_device(label, backup_passphrase)
 
 def wipe_device(client):
-    try:
-        return client.wipe_device()
-    except UnavailableActionError as e:
-        return {'error': str(e), 'code': UNAVAILABLE_ACTION}
+    return client.wipe_device()
 
 def restore_device(client, label):
-    try:
-        return client.restore_device(label)
-    except UnavailableActionError as e:
-        return {'error': str(e), 'code': UNAVAILABLE_ACTION}
-    except DeviceAlreadyInitError as e:
-        return {'error': str(e), 'code': DEVICE_ALREADY_INIT}
-    except ValueError as e:
-        return {'error': str(e), 'code': BAD_ARGUMENT}
+    return client.restore_device(label)
 
 def backup_device(client, label='', backup_passphrase=''):
-    try:
-        return client.backup_device(label, backup_passphrase)
-    except UnavailableActionError as e:
-        return {'error': str(e), 'code': UNAVAILABLE_ACTION}
-    except ValueError as e:
-        return {'error': str(e), 'code': BAD_ARGUMENT}
+    return client.backup_device(label, backup_passphrase)
 
 def prompt_pin(client):
-    try:
-        return client.prompt_pin()
-    except DeviceAlreadyUnlockedError as e:
-        return {'error': str(e), 'code': DEVICE_ALREADY_UNLOCKED}
+    return client.prompt_pin()
 
 def send_pin(client, pin):
-    try:
-        return client.send_pin(pin)
-    except DeviceAlreadyUnlockedError as e:
-        return {'error': str(e), 'code': DEVICE_ALREADY_UNLOCKED}
-    except ValueError as e:
-        return {'error': str(e), 'code': BAD_ARGUMENT}
+    return client.send_pin(pin)
diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py
index 8a7f772..26aa4f9 100644
--- a/hwilib/devices/coldcard.py
+++ b/hwilib/devices/coldcard.py
@@ -1,8 +1,9 @@
 # Coldcard interaction script
 
-from ..hwwclient import HardwareWalletClient, UnavailableActionError
+from ..hwwclient import HardwareWalletClient
+from ..errors import ActionCanceledError, BadArgumentError, DeviceBusyError, UnavailableActionError, DeviceFailureError
 from ckcc.client import ColdcardDevice, COINKITE_VID, CKCC_PID
-from ckcc.protocol import CCProtocolPacker, CCProtoError
+from ckcc.protocol import CCProtocolPacker, CCBusyError, CCProtoError, CCUserRefused
 from ckcc.constants import MAX_BLK_LEN, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH
 from ..base58 import xpub_main_2_test, get_xpub_fingerprint_hex
 from hashlib import sha256
@@ -17,6 +18,18 @@ import time
 CC_SIMULATOR_SOCK = '/tmp/ckcc-simulator.sock'
 # Using the simulator: https://github.com/Coldcard/firmware/blob/master/unix/README.md
 
+def coldcard_exception(f):
+    def func(*args, **kwargs):
+        try:
+            return f(*args, **kwargs)
+        except CCProtoError as e:
+            raise BadArgumentError(str(e))
+        except CCUserRefused as e:
+            raise ActionCanceledError('{} canceled'.format(f.__name__))
+        except CCBusyError as e:
+            raise DeviceBusyError(str(e))
+    return func
+
 # This class extends the HardwareWalletClient for ColdCard specific things
 class ColdcardClient(HardwareWalletClient):
 
@@ -32,6 +45,7 @@ class ColdcardClient(HardwareWalletClient):
 
     # Must return a dict with the xpub
     # Retrieves the public key at the specified BIP 32 derivation path
+    @coldcard_exception
     def get_pubkey_at_path(self, path):
         self.device.check_mitm()
         path = path.replace('h', '\'')
@@ -44,6 +58,7 @@ class ColdcardClient(HardwareWalletClient):
 
     # Must return a hex string with the signed transaction
     # The tx must be in the combined unsigned transaction format
+    @coldcard_exception
     def sign_tx(self, tx):
         self.device.check_mitm()
 
@@ -70,7 +85,7 @@ class ColdcardClient(HardwareWalletClient):
         result = self.device.send_recv(CCProtocolPacker.sha256())
         assert len(result) == 32
         if result != expect:
-            raise ValueError("Wrong checksum:\nexpect: %s\n   got: %s" % (b2a_hex(expect).decode('ascii'), b2a_hex(result).decode('ascii')))
+            raise DeviceFailureError("Wrong checksum:\nexpect: %s\n   got: %s" % (b2a_hex(expect).decode('ascii'), b2a_hex(result).decode('ascii')))
 
         # start the signing process
         ok = self.device.send_recv(CCProtocolPacker.sign_transaction(sz, expect), timeout=None)
@@ -88,7 +103,7 @@ class ColdcardClient(HardwareWalletClient):
             break
 
         if len(done) != 2:
-            raise ValueError('Failed: %r' % done)
+            raise DeviceFailureError('Failed: %r' % done)
 
         result_len, result_sha = done
 
@@ -97,6 +112,7 @@ class ColdcardClient(HardwareWalletClient):
 
     # Must return a base64 encoded string with the signed message
     # The message can be any string. keypath is the bip 32 derivation path for the key to sign with
+    @coldcard_exception
     def sign_message(self, message, keypath):
         self.device.check_mitm()
         keypath = keypath.replace('h', '\'')
@@ -108,7 +124,7 @@ class ColdcardClient(HardwareWalletClient):
             if self.device.is_simulator:
                 self.device.send_recv(CCProtocolPacker.sim_keypress(b'y'))
         except CCProtoError as e:
-            raise ValueError(str(e))
+            raise DeviceFailureError(str(e))
 
         while 1:
             time.sleep(0.250)
@@ -119,7 +135,7 @@ class ColdcardClient(HardwareWalletClient):
             break
 
         if len(done) != 2:
-            raise ValueError('Failed: %r' % done)
+            raise DeviceFailureError('Failed: %r' % done)
 
         addr, raw = done
 
@@ -127,6 +143,7 @@ class ColdcardClient(HardwareWalletClient):
         return {"signature": sig}
 
     # Display address of specified type on the device. Only supports single-key based addresses.
+    @coldcard_exception
     def display_address(self, keypath, p2sh_p2wpkh, bech32):
         self.device.check_mitm()
         keypath = keypath.replace('h', '\'')
@@ -156,6 +173,7 @@ class ColdcardClient(HardwareWalletClient):
         raise UnavailableActionError('The Coldcard does not support restoring via software')
 
     # Begin backup process
+    @coldcard_exception
     def backup_device(self, label='', passphrase=''):
         self.device.check_mitm()
 
@@ -170,7 +188,7 @@ class ColdcardClient(HardwareWalletClient):
             break
 
         if len(done) != 2:
-            raise ValueError('Failed: %r' % done)
+            raise DeviceFailureError('Failed: %r' % done)
 
         result_len, result_sha = done
 
diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py
index 7a8ab6f..8a86646 100644
--- a/hwilib/devices/digitalbitbox.py
+++ b/hwilib/devices/digitalbitbox.py
@@ -14,7 +14,8 @@ import socket
 import sys
 import time
 
-from ..hwwclient import HardwareWalletClient, NoPasswordError, UnavailableActionError
+from ..hwwclient import HardwareWalletClient
+from ..errors import ActionCanceledError, BadArgumentError, DeviceFailureError, DeviceNotReadyError, NoPasswordError, UnavailableActionError, DeviceFailureError
 from ..serializations import CTransaction, PSBT, hash256, hash160, ser_sig_der, ser_sig_compact, ser_compact_size
 from ..base58 import get_xpub_fingerprint, decode, to_address, xpub_main_2_test, get_xpub_fingerprint_hex
 
@@ -30,6 +31,90 @@ HWW_CMD = 0x80 + 0x40 + 0x01
 DBB_VENDOR_ID = 0x03eb
 DBB_DEVICE_ID = 0x2402
 
+# Errors codes from the device
+bad_args = [
+    102, # The password length must be at least " STRINGIFY(PASSWORD_LEN_MIN) " characters.
+    103, # No input received.
+    104, # Invalid command.
+    105, # Only one command allowed at a time.
+    109, # JSON parse error.
+    204, # Invalid seed.
+    253, # Incorrect serialized pubkey length. A 33-byte hexadecimal value (66 characters) is expected.
+    254, # Incorrect serialized pubkey hash length. A 32-byte hexadecimal value (64 characters) is expected.
+    256, # Failed to pair with second factor, because the previously received hash of the public key does not match the computed hash of the public key.
+    300, # Incorrect pubkey length. A 33-byte hexadecimal value (66 characters) is expected.
+    301, # Incorrect hash length. A 32-byte hexadecimal value (64 characters) is expected.
+    304, # Incorrect TFA pin.
+    411, # Filenames limited to alphanumeric values, hyphens, and underscores.
+    412, # Please provide an encryption key.
+    112, # Device password matches reset password. Disabling reset password.
+]
+
+device_failures = [
+    101, # Please set a password.
+    107, # Output buffer overflow.
+    200, # Seed creation requires an SD card for automatic encrypted backup of the seed.
+    250, # Master key not present.
+    251, # Could not generate key.
+    252, # Could not generate ECDH secret.
+    303, # Could not sign.
+    400, # Please insert SD card.
+    401, # Could not mount the SD card.
+    402, # Could not open a file to write - it may already exist.
+    403, # Could not open the directory.
+    405, # Could not write the file.
+    407, # Could not read the file.
+    408, # May not have erased all files (or no file present).
+    410, # Backup file does not match wallet.
+    500, # Chip communication error.
+    501, # Could not read flash.
+    502, # Could not encrypt.
+    110, # Too many failed access attempts. Device reset.
+    111, # Device locked. Erase device to access this command.
+    113, # Due to many login attempts, the next login requires holding the touch button for 3 seconds.
+    900, # attempts remain before the device is reset.
+    901, # Ignored for non-embedded testing.
+    902, # Too many backup files to read. The list is truncated.
+    903, # attempts remain before the device is reset. The next login requires holding the touch button.
+]
+
+cancels = [
+    600, # Aborted by user.
+    601, # Touchbutton timed out.
+]
+
+ERR_MEM_SETUP = 503 # Device initialization in progress.
+
+class DBBError(Exception):
+    def __init__(self, error):
+        Exception.__init__(self)
+        self.error = error
+
+    def get_error(self):
+        return self.error['error']['message']
+
+    def get_code(self):
+        return self.error['error']['code']
+
+    def __str__(self):
+        return 'Error: {}, Code: {}'.format(self.error['error']['message'], self.error['error']['code'])
+
+def digitalbitbox_exception(f):
+    def func(*args, **kwargs):
+        try:
+            return f(*args, **kwargs)
+        except DBBError as e:
+            if e.get_code() in bad_args:
+                raise BadArgumentError(e.get_error())
+            elif e.get_code() in device_failures:
+                raise DeviceFailureError(e.get_error())
+            elif e.get_code() in cancels:
+                raise ActionCanceledError(e.get_error())
+            elif e.get_code() == ERR_MEM_SETUP:
+                raise DeviceNotReadyError(e.get_error())
+
+    return func
+
 def aes_encrypt_with_iv(key, iv, data):
     aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv)
     aes = pyaes.Encrypter(aes_cbc)
@@ -75,7 +160,7 @@ def to_string(x, enc):
     if isinstance(x, str):
         return x
     else:
-        raise TypeError("Not a string or bytes like object")
+        raise DeviceFailureError("Not a string or bytes like object")
 
 class BitboxSimulator():
     def __init__(self, ip, port):
@@ -220,12 +305,13 @@ class DigitalbitboxClient(HardwareWalletClient):
 
     # Must return a dict with the xpub
     # Retrieves the public key at the specified BIP 32 derivation path
+    @digitalbitbox_exception
     def get_pubkey_at_path(self, path):
         if '\'' not in path and 'h' not in path and 'H' not in path:
-            raise ValueError('The digital bitbox requires one part of the derivation path to be derived using hardened keys')
+            raise BadArgumentError('The digital bitbox requires one part of the derivation path to be derived using hardened keys')
         reply = send_encrypt('{"xpub":"' + path + '"}', self.password, self.device)
         if 'error' in reply:
-            return reply
+            raise DBBError(reply)
 
         if self.is_testnet:
             return {'xpub':xpub_main_2_test(reply['xpub'])}
@@ -234,6 +320,7 @@ class DigitalbitboxClient(HardwareWalletClient):
 
     # Must return a hex string with the signed transaction
     # The tx must be in the PSBT format
+    @digitalbitbox_exception
     def sign_tx(self, tx):
 
         # Create a transaction with all scriptsigs blanekd out
@@ -358,12 +445,12 @@ class DigitalbitboxClient(HardwareWalletClient):
         reply = send_encrypt(to_send, self.password, self.device)
         logging.debug(reply)
         if 'error' in reply:
-            return reply
+            raise DBBError(reply)
         print("Touch the device for 3 seconds to sign. Touch briefly to cancel", file=sys.stderr)
         reply = send_encrypt(to_send, self.password, self.device)
         logging.debug(reply)
         if 'error' in reply:
-            return reply
+            raise DBBError(reply)
 
         # Extract sigs
         sigs = []
@@ -383,6 +470,7 @@ class DigitalbitboxClient(HardwareWalletClient):
 
     # Must return a base64 encoded string with the signed message
     # The message can be any string
+    @digitalbitbox_exception
     def sign_message(self, message, keypath):
         to_hash = b""
         to_hash += self.message_magic
@@ -400,12 +488,12 @@ class DigitalbitboxClient(HardwareWalletClient):
         reply = send_encrypt(to_send, self.password, self.device)
         logging.debug(reply)
         if 'error' in reply:
-            return reply
+            raise DBBError(reply)
         print("Touch the device for 3 seconds to sign. Touch briefly to cancel", file=sys.stderr)
         reply = send_encrypt(to_send, self.password, self.device)
         logging.debug(reply)
         if 'error' in reply:
-            return reply
+            raise DBBError(reply)
 
         sig = binascii.unhexlify(reply['sign'][0]['sig'])
         r = sig[0:32]
@@ -421,6 +509,7 @@ class DigitalbitboxClient(HardwareWalletClient):
         raise UnavailableActionError('The Digital Bitbox does not have a screen to display addresses on')
 
     # Setup a new device
+    @digitalbitbox_exception
     def setup_device(self, label='', passphrase=''):
         # Make sure this is not initialized
         reply = send_encrypt('{"device" : "info"}', self.password, self.device)
@@ -429,7 +518,7 @@ class DigitalbitboxClient(HardwareWalletClient):
 
         # Need a wallet name and backup passphrase
         if not label or not passphrase:
-            raise ValueError('THe label and backup passphrase for a new Digital Bitbox wallet must be specified and cannot be empty')
+            raise BadArgumentError('THe label and backup passphrase for a new Digital Bitbox wallet must be specified and cannot be empty')
 
         # Set password
         to_send = {'password': self.password}
@@ -445,6 +534,7 @@ class DigitalbitboxClient(HardwareWalletClient):
         return {'success': True}
 
     # Wipe this device
+    @digitalbitbox_exception
     def wipe_device(self):
         reply = send_encrypt('{"reset" : "__ERASE__"}', self.password, self.device)
         if 'error' in reply:
@@ -456,6 +546,7 @@ class DigitalbitboxClient(HardwareWalletClient):
         raise UnavailableActionError('The Digital Bitbox does not support restoring via software')
 
     # Begin backup process
+    @digitalbitbox_exception
     def backup_device(self, label='', passphrase=''):
         key = stretch_backup_key(passphrase)
         backup_filename = format_backup_filename(label)
diff --git a/hwilib/devices/keepkey.py b/hwilib/devices/keepkey.py
index 3ff53f1..4a6e979 100644
--- a/hwilib/devices/keepkey.py
+++ b/hwilib/devices/keepkey.py
@@ -1,6 +1,7 @@
 # KeepKey interaction script
 
-from ..hwwclient import DeviceAlreadyUnlockedError, HardwareWalletClient, UnavailableActionError, DeviceNotReadyError
+from ..hwwclient import HardwareWalletClient
+from ..errors import BadArgumentError, DeviceAlreadyUnlockedError, DeviceConnectionError, UnavailableActionError, DeviceNotReadyError
 from keepkeylib.transport_hid import HidTransport
 from keepkeylib.transport_udp import UDPTransport
 from keepkeylib.client import BaseClient, DebugWireMixin, DebugLinkMixin, ProtocolMixin, TextUIMixin
@@ -69,7 +70,7 @@ class TxAPIPSBT(TxApi):
             o.amount = psbt_in.witness_utxo.nValue
             o.script_pubkey = psbt_in.witness_utxo.scriptPubKey
         else:
-            raise ValueError('{} is not an input in this transaction'.format(txhash))
+            raise BadArgumentError('{} is not an input in this transaction'.format(txhash))
 
         return t
 
@@ -119,6 +120,16 @@ class KeepKey(NoInitMixin, TextUIMixin, BaseClient):
 class KeepKeyDebug(NoInitMixin, DebugLinkMixin, DebugWireMixin, BaseClient):
     pass
 
+def keepkey_exception(f):
+    def func(*args, **kwargs):
+        try:
+            return f(*args, **kwargs)
+        except ValueError as e:
+            raise BadArgumentError(str(e))
+        except OSError as e:
+            raise DeviceConnectionError(str(e))
+    return func
+
 # This class extends the HardwareWalletClient for Digital Bitbox specific things
 class KeepkeyClient(HardwareWalletClient):
 
@@ -155,6 +166,7 @@ class KeepkeyClient(HardwareWalletClient):
 
     # Must return a dict with the xpub
     # Retrieves the public key at the specified BIP 32 derivation path
+    @keepkey_exception
     def get_pubkey_at_path(self, path):
         self._check_unlocked()
         path = path.replace('h', '\'')
@@ -168,6 +180,7 @@ class KeepkeyClient(HardwareWalletClient):
 
     # Must return a hex string with the signed transaction
     # The tx must be in the combined unsigned transaction format
+    @keepkey_exception
     def sign_tx(self, tx):
         self._check_unlocked()
 
@@ -292,7 +305,7 @@ class KeepkeyClient(HardwareWalletClient):
                     if wit:
                         txoutput.address = bech32.encode(bech32_hrp, ver, prog)
                     else:
-                        raise TypeError("Output is not an address")
+                        raise BadArgumentError("Output is not an address")
 
                 # append to outputs
                 outputs.append(txoutput)
@@ -320,6 +333,7 @@ class KeepkeyClient(HardwareWalletClient):
 
     # Must return a base64 encoded string with the signed message
     # The message can be any string
+    @keepkey_exception
     def sign_message(self, message, keypath):
         self._check_unlocked()
         keypath = keypath.replace('h', '\'')
@@ -329,6 +343,7 @@ class KeepkeyClient(HardwareWalletClient):
         return {'signature': base64.b64encode(result.signature).decode('utf-8')}
 
     # Display address of specified type on the device. Only supports single-key based addresses.
+    @keepkey_exception
     def display_address(self, keypath, p2sh_p2wpkh, bech32):
         self._check_unlocked()
         keypath = keypath.replace('h', '\'')
@@ -343,6 +358,7 @@ class KeepkeyClient(HardwareWalletClient):
         return {'address': address}
 
     # Setup a new device
+    @keepkey_exception
     def setup_device(self, label='', passphrase=''):
         if self.client.features.initialized:
             raise DeviceAlreadyInitError('Device is already initialized. Use wipe first and try again')
@@ -350,17 +366,20 @@ class KeepkeyClient(HardwareWalletClient):
         return {'success': True}
 
     # Wipe this device
+    @keepkey_exception
     def wipe_device(self):
         self._check_unlocked()
         self.client.wipe_device()
         return {'success': True}
 
     # Restore device from mnemonic or xprv
+    @keepkey_exception
     def restore_device(self, label=''):
         self.client.recovery_device(False, 24, bool(self.password), True, label, 'english')
         return {'success': True}
 
     # Begin backup process
+    @keepkey_exception
     def backup_device(self, label='', passphrase=''):
         raise UnavailableActionError('The Keepkey does not support creating a backup via software')
 
@@ -369,6 +388,7 @@ class KeepkeyClient(HardwareWalletClient):
         self.client.close()
 
     # Prompt for a pin on device
+    @keepkey_exception
     def prompt_pin(self):
         self.client.init_device()
         if not self.client.features.pin_protection:
@@ -381,9 +401,10 @@ class KeepkeyClient(HardwareWalletClient):
         return {'success': True}
 
     # Send the pin
+    @keepkey_exception
     def send_pin(self, pin):
         if not pin.isdigit():
-            raise ValueError("Non-numeric PIN provided")
+            raise BadArgumentError("Non-numeric PIN provided")
         resp = self.client.call_raw(messages.PinMatrixAck(pin=pin))
         if isinstance(resp, messages.Failure):
             self.client.features = self.client.call_raw(messages.GetFeatures())
diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py
index 1f8b56f..7790b18 100644
--- a/hwilib/devices/ledger.py
+++ b/hwilib/devices/ledger.py
@@ -1,6 +1,7 @@
 # Ledger interaction script
 
-from ..hwwclient import HardwareWalletClient, UnavailableActionError
+from ..hwwclient import HardwareWalletClient
+from ..errors import ActionCanceledError, BadArgumentError, DeviceConnectionError, DeviceFailureError, UnavailableActionError
 from btchip.btchip import *
 from btchip.btchipUtils import *
 import base64
@@ -15,6 +16,37 @@ import logging
 LEDGER_VENDOR_ID = 0x2c97
 LEDGER_DEVICE_ID = 0x0001
 
+bad_args = [
+    0x6700, # BTCHIP_SW_INCORRECT_LENGTH
+    0x6A80, # BTCHIP_SW_INCORRECT_DATA
+    0x6B00, # BTCHIP_SW_INCORRECT_P1_P2
+    0x6D00, # BTCHIP_SW_INS_NOT_SUPPORTED
+]
+
+cancels = [
+    0x6982, # BTCHIP_SW_SECURITY_STATUS_NOT_SATISFIED
+    0x6985, # BTCHIP_SW_CONDITIONS_OF_USE_NOT_SATISFIED
+]
+
+def ledger_exception(f):
+    def func(*args, **kwargs):
+        try:
+            return f(*args, **kwargs)
+        except ValueError as e:
+            raise BadArgumentError(str(e))
+        except BTChipException as e:
+            if e.sw in bad_args:
+                raise BadArgumentError('Bad argument')
+            elif e.sw == 0x6F00: # BTCHIP_SW_TECHNICAL_PROBLEM
+                raise DeviceFailureError(e.message)
+            elif e.sw == 0x6FAA: # BTCHIP_SW_HALTED
+                raise DeviceConnectionError('Device is asleep')
+            elif e.sw in cancels:
+                raise ActionCanceledError('{} canceled'.format(f.__name__))
+            else:
+                raise e
+    return func
+
 # This class extends the HardwareWalletClient for Ledger Nano S specific things
 class LedgerClient(HardwareWalletClient):
 
@@ -29,6 +61,7 @@ class LedgerClient(HardwareWalletClient):
 
     # Must return a dict with the xpub
     # Retrieves the public key at the specified BIP 32 derivation path
+    @ledger_exception
     def get_pubkey_at_path(self, path):
         path = path[2:]
         path = path.replace('h', '\'')
@@ -75,6 +108,7 @@ class LedgerClient(HardwareWalletClient):
     # Must return a hex string with the signed transaction
     # The tx must be in the combined unsigned transaction format
     # Current only supports segwit signing
+    @ledger_exception
     def sign_tx(self, tx):
         c_tx = CTransaction(tx.tx)
         tx_bytes = c_tx.serialize_with_witness()
@@ -222,6 +256,7 @@ class LedgerClient(HardwareWalletClient):
 
     # Must return a base64 encoded string with the signed message
     # The message can be any string
+    @ledger_exception
     def sign_message(self, message, keypath):
         message = bytearray(message, 'utf-8')
         keypath = keypath[2:]
@@ -244,6 +279,7 @@ class LedgerClient(HardwareWalletClient):
 
         return {"signature":base64.b64encode(sig).decode('utf-8')}
 
+    @ledger_exception
     def display_address(self, keypath, p2sh_p2wpkh, bech32):
         output = self.app.getWalletPublicKey(keypath[2:], True, (p2sh_p2wpkh or bech32), bech32)
         return {'address': output['address'][12:-2]} # HACK: A bug in getWalletPublicKey results in the address being returned as the string "bytearray(b'<address>')". This extracts the actual address to work around this.
diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py
index e950264..d6d5de7 100644
--- a/hwilib/devices/trezor.py
+++ b/hwilib/devices/trezor.py
@@ -1,8 +1,10 @@
 # Trezor interaction script
 
-from ..hwwclient import HardwareWalletClient, DeviceAlreadyInitError, DeviceAlreadyUnlockedError, UnavailableActionError, DeviceNotReadyError
+from ..hwwclient import HardwareWalletClient
+from ..errors import ActionCanceledError, BadArgumentError, DeviceAlreadyInitError, DeviceAlreadyUnlockedError, DeviceConnectionError, UnavailableActionError, DeviceNotReadyError
 from trezorlib.client import TrezorClient as Trezor
 from trezorlib.debuglink import TrezorClientDebugLink
+from trezorlib.exceptions import Cancelled
 from trezorlib.transport import enumerate_devices, get_transport
 from trezorlib.ui import ClickUI, mnemonic_words, PIN_MATRIX_DESCRIPTION
 from trezorlib import protobuf, tools, btc, device
@@ -10,6 +12,7 @@ from trezorlib import messages as proto
 from ..base58 import get_xpub_fingerprint, decode, to_address, xpub_main_2_test, get_xpub_fingerprint_hex
 from ..serializations import ser_uint256, uint256_from_str
 from .. import bech32
+from usb1 import USBErrorNoDevice
 
 import base64
 import binascii
@@ -65,6 +68,18 @@ class TrezorNoInit(Trezor):
 
         self.session_counter = 0
 
+def trezor_exception(f):
+    def func(*args, **kwargs):
+        try:
+            return f(*args, **kwargs)
+        except ValueError as e:
+            raise BadArgumentError(str(e))
+        except Cancelled as e:
+            raise ActionCanceledError('{} canceled'.format(f.__name__))
+        except USBErrorNoDevice as e:
+            raise DeviceConnectionError('Device disconnected')
+    return func
+
 # This class extends the HardwareWalletClient for Trezor specific things
 class TrezorClient(HardwareWalletClient):
 
@@ -92,9 +107,13 @@ class TrezorClient(HardwareWalletClient):
 
     # Must return a dict with the xpub
     # Retrieves the public key at the specified BIP 32 derivation path
+    @trezor_exception
     def get_pubkey_at_path(self, path):
         self._check_unlocked()
-        expanded_path = tools.parse_path(path)
+        try:
+            expanded_path = tools.parse_path(path)
+        except ValueError as e:
+            raise BadArgumentError(str(e))
         output = btc.get_public_node(self.client, expanded_path)
         if self.is_testnet:
             return {'xpub':xpub_main_2_test(output.xpub)}
@@ -103,6 +122,7 @@ class TrezorClient(HardwareWalletClient):
 
     # Must return a hex string with the signed transaction
     # The tx must be in the psbt format
+    @trezor_exception
     def sign_tx(self, tx):
         self._check_unlocked()
 
@@ -225,7 +245,7 @@ class TrezorClient(HardwareWalletClient):
                     if wit:
                         txoutput.address = bech32.encode(bech32_hrp, ver, prog)
                     else:
-                        raise TypeError("Output is not an address")
+                        raise BadArgumentError("Output is not an address")
 
                 # append to outputs
                 outputs.append(txoutput)
@@ -281,6 +301,7 @@ class TrezorClient(HardwareWalletClient):
 
     # Must return a base64 encoded string with the signed message
     # The message can be any string
+    @trezor_exception
     def sign_message(self, message, keypath):
         self._check_unlocked()
         path = tools.parse_path(keypath)
@@ -288,6 +309,7 @@ class TrezorClient(HardwareWalletClient):
         return {'signature': base64.b64encode(result.signature).decode('utf-8')}
 
     # Display address of specified type on the device. Only supports single-key based addresses.
+    @trezor_exception
     def display_address(self, keypath, p2sh_p2wpkh, bech32):
         self._check_unlocked()
         expanded_path = tools.parse_path(keypath)
@@ -301,7 +323,9 @@ class TrezorClient(HardwareWalletClient):
         return {'address': address}
 
     # Setup a new device
+    @trezor_exception
     def setup_device(self, label='', passphrase=''):
+        self.client.init_device()
         if self.client.features.initialized:
             raise DeviceAlreadyInitError('Device is already initialized. Use wipe first and try again')
         passphrase_enabled = False
@@ -311,12 +335,14 @@ class TrezorClient(HardwareWalletClient):
         return {'success': True}
 
     # Wipe this device
+    @trezor_exception
     def wipe_device(self):
         self._check_unlocked()
         device.wipe(self.client)
         return {'success': True}
 
     # Restore device from mnemonic or xprv
+    @trezor_exception
     def restore_device(self, label=''):
         passphrase_enabled = False
         device.recover(self.client, label=label, input_callback=mnemonic_words, passphrase_protection=bool(self.password))
@@ -327,10 +353,12 @@ class TrezorClient(HardwareWalletClient):
         raise UnavailableActionError('The Trezor does not support creating a backup via software')
 
     # Close the device
+    @trezor_exception
     def close(self):
         self.client.close()
 
     # Prompt for a pin on device
+    @trezor_exception
     def prompt_pin(self):
         self.client.init_device()
         if not self.client.features.pin_protection:
@@ -343,6 +371,7 @@ class TrezorClient(HardwareWalletClient):
         return {'success': True}
 
     # Send the pin
+    @trezor_exception
     def send_pin(self, pin):
         self.client.features = self.client.call_raw(proto.GetFeatures())
         if isinstance(self.client.features, proto.Features):
@@ -351,7 +380,7 @@ class TrezorClient(HardwareWalletClient):
             if self.client.features.pin_cached:
                 raise DeviceAlreadyUnlockedError('The PIN has already been sent to this device')
         if not pin.isdigit():
-            raise ValueError("Non-numeric PIN provided")
+            raise BadArgumentError("Non-numeric PIN provided")
         resp = self.client.call_raw(proto.PinMatrixAck(pin=pin))
         if isinstance(resp, proto.Failure):
             return {'success': False}
diff --git a/hwilib/errors.py b/hwilib/errors.py
new file mode 100644
index 0000000..899cc85
--- /dev/null
+++ b/hwilib/errors.py
@@ -0,0 +1,86 @@
+# Defines errors and error codes
+
+# Error codes
+NO_DEVICE_PATH = -1
+NO_DEVICE_TYPE = -2
+DEVICE_CONN_ERROR = -3
+UNKNWON_DEVICE_TYPE = -4
+INVALID_TX = -5
+NO_PASSWORD = -6
+BAD_ARGUMENT = -7
+NOT_IMPLEMENTED = -8
+UNAVAILABLE_ACTION = -9
+DEVICE_ALREADY_INIT = -10
+DEVICE_ALREADY_UNLOCKED = -11
+DEVICE_NOT_READY = -12
+UNKNOWN_ERROR = -13
+ACTION_CANCELED = -14
+DEVICE_BUSY = -15
+
+# Exceptions
+class HWWError(Exception):
+    def __init__(self, msg, code):
+        Exception.__init__(self)
+        self.code = code
+        self.msg = msg
+
+    def get_code(self):
+        return self.code
+
+    def get_msg(self):
+        return self.msg
+
+    def __str__(self):
+        return self.msg
+
+class NoPasswordError(HWWError):
+    def __init__(self, msg):
+        HWWError.__init__(self, msg, NO_PASSWORD)
+
+class UnavailableActionError(HWWError):
+    def __init__(self, msg):
+        HWWError.__init__(self, msg, UNAVAILABLE_ACTION)
+
+class DeviceAlreadyInitError(HWWError):
+    def __init__(self, msg):
+        HWWError.__init__(self, msg, DEVICE_ALREADY_INIT)
+
+class DeviceNotReadyError(HWWError):
+    def __init__(self, msg):
+        HWWError.__init__(self, msg, DEVICE_NOT_READY)
+
+class DeviceAlreadyUnlockedError(HWWError):
+    def __init__(self, msg):
+        HWWError.__init__(self, msg, DEVICE_ALREADY_UNLOCKED)
+
+class UnknownDeviceError(HWWError):
+    def __init__(self, msg):
+        HWWError.__init__(self, msg, UNKNWON_DEVICE_TYPE)
+
+class NotImplementedError(HWWError):
+    def __init__(self, msg):
+        HWWError.__init__(self, msg, NOT_IMPLEMENTED)
+
+class PSBTSerializationError(HWWError):
+    def __init__(self, msg):
+        HWWError.__init__(self, msg, INVALID_TX)
+
+class BadArgumentError(HWWError):
+    def __init__(self, msg):
+        HWWError.__init__(self, msg, BAD_ARGUMENT)
+
+class DeviceFailureError(HWWError):
+    def __init__(self, msg):
+        HWWError.__init__(self, msg, UNKNOWN_ERROR)
+
+class ActionCanceledError(HWWError):
+    def __init__(self, msg):
+        HWWError.__init__(self, msg, ACTION_CANCELED)
+
+class DeviceConnectionError(HWWError):
+    def __init__(self, msg):
+        HWWError.__init__(self, msg, DEVICE_CONN_ERROR)
+
+class DeviceBusyError(HWWError):
+    def __init__(self, msg):
+        HWWError.__init__(self, msg, DEVICE_BUSY)
diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py
index c7ae2ad..bd2109f 100644
--- a/hwilib/hwwclient.py
+++ b/hwilib/hwwclient.py
@@ -61,23 +61,3 @@ class HardwareWalletClient(object):
     # Send pin
     def send_pin(self):
         raise NotImplementedError('The HardwareWalletClient base class does not implement this method')
-
-class NoPasswordError(Exception):
-    def __init__(self,*args,**kwargs):
-        Exception.__init__(self,*args,**kwargs)
-
-class UnavailableActionError(Exception):
-    def __init__(self,*args,**kwargs):
-        Exception.__init__(self,*args,**kwargs)
-
-class DeviceAlreadyInitError(Exception):
-    def __init__(self,*args,**kwargs):
-        Exception.__init__(self,*args,**kwargs)
-
-class DeviceNotReadyError(Exception):
-    def __init__(self,*args,**kwargs):
-        Exception.__init__(self,*args,**kwargs)
-
-class DeviceAlreadyUnlockedError(Exception):
-    def __init__(self,*args,**kwargs):
-        Exception.__init__(self,*args,**kwargs)
diff --git a/hwilib/serializations.py b/hwilib/serializations.py
index e8eb72c..839ff18 100644
--- a/hwilib/serializations.py
+++ b/hwilib/serializations.py
@@ -17,6 +17,7 @@ ser_*, deser_*: functions that handle serialization/deserialization
 
 from io import BytesIO, BufferedReader
 from codecs import encode
+from .errors import PSBTSerializationError
 import struct
 import binascii
 import hashlib
@@ -493,10 +494,10 @@ class CTransaction(object):
 
 def DeserializeHDKeypath(f, key, hd_keypaths):
     if len(key) != 34 and len(key) != 66:
-        raise IOError("Size of key was not the expected size for the type partial signature pubkey")
+        raise PSBTSerializationError("Size of key was not the expected size for the type partial signature pubkey")
     pubkey = key[1:]
     if pubkey in hd_keypaths:
-        raise IOError("Duplicate key, input partial signature for pubkey already provided")
+        raise PSBTSerializationError("Duplicate key, input partial signature for pubkey already provided")
 
     value = deser_string(f)
     hd_keypaths[pubkey] = struct.unpack("<" + "I" * (len(value) // 4), value)
@@ -551,9 +552,9 @@ class PartiallySignedInput:
 
             if key_type == 0:
                 if self.non_witness_utxo:
-                    raise IOError("Duplicate Key, input non witness utxo already provided")
+                    raise PSBTSerializationError("Duplicate Key, input non witness utxo already provided")
                 elif len(key) != 1:
-                    raise IOError("non witness utxo key is more than one byte type")
+                    raise PSBTSerializationError("non witness utxo key is more than one byte type")
                 self.non_witness_utxo = CTransaction()
                 value = BufferedReader(BytesIO(deser_string(f)))
                 self.non_witness_utxo.deserialize(value)
@@ -561,43 +562,43 @@ class PartiallySignedInput:
 
             elif key_type == 1:
                 if self.witness_utxo:
-                    raise IOError("Duplicate Key, input witness utxo already provided")
+                    raise PSBTSerializationError("Duplicate Key, input witness utxo already provided")
                 elif len(key) != 1:
-                    raise IOError("witness utxo key is more than one byte type")
+                    raise PSBTSerializationError("witness utxo key is more than one byte type")
                 self.witness_utxo = CTxOut()
                 value = BufferedReader(BytesIO(deser_string(f)))
                 self.witness_utxo.deserialize(value)
 
             elif key_type == 2:
                 if len(key) != 34 and len(key) != 66:
-                    raise IOError("Size of key was not the expected size for the type partial signature pubkey")
+                    raise PSBTSerializationError("Size of key was not the expected size for the type partial signature pubkey")
                 pubkey = key[1:]
                 if pubkey in self.partial_sigs:
-                    raise IOError("Duplicate key, input partial signature for pubkey already provided")
+                    raise PSBTSerializationError("Duplicate key, input partial signature for pubkey already provided")
 
                 sig = deser_string(f)
                 self.partial_sigs[pubkey] = sig;
 
             elif key_type == 3:
                 if self.sighash > 0:
-                    raise IOError("Duplicate key, input sighash type already provided")
+                    raise PSBTSerializationError("Duplicate key, input sighash type already provided")
                 elif len(key) != 1:
-                    raise IOError("sighash key is more than one byte type")
+                    raise PSBTSerializationError("sighash key is more than one byte type")
                 value = deser_string(f)
                 self.sighash = struct.unpack("<I", value)[0]
 
             elif key_type == 4:
                 if len(self.redeem_script) != 0:
-                    raise IOError("Duplicate key, input redeemScript already provided")
+                    raise PSBTSerializationError("Duplicate key, input redeemScript already provided")
                 elif len(key) != 1:
-                    raise IOError("redeemScript key is more than one byte type")
+                    raise PSBTSerializationError("redeemScript key is more than one byte type")
                 self.redeem_script = deser_string(f)
 
             elif key_type == 5:
                 if len(self.witness_script) != 0:
-                    raise IOError("Duplicate key, input witnessScript already provided")
+                    raise PSBTSerializationError("Duplicate key, input witnessScript already provided")
                 elif len(key) != 1:
-                    raise IOError("witnessScript key is more than one byte type")
+                    raise PSBTSerializationError("witnessScript key is more than one byte type")
                 self.witness_script = deser_string(f)
 
             elif key_type == 6:
@@ -605,22 +606,22 @@ class PartiallySignedInput:
 
             elif key_type == 7:
                 if len(self.final_script_sig) != 0:
-                    raise IOError("Duplicate key, input final scriptSig already provided")
+                    raise PSBTSerializationError("Duplicate key, input final scriptSig already provided")
                 elif len(key) != 1:
-                    raise IOError("final scriptSig key is more than one byte type")
+                    raise PSBTSerializationError("final scriptSig key is more than one byte type")
                 self.final_script_sig = deser_string(f)
 
             elif key_type == 8:
                 if not self.final_script_witness.is_null():
-                    raise IOError("Duplicate key, input final scriptWitness already provided")
+                    raise PSBTSerializationError("Duplicate key, input final scriptWitness already provided")
                 elif len(key) != 1:
-                    raise IOError("final scriptWitness key is more than one byte type")
+                    raise PSBTSerializationError("final scriptWitness key is more than one byte type")
                 value = BufferedReader(BytesIO(deser_string(f)))
                 self.final_script_witness.deserialize(value)
 
             else:
                 if key in self.unknown:
-                    raise IOError("Duplicate key, key for unknown value already provided")
+                    raise PSBTSerializationError("Duplicate key, key for unknown value already provided")
                 value = deser_string(f)
                 self.unknown[key] = value
 
@@ -712,16 +713,16 @@ class PartiallySignedOutput:
 
             if key_type == 0:
                 if len(self.redeem_script) != 0:
-                    raise IOError("Duplicate key, output redeemScript already provided")
+                    raise PSBTSerializationError("Duplicate key, output redeemScript already provided")
                 elif len(key) != 1:
-                    raise IOError("Output redeemScript key is more than one byte type")
+                    raise PSBTSerializationError("Output redeemScript key is more than one byte type")
                 self.redeem_script = deser_string(f)
 
             elif key_type == 1:
                 if len(self.witness_script) != 0:
-                    raise IOError("Duplicate key, output witnessScript already provided")
+                    raise PSBTSerializationError("Duplicate key, output witnessScript already provided")
                 elif len(key) != 1:
-                    raise IOError("Output witnessScript key is more than one byte type")
+                    raise PSBTSerializationError("Output witnessScript key is more than one byte type")
                 self.witness_script = deser_string(f)
 
             elif key_type == 2:
@@ -729,7 +730,7 @@ class PartiallySignedOutput:
 
             else:
                 if key in self.unknown:
-                    raise IOError("Duplicate key, key for unknown value already provided")
+                    raise PSBTSerializationError("Duplicate key, key for unknown value already provided")
                 value = deser_string(f)
                 self.unknown[key] = value
 
@@ -772,7 +773,7 @@ class PSBT(object):
         # Read the magic bytes
         magic = f.read(5)
         if magic != b"psbt\xff":
-            raise IOError("invalid magic")
+            raise PSBTSerializationError("invalid magic")
 
         # Read loop
         separators = 0
@@ -796,9 +797,9 @@ class PSBT(object):
             if key_type == 0x00:
                 # Checks for correctness
                 if not self.tx.is_null:
-                    raise IOError("Duplicate key, unsigned tx already provided")
+                    raise PSBTSerializationError("Duplicate key, unsigned tx already provided")
                 elif len(key) > 1:
-                    raise IOError("Global unsigned tx key is more than one byte type")
+                    raise PSBTSerializationError("Global unsigned tx key is more than one byte type")
 
                 # read in value
                 value = BufferedReader(BytesIO(deser_string(f)))
@@ -807,17 +808,17 @@ class PSBT(object):
                 # Make sure that all scriptSigs and scriptWitnesses are empty
                 for txin in self.tx.vin:
                     if len(txin.scriptSig) != 0 or not self.tx.wit.is_null():
-                        raise IOError("Unsigned tx does not have empty scriptSigs and scriptWitnesses")
+                        raise PSBTSerializationError("Unsigned tx does not have empty scriptSigs and scriptWitnesses")
 
             else:
                 if key in self.unknown:
-                    raise IOError("Duplicate key, key for unknown value already provided")
+                    raise PSBTSerializationError("Duplicate key, key for unknown value already provided")
                 value = deser_string(f)
                 self.unknown[key] = value
 
         # make sure that we got an unsigned tx
         if self.tx.is_null():
-            raise IOError("No unsigned trasaction was provided")
+            raise PSBTSerializationError("No unsigned trasaction was provided")
 
         # Read input data
         for txin in self.tx.vin:
@@ -828,10 +829,10 @@ class PSBT(object):
             self.inputs.append(input)
 
             if input.non_witness_utxo and input.non_witness_utxo.rehash() and input.non_witness_utxo.sha256 != txin.prevout.sha256:
-                raise IOError("Non-witness UTXO does not match outpoint hash")
+                raise PSBTSerializationError("Non-witness UTXO does not match outpoint hash")
 
         if (len(self.inputs) != len(self.tx.vin)):
-            raise IOError("Inputs provided does not match the number of inputs in transaction")
+            raise PSBTSerializationError("Inputs provided does not match the number of inputs in transaction")
 
         # Read output data
         for txout in self.tx.vout:
@@ -842,10 +843,10 @@ class PSBT(object):
             self.outputs.append(output)
         
         if len(self.outputs) != len(self.tx.vout):
-            raise IOError("Outputs provided does not match the number of outputs in transaction")
+            raise PSBTSerializationError("Outputs provided does not match the number of outputs in transaction")
 
         if not self.is_sane():
-            raise IOError("PSBT is not sane")
+            raise PSBTSerializationError("PSBT is not sane")
 
     def serialize(self):
         r = b""


Test patch:
diff --git a/test/test_psbt.py b/test/test_psbt.py
index d44fbf3..f898d02 100755
--- a/test/test_psbt.py
+++ b/test/test_psbt.py
@@ -1,6 +1,7 @@
 #! /usr/bin/env python3
 
 from hwilib.serializations import PSBT
+from hwilib.errors import PSBTSerializationError
 import json
 import os
 import unittest
@@ -15,7 +16,7 @@ class TestPSBT(unittest.TestCase):
     def test_invalid_psbt(self):
         for invalid in self.data['invalid']:
             with self.subTest(invalid=invalid):
-                with self.assertRaises(IOError) as cm:
+                with self.assertRaises(PSBTSerializationError) as cm:
                     psbt = PSBT()
                     psbt.deserialize(invalid)