Skip to content

Commit

Permalink
fix: addressing internal feedback (batch 6) (#139)
Browse files Browse the repository at this point in the history
* chore: align error message with ts

* fix: ensure root of unnamed resources response is parsed into typed class

* chore: match ts behaviour with regards to additional args passed to a ABI method call

---------

Co-authored-by: Neil Campbell <[email protected]>
  • Loading branch information
aorumbayev and neilcampbell authored Feb 14, 2025
1 parent 24b2d19 commit 4c661c0
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 35 deletions.
5 changes: 5 additions & 0 deletions src/algokit_utils/applications/app_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1900,6 +1900,11 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912
method = self._app_spec.get_arc56_method(method_name_or_signature)
result: list[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] = []

if args and len(method.args) < len(args):
raise ValueError(
f"Unexpected arg at position {len(method.args)}. {method.name} only expects {len(method.args)} args"
)

for i, method_arg in enumerate(method.args):
arg_value = args[i] if args and i < len(args) else None

Expand Down
82 changes: 48 additions & 34 deletions src/algokit_utils/transactions/transaction_composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,17 +591,35 @@ class SendAtomicTransactionComposerResults:
simulate_response: dict[str, Any] | None = None


class UnnamedResourcesAccessed:
"""Information about unnamed resource access."""

def __init__(self, resources_accessed: dict[str, Any] | None = None):
resources = resources_accessed or {}

if not isinstance(resources, dict):
raise TypeError(f"Expected dictionary object, got {type(resources_accessed)}")

self.accounts: list[str] | None = resources.get("accounts", None)
self.app_locals: list[dict[str, Any]] | None = resources.get("app-locals", None)
self.apps: list[int] | None = resources.get("apps", None)
self.asset_holdings: list[dict[str, Any]] | None = resources.get("asset-holdings", None)
self.assets: list[int] | None = resources.get("assets", None)
self.boxes: list[dict[str, Any]] | None = resources.get("boxes", None)
self.extra_box_refs: int | None = resources.get("extra-box-refs", None)


@dataclass
class ExecutionInfoTxn:
unnamed_resources_accessed: dict | None = None
unnamed_resources_accessed: UnnamedResourcesAccessed | None = None
required_fee_delta: int = 0


@dataclass
class ExecutionInfo:
"""Information about transaction execution from simulation."""

group_unnamed_resources_accessed: dict[str, Any] | None = None
group_unnamed_resources_accessed: UnnamedResourcesAccessed | None = None
txns: list[ExecutionInfoTxn] | None = None


Expand Down Expand Up @@ -706,7 +724,7 @@ def _get_group_execution_info( # noqa: C901, PLR0912
)
failed_at = group_response.get("failed-at", [0])[0]
raise ValueError(
f"Error during resource population simulation in transaction {failed_at}: "
f"Error resolving execution info via simulate in transaction {failed_at}: "
f"{group_response['failure-message']}"
)

Expand Down Expand Up @@ -745,15 +763,15 @@ def calculate_inner_fee_delta(inner_txns: list[dict], acc: int = 0) -> int:

txn_results.append(
ExecutionInfoTxn(
unnamed_resources_accessed=txn_result_raw.get("unnamed-resources-accessed")
unnamed_resources_accessed=UnnamedResourcesAccessed(txn_result_raw.get("unnamed-resources-accessed"))
if populate_app_call_resources
else None,
required_fee_delta=required_fee_delta,
)
)

return ExecutionInfo(
group_unnamed_resources_accessed=group_response.get("unnamed-resources-accessed")
group_unnamed_resources_accessed=UnnamedResourcesAccessed(group_response.get("unnamed-resources-accessed"))
if populate_app_call_resources
else None,
txns=txn_results,
Expand Down Expand Up @@ -1052,11 +1070,11 @@ def is_appl_below_limit(t: TransactionWithSigner) -> bool:
resources = txn_info.unnamed_resources_accessed
if resources and is_app_txn:
app_txn = group[i].txn
if resources.get("boxes") or resources.get("extra-box-refs"):
if resources.boxes or resources.extra_box_refs:
raise ValueError("Unexpected boxes at transaction level")
if resources.get("appLocals"):
if resources.app_locals:
raise ValueError("Unexpected app local at transaction level")
if resources.get("assetHoldings"):
if resources.asset_holdings:
raise ValueError("Unexpected asset holding at transaction level")

# Update application call fields
Expand All @@ -1066,10 +1084,10 @@ def is_appl_below_limit(t: TransactionWithSigner) -> bool:
boxes = list(getattr(app_txn, "boxes", []) or [])

# Add new resources
accounts.extend(resources.get("accounts", []))
foreign_apps.extend(resources.get("apps", []))
foreign_assets.extend(resources.get("assets", []))
boxes.extend(resources.get("boxes", []))
accounts.extend(resources.accounts or [])
foreign_apps.extend(resources.apps or [])
foreign_assets.extend(resources.assets or [])
boxes.extend(resources.boxes or [])

# Validate limits
if len(accounts) > MAX_APP_CALL_ACCOUNT_REFERENCES:
Expand Down Expand Up @@ -1113,45 +1131,41 @@ def is_appl_below_limit(t: TransactionWithSigner) -> bool:
group_resources = execution_info.group_unnamed_resources_accessed
if group_resources:
# Handle cross-reference resources first
for app_local in group_resources.get("appLocals", []):
for app_local in group_resources.app_locals or []:
populate_group_resource(group, app_local, "appLocal")
# Remove processed resources
if "accounts" in group_resources:
group_resources["accounts"] = [
acc for acc in group_resources["accounts"] if acc != app_local["account"]
]
if "apps" in group_resources:
group_resources["apps"] = [app for app in group_resources["apps"] if int(app) != int(app_local["app"])]
if group_resources.accounts:
group_resources.accounts = [acc for acc in group_resources.accounts if acc != app_local["account"]]
if group_resources.apps:
group_resources.apps = [app for app in group_resources.apps if int(app) != int(app_local["app"])]

for asset_holding in group_resources.get("assetHoldings", []):
for asset_holding in group_resources.asset_holdings or []:
populate_group_resource(group, asset_holding, "assetHolding")
# Remove processed resources
if "accounts" in group_resources:
group_resources["accounts"] = [
acc for acc in group_resources["accounts"] if acc != asset_holding["account"]
]
if "assets" in group_resources:
group_resources["assets"] = [
asset for asset in group_resources["assets"] if int(asset) != int(asset_holding["asset"])
if group_resources.accounts:
group_resources.accounts = [acc for acc in group_resources.accounts if acc != asset_holding["account"]]
if group_resources.assets:
group_resources.assets = [
asset for asset in group_resources.assets if int(asset) != int(asset_holding["asset"])
]

# Handle remaining resources
for account in group_resources.get("accounts", []):
for account in group_resources.accounts or []:
populate_group_resource(group, account, "account")

for box in group_resources.get("boxes", []):
for box in group_resources.boxes or []:
populate_group_resource(group, box, "box")
if "apps" in group_resources:
group_resources["apps"] = [app for app in group_resources["apps"] if int(app) != int(box["app"])]
if group_resources.apps:
group_resources.apps = [app for app in group_resources.apps if int(app) != int(box["app"])]

for asset in group_resources.get("assets", []):
for asset in group_resources.assets or []:
populate_group_resource(group, asset, "asset")

for app in group_resources.get("apps", []):
for app in group_resources.apps or []:
populate_group_resource(group, app, "app")

# Handle extra box references
extra_box_refs = group_resources.get("extra-box-refs", 0)
extra_box_refs = group_resources.extra_box_refs or 0
for _ in range(extra_box_refs):
populate_group_resource(group, {"app": 0, "name": ""}, "box")

Expand Down
15 changes: 15 additions & 0 deletions tests/applications/test_app_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,21 @@ def test_create_then_call_app(factory: AppFactory) -> None:
assert call.abi_return == "Hello, test"


def test_call_app_with_too_many_args(factory: AppFactory) -> None:
app_client, _ = factory.send.bare.create(
compilation_params={
"updatable": False,
"deletable": False,
"deploy_time_params": {
"VALUE": 1,
},
},
)

with pytest.raises(Exception, match="Unexpected arg at position 1. call_abi only expects 1 args"):
app_client.send.call(AppClientMethodCallParams(method="call_abi", args=["test", "extra"]))


def test_call_app_with_rekey(funded_account: SigningAccount, algorand: AlgorandClient, factory: AppFactory) -> None:
rekey_to = algorand.account.random()

Expand Down
2 changes: 1 addition & 1 deletion tests/transactions/test_resource_packing.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ def test_error_during_simulate(self) -> None:
"populate_app_call_resources": True,
},
)
assert "Error during resource population simulation in transaction 0" in exc_info.value.logic_error_str
assert "Error resolving execution info via simulate in transaction 0" in exc_info.value.logic_error_str

def test_box_with_txn_arg(self, algorand: AlgorandClient, funded_account: SigningAccount) -> None:
payment = PaymentTxn(
Expand Down

1 comment on commit 4c661c0

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
src/algokit_utils
   _debugging.py1411887%21, 40–42, 45, 54, 62, 81, 87, 96, 110–114, 133, 141–143
   account.py330%1–12
   algorand.py981585%62–63, 74–75, 95–96, 108–110, 119–120, 214, 234, 247, 263
   application_client.py330%1–11
   application_specification.py770%1–39
   config.py782173%23, 30–34, 73–74, 90, 95, 100, 111–116, 141, 144–146, 149, 151
   deploy.py330%1–10
   logic_error.py330%1–10
src/algokit_utils/_legacy_v2
   _ensure_funded.py71199%100
   _transfer.py70396%14, 75–76
   account.py931386%15–18, 70–74, 109, 126, 156, 159, 203
   application_client.py5377786%56–57, 176, 181, 210, 322, 327–328, 330, 332, 801, 816, 834–837, 931, 971, 983, 996, 1038, 1098–1104, 1108–1113, 1115, 1151, 1158, 1271, 1307, 1321, 1359–1361, 1363, 1373–1430, 1441–1446, 1466–1469
   asset.py80495%24–27
   common.py13192%13
   deploy.py4072295%32–35, 170, 174–175, 193, 249, 339–340, 361, 395, 406–414, 429, 437, 593–594, 618
   network_clients.py73593%77–78, 101–102, 135
src/algokit_utils/accounts
   account_manager.py2123384%161, 180–181, 205–210, 228–229, 246, 263–268, 315–317, 346, 385, 405–409, 422, 481, 502, 659, 765, 844, 849, 868–869, 892
   kmd_account_manager.py741086%49–55, 96, 152, 159
src/algokit_utils/applications
   abi.py1327146%14, 77, 113, 119, 121, 123, 127–128, 143–162, 178, 181–187, 203–216, 229–241, 256–267
   app_client.py70421470%66–74, 133, 141, 335–338, 341, 344, 347, 350, 362–365, 368, 371, 374, 377, 389, 398, 407, 410–477, 490–548, 572–574, 591–594, 602–605, 613–616, 624–627, 635–638, 649–652, 665, 761–764, 798, 810, 822, 834, 846, 861, 876, 886, 896, 906, 916, 926, 963–976, 992, 1009, 1026, 1043, 1064, 1085, 1175, 1343, 1390–1391, 1422, 1424, 1430, 1475–1483, 1516–1519, 1522–1525, 1546, 1594, 1657–1659, 1672–1679, 1720, 1737, 1739, 1742, 1746, 1873, 1884–1885, 1913, 1923–1925, 1928–1950, 1960, 1969–1984, 1989–1994
   app_deployer.py2143584%98, 196, 203, 213–218, 221–225, 297–298, 507, 516–519, 532, 537, 543, 552–557, 564, 578, 589–630
   app_factory.py2662790%351, 361, 364, 508, 516, 528, 677, 691–696, 702–703, 721, 746, 757–758, 794, 802–816
   app_manager.py2181693%226, 285–286, 318–323, 349, 367–368, 398, 419–422, 445, 454
src/algokit_utils/applications/app_spec
   arc32.py95892%198–207
   arc56.py4703692%72–74, 183, 318, 336–337, 415, 437–439, 495, 504, 506, 574, 769, 779, 783, 943, 945, 978–989, 1002, 1004–1005, 1021
src/algokit_utils/assets
   asset_manager.py1061289%266–267, 276, 282–306, 316
src/algokit_utils/clients
   client_manager.py1735469%24–26, 74–81, 95, 127–129, 185, 196–199, 224, 263, 298–303, 338–340, 373, 390, 412, 446–449, 482–485, 519–522, 552–555, 573–598, 628, 637, 645
   dispenser_api_client.py841286%129–130, 134–137, 172–174, 193–195
src/algokit_utils/errors
   logic_error.py561180%14, 105–121
src/algokit_utils/models
   account.py921584%32, 34, 80–81, 128, 136, 144, 168–175, 188, 196, 209, 217
   amount.py1061784%35, 42, 88, 98, 104, 112, 117–119, 126, 131–133, 140, 147, 160, 166
   application.py42198%8
   state.py36586%51, 55–58
   transaction.py51394%66, 86, 91
src/algokit_utils/protocols
   account.py11282%17, 22
   typed_clients.py24483%12–24
src/algokit_utils/transactions
   transaction_composer.py96611089%34–41, 601, 640, 643, 648, 652–663, 695, 736, 803, 810, 832, 865, 962–991, 1020, 1038–1040, 1046–1061, 1066, 1074, 1076, 1078, 1094, 1100, 1138, 1146, 1159, 1253, 1303, 1552–1553, 1592–1593, 1634, 1845, 1848, 1867, 1872, 1896–1898, 1935–1971, 1978–1985, 2126, 2130, 2261, 2263–2264, 2314–2315
   transaction_creator.py75791%116, 121, 126, 131, 136, 141, 156
   transaction_sender.py1481093%88, 243, 286–287, 431–436, 441–442, 487
TOTAL626391285% 

Tests Skipped Failures Errors Time
402 0 💤 0 ❌ 0 🔥 5m 46s ⏱️

Please sign in to comment.