Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
12a8e0e
fix: expose additional imports
daniel-makerx Mar 23, 2023
cb4f11e
feat: extract call parameters into their own datatypes
daniel-makerx Mar 23, 2023
dab18bd
chore: fix linting
daniel-makerx Mar 23, 2023
4438ad5
fix: add transaction details to deploy response
daniel-makerx Mar 23, 2023
c93ae11
chore: remove usages of cast
daniel-makerx Mar 23, 2023
912e6e9
feat: move ABI args to kwargs
daniel-makerx Mar 23, 2023
4a6b7de
chore: import fixes
daniel-makerx Mar 24, 2023
7201b44
fix: expose execute_atc
daniel-makerx Mar 24, 2023
db06816
fix: sort method_config when exporting
daniel-makerx Mar 24, 2023
2044a80
fix: flatten ABIResult into responses
daniel-makerx Mar 24, 2023
9af0328
test: add application client resolve tests
daniel-makerx Mar 24, 2023
4dec59f
test: move tests around
daniel-makerx Mar 24, 2023
7c48058
fix: add additional call parameters
daniel-makerx Mar 24, 2023
25a3f86
feat: move template substitution to constructor
daniel-makerx Mar 24, 2023
00d84cd
fix: improve method signatures
daniel-makerx Mar 24, 2023
4a5c6ed
chore: reorder some arguments
daniel-makerx Mar 24, 2023
a6264d6
refactor: extract some methods and move into other modules
daniel-makerx Mar 27, 2023
8488940
tests: reorganize tests
daniel-makerx Mar 27, 2023
5844079
test: add call parameter tests
daniel-makerx Mar 27, 2023
490ff73
test: add update and delete tests
daniel-makerx Mar 27, 2023
6262481
test: add opt_in, close_out, clear_state tests
daniel-makerx Mar 27, 2023
0cea97d
feat: add overloads for application client methods
daniel-makerx Mar 28, 2023
fb40d7a
feat: finish transfer implementation
daniel-makerx Mar 28, 2023
b77334d
test: improve test performance by reducing number of accounts created
daniel-makerx Mar 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 42 additions & 21 deletions src/algokit_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,28 @@
transfer,
)
from algokit_utils.account import (
create_kmd_wallet_account,
get_account,
get_account_from_mnemonic,
get_dispenser_account,
get_kmd_wallet_account,
get_or_create_kmd_wallet_account,
get_sandbox_default_account,
)
from algokit_utils.app import (
DELETABLE_TEMPLATE_NAME,
NOTE_PREFIX,
UPDATABLE_TEMPLATE_NAME,
AppDeployMetaData,
AppLookup,
AppMetaData,
AppReference,
DeploymentFailedError,
get_creator_apps,
replace_template_variables,
)
from algokit_utils.application_client import (
ABICallArgs,
ABITransactionResponse,
ABICallArgsDict,
ABICreateCallArgs,
ABICreateCallArgsDict,
ApplicationClient,
DeployResponse,
OnSchemaBreak,
OnUpdate,
OperationPerformed,
CommonCallParameters,
CommonCallParametersDict,
CreateCallParameters,
CreateCallParametersDict,
OnCompleteCallParameters,
OnCompleteCallParametersDict,
Program,
TransactionResponse,
execute_atc_with_logic_error,
get_app_id_from_tx_id,
get_next_version,
num_extra_program_pages,
Expand All @@ -46,8 +39,24 @@
MethodHints,
OnCompleteActionName,
)
from algokit_utils.deploy import (
DELETABLE_TEMPLATE_NAME,
NOTE_PREFIX,
UPDATABLE_TEMPLATE_NAME,
AppDeployMetaData,
AppLookup,
AppMetaData,
AppReference,
DeploymentFailedError,
DeployResponse,
OnSchemaBreak,
OnUpdate,
OperationPerformed,
get_creator_apps,
replace_template_variables,
)
from algokit_utils.logic_error import LogicError
from algokit_utils.models import Account
from algokit_utils.models import ABITransactionResponse, Account, TransactionResponse
from algokit_utils.network_clients import (
AlgoClientConfig,
get_algod_client,
Expand All @@ -57,6 +66,7 @@
)

__all__ = [
"create_kmd_wallet_account",
"get_account_from_mnemonic",
"get_or_create_kmd_wallet_account",
"get_sandbox_default_account",
Expand All @@ -74,14 +84,23 @@
"get_creator_apps",
"replace_template_variables",
"ABICallArgs",
"ABITransactionResponse",
"ABICallArgsDict",
"ABICreateCallArgs",
"ABICreateCallArgsDict",
"ApplicationClient",
"CommonCallParameters",
"CommonCallParametersDict",
"CreateCallParameters",
"CreateCallParametersDict",
"OnCompleteCallParameters",
"OnCompleteCallParametersDict",
"ApplicationClient",
"DeployResponse",
"OnUpdate",
"OnSchemaBreak",
"OperationPerformed",
"Program",
"TransactionResponse",
"execute_atc_with_logic_error",
"get_app_id_from_tx_id",
"get_next_version",
"num_extra_program_pages",
Expand All @@ -94,7 +113,9 @@
"OnCompleteActionName",
"MethodHints",
"LogicError",
"ABITransactionResponse",
"Account",
"TransactionResponse",
"AlgoClientConfig",
"get_algod_client",
"get_indexer_client",
Expand Down
72 changes: 54 additions & 18 deletions src/algokit_utils/_transfer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import dataclasses
import logging

from algosdk.transaction import PaymentTxn
import algosdk.transaction
from algosdk.account import address_from_private_key
from algosdk.atomic_transaction_composer import AccountTransactionSigner
from algosdk.transaction import PaymentTxn, SuggestedParams
from algosdk.v2client.algod import AlgodClient

from algokit_utils.models import Account
Expand All @@ -12,30 +15,63 @@

@dataclasses.dataclass(kw_only=True)
class TransferParameters:
from_account: Account
from_account: Account | AccountTransactionSigner
"""The account (with private key) or signer that will send the µALGOs"""
to_address: str
amount: int
note: str | None = None
max_fee_in_algos: float | None = None
"""The account address that will receive the µALGOs"""
micro_algos: int
"""The amount of µALGOs to send"""
suggested_params: SuggestedParams | None = None
"""(optional) transaction parameters"""
note: str | bytes | None = None
"""(optional) transaction note"""
fee_micro_algos: int | None = None
"""(optional) The flat fee you want to pay, useful for covering extra fees in a transaction group or app call"""
max_fee_micro_algos: int | None = None
"""(optional)The maximum fee that you are happy to pay (default: unbounded) -
if this is set it's possible the transaction could get rejected during network congestion"""


def transfer(transfer_parameters: TransferParameters, client: AlgodClient) -> tuple[PaymentTxn, str]:
suggested_params = client.suggested_params()
def _check_fee(transaction: PaymentTxn, max_fee: int | None) -> None:
if max_fee is not None:
# Once a transaction has been constructed by algosdk, transaction.fee indicates what the total transaction fee
# Will be based on the current suggested fee-per-byte value.
if transaction.fee > max_fee:
raise Exception(
f"Cancelled transaction due to high network congestion fees. "
f"Algorand suggested fees would cause this transaction to cost {transaction.fee} µALGOs. "
f"Cap for this transaction is {max_fee} µALGOs."
)
elif transaction.fee > algosdk.constants.MIN_TXN_FEE:
logger.warning(
f"Algorand network congestion fees are in effect. "
f"This transaction will incur a fee of {transaction.fee} µALGOs."
)


def transfer(client: AlgodClient, parameters: TransferParameters) -> PaymentTxn:
suggested_params = parameters.suggested_params or client.suggested_params()
from_account = parameters.from_account
sender = address_from_private_key(from_account.private_key) # type: ignore[no-untyped-call]
transaction = PaymentTxn(
sender=transfer_parameters.from_account.address,
sender=sender,
receiver=parameters.to_address,
amt=parameters.micro_algos,
note=parameters.note.encode("utf-8") if isinstance(parameters.note, str) else parameters.note,
sp=suggested_params,
receiver=transfer_parameters.to_address,
amt=transfer_parameters.amount,
close_remainder_to=None,
note=transfer_parameters.note.encode("utf-8") if transfer_parameters.note else None,
rekey_to=None,
) # type: ignore[no-untyped-call]
# TODO: max fee
from_account = transfer_parameters.from_account
if parameters.fee_micro_algos:
transaction.fee = parameters.fee_micro_algos

if not suggested_params.flat_fee:
_check_fee(transaction, parameters.max_fee_micro_algos)
signed_transaction = transaction.sign(from_account.private_key) # type: ignore[no-untyped-call]
send_response = client.send_transaction(signed_transaction)
client.send_transaction(signed_transaction)

txid = transaction.get_txid() # type: ignore[no-untyped-call]
logger.debug(f"Sent transaction {txid} type={transaction.type} from {from_account.address}")
logger.debug(
f"Sent transaction {txid} type={transaction.type} from "
f"{address_from_private_key(from_account.private_key)}" # type: ignore[no-untyped-call]
)

return transaction, send_response
return transaction
54 changes: 32 additions & 22 deletions src/algokit_utils/account.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
import os
from collections.abc import Callable
from typing import Any, cast
from typing import Any

from algosdk.account import address_from_private_key
from algosdk.kmd import KMDClient
Expand Down Expand Up @@ -31,40 +31,49 @@ def get_account_from_mnemonic(mnemonic: str) -> Account:
return Account(private_key, address)


def create_kmd_wallet_account(kmd_client: KMDClient, name: str) -> Account:
wallet_id = kmd_client.create_wallet(name, "")["id"] # type: ignore[no-untyped-call]
wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") # type: ignore[no-untyped-call]
kmd_client.generate_key(wallet_handle) # type: ignore[no-untyped-call]

key_ids: list[str] = kmd_client.list_keys(wallet_handle) # type: ignore[no-untyped-call]
account_key = key_ids[0]

private_account_key = kmd_client.export_key(wallet_handle, "", account_key) # type: ignore[no-untyped-call]
return get_account_from_mnemonic(from_private_key(private_account_key)) # type: ignore[no-untyped-call]


def get_or_create_kmd_wallet_account(
client: AlgodClient, name: str, fund_with: int | None, kmd_client: KMDClient | None = None
client: AlgodClient, name: str, fund_with_algos: float = 1000, kmd_client: KMDClient | None = None
) -> Account:
kmd_client = kmd_client or get_kmd_client_from_algod_client(client)
fund_with = 1000 if fund_with is None else fund_with
account = get_kmd_wallet_account(client, kmd_client, name)

if account:
account_info = cast(dict[str, Any], client.account_info(account.address))
account_info = client.account_info(account.address)
assert isinstance(account_info, dict)
if account_info["amount"] > 0:
return account
logger.debug(f"Found existing account in Sandbox with name '{name}'." f"But no funds in the account.")
logger.debug(f"Found existing account in Sandbox with name '{name}', but no funds in the account.")
else:
wallet_id = kmd_client.create_wallet(name, "")["id"] # type: ignore[no-untyped-call]
wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") # type: ignore[no-untyped-call]
kmd_client.generate_key(wallet_handle) # type: ignore[no-untyped-call]
account = create_kmd_wallet_account(kmd_client, name)

account = get_kmd_wallet_account(client, kmd_client, name)
assert account
logger.debug(
f"Couldn't find existing account in Sandbox with name '{name}'. "
f"So created account {account.address} with keys stored in KMD."
)

logger.debug(f"Funding account {account.address} with {fund_with} ALGOs")
logger.debug(f"Funding account {account.address} with {fund_with_algos} ALGOs")

transfer(
TransferParameters(
from_account=get_dispenser_account(client),
to_address=account.address,
amount=algos_to_microalgos(fund_with), # type: ignore[no-untyped-call]
),
client,
)
if fund_with_algos:
transfer(
client,
TransferParameters(
from_account=get_dispenser_account(client),
to_address=account.address,
micro_algos=algos_to_microalgos(fund_with_algos), # type: ignore[no-untyped-call]
),
)

return account

Expand Down Expand Up @@ -105,7 +114,8 @@ def get_kmd_wallet_account(
matched_account_key = None
if predicate:
for key in key_ids:
account = cast(dict[str, Any], client.account_info(key))
account = client.account_info(key)
assert isinstance(account, dict)
if predicate(account):
matched_account_key = key
else:
Expand All @@ -119,15 +129,15 @@ def get_kmd_wallet_account(


def get_account(
client: AlgodClient, name: str, fund_with: int | None = None, kmd_client: KMDClient | None = None
client: AlgodClient, name: str, fund_with_algos: float = 1000, kmd_client: KMDClient | None = None
) -> Account:
mnemonic_key = f"{name.upper()}_MNEMONIC"
mnemonic = os.getenv(mnemonic_key)
if mnemonic:
return get_account_from_mnemonic(mnemonic)

if is_sandbox(client):
account = get_or_create_kmd_wallet_account(client, name, fund_with, kmd_client)
account = get_or_create_kmd_wallet_account(client, name, fund_with_algos, kmd_client)
os.environ[mnemonic_key] = from_private_key(account.private_key) # type: ignore[no-untyped-call]
return account

Expand Down
Loading