Skip to content

Commit

Permalink
Merge pull request #2 from ulamlabs/exception-handling
Browse files Browse the repository at this point in the history
Improved exception handling, cleanup
  • Loading branch information
ksiazkowicz committed Apr 7, 2020
2 parents e452e51 + ab6a30e commit cabfb35
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 97 deletions.
9 changes: 9 additions & 0 deletions aioxrpy/decimals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from decimal import Decimal


def xrp_to_drops(amount):
return int((Decimal(1000000) * amount).quantize(Decimal('1')))


def drops_to_xrp(amount):
return Decimal(amount) / Decimal(1000000)
30 changes: 28 additions & 2 deletions aioxrpy/definitions.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""
Ripple definitions.json converted to Python object
Ripple type and field definitions
"""
from collections import defaultdict
from dataclasses import dataclass
from enum import IntEnum
from enum import Enum, IntEnum
import json
import os
from typing import Dict
Expand Down Expand Up @@ -55,6 +55,32 @@ class RippleType(IntEnum):
STArray = 15


class RippleTransactionFlags(IntEnum):
FullyCanonicalSig = 0x80000000


class RippleTransactionResultCategory(str, Enum):
"""
Enum containing Ripple transaction categories.
https://xrpl.org/tec-codes.html
The original abbreviations for transaction result categories (tec, tel
codes) are not expanded anywhere so I had to get creative with the names.
"""
# transaction failed but you had to pay the fees for submitting it anyway
CostlyFailure = 'tec'
# transaction failed on our end
LocalFailure = 'tel'
# transaction was malformed, ie. tried to send negative amount
MalformedFailure = 'tem'
# transaction failed but maybe if some other transaction was applied first
# maybe it could've?! 🤔
RetriableFailure = 'ter'
# transaction failed, but it "could've succeeded in a theoretical ledger"
Failure = 'tef'
Success = 'tes'


@dataclass
class RippleField:
name: str
Expand Down
118 changes: 46 additions & 72 deletions aioxrpy/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,94 +1,68 @@
class RippleBaseException(Exception):
pass


class RippleUnfundedPaymentException(RippleBaseException):
error = 'unfunded_payment'


class RippleSerializerUnsupportedTypeException(RippleBaseException):
error = 'serializer_unsupported_type'


class UnknownRippleError(RippleBaseException):
error = 'unknown_error'


class DestinationDoesntExistError(RippleBaseException):
error = 'no_dst'
from aioxrpy.definitions import RippleTransactionResultCategory


class NotEnoughAmountToCreateDestinationError(DestinationDoesntExistError):
error = 'no_dst_insuf_xrp'


class NeedMasterKeyError(RippleBaseException):
error = 'need_master_key'
class RippleBaseException(Exception):
def __init__(self, error, payload={}):
self.payload = payload
self.error = error


class InsufficientReserveError(RippleBaseException):
error = 'insufficient_reserve'
class RippleTransactionException(RippleBaseException):
def __init__(self, error, category, payload={}):
super().__init__(error, payload)
self.category = category


class InsufficientReserveOfferError(InsufficientReserveError):
error = 'insufficient_reserve_offer'
class RippleTransactionCostlyFailureException(RippleTransactionException):
def __init__(self, error, payload={}):
super().__init__(
error, RippleTransactionResultCategory.CostlyFailure, payload
)


class InsufficientReserveLineError(InsufficientReserveError):
error = 'insufficient_reserve_line'
class RippleTransactionLocalFailureException(RippleTransactionException):
def __init__(self, error, payload={}):
super().__init__(
error, RippleTransactionResultCategory.LocalFailure, payload
)


class AssetsFrozenError(RippleBaseException):
error = 'frozen'
class RippleTransactionMalformedException(RippleTransactionException):
def __init__(self, error, payload={}):
super().__init__(
error, RippleTransactionResultCategory.MalformedFailure, payload
)


class BadAmountError(RippleBaseException):
error = 'bad_amount'
class RippleTransactionRetriableException(RippleTransactionException):
def __init__(self, error, payload={}):
super().__init__(
error, RippleTransactionResultCategory.RetriableFailure, payload
)


class BadFeeError(RippleBaseException):
error = 'bad_fee'
class RippleTransactionFailureException(RippleTransactionException):
def __init__(self, error, payload={}):
super().__init__(
error, RippleTransactionResultCategory.Failure, payload
)


class AccountNotFoundError(RippleBaseException):
error = 'act_not_found'
class RippleSerializerUnsupportedTypeException(RippleBaseException):
def __init__(self, payload={}):
super().__init__('serializer_unsupported_type', payload)


class ValidatedLedgerUnavailable(RippleBaseException):
# Custom error for when validated_ledger field is missing
error = 'validated_ledger_unavailable'
class UnknownRippleException(RippleBaseException):
def __init__(self, payload={}):
super().__init__('unknown_error', payload)


def ripple_error_to_exception(error):
return {
'actNotFound': AccountNotFoundError
}.get(
error,
UnknownRippleError({'error': error})
)
class InvalidTransactionException(RippleBaseException):
def __init__(self, payload={}):
super().__init__('invalid_transaction', payload)


def ripple_result_to_exception(category, code):
"""
https://xrpl.org/tec-codes.html
"""
return {
'UNFUNDED_PAYMENT': RippleUnfundedPaymentException,
'NO_DST': DestinationDoesntExistError,
'NO_DST_INSUF_XRP': NotEnoughAmountToCreateDestinationError,
'NEED_MASTER_KEY': NeedMasterKeyError,
'INSUFFICIENT_RESERVE': InsufficientReserveError,
'INSUFFICIENT_RESERVE_OFFER': InsufficientReserveOfferError,
'INSUFFICIENT_RESERVE_LINE': InsufficientReserveLineError,
'FROZEN': AssetsFrozenError,
'BAD_AMOUNT': BadAmountError,
'BAD_FEE': BadFeeError,
}.get(
code,
UnknownRippleError(
data={
'category': category,
'code': code
}
)
)
class AccountNotFoundException(RippleBaseException):
def __init__(self, payload={}):
super().__init__('act_not_found', payload)
50 changes: 37 additions & 13 deletions aioxrpy/rpc.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
from decimal import Decimal as D

from aiohttp.client import ClientSession

from aioxrpy.exceptions import ripple_error_to_exception


def xrp_to_drops(amount):
return int((D(1000000) * amount).quantize(D('1')))


def drops_to_xrp(amount):
return D(amount) / D(1000000)
from aioxrpy import exceptions
from aioxrpy.definitions import RippleTransactionResultCategory


class RippleJsonRpc:
Expand All @@ -30,7 +21,12 @@ async def post(self, method, *args):
result = resp_dict.get('result')
error = result.get('error')
if error:
raise ripple_error_to_exception(error)
raise {
'actNotFound': exceptions.AccountNotFoundException,
'invalidTransaction': (
exceptions.InvalidTransactionException
)
}.get(error, exceptions.UnknownRippleException)(result)
return result

async def account_info(self, account, ledger_index='closed'):
Expand All @@ -55,7 +51,35 @@ async def ledger_closed(self):
return await self.post('ledger_closed')

async def submit(self, tx_blob):
return await self.post('submit', {'tx_blob': tx_blob})
"""
Submits transaction to JSON-RPC and handles `engine_result` value,
mapping error codes to exceptions
"""
result = await self.post('submit', {'tx_blob': tx_blob})
engine_result = result.get('engine_result')
category, code = engine_result[:3], engine_result[3:]

if category != RippleTransactionResultCategory.Success:
# Map category to exception
raise {
RippleTransactionResultCategory.CostlyFailure: (
exceptions.RippleTransactionCostlyFailureException
),
RippleTransactionResultCategory.LocalFailure: (
exceptions.RippleTransactionLocalFailureException
),
RippleTransactionResultCategory.MalformedFailure: (
exceptions.RippleTransactionMalformedException
),
RippleTransactionResultCategory.RetriableFailure: (
exceptions.RippleTransactionRetriableException
),
RippleTransactionResultCategory.Failure: (
exceptions.RippleTransactionFailureException
)
}[category](code)

return result

async def server_info(self):
return (await self.post('server_info'))['info']
19 changes: 14 additions & 5 deletions aioxrpy/sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@

import base58
from ecdsa import curves, SigningKey
from ecdsa.util import sigencode_der
from ecdsa.util import sigencode_der, sigdecode_der

from aioxrpy.address import decode_address, encode_address
from aioxrpy.definitions import RippleTransactionFlags
from aioxrpy.serializer import serialize


tfFullyCanonicalSig = 0x80000000


def sign_transaction(transaction, private_key, flag_canonical=True):
"""High-level signing function.hexlify
Expand All @@ -25,13 +23,20 @@ def sign_transaction(transaction, private_key, flag_canonical=True):
"""
if flag_canonical:
transaction['Flags'] = (
transaction.get('Flags', 0) | tfFullyCanonicalSig
transaction.get('Flags', 0)
| RippleTransactionFlags.FullyCanonicalSig
)
sig = signature_for_transaction(transaction, private_key)
transaction['TxnSignature'] = sig
return transaction


def verify_transaction(transaction, public_key, flag_canonical=True):
signature = transaction.pop('TxnSignature')
signing_hash = create_signing_hash(transaction)
return ecdsa_verify(public_key, signing_hash, signature)


def signature_for_transaction(transaction, private_key, ismulti=False):
"""Calculate the fully-canonical signature of the transaction.
Expand Down Expand Up @@ -138,6 +143,10 @@ def ecdsa_sign(key, signing_hash, **kw):
return der_coded


def ecdsa_verify(key, signing_hash, signature):
return key.verify_digest(signature, signing_hash, sigdecode=sigdecode_der)


def ecdsa_make_canonical(r, s):
"""Make sure the ECDSA signature is the canonical one.
Expand Down
6 changes: 6 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ Addresses
:members:
:undoc-members:

Decimals
--------
.. automodule:: aioxrpy.decimals
:members:
:undoc-members:

Definitions
-----------
.. automodule:: aioxrpy.definitions
Expand Down
11 changes: 11 additions & 0 deletions tests/test_decimals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from decimal import Decimal

from aioxrpy import decimals


def test_xrp_to_drops():
assert decimals.xrp_to_drops(Decimal('123.456789')) == 123456789


def test_drops_to_xrp():
assert decimals.drops_to_xrp(123456789) == Decimal('123.456789')
20 changes: 17 additions & 3 deletions tests/test_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ def ar():
async def test_error_mapping(rpc, ar):
# known exception
ar.post(rpc.URL, payload={'result': {'error': 'actNotFound'}})
with pytest.raises(exceptions.AccountNotFoundError):
with pytest.raises(exceptions.AccountNotFoundException):
await rpc.post('account_info', {'account': 'wrongname'})

# unknown exception
ar.post(rpc.URL, payload={'result': {'error': 'everythingIsFine'}})
with pytest.raises(exceptions.UnknownRippleError):
with pytest.raises(exceptions.UnknownRippleException):
await rpc.post('fee')

payload = {
Expand Down Expand Up @@ -66,7 +66,21 @@ async def test_ledger_closed(rpc, mock_post):


async def test_submit(rpc, mock_post):
await rpc.submit('0123ffc')
response = {
'engine_result': 'tesSUCCESS'
}
mock_post.side_effect = asyncio.coroutine(lambda *args, **kwargs: response)
assert await rpc.submit('0123ffc') == response
mock_post.assert_called_with('submit', {'tx_blob': '0123ffc'})


async def test_submit_error_mapping(rpc, mock_post):
response = {
'engine_result': 'tecNO_DST_INSUF_XRP'
}
mock_post.side_effect = asyncio.coroutine(lambda *args, **kwargs: response)
with pytest.raises(exceptions.RippleTransactionCostlyFailureException):
await rpc.submit('0123ffc')
mock_post.assert_called_with('submit', {'tx_blob': '0123ffc'})


Expand Down

0 comments on commit cabfb35

Please sign in to comment.