Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error Handling #49

Merged
merged 14 commits into from
Jan 4, 2023
12 changes: 4 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,10 @@
* Added note to application call transactions. The note (`tinyman/<v1|v2>:j{"origin":"<client-name>"}`) follows [Algorand Transaction Note Field Conventions ARC-2](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md). [#51](https://github.com/tinymanorg/tinyman-py-sdk/pull/51)
* Added `version` property and `generate_app_call_note` method to `TinymanClient` classes. [#51](https://github.com/tinymanorg/tinyman-py-sdk/pull/51)
* Added `get_version` and `generate_app_call_note` to `tinyman.utils`. [#51](https://github.com/tinymanorg/tinyman-py-sdk/pull/51)

### Changed

* ...

### Removed
* ...

* Improved error handling [#49](https://github.com/tinymanorg/tinyman-py-sdk/pull/49/files).
- Added `TealishMap`.
- Added `AlgodError`, `LogicError`, `OverspendError` exception classes.
- Added `amm_approval.map.json` for V2.

## 2.0.0

Expand Down
4 changes: 1 addition & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
install_requires=["py-algorand-sdk >= 1.10.0"],
packages=setuptools.find_packages(),
python_requires=">=3.8",
package_data={
"tinyman.v1": ["asc.json"],
},
package_data={"tinyman.v1": ["asc.json"], "tinyman.v2": ["amm_approval.map.json"]},
include_package_data=True,
)
10 changes: 6 additions & 4 deletions tinyman/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from typing import Optional

from algosdk.error import AlgodHTTPError
from algosdk.future.transaction import wait_for_confirmation
from algosdk.v2client.algod import AlgodClient

Expand Down Expand Up @@ -38,15 +37,18 @@ def fetch_asset(self, asset_id):
def submit(self, transaction_group, wait=False):
try:
txid = self.algod.send_transactions(transaction_group.signed_transactions)
except AlgodHTTPError as e:
raise Exception(str(e))

except Exception as e:
self.handle_error(e, transaction_group)
if wait:
txn_info = wait_for_confirmation(self.algod, txid)
txn_info["txid"] = txid
return txn_info
return {"txid": txid}

def handle_error(self, exception, transaction_group):
error_message = str(exception)
raise Exception(error_message) from None

def prepare_asset_optin_transactions(
self, asset_id, user_address=None, suggested_params=None
):
Expand Down
23 changes: 23 additions & 0 deletions tinyman/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class AlgodError(Exception):
def __init__(self, message: str) -> None:
super().__init__(message)
self.message = message

def __str__(self):
return self.message


class LogicError(AlgodError):
def __init__(self, message, txn_id=None, pc=None, app_id=None) -> None:
super().__init__(message)
self.txn_id = txn_id
self.pc = pc
self.app_id = app_id


class OverspendError(AlgodError):
def __init__(self, txn_id, address, amount) -> None:
super().__init__(f"Overspend by {address}. Tried to spend {amount}")
self.txn_id = txn_id
self.address = address
self.amount = amount
28 changes: 28 additions & 0 deletions tinyman/tealishmap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from typing import Dict, Optional, Any


class TealishMap:
def __init__(self, map: Dict[str, Any]) -> None:
self.pc_teal = map.get("pc_teal", [])
self.teal_tealish = map.get("teal_tealish", [])
self.errors: Dict[int, str] = {
int(k): v for k, v in map.get("errors", {}).items()
}

def get_tealish_line_for_pc(self, pc: int) -> Optional[int]:
teal_line = self.get_teal_line_for_pc(pc)
if teal_line is not None:
return self.get_tealish_line_for_teal(teal_line)
return None

def get_teal_line_for_pc(self, pc: int) -> Optional[int]:
return self.pc_teal[pc]

def get_tealish_line_for_teal(self, teal_line: int) -> int:
return self.teal_tealish[teal_line]

def get_error_for_pc(self, pc: int) -> Optional[str]:
tealish_line = self.get_tealish_line_for_pc(pc)
if tealish_line is not None:
return self.errors.get(tealish_line, None)
return None
34 changes: 32 additions & 2 deletions tinyman/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
assign_group_id,
wait_for_confirmation,
)
from .errors import AlgodError, LogicError, OverspendError

from tinyman.v1.constants import (
MAINNET_VALIDATOR_APP_ID_V1_1,
Expand Down Expand Up @@ -236,8 +237,7 @@ def submit(self, algod, wait=False):
try:
txid = algod.send_transactions(self.signed_transactions)
except AlgodHTTPError as e:
raise Exception(str(e))

raise Exception(e) from None
if wait:
txn_info = wait_for_confirmation(algod, txid)
txn_info["txid"] = txid
Expand All @@ -247,3 +247,33 @@ def submit(self, algod, wait=False):
def __add__(self, other):
transactions = self.transactions + other.transactions
return TransactionGroup(transactions)


def parse_error(exception):
error_message = str(exception)
pattern = r"Remember: transaction ([A-Z0-9]+):"
try:
txn_id = re.findall(pattern, error_message)[0]
except IndexError:
return AlgodError(error_message)

if "logic eval error" in error_message:
pattern = r"error: (.+?). Details: pc=(\d+)"
error, pc = re.findall(pattern, error_message)[0]
return LogicError(error, txn_id=txn_id, pc=pc)

if "overspend" in error_message:
pattern = r"overspend \(account (.+?),.+tried to spend {(\d+)}\)"
address, amount = re.findall(pattern, error_message)[0]
return OverspendError(txn_id=txn_id, address=address, amount=amount)

return AlgodError(error_message)


def find_app_id_from_txn_id(transaction_group, txn_id):
app_id = None
for txn in transaction_group.transactions:
if txn.get_txid() == txn_id:
app_id = txn.index
break
return app_id
39 changes: 39 additions & 0 deletions tinyman/v2/amm_approval.map.json

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions tinyman/v2/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,26 @@
MAINNET_VALIDATOR_APP_ID,
)

from tinyman.utils import find_app_id_from_txn_id, parse_error
from .utils import lookup_error
from tinyman.errors import LogicError


class TinymanV2Client(BaseTinymanClient):
def fetch_pool(self, asset_a, asset_b, fetch=True):
from .pools import Pool

return Pool(self, asset_a, asset_b, fetch=fetch)

def handle_error(self, exception, txn_group):
error = parse_error(exception)
if isinstance(error, LogicError):
app_id = find_app_id_from_txn_id(txn_group, error.txn_id)
if app_id in (TESTNET_VALIDATOR_APP_ID, MAINNET_VALIDATOR_APP_ID):
error.app_id = app_id
error.message = lookup_error(error.pc, error.message)
raise error from None


class TinymanV2TestnetClient(TinymanV2Client):
def __init__(self, algod_client: AlgodClient, user_address=None):
Expand Down
21 changes: 20 additions & 1 deletion tinyman/v2/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import importlib.resources
import json
from base64 import b64decode

import tinyman.v2
from tinyman.tealishmap import TealishMap
from tinyman.utils import bytes_to_int

tealishmap = TealishMap(
json.loads(importlib.resources.read_text(tinyman.v2, "amm_approval.map.json"))
)

def decode_logs(logs: "list[[bytes, str]]") -> dict:

def decode_logs(logs: "list") -> dict:
decoded_logs = dict()
for log in logs:
if type(log) == str:
Expand Down Expand Up @@ -35,3 +43,14 @@ def get_state_from_account_info(account_info, app_id):
except KeyError:
return {}
return app_state


def lookup_error(pc, error_message):
tealish_line_no = tealishmap.get_tealish_line_for_pc(int(pc))
if "assert failed" in error_message or "err opcode executed" in error_message:
custom_error_message = tealishmap.get_error_for_pc(int(pc))
if custom_error_message:
error_message = custom_error_message

error_message = f"{error_message} @ line {tealish_line_no}"
return error_message