Skip to content

Commit

Permalink
feat: initial implementation of TransactionComposer
Browse files Browse the repository at this point in the history
  • Loading branch information
aorumbayev committed Nov 1, 2024
1 parent 1e89e6e commit f4b201d
Show file tree
Hide file tree
Showing 21 changed files with 1,707 additions and 782 deletions.
6 changes: 3 additions & 3 deletions legacy_v2_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,11 @@ def generate_test_asset(algod_client: "AlgodClient", sender: Account, total: int
note=None,
lease=None,
rekey_to=None,
) # type: ignore[no-untyped-call]
)

signed_transaction = txn.sign(sender.private_key) # type: ignore[no-untyped-call]
signed_transaction = txn.sign(sender.private_key)
algod_client.send_transaction(signed_transaction)
ptx = algod_client.pending_transaction_info(txn.get_txid()) # type: ignore[no-untyped-call]
ptx = algod_client.pending_transaction_info(txn.get_txid())

if isinstance(ptx, dict) and "asset-index" in ptx and isinstance(ptx["asset-index"], int):
return ptx["asset-index"]
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ suppress-none-returning = true
[tool.ruff.lint.per-file-ignores]
"src/algokit_utils/beta/*" = ["ERA001", "E501", "PLR0911"]
"path/to/file.py" = ["E402"]
"tests/clients/test_algorand_client.py" = ["ERA001"]

[tool.poe.tasks]
docs = ["docs-html-only", "docs-md-only"]
Expand Down
2 changes: 2 additions & 0 deletions src/algokit_utils/assets/asset_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class AssetManager:
"""A manager for Algorand assets"""
60 changes: 17 additions & 43 deletions src/algokit_utils/clients/algorand_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from typing_extensions import Self

from algokit_utils.accounts.account_manager import AccountManager
from algokit_utils.applications.app_manager import AppManager
from algokit_utils.assets.asset_manager import AssetManager
from algokit_utils.clients.client_manager import AlgoSdkClients, ClientManager
from algokit_utils.network_clients import (
AlgoClientConfigs,
Expand All @@ -31,6 +33,8 @@
PaymentParams,
TransactionComposer,
)
from algokit_utils.transactions.transaction_creator import AlgorandClientTransactionCreator
from algokit_utils.transactions.transaction_sender import AlgorandClientTransactionSender

__all__ = [
"AlgorandClient",
Expand Down Expand Up @@ -89,6 +93,15 @@ class AlgorandClient:
def __init__(self, config: AlgoClientConfigs | AlgoSdkClients):
self._client_manager: ClientManager = ClientManager(config)
self._account_manager: AccountManager = AccountManager(self._client_manager)
self._asset_manager: AssetManager = AssetManager() # TODO: implement
self._app_manager: AppManager = AppManager(self._client_manager.algod) # TODO: implement
self._transaction_sender = AlgorandClientTransactionSender(
new_group=lambda: self.new_group(),
asset_manager=self._asset_manager,
app_manager=self._app_manager,
algod_client=self._client_manager.algod,
)
self._transaction_creator = AlgorandClientTransactionCreator() # TODO: implement

self._cached_suggested_params: SuggestedParams | None = None
self._cached_suggested_params_expiry: float | None = None
Expand Down Expand Up @@ -187,53 +200,14 @@ def new_group(self) -> TransactionComposer:
)

@property
def send(self) -> AlgorandClientSendMethods:
def send(self) -> AlgorandClientTransactionSender:
"""Methods for sending a transaction and waiting for confirmation"""
return AlgorandClientSendMethods(
payment=lambda params: self._unwrap_single_send_result(self.new_group().add_payment(params).execute()),
asset_create=lambda params: self._unwrap_single_send_result(
self.new_group().add_asset_create(params).execute()
),
asset_config=lambda params: self._unwrap_single_send_result(
self.new_group().add_asset_config(params).execute()
),
asset_freeze=lambda params: self._unwrap_single_send_result(
self.new_group().add_asset_freeze(params).execute()
),
asset_destroy=lambda params: self._unwrap_single_send_result(
self.new_group().add_asset_destroy(params).execute()
),
asset_transfer=lambda params: self._unwrap_single_send_result(
self.new_group().add_asset_transfer(params).execute()
),
app_call=lambda params: self._unwrap_single_send_result(self.new_group().add_app_call(params).execute()),
online_key_reg=lambda params: self._unwrap_single_send_result(
self.new_group().add_online_key_reg(params).execute()
),
method_call=lambda params: self._unwrap_single_send_result(
self.new_group().add_method_call(params).execute()
),
asset_opt_in=lambda params: self._unwrap_single_send_result(
self.new_group().add_asset_opt_in(params).execute()
),
)
return self._transaction_sender

@property
def transactions(self) -> AlgorandClientTransactionMethods:
def create_transaction(self) -> AlgorandClientTransactionCreator:
"""Methods for building transactions"""

return AlgorandClientTransactionMethods(
payment=lambda params: self.new_group().add_payment(params).build_group()[0].txn,
asset_create=lambda params: self.new_group().add_asset_create(params).build_group()[0].txn,
asset_config=lambda params: self.new_group().add_asset_config(params).build_group()[0].txn,
asset_freeze=lambda params: self.new_group().add_asset_freeze(params).build_group()[0].txn,
asset_destroy=lambda params: self.new_group().add_asset_destroy(params).build_group()[0].txn,
asset_transfer=lambda params: self.new_group().add_asset_transfer(params).build_group()[0].txn,
app_call=lambda params: self.new_group().add_app_call(params).build_group()[0].txn,
online_key_reg=lambda params: self.new_group().add_online_key_reg(params).build_group()[0].txn,
method_call=lambda params: [txn.txn for txn in self.new_group().add_method_call(params).build_group()],
asset_opt_in=lambda params: self.new_group().add_asset_opt_in(params).build_group()[0].txn,
)
return self._transaction_creator

@staticmethod
def default_local_net() -> "AlgorandClient":
Expand Down
60 changes: 45 additions & 15 deletions src/algokit_utils/models/amount.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
from decimal import Decimal

import algosdk
from typing_extensions import Self


class AlgoAmount:
def __init__(self, amount: dict[str, float | int | Decimal]):
def __init__(self, amount: dict[str, int | Decimal]):
if "microAlgos" in amount:
self.amount_in_micro_algo = int(amount["microAlgos"])
elif "microAlgo" in amount:
Expand All @@ -27,67 +28,96 @@ def micro_algo(self) -> int:
return self.amount_in_micro_algo

@property
def algos(self) -> Decimal:
def algos(self) -> int | Decimal:
return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return]

@property
def algo(self) -> Decimal:
def algo(self) -> int | Decimal:
return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return]

@staticmethod
def from_algos(amount: float | Decimal) -> AlgoAmount:
def from_algos(amount: int | Decimal) -> AlgoAmount:
return AlgoAmount({"algos": amount})

@staticmethod
def from_algo(amount: float | Decimal) -> AlgoAmount:
def from_algo(amount: int | Decimal) -> AlgoAmount:
return AlgoAmount({"algo": amount})

@staticmethod
def from_micro_algos(amount: int) -> AlgoAmount:
def from_micro_algos(amount: int | Decimal) -> AlgoAmount:
return AlgoAmount({"microAlgos": amount})

@staticmethod
def from_micro_algo(amount: int) -> AlgoAmount:
def from_micro_algo(amount: int | Decimal) -> AlgoAmount:
return AlgoAmount({"microAlgo": amount})

def __str__(self) -> str:
"""Return a string representation of the amount."""
return f"{self.micro_algo:,} µALGO"

def __int__(self) -> int:
"""Return the amount as an integer number of microAlgos."""
return self.micro_algos

def __add__(self, other: int | Decimal | AlgoAmount) -> AlgoAmount:
if isinstance(other, AlgoAmount):
total_micro_algos = self.micro_algos + other.micro_algos
elif isinstance(other, (int | Decimal)):
total_micro_algos = self.micro_algos + int(other)
else:
raise TypeError(f"Unsupported operand type(s) for +: 'AlgoAmount' and '{type(other).__name__}'")
return AlgoAmount.from_micro_algos(total_micro_algos)

def __radd__(self, other: int | Decimal) -> AlgoAmount:
return self.__add__(other)

def __iadd__(self, other: int | Decimal | AlgoAmount) -> Self:
if isinstance(other, AlgoAmount):
self.amount_in_micro_algo += other.micro_algos
elif isinstance(other, (int | Decimal)):
self.amount_in_micro_algo += int(other)
else:
raise TypeError(f"Unsupported operand type(s) for +: 'AlgoAmount' and '{type(other).__name__}'")
return self

def __eq__(self, other: object) -> bool:
if isinstance(other, AlgoAmount):
return self.amount_in_micro_algo == other.amount_in_micro_algo
elif isinstance(other, int | Decimal):
return self.amount_in_micro_algo == int(other)
raise NotImplementedError
raise TypeError(f"Unsupported operand type(s) for ==: 'AlgoAmount' and '{type(other).__name__}'")

def __ne__(self, other: object) -> bool:
if isinstance(other, AlgoAmount):
return self.amount_in_micro_algo != other.amount_in_micro_algo
elif isinstance(other, (int | Decimal)):
elif isinstance(other, int | Decimal):
return self.amount_in_micro_algo != int(other)
raise NotImplementedError
raise TypeError(f"Unsupported operand type(s) for !=: 'AlgoAmount' and '{type(other).__name__}'")

def __lt__(self, other: object) -> bool:
if isinstance(other, AlgoAmount):
return self.amount_in_micro_algo < other.amount_in_micro_algo
elif isinstance(other, (int | Decimal)):
elif isinstance(other, int | Decimal):
return self.amount_in_micro_algo < int(other)
raise NotImplementedError
raise TypeError(f"Unsupported operand type(s) for <: 'AlgoAmount' and '{type(other).__name__}'")

def __le__(self, other: object) -> bool:
if isinstance(other, AlgoAmount):
return self.amount_in_micro_algo <= other.amount_in_micro_algo
elif isinstance(other, int | Decimal):
return self.amount_in_micro_algo <= int(other)
raise NotImplementedError
raise TypeError(f"Unsupported operand type(s) for <=: 'AlgoAmount' and '{type(other).__name__}'")

def __gt__(self, other: object) -> bool:
if isinstance(other, AlgoAmount):
return self.amount_in_micro_algo > other.amount_in_micro_algo
elif isinstance(other, int | Decimal):
return self.amount_in_micro_algo > int(other)
raise NotImplementedError
raise TypeError(f"Unsupported operand type(s) for >: 'AlgoAmount' and '{type(other).__name__}'")

def __ge__(self, other: object) -> bool:
if isinstance(other, AlgoAmount):
return self.amount_in_micro_algo >= other.amount_in_micro_algo
elif isinstance(other, int | Decimal):
return self.amount_in_micro_algo >= int(other)
raise NotImplementedError
raise TypeError(f"Unsupported operand type(s) for >=: 'AlgoAmount' and '{type(other).__name__}'")
30 changes: 30 additions & 0 deletions src/algokit_utils/transactions/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from typing import Any, Literal, TypedDict


# Define specific types for different formats
class BaseArc2Note(TypedDict):
"""Base ARC-0002 transaction note structure"""

dapp_name: str


class StringFormatArc2Note(BaseArc2Note):
"""ARC-0002 note for string-based formats (m/b/u)"""

format: Literal["m", "b", "u"]
data: str


class JsonFormatArc2Note(BaseArc2Note):
"""ARC-0002 note for JSON format"""

format: Literal["j"]
data: str | dict[str, Any] | list[Any] | int | None


# Combined type for all valid ARC-0002 notes
# See: https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md
Arc2TransactionNote = StringFormatArc2Note | JsonFormatArc2Note

TransactionNoteData = str | None | int | list[Any] | dict[str, Any]
TransactionNote = bytes | TransactionNoteData | Arc2TransactionNote
Loading

0 comments on commit f4b201d

Please sign in to comment.