Skip to content

Commit 6d7bb66

Browse files
authored
feat: send payment transaction with algokit core
feat: send payment transaction with algokit core
2 parents fe04d2f + 231395a commit 6d7bb66

File tree

10 files changed

+506
-21
lines changed

10 files changed

+506
-21
lines changed

.github/workflows/build-python.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
uses: ./.github/actions/setup-poetry
2323

2424
- name: Install dependencies
25-
run: poetry install --no-interaction
25+
run: poetry install --no-interaction --without=experimental
2626

2727
- name: pytest + coverage
2828
shell: bash

poetry.lock

Lines changed: 316 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@ py-algorand-sdk = "^2.4.0"
1212
httpx = ">=0.23.1,<=0.28.1"
1313
typing-extensions = ">=4.6.0"
1414

15+
[tool.poetry.group.experimental.dependencies]
16+
algokit-transact = [
17+
# macOS
18+
{url = "https://github.com/algorandfoundation/algokit-core/releases/download/python%2Falgokit_transact%401.0.0-alpha.10/algokit_transact-1.0.0a10-py3-none-macosx_10_12_x86_64.whl", markers = "sys_platform == 'darwin' and platform_machine == 'x86_64'"},
19+
{url = "https://github.com/algorandfoundation/algokit-core/releases/download/python%2Falgokit_transact%401.0.0-alpha.10/algokit_transact-1.0.0a10-py3-none-macosx_11_0_arm64.whl", markers = "sys_platform == 'darwin' and platform_machine == 'arm64'"},
20+
21+
# Windows
22+
{url = "https://github.com/algorandfoundation/algokit-core/releases/download/python%2Falgokit_transact%401.0.0-alpha.10/algokit_transact-1.0.0a10-py3-none-win_amd64.whl", markers = "sys_platform == 'win32' and platform_machine == 'AMD64'"},
23+
24+
# Linux - manylinux (for most common glibc-based distributions like Ubuntu, Debian, Fedora)
25+
{url = "https://github.com/algorandfoundation/algokit-core/releases/download/python%2Falgokit_transact%401.0.0-alpha.10/algokit_transact-1.0.0a10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", markers = "sys_platform == 'linux' and platform_machine == 'aarch64'"},
26+
{url = "https://github.com/algorandfoundation/algokit-core/releases/download/python%2Falgokit_transact%401.0.0-alpha.10/algokit_transact-1.0.0a10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", markers = "sys_platform == 'linux' and platform_machine == 'x86_64'"},
27+
]
28+
algokit-algod-api = {url = "https://github.com/algorandfoundation/algokit-core/releases/download/python%2Falgod_api%401.0.0-alpha.11/algokit_algod_api-1.0.0a11-py3-none-any.whl"}
29+
1530
[tool.poetry.group.dev.dependencies]
1631
pytest = "^8"
1732
ruff = ">=0.1.6,<=0.11.8"
@@ -27,7 +42,7 @@ poethepoet = ">=0.19,<0.35"
2742
pytest-httpx = "^0.35.0"
2843
pytest-xdist = "^3.6.1"
2944
linkify-it-py = "^2.0.3"
30-
setuptools = "^75.2.0"
45+
setuptools = "^80.9.0"
3146
pydoclint = "^0.6.0"
3247
pytest-sugar = "^1.0.0"
3348
types-deprecated = "^1.2.15.20241117"
@@ -130,7 +145,9 @@ suppress-none-returning = true
130145
"tests/clients/test_algorand_client.py" = ["ERA001"]
131146
"src/algokit_utils/_legacy_v2/**/*" = ["E501"]
132147
"tests/**/*" = ["PLR2004"]
133-
"src/algokit_utils/__init__.py" = ["I001", "RUF022"] # Ignore import sorting for __init__.py
148+
"src/algokit_utils/__init__.py" = ["I001", "RUF022", "E402"] # Ignore import sorting for __init__.py
149+
"src/algokit_utils/transactions/_algokit_core_bridge.py" = ["ANN001"]
150+
"src/algokit_utils/clients/_algokit_core_bridge.py" = ["ANN001", "ANN202","ANN204"]
134151

135152
[tool.poe.tasks]
136153
docs = ["docs-html-only", "docs-md-only"]
@@ -174,6 +191,14 @@ disallow_untyped_calls = false
174191
module = ["tests.transactions.test_transaction_composer"]
175192
disable_error_code = ["call-overload", "union-attr"]
176193

194+
[[tool.mypy.overrides]]
195+
module = ["algokit_utils.clients._algokit_core_bridge"]
196+
disable_error_code = ["attr-defined", "no-untyped-def", "no-untyped-call"]
197+
198+
[[tool.mypy.overrides]]
199+
module = ["algokit_utils.transactions._algokit_core_bridge"]
200+
disable_error_code = ["attr-defined", "no-untyped-def", "no-any-return"]
201+
177202
[tool.semantic_release]
178203
version_toml = "pyproject.toml:tool.poetry.version"
179204
remove_dist = false

src/algokit_utils/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
etc.
1010
"""
1111

12+
import importlib.util
13+
14+
_EXPERIMENTAL_DEPENDENCIES_INSTALLED: bool | None = importlib.util.find_spec("algokit_algod_api") is not None
15+
1216
# Core types and utilities that are commonly used
1317
from algokit_utils.applications import * # noqa: F403
1418
from algokit_utils.assets import * # noqa: F403
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import base64
2+
from collections.abc import Iterable
3+
4+
from algosdk.encoding import msgpack_encode
5+
from algosdk.transaction import GenericSignedTransaction
6+
from algosdk.v2client.algod import AlgodClient
7+
8+
from algokit_utils import _EXPERIMENTAL_DEPENDENCIES_INSTALLED
9+
10+
if not _EXPERIMENTAL_DEPENDENCIES_INSTALLED:
11+
raise ImportError(
12+
"Installing experimental dependencies is necessary to use AlgodClientWithCore. "
13+
"Install this package with --group=experimental"
14+
)
15+
16+
import algokit_algod_api
17+
18+
19+
class AlgodClientWithCore:
20+
"""
21+
A decorator for AlgodClient that extends its functionality with algokit_algod_api capabilities.
22+
This class wraps an AlgodClient instance while maintaining the same interface.
23+
"""
24+
25+
def __init__(self, algod_client: AlgodClient):
26+
self._algod_client = algod_client
27+
28+
configuration = algokit_algod_api.Configuration(
29+
host=algod_client.algod_address, api_key={"api_key": self._algod_client.algod_token}
30+
)
31+
api_client = algokit_algod_api.ApiClient(configuration)
32+
self._algod_core_client = algokit_algod_api.AlgodApi(api_client=api_client)
33+
34+
def send_raw_transaction(self, txn):
35+
"""
36+
Override the method to send a raw transaction using algokit_algod_api.
37+
"""
38+
return self._algod_core_client.raw_transaction(base64.b64decode(txn))
39+
40+
def send_transactions(self, txns: Iterable[GenericSignedTransaction]):
41+
"""
42+
Override the method to send multiple transactions using algokit_algod_api.
43+
"""
44+
return self.send_raw_transaction(
45+
base64.b64encode(b"".join(base64.b64decode(msgpack_encode(txn)) for txn in txns))
46+
)
47+
48+
def __getattr__(self, name):
49+
"""
50+
Delegate all other method calls to the wrapped AlgodClient instance.
51+
"""
52+
return getattr(self._algod_client, name)

src/algokit_utils/clients/client_manager.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from algosdk.v2client.algod import AlgodClient
1414
from algosdk.v2client.indexer import IndexerClient
1515

16+
from algokit_utils import _EXPERIMENTAL_DEPENDENCIES_INSTALLED
1617
from algokit_utils._legacy_v2.application_specification import ApplicationSpecification
1718
from algokit_utils.applications.app_deployer import ApplicationLookup
1819
from algokit_utils.applications.app_spec.arc56 import Arc56Contract
@@ -119,7 +120,12 @@ def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients, algor
119120
if clients_or_configs.kmd_config
120121
else None,
121122
)
122-
self._algod = _clients.algod
123+
if not _EXPERIMENTAL_DEPENDENCIES_INSTALLED:
124+
self._algod = _clients.algod
125+
else:
126+
from algokit_utils.clients._algokit_core_bridge import AlgodClientWithCore
127+
128+
self._algod = AlgodClientWithCore(_clients.algod) # type: ignore[assignment]
123129
self._indexer = _clients.indexer
124130
self._kmd = _clients.kmd
125131
self._algorand = algorand_client

src/algokit_utils/errors/logic_error.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class LogicErrorData(TypedDict):
3333
def parse_logic_error(
3434
error_str: str,
3535
) -> LogicErrorData | None:
36-
match = re.match(LOGIC_ERROR, error_str)
36+
match = re.search(LOGIC_ERROR, error_str, re.DOTALL)
3737
if match is None:
3838
return None
3939

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import base64
2+
from typing import cast
3+
4+
import algosdk.transaction
5+
from algokit_transact import (
6+
FeeParams,
7+
PaymentTransactionFields,
8+
Transaction,
9+
TransactionType,
10+
address_from_string,
11+
assign_fee,
12+
encode_transaction_raw,
13+
)
14+
15+
16+
def build_payment_with_core( # noqa: PLR0913
17+
sender,
18+
sp,
19+
receiver,
20+
amt,
21+
close_remainder_to=None,
22+
note=None,
23+
lease=None,
24+
rekey_to=None,
25+
static_fee=None,
26+
max_fee=None,
27+
extra_fee=None,
28+
) -> algosdk.transaction.PaymentTxn:
29+
# Determine static fee based on parameters or suggested params
30+
static_fee_value = None
31+
if static_fee is not None:
32+
static_fee_value = static_fee
33+
elif sp.flat_fee:
34+
static_fee_value = sp.fee
35+
36+
txn = Transaction(
37+
transaction_type=TransactionType.PAYMENT,
38+
sender=address_from_string(sender),
39+
fee=static_fee_value,
40+
first_valid=sp.first,
41+
last_valid=sp.last,
42+
genesis_hash=base64.b64decode(sp.gh),
43+
genesis_id=sp.gen,
44+
note=note,
45+
lease=lease,
46+
rekey_to=address_from_string(rekey_to) if rekey_to else None,
47+
payment=PaymentTransactionFields(
48+
receiver=address_from_string(receiver),
49+
amount=amt,
50+
close_remainder_to=address_from_string(close_remainder_to) if close_remainder_to else None,
51+
),
52+
)
53+
54+
if txn.fee is not None:
55+
# Static fee is already set, encode and return directly
56+
return cast(
57+
algosdk.transaction.PaymentTxn,
58+
algosdk.encoding.msgpack_decode(base64.b64encode(encode_transaction_raw(txn)).decode("utf-8")),
59+
)
60+
else:
61+
# Use assign_fee with fee parameters
62+
min_fee = sp.min_fee or algosdk.constants.MIN_TXN_FEE
63+
txn_with_fee = assign_fee(
64+
txn,
65+
FeeParams(
66+
fee_per_byte=sp.fee,
67+
min_fee=min_fee,
68+
max_fee=max_fee,
69+
extra_fee=extra_fee,
70+
),
71+
)
72+
73+
return cast(
74+
algosdk.transaction.PaymentTxn,
75+
algosdk.encoding.msgpack_decode(base64.b64encode(encode_transaction_raw(txn_with_fee)).decode("utf-8")),
76+
)

src/algokit_utils/transactions/transaction_composer.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from algosdk.v2client.models.simulate_request import SimulateRequest
2323
from typing_extensions import deprecated
2424

25+
from algokit_utils import _EXPERIMENTAL_DEPENDENCIES_INSTALLED
2526
from algokit_utils.applications.abi import ABIReturn, ABIValue
2627
from algokit_utils.applications.app_manager import AppManager
2728
from algokit_utils.applications.app_spec.arc56 import Method as Arc56Method
@@ -30,6 +31,9 @@
3031
from algokit_utils.models.transaction import SendParams, TransactionWrapper
3132
from algokit_utils.protocols.account import TransactionSignerAccountProtocol
3233

34+
if _EXPERIMENTAL_DEPENDENCIES_INSTALLED:
35+
from algokit_utils.transactions._algokit_core_bridge import build_payment_with_core
36+
3337
if TYPE_CHECKING:
3438
from collections.abc import Callable
3539

@@ -1861,6 +1865,17 @@ def send(
18611865
)
18621866
except algosdk.error.AlgodHTTPError as e:
18631867
raise Exception(f"Transaction failed: {e}") from e
1868+
# We need this code to handle separately an exception thrown by the experimental AlgoKit Algod Client.
1869+
# However, we can't just import the dependency (as it may not be there) and
1870+
# we still need to re-throw the exception in all other cases.
1871+
except Exception as e:
1872+
if _EXPERIMENTAL_DEPENDENCIES_INSTALLED:
1873+
from algokit_algod_api.exceptions import BadRequestException
1874+
1875+
if isinstance(e, BadRequestException):
1876+
raise Exception(f"Transaction failed: {e}") from e
1877+
raise e
1878+
raise e
18641879

18651880
def _handle_simulate_error(self, simulate_response: SimulateAtomicTransactionResponse) -> None:
18661881
# const failedGroup = simulateResponse?.txnGroups[0]
@@ -2250,7 +2265,10 @@ def _build_payment(
22502265
"close_remainder_to": params.close_remainder_to,
22512266
}
22522267

2253-
return self._common_txn_build_step(lambda x: algosdk.transaction.PaymentTxn(**x), params, txn_params)
2268+
if _EXPERIMENTAL_DEPENDENCIES_INSTALLED:
2269+
return self._common_txn_build_step(lambda x: build_payment_with_core(**x), params, txn_params)
2270+
else:
2271+
return self._common_txn_build_step(lambda x: algosdk.transaction.PaymentTxn(**x), params, txn_params)
22542272

22552273
def _build_asset_create(
22562274
self, params: AssetCreateParams, suggested_params: algosdk.transaction.SuggestedParams

tests/transactions/test_transaction_sender.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66
from algosdk.transaction import OnComplete
77

8-
from algokit_utils import SigningAccount
8+
from algokit_utils import _EXPERIMENTAL_DEPENDENCIES_INSTALLED, SigningAccount
99
from algokit_utils._legacy_v2.application_specification import ApplicationSpecification
1010
from algokit_utils.algorand import AlgorandClient
1111
from algokit_utils.applications.app_manager import AppManager
@@ -451,8 +451,8 @@ def test_payment_logging(
451451
)
452452
)
453453

454-
assert mock_debug.call_count == 1
455-
log_message = mock_debug.call_args[0][0]
454+
assert mock_debug.call_count == 1 if not _EXPERIMENTAL_DEPENDENCIES_INSTALLED else 2
455+
log_message = mock_debug.call_args_list[0][0][0]
456456
assert "Sending 1,000,000 µALGO" in log_message
457457
assert sender.address in log_message
458458
assert receiver.address in log_message

0 commit comments

Comments
 (0)