Skip to content

Commit 5bd48b7

Browse files
committed
feat: decoupling from algosdk; installing latest algokit utils
1 parent 60a9453 commit 5bd48b7

File tree

41 files changed

+947
-361
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+947
-361
lines changed

poetry.lock

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

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ keyring = "25.6.0"
2424
# this version has been tested to work with the vendored file
2525
pyjwt = "^2.10.1"
2626
cryptography = "^44.0.2" # pyjwt has a weak dependency on cryptography and explicitly requires it in the vendored file, hence the lock
27-
algokit-utils = "^4.0.1"
27+
algokit-utils = "^5.0.0a2"
2828
multiformats = "0.3.1"
2929
multiformats_config = "0.3.1" # pinned this to be in lockstep with multiformats
3030
jsondiff = "^2.0.0"
@@ -65,7 +65,7 @@ docs_generate = "sphinx-build -b markdown -E docs/sphinx docs/cli"
6565
docs_toc = "gfm-toc docs/cli/index.md -e 3"
6666
docs_title = {shell = "(echo \"# AlgoKit CLI Reference Documentation\\n\\n\"; cat docs/cli/index.md) > docs/cli/temp.md && mv docs/cli/temp.md docs/cli/index.md"}
6767
docs = ["docs_generate", "docs_toc", "docs_title"]
68-
package_unix = "pyinstaller --clean --onedir --hidden-import jinja2_ansible_filters --hidden-import multiformats_config --copy-metadata algokit --name algokit --noconfirm src/algokit/__main__.py --add-data './misc/multiformats_config:multiformats_config/' --add-data './src/algokit/resources:algokit/resources/'"
68+
package_unix = "pyinstaller --clean --onedir --hidden-import jinja2_ansible_filters --hidden-import multiformats_config --hidden-import algokit_algod_client --hidden-import algokit_transact --hidden-import algokit_common --hidden-import algokit_algosdk --hidden-import algokit_algo25 --copy-metadata algokit --name algokit --noconfirm src/algokit/__main__.py --add-data './misc/multiformats_config:multiformats_config/' --add-data './src/algokit/resources:algokit/resources/'"
6969
package_windows = { cmd = "scripts/package_windows.bat" }
7070
package_mac = { cmd = "scripts/package_mac.sh" }
7171

scripts/package_mac.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/bin/bash
22

3-
CMD="pyinstaller --clean --onedir --hidden-import jinja2_ansible_filters --hidden-import multiformats_config --copy-metadata algokit --name algokit --noconfirm src/algokit/__main__.py --add-data './misc/multiformats_config/multibase-table.json:multiformats_config/' --add-data './misc/multiformats_config/multicodec-table.json:multiformats_config/' --add-data './src/algokit/resources:algokit/resources/'"
3+
CMD="pyinstaller --clean --onedir --hidden-import jinja2_ansible_filters --hidden-import multiformats_config --hidden-import algokit_algod_client --hidden-import algokit_transact --hidden-import algokit_common --hidden-import algokit_algosdk --hidden-import algokit_algo25 --copy-metadata algokit --name algokit --noconfirm src/algokit/__main__.py --add-data './misc/multiformats_config/multibase-table.json:multiformats_config/' --add-data './misc/multiformats_config/multicodec-table.json:multiformats_config/' --add-data './src/algokit/resources:algokit/resources/'"
44

55
if [ ! -z "$APPLE_BUNDLE_ID" ]; then
66
CMD="$CMD --osx-bundle-identifier \"$APPLE_BUNDLE_ID\""

scripts/package_windows.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
@echo off
2-
pyinstaller --clean --onedir --hidden-import jinja2_ansible_filters --hidden-import multiformats_config --copy-metadata algokit --name algokit --noconfirm src/algokit/__main__.py --add-data ./misc/multiformats_config;multiformats_config/ --add-data ./src/algokit/resources;algokit/resources/
2+
pyinstaller --clean --onedir --hidden-import jinja2_ansible_filters --hidden-import multiformats_config --hidden-import algokit_algod_client --hidden-import algokit_transact --hidden-import algokit_common --hidden-import algokit_algosdk --hidden-import algokit_algo25 --copy-metadata algokit --name algokit --noconfirm src/algokit/__main__.py --add-data ./misc/multiformats_config;multiformats_config/ --add-data ./src/algokit/resources;algokit/resources/

src/algokit/cli/project/deploy.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import base64
12
import logging
23
import os
34
import typing as t
45
from pathlib import Path
56

67
import click
7-
from algosdk.mnemonic import from_private_key
8+
from algokit_utils.algo25 import secret_key_to_mnemonic
89

910
from algokit.cli.common.utils import MutuallyExclusiveOption, sanitize_extra_args
1011
from algokit.core import proc
@@ -47,7 +48,9 @@ def _ensure_aliases(
4748
raise click.ClickException(f"Error: missing {alias} alias")
4849
if not alias_data.private_key:
4950
raise click.ClickException(f"Error: missing private key for {alias} alias")
50-
config_env[key] = from_private_key(alias_data.private_key) # type: ignore[no-untyped-call]
51+
# Private key is stored as base64 string, decode to bytes for mnemonic conversion
52+
private_key_bytes = base64.b64decode(alias_data.private_key)
53+
config_env[key] = secret_key_to_mnemonic(private_key_bytes)
5154
logger.debug(f"Loaded {alias} alias mnemonic as {key} environment variable")
5255

5356

src/algokit/cli/tasks/assets.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import logging
22

33
import click
4-
from algosdk import error
5-
from algosdk.v2client.algod import AlgodClient
4+
from algokit_utils.clients import AlgodClient, UnexpectedStatusError
65

76
from algokit.cli.common.constants import AlgorandNetwork, ExplorerEntityType
87
from algokit.cli.common.utils import get_explorer_url
@@ -73,7 +72,7 @@ def opt_in_command(asset_ids: tuple[int], account: str, network: AlgorandNetwork
7372
for asset_opt_int_result in response:
7473
explorer_url = get_explorer_url(asset_opt_int_result.transaction_id, network, ExplorerEntityType.ASSET)
7574
click.echo(f"Check opt-in status for asset {asset_opt_int_result.asset_id} at: {explorer_url}")
76-
except error.AlgodHTTPError as err:
75+
except UnexpectedStatusError as err:
7776
raise click.ClickException(str(err)) from err
7877
except ValueError as err:
7978
logger.debug(err, exc_info=True)
@@ -140,7 +139,7 @@ def opt_out_command(*, asset_ids: tuple[int], account: str, network: AlgorandNet
140139
asset_opt_out_result.transaction_id, network, ExplorerEntityType.TRANSACTION
141140
)
142141
click.echo(f"Check opt-in status for asset {asset_opt_out_result.asset_id} at: {transaction_url}")
143-
except error.AlgodHTTPError as err:
142+
except UnexpectedStatusError as err:
144143
raise click.ClickException(str(err)) from err
145144
except ConnectionRefusedError as err:
146145
raise click.ClickException(str(err)) from err

src/algokit/cli/tasks/mint.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from pathlib import Path
66

77
import click
8-
from algokit_utils import AlgoAmount, SigningAccount
9-
from algosdk.error import AlgodHTTPError
8+
from algokit_utils import AlgoAmount
9+
from algokit_utils.clients import UnexpectedStatusError
1010

1111
from algokit.cli.common.constants import AlgorandNetwork, ExplorerEntityType
1212
from algokit.cli.common.utils import get_explorer_url
@@ -16,6 +16,7 @@
1616
run_callback_once,
1717
validate_balance,
1818
)
19+
from algokit.core.signing_account import SigningAccount
1920
from algokit.core.tasks.ipfs import (
2021
PinataBadRequestError,
2122
PinataForbiddenError,
@@ -332,7 +333,7 @@ def mint( # noqa: PLR0913
332333
) as ex:
333334
logger.debug(ex)
334335
raise click.ClickException(repr(ex)) from ex
335-
except AlgodHTTPError as ex:
336+
except UnexpectedStatusError as ex:
336337
raise click.ClickException(str(ex)) from ex
337338
except Exception as ex:
338339
logger.debug(ex, exc_info=True)

src/algokit/cli/tasks/send_transaction.py

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import base64
12
import json
23
import logging
34
from pathlib import Path
45
from typing import TYPE_CHECKING, cast
56

67
import click
7-
from algosdk import encoding, error
8-
from algosdk.transaction import SignedTransaction, retrieve_from_file
8+
import msgpack
9+
from algokit_utils.clients import UnexpectedStatusError
10+
from algokit_utils.transact import SignedTransaction, decode_signed_transaction, encode_signed_transaction
911

1012
from algokit.cli.common.constants import AlgorandNetwork, ExplorerEntityType
1113
from algokit.cli.common.utils import MutuallyExclusiveOption, get_explorer_url
@@ -34,6 +36,60 @@ def _is_sign_task_output_txn(item: dict) -> bool:
3436
return isinstance(item, dict) and all(key in item for key in ["transaction_id", "content"])
3537

3638

39+
def _retrieve_transactions_from_file(file_path: Path) -> list[SignedTransaction]:
40+
"""
41+
Load signed transactions from a file containing msgpack-encoded data.
42+
43+
Args:
44+
file_path: Path to the file containing signed transaction(s)
45+
46+
Returns:
47+
A list of SignedTransaction objects
48+
49+
Raises:
50+
click.ClickException: If the file cannot be read or decoded
51+
"""
52+
try:
53+
file_content = file_path.read_bytes()
54+
55+
# Try to decode as a single transaction first
56+
try:
57+
return [decode_signed_transaction(file_content)]
58+
except Exception:
59+
pass
60+
61+
# Try to parse multiple concatenated msgpack objects
62+
transactions: list[SignedTransaction] = []
63+
offset = 0
64+
65+
while offset < len(file_content):
66+
# Use msgpack Unpacker to find the boundaries of each msgpack object
67+
unpacker = msgpack.Unpacker(raw=True, strict_map_key=False)
68+
unpacker.feed(file_content[offset:])
69+
try:
70+
_ = unpacker.unpack() # Advance to get the position
71+
msgpack_bytes_consumed = unpacker.tell()
72+
73+
# Extract just this msgpack object
74+
encoded_txn = file_content[offset : offset + msgpack_bytes_consumed]
75+
offset += msgpack_bytes_consumed
76+
77+
# Decode the signed transaction
78+
stx = decode_signed_transaction(encoded_txn)
79+
transactions.append(stx)
80+
except msgpack.OutOfData:
81+
break
82+
except Exception:
83+
break
84+
85+
if transactions:
86+
return transactions
87+
raise ValueError("No valid transactions found in file") from None
88+
except Exception as ex:
89+
logger.debug(ex, exc_info=True)
90+
raise click.ClickException(f"Failed to read transactions from file: {ex}") from ex
91+
92+
3793
def _load_from_stdin() -> list[SignedTransaction]:
3894
"""
3995
Load transaction data from standard input and convert it into a list of SignedTransaction objects.
@@ -59,7 +115,7 @@ def _load_from_stdin() -> list[SignedTransaction]:
59115
raise click.ClickException("Invalid piped transaction content!")
60116

61117
# Convert the content into SignedTransaction objects
62-
return [encoding.msgpack_decode(item["content"]) for item in file_content] # type: ignore[no-untyped-call]
118+
return [decode_signed_transaction(base64.b64decode(item["content"])) for item in file_content]
63119

64120

65121
def _get_signed_transactions(file: Path | None = None, transaction: str | None = None) -> list[SignedTransaction]:
@@ -80,17 +136,17 @@ def _get_signed_transactions(file: Path | None = None, transaction: str | None =
80136
"""
81137
try:
82138
if file:
83-
txns = retrieve_from_file(str(file)) # type: ignore[no-untyped-call]
139+
txns = _retrieve_transactions_from_file(file)
84140
elif transaction:
85-
txns = [encoding.msgpack_decode(transaction)] # type: ignore[no-untyped-call]
141+
txns = [decode_signed_transaction(base64.b64decode(transaction))]
86142
else:
87143
txns = _load_from_stdin()
88144

89145
for txn in txns:
90146
if not isinstance(txn, SignedTransaction):
91147
raise click.ClickException("Supplied transaction is not signed!")
92148

93-
return cast("list[SignedTransaction]", txns)
149+
return txns
94150

95151
except Exception as ex:
96152
logger.debug(ex, exc_info=True)
@@ -112,16 +168,21 @@ def _send_transactions(network: AlgorandNetwork, txns: list[SignedTransaction])
112168
"""
113169
algod_client = load_algod_client(network)
114170

115-
if any(txn.transaction.group for txn in txns):
116-
txid = algod_client.send_transactions(txns)
171+
if any(txn.txn.group for txn in txns):
172+
# Send all transactions as a group
173+
encoded_txns = [encode_signed_transaction(txn) for txn in txns]
174+
result = algod_client.send_raw_transaction(encoded_txns)
175+
txid = result.tx_id
117176
click.echo(f"Transaction group successfully sent with txid: {txid}")
118177
click.echo(
119178
f"Check transaction group status at: {get_explorer_url(txid, network, ExplorerEntityType.TRANSACTION)}"
120179
)
121180
else:
122181
for index, txn in enumerate(txns, start=1):
123182
click.echo(f"\nSending transaction {index}/{len(txns)}")
124-
txid = algod_client.send_transaction(txn)
183+
encoded_txn = encode_signed_transaction(txn)
184+
result = algod_client.send_raw_transaction(encoded_txn)
185+
txid = result.tx_id
125186
click.echo(f"Transaction successfully sent with txid: {txid}")
126187
click.echo(
127188
f"Check transaction status at: {get_explorer_url(txid, network, ExplorerEntityType.TRANSACTION)}"
@@ -169,7 +230,7 @@ def send(*, file: Path | None, transaction: str | None, network: AlgorandNetwork
169230

170231
try:
171232
_send_transactions(network, txns)
172-
except error.AlgodHTTPError as ex:
233+
except UnexpectedStatusError as ex:
173234
raise click.ClickException(str(ex)) from ex
174235
except Exception as ex:
175236
logger.debug(ex, exc_info=True)

0 commit comments

Comments
 (0)