algokit-utils 2.2.2b5__py3-none-any.whl → 2.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of algokit-utils might be problematic. Click here for more details.
- algokit_utils/_transfer.py +11 -6
- algokit_utils/application_client.py +11 -18
- algokit_utils/beta/account_manager.py +200 -0
- algokit_utils/beta/algorand_client.py +319 -0
- algokit_utils/beta/client_manager.py +78 -0
- algokit_utils/beta/composer.py +716 -0
- algokit_utils/deploy.py +6 -20
- algokit_utils/models.py +5 -107
- algokit_utils/network_clients.py +17 -0
- {algokit_utils-2.2.2b5.dist-info → algokit_utils-2.3.0.dist-info}/METADATA +1 -1
- algokit_utils-2.3.0.dist-info/RECORD +24 -0
- algokit_utils-2.2.2b5.dist-info/RECORD +0 -20
- {algokit_utils-2.2.2b5.dist-info → algokit_utils-2.3.0.dist-info}/LICENSE +0 -0
- {algokit_utils-2.2.2b5.dist-info → algokit_utils-2.3.0.dist-info}/WHEEL +0 -0
algokit_utils/_transfer.py
CHANGED
|
@@ -86,7 +86,7 @@ def transfer(client: "AlgodClient", parameters: TransferParameters) -> PaymentTx
|
|
|
86
86
|
params = parameters
|
|
87
87
|
params.suggested_params = parameters.suggested_params or client.suggested_params()
|
|
88
88
|
from_account = params.from_account
|
|
89
|
-
sender =
|
|
89
|
+
sender = _get_address(from_account)
|
|
90
90
|
transaction = PaymentTxn(
|
|
91
91
|
sender=sender,
|
|
92
92
|
receiver=params.to_address,
|
|
@@ -105,7 +105,7 @@ def transfer_asset(client: "AlgodClient", parameters: TransferAssetParameters) -
|
|
|
105
105
|
|
|
106
106
|
params = parameters
|
|
107
107
|
params.suggested_params = parameters.suggested_params or client.suggested_params()
|
|
108
|
-
sender =
|
|
108
|
+
sender = _get_address(parameters.from_account)
|
|
109
109
|
suggested_params = parameters.suggested_params or client.suggested_params()
|
|
110
110
|
xfer_txn = AssetTransferTxn(
|
|
111
111
|
sp=suggested_params,
|
|
@@ -139,9 +139,14 @@ def _send_transaction(
|
|
|
139
139
|
client.send_transaction(signed_transaction)
|
|
140
140
|
|
|
141
141
|
txid = transaction.get_txid() # type: ignore[no-untyped-call]
|
|
142
|
-
logger.debug(
|
|
143
|
-
f"Sent transaction {txid} type={transaction.type} from "
|
|
144
|
-
f"{address_from_private_key(parameters.from_account.private_key)}" # type: ignore[no-untyped-call]
|
|
145
|
-
)
|
|
142
|
+
logger.debug(f"Sent transaction {txid} type={transaction.type} from {_get_address(parameters.from_account)}")
|
|
146
143
|
|
|
147
144
|
return transaction
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _get_address(account: Account | AccountTransactionSigner) -> str:
|
|
148
|
+
if type(account) is Account:
|
|
149
|
+
return account.address
|
|
150
|
+
else:
|
|
151
|
+
address = address_from_private_key(account.private_key) # type: ignore[no-untyped-call]
|
|
152
|
+
return str(address)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import copy
|
|
3
|
-
import dataclasses
|
|
4
3
|
import json
|
|
5
4
|
import logging
|
|
6
5
|
import re
|
|
@@ -59,6 +58,7 @@ if typing.TYPE_CHECKING:
|
|
|
59
58
|
from algosdk.v2client.algod import AlgodClient
|
|
60
59
|
from algosdk.v2client.indexer import IndexerClient
|
|
61
60
|
|
|
61
|
+
|
|
62
62
|
logger = logging.getLogger(__name__)
|
|
63
63
|
|
|
64
64
|
|
|
@@ -375,7 +375,7 @@ class ApplicationClient:
|
|
|
375
375
|
) -> None:
|
|
376
376
|
"""Adds a signed transaction with application id == 0 and the schema and source of client's app_spec to atc"""
|
|
377
377
|
approval_program, clear_program = self._check_is_compiled()
|
|
378
|
-
transaction_parameters = _convert_transaction_parameters(
|
|
378
|
+
transaction_parameters = _convert_transaction_parameters(transaction_parameters)
|
|
379
379
|
|
|
380
380
|
extra_pages = transaction_parameters.extra_pages or num_extra_program_pages(
|
|
381
381
|
approval_program.raw_binary, clear_program.raw_binary
|
|
@@ -388,7 +388,7 @@ class ApplicationClient:
|
|
|
388
388
|
abi_args=abi_kwargs,
|
|
389
389
|
on_complete=transaction_parameters.on_complete or transaction.OnComplete.NoOpOC,
|
|
390
390
|
call_config=au_spec.CallConfig.CREATE,
|
|
391
|
-
parameters=
|
|
391
|
+
parameters=transaction_parameters,
|
|
392
392
|
approval_program=approval_program.raw_binary,
|
|
393
393
|
clear_program=clear_program.raw_binary,
|
|
394
394
|
global_schema=self.app_spec.global_state_schema,
|
|
@@ -567,12 +567,12 @@ class ApplicationClient:
|
|
|
567
567
|
**abi_kwargs: ABIArgType,
|
|
568
568
|
) -> None:
|
|
569
569
|
"""Adds a signed transaction with specified parameters to atc"""
|
|
570
|
-
_parameters = _convert_transaction_parameters(
|
|
570
|
+
_parameters = _convert_transaction_parameters(transaction_parameters)
|
|
571
571
|
self.add_method_call(
|
|
572
572
|
atc,
|
|
573
573
|
abi_method=call_abi_method,
|
|
574
574
|
abi_args=abi_kwargs,
|
|
575
|
-
parameters=
|
|
575
|
+
parameters=_parameters,
|
|
576
576
|
on_complete=_parameters.on_complete or transaction.OnComplete.NoOpOC,
|
|
577
577
|
)
|
|
578
578
|
|
|
@@ -607,7 +607,7 @@ class ApplicationClient:
|
|
|
607
607
|
) -> TransactionResponse | ABITransactionResponse:
|
|
608
608
|
"""Submits a signed transaction with specified parameters"""
|
|
609
609
|
atc = AtomicTransactionComposer()
|
|
610
|
-
_parameters = _convert_transaction_parameters(
|
|
610
|
+
_parameters = _convert_transaction_parameters(transaction_parameters)
|
|
611
611
|
self.compose_call(
|
|
612
612
|
atc,
|
|
613
613
|
call_abi_method=call_abi_method,
|
|
@@ -1003,7 +1003,7 @@ class ApplicationClient:
|
|
|
1003
1003
|
if app_id is None:
|
|
1004
1004
|
self._load_reference_and_check_app_id()
|
|
1005
1005
|
app_id = self.app_id
|
|
1006
|
-
parameters = _convert_transaction_parameters(
|
|
1006
|
+
parameters = _convert_transaction_parameters(parameters)
|
|
1007
1007
|
method = self._resolve_method(abi_method, abi_args, on_complete, call_config)
|
|
1008
1008
|
sp = parameters.suggested_params or self.suggested_params or self.algod_client.suggested_params()
|
|
1009
1009
|
signer, sender = self.resolve_signer_sender(parameters.signer, parameters.sender)
|
|
@@ -1317,18 +1317,11 @@ def _create_simulate_traces(simulate: SimulateAtomicTransactionResponse) -> list
|
|
|
1317
1317
|
return traces
|
|
1318
1318
|
|
|
1319
1319
|
|
|
1320
|
-
_TParams = typing.TypeVar("_TParams", TransactionParameters, OnCompleteCallParameters, CreateCallParameters)
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
1320
|
def _convert_transaction_parameters(
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
)
|
|
1327
|
-
|
|
1328
|
-
return cls()
|
|
1329
|
-
args_dict = args.__dict__ if not isinstance(args, dict) else (args or {})
|
|
1330
|
-
_args = {f.name: args_dict[f.name] for f in dataclasses.fields(cls) if f.name in args_dict}
|
|
1331
|
-
return cls(**_args)
|
|
1321
|
+
args: TransactionParameters | TransactionParametersDict | None,
|
|
1322
|
+
) -> CreateCallParameters:
|
|
1323
|
+
_args = args.__dict__ if isinstance(args, TransactionParameters) else (args or {})
|
|
1324
|
+
return CreateCallParameters(**_args)
|
|
1332
1325
|
|
|
1333
1326
|
|
|
1334
1327
|
def get_sender_from_signer(signer: TransactionSigner | None) -> str | None:
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from algokit_utils.account import get_dispenser_account, get_kmd_wallet_account, get_localnet_default_account
|
|
6
|
+
from algosdk.account import generate_account
|
|
7
|
+
from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner
|
|
8
|
+
from typing_extensions import Self
|
|
9
|
+
|
|
10
|
+
from .client_manager import ClientManager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class AddressAndSigner:
|
|
15
|
+
address: str
|
|
16
|
+
signer: TransactionSigner
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AccountManager:
|
|
20
|
+
"""Creates and keeps track of addresses and signers"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, client_manager: ClientManager):
|
|
23
|
+
"""
|
|
24
|
+
Create a new account manager.
|
|
25
|
+
|
|
26
|
+
:param client_manager: The ClientManager client to use for algod and kmd clients
|
|
27
|
+
"""
|
|
28
|
+
self._client_manager = client_manager
|
|
29
|
+
self._accounts = dict[str, TransactionSigner]()
|
|
30
|
+
self._default_signer: TransactionSigner | None = None
|
|
31
|
+
|
|
32
|
+
def set_default_signer(self, signer: TransactionSigner) -> Self:
|
|
33
|
+
"""
|
|
34
|
+
Sets the default signer to use if no other signer is specified.
|
|
35
|
+
|
|
36
|
+
:param signer: The signer to use, either a `TransactionSigner` or a `TransactionSignerAccount`
|
|
37
|
+
:return: The `AccountManager` so method calls can be chained
|
|
38
|
+
"""
|
|
39
|
+
self._default_signer = signer
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def set_signer(self, sender: str, signer: TransactionSigner) -> Self:
|
|
43
|
+
"""
|
|
44
|
+
Tracks the given account for later signing.
|
|
45
|
+
|
|
46
|
+
:param sender: The sender address to use this signer for
|
|
47
|
+
:param signer: The signer to sign transactions with for the given sender
|
|
48
|
+
:return: The AccountCreator instance for method chaining
|
|
49
|
+
"""
|
|
50
|
+
self._accounts[sender] = signer
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def get_signer(self, sender: str) -> TransactionSigner:
|
|
54
|
+
"""
|
|
55
|
+
Returns the `TransactionSigner` for the given sender address.
|
|
56
|
+
|
|
57
|
+
If no signer has been registered for that address then the default signer is used if registered.
|
|
58
|
+
|
|
59
|
+
:param sender: The sender address
|
|
60
|
+
:return: The `TransactionSigner` or throws an error if not found
|
|
61
|
+
"""
|
|
62
|
+
signer = self._accounts.get(sender, None) or self._default_signer
|
|
63
|
+
if not signer:
|
|
64
|
+
raise ValueError(f"No signer found for address {sender}")
|
|
65
|
+
return signer
|
|
66
|
+
|
|
67
|
+
def get_information(self, sender: str) -> dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
Returns the given sender account's current status, balance and spendable amounts.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA"
|
|
73
|
+
account_info = account.get_information(address)
|
|
74
|
+
|
|
75
|
+
`Response data schema details <https://developer.algorand.org/docs/rest-apis/algod/#get-v2accountsaddress>`_
|
|
76
|
+
|
|
77
|
+
:param sender: The address of the sender/account to look up
|
|
78
|
+
:return: The account information
|
|
79
|
+
"""
|
|
80
|
+
info = self._client_manager.algod.account_info(sender)
|
|
81
|
+
assert isinstance(info, dict)
|
|
82
|
+
return info
|
|
83
|
+
|
|
84
|
+
def get_asset_information(self, sender: str, asset_id: int) -> dict[str, Any]:
|
|
85
|
+
info = self._client_manager.algod.account_asset_info(sender, asset_id)
|
|
86
|
+
assert isinstance(info, dict)
|
|
87
|
+
return info
|
|
88
|
+
|
|
89
|
+
# TODO
|
|
90
|
+
# def from_mnemonic(self, mnemonic_secret: str, sender: Optional[str] = None) -> AddrAndSigner:
|
|
91
|
+
# """
|
|
92
|
+
# Tracks and returns an Algorand account with secret key loaded (i.e. that can sign transactions) by taking the mnemonic secret.
|
|
93
|
+
|
|
94
|
+
# Example:
|
|
95
|
+
# account = account.from_mnemonic("mnemonic secret ...")
|
|
96
|
+
# rekeyed_account = account.from_mnemonic("mnemonic secret ...", "SENDERADDRESS...")
|
|
97
|
+
|
|
98
|
+
# :param mnemonic_secret: The mnemonic secret representing the private key of an account; **Note: Be careful how the mnemonic is handled**,
|
|
99
|
+
# never commit it into source control and ideally load it from the environment (ideally via a secret storage service) rather than the file system.
|
|
100
|
+
# :param sender: The optional sender address to use this signer for (aka a rekeyed account)
|
|
101
|
+
# :return: The account
|
|
102
|
+
# """
|
|
103
|
+
# account = mnemonic_account(mnemonic_secret)
|
|
104
|
+
# return self.signer_account(rekeyed_account(account, sender) if sender else account)
|
|
105
|
+
|
|
106
|
+
def from_kmd(
|
|
107
|
+
self,
|
|
108
|
+
name: str,
|
|
109
|
+
predicate: Callable[[dict[str, Any]], bool] | None = None,
|
|
110
|
+
) -> AddressAndSigner:
|
|
111
|
+
"""
|
|
112
|
+
Tracks and returns an Algorand account with private key loaded from the given KMD wallet (identified by name).
|
|
113
|
+
|
|
114
|
+
Example (Get default funded account in a LocalNet):
|
|
115
|
+
default_dispenser_account = account.from_kmd('unencrypted-default-wallet',
|
|
116
|
+
lambda a: a['status'] != 'Offline' and a['amount'] > 1_000_000_000
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
:param name: The name of the wallet to retrieve an account from
|
|
120
|
+
:param predicate: An optional filter to use to find the account (otherwise it will return a random account from the wallet)
|
|
121
|
+
:return: The account
|
|
122
|
+
"""
|
|
123
|
+
account = get_kmd_wallet_account(
|
|
124
|
+
name=name, predicate=predicate, client=self._client_manager.algod, kmd_client=self._client_manager.kmd
|
|
125
|
+
)
|
|
126
|
+
if not account:
|
|
127
|
+
raise ValueError(f"Unable to find KMD account {name}{' with predicate' if predicate else ''}")
|
|
128
|
+
|
|
129
|
+
self.set_signer(account.address, account.signer)
|
|
130
|
+
return AddressAndSigner(address=account.address, signer=account.signer)
|
|
131
|
+
|
|
132
|
+
# TODO
|
|
133
|
+
# def multisig(
|
|
134
|
+
# self, multisig_params: algosdk.MultisigMetadata, signing_accounts: Union[algosdk.Account, SigningAccount]
|
|
135
|
+
# ) -> TransactionSignerAccount:
|
|
136
|
+
# """
|
|
137
|
+
# Tracks and returns an account that supports partial or full multisig signing.
|
|
138
|
+
|
|
139
|
+
# Example:
|
|
140
|
+
# account = account.multisig(
|
|
141
|
+
# {
|
|
142
|
+
# "version": 1,
|
|
143
|
+
# "threshold": 1,
|
|
144
|
+
# "addrs": ["ADDRESS1...", "ADDRESS2..."]
|
|
145
|
+
# },
|
|
146
|
+
# account.from_environment('ACCOUNT1')
|
|
147
|
+
# )
|
|
148
|
+
|
|
149
|
+
# :param multisig_params: The parameters that define the multisig account
|
|
150
|
+
# :param signing_accounts: The signers that are currently present
|
|
151
|
+
# :return: A multisig account wrapper
|
|
152
|
+
# """
|
|
153
|
+
# return self.signer_account(multisig_account(multisig_params, signing_accounts))
|
|
154
|
+
|
|
155
|
+
def random(self) -> AddressAndSigner:
|
|
156
|
+
"""
|
|
157
|
+
Tracks and returns a new, random Algorand account with secret key loaded.
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
account = account.random()
|
|
161
|
+
|
|
162
|
+
:return: The account
|
|
163
|
+
"""
|
|
164
|
+
(sk, addr) = generate_account() # type: ignore[no-untyped-call]
|
|
165
|
+
signer = AccountTransactionSigner(sk)
|
|
166
|
+
|
|
167
|
+
self.set_signer(addr, signer)
|
|
168
|
+
|
|
169
|
+
return AddressAndSigner(address=addr, signer=signer)
|
|
170
|
+
|
|
171
|
+
def dispenser(self) -> AddressAndSigner:
|
|
172
|
+
"""
|
|
173
|
+
Returns an account (with private key loaded) that can act as a dispenser.
|
|
174
|
+
|
|
175
|
+
Example:
|
|
176
|
+
account = account.dispenser()
|
|
177
|
+
|
|
178
|
+
If running on LocalNet then it will return the default dispenser account automatically,
|
|
179
|
+
otherwise it will load the account mnemonic stored in os.environ['DISPENSER_MNEMONIC'].
|
|
180
|
+
|
|
181
|
+
:return: The account
|
|
182
|
+
"""
|
|
183
|
+
acct = get_dispenser_account(self._client_manager.algod)
|
|
184
|
+
|
|
185
|
+
self.set_signer(acct.address, acct.signer)
|
|
186
|
+
|
|
187
|
+
return AddressAndSigner(address=acct.address, signer=acct.signer)
|
|
188
|
+
|
|
189
|
+
def localnet_dispenser(self) -> AddressAndSigner:
|
|
190
|
+
"""
|
|
191
|
+
Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts).
|
|
192
|
+
|
|
193
|
+
Example:
|
|
194
|
+
account = account.localnet_dispenser()
|
|
195
|
+
|
|
196
|
+
:return: The account
|
|
197
|
+
"""
|
|
198
|
+
acct = get_localnet_default_account(self._client_manager.algod)
|
|
199
|
+
self.set_signer(acct.address, acct.signer)
|
|
200
|
+
return AddressAndSigner(address=acct.address, signer=acct.signer)
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import time
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from algokit_utils.beta.account_manager import AccountManager
|
|
8
|
+
from algokit_utils.beta.client_manager import AlgoSdkClients, ClientManager
|
|
9
|
+
from algokit_utils.beta.composer import (
|
|
10
|
+
AlgokitComposer,
|
|
11
|
+
AppCallParams,
|
|
12
|
+
AssetConfigParams,
|
|
13
|
+
AssetCreateParams,
|
|
14
|
+
AssetDestroyParams,
|
|
15
|
+
AssetFreezeParams,
|
|
16
|
+
AssetOptInParams,
|
|
17
|
+
AssetTransferParams,
|
|
18
|
+
MethodCallParams,
|
|
19
|
+
OnlineKeyRegParams,
|
|
20
|
+
PayParams,
|
|
21
|
+
)
|
|
22
|
+
from algokit_utils.network_clients import (
|
|
23
|
+
AlgoClientConfigs,
|
|
24
|
+
get_algod_client,
|
|
25
|
+
get_algonode_config,
|
|
26
|
+
get_default_localnet_config,
|
|
27
|
+
get_indexer_client,
|
|
28
|
+
get_kmd_client,
|
|
29
|
+
)
|
|
30
|
+
from algosdk.atomic_transaction_composer import AtomicTransactionResponse, TransactionSigner
|
|
31
|
+
from algosdk.transaction import SuggestedParams, Transaction, wait_for_confirmation
|
|
32
|
+
from typing_extensions import Self
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"AlgorandClient",
|
|
36
|
+
"AssetCreateParams",
|
|
37
|
+
"AssetOptInParams",
|
|
38
|
+
"MethodCallParams",
|
|
39
|
+
"PayParams",
|
|
40
|
+
"AssetFreezeParams",
|
|
41
|
+
"AssetConfigParams",
|
|
42
|
+
"AssetDestroyParams",
|
|
43
|
+
"AppCallParams",
|
|
44
|
+
"OnlineKeyRegParams",
|
|
45
|
+
"AssetTransferParams",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class AlgorandClientSendMethods:
|
|
51
|
+
"""
|
|
52
|
+
Methods used to send a transaction to the network and wait for confirmation
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
payment: Callable[[PayParams], dict[str, Any]]
|
|
56
|
+
asset_create: Callable[[AssetCreateParams], dict[str, Any]]
|
|
57
|
+
asset_config: Callable[[AssetConfigParams], dict[str, Any]]
|
|
58
|
+
asset_freeze: Callable[[AssetFreezeParams], dict[str, Any]]
|
|
59
|
+
asset_destroy: Callable[[AssetDestroyParams], dict[str, Any]]
|
|
60
|
+
asset_transfer: Callable[[AssetTransferParams], dict[str, Any]]
|
|
61
|
+
app_call: Callable[[AppCallParams], dict[str, Any]]
|
|
62
|
+
online_key_reg: Callable[[OnlineKeyRegParams], dict[str, Any]]
|
|
63
|
+
method_call: Callable[[MethodCallParams], dict[str, Any]]
|
|
64
|
+
asset_opt_in: Callable[[AssetOptInParams], dict[str, Any]]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class AlgorandClientTransactionMethods:
|
|
69
|
+
"""
|
|
70
|
+
Methods used to form a transaction without signing or sending to the network
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
payment: Callable[[PayParams], Transaction]
|
|
74
|
+
asset_create: Callable[[AssetCreateParams], Transaction]
|
|
75
|
+
asset_config: Callable[[AssetConfigParams], Transaction]
|
|
76
|
+
asset_freeze: Callable[[AssetFreezeParams], Transaction]
|
|
77
|
+
asset_destroy: Callable[[AssetDestroyParams], Transaction]
|
|
78
|
+
asset_transfer: Callable[[AssetTransferParams], Transaction]
|
|
79
|
+
app_call: Callable[[AppCallParams], Transaction]
|
|
80
|
+
online_key_reg: Callable[[OnlineKeyRegParams], Transaction]
|
|
81
|
+
method_call: Callable[[MethodCallParams], list[Transaction]]
|
|
82
|
+
asset_opt_in: Callable[[AssetOptInParams], Transaction]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AlgorandClient:
|
|
86
|
+
"""A client that brokers easy access to Algorand functionality."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, config: AlgoClientConfigs | AlgoSdkClients):
|
|
89
|
+
self._client_manager: ClientManager = ClientManager(config)
|
|
90
|
+
self._account_manager: AccountManager = AccountManager(self._client_manager)
|
|
91
|
+
|
|
92
|
+
self._cached_suggested_params: SuggestedParams | None = None
|
|
93
|
+
self._cached_suggested_params_expiry: float | None = None
|
|
94
|
+
self._cached_suggested_params_timeout: int = 3_000 # three seconds
|
|
95
|
+
|
|
96
|
+
self._default_validity_window: int = 10
|
|
97
|
+
|
|
98
|
+
def _unwrap_single_send_result(self, results: AtomicTransactionResponse) -> dict[str, Any]:
|
|
99
|
+
return {
|
|
100
|
+
"confirmation": wait_for_confirmation(self._client_manager.algod, results.tx_ids[0]),
|
|
101
|
+
"tx_id": results.tx_ids[0],
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def set_default_validity_window(self, validity_window: int) -> Self:
|
|
105
|
+
"""
|
|
106
|
+
Sets the default validity window for transactions.
|
|
107
|
+
|
|
108
|
+
:param validity_window: The number of rounds between the first and last valid rounds
|
|
109
|
+
:return: The `AlgorandClient` so method calls can be chained
|
|
110
|
+
"""
|
|
111
|
+
self._default_validity_window = validity_window
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def set_default_signer(self, signer: TransactionSigner) -> Self:
|
|
115
|
+
"""
|
|
116
|
+
Sets the default signer to use if no other signer is specified.
|
|
117
|
+
|
|
118
|
+
:param signer: The signer to use, either a `TransactionSigner` or a `TransactionSignerAccount`
|
|
119
|
+
:return: The `AlgorandClient` so method calls can be chained
|
|
120
|
+
"""
|
|
121
|
+
self._account_manager.set_default_signer(signer)
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
def set_signer(self, sender: str, signer: TransactionSigner) -> Self:
|
|
125
|
+
"""
|
|
126
|
+
Tracks the given account for later signing.
|
|
127
|
+
|
|
128
|
+
:param sender: The sender address to use this signer for
|
|
129
|
+
:param signer: The signer to sign transactions with for the given sender
|
|
130
|
+
:return: The `AlgorandClient` so method calls can be chained
|
|
131
|
+
"""
|
|
132
|
+
self._account_manager.set_signer(sender, signer)
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
def set_suggested_params(self, suggested_params: SuggestedParams, until: float | None = None) -> Self:
|
|
136
|
+
"""
|
|
137
|
+
Sets a cache value to use for suggested params.
|
|
138
|
+
|
|
139
|
+
:param suggested_params: The suggested params to use
|
|
140
|
+
:param until: A timestamp until which to cache, or if not specified then the timeout is used
|
|
141
|
+
:return: The `AlgorandClient` so method calls can be chained
|
|
142
|
+
"""
|
|
143
|
+
self._cached_suggested_params = suggested_params
|
|
144
|
+
self._cached_suggested_params_expiry = until or time.time() + self._cached_suggested_params_timeout
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
def set_suggested_params_timeout(self, timeout: int) -> Self:
|
|
148
|
+
"""
|
|
149
|
+
Sets the timeout for caching suggested params.
|
|
150
|
+
|
|
151
|
+
:param timeout: The timeout in milliseconds
|
|
152
|
+
:return: The `AlgorandClient` so method calls can be chained
|
|
153
|
+
"""
|
|
154
|
+
self._cached_suggested_params_timeout = timeout
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
def get_suggested_params(self) -> SuggestedParams:
|
|
158
|
+
"""Get suggested params for a transaction (either cached or from algod if the cache is stale or empty)"""
|
|
159
|
+
if self._cached_suggested_params and (
|
|
160
|
+
self._cached_suggested_params_expiry is None or self._cached_suggested_params_expiry > time.time()
|
|
161
|
+
):
|
|
162
|
+
return copy.deepcopy(self._cached_suggested_params)
|
|
163
|
+
|
|
164
|
+
self._cached_suggested_params = self._client_manager.algod.suggested_params()
|
|
165
|
+
self._cached_suggested_params_expiry = time.time() + self._cached_suggested_params_timeout
|
|
166
|
+
|
|
167
|
+
return copy.deepcopy(self._cached_suggested_params)
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def client(self) -> ClientManager:
|
|
171
|
+
"""Get clients, including algosdk clients and app clients."""
|
|
172
|
+
return self._client_manager
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def account(self) -> AccountManager:
|
|
176
|
+
"""Get or create accounts that can sign transactions."""
|
|
177
|
+
return self._account_manager
|
|
178
|
+
|
|
179
|
+
def new_group(self) -> AlgokitComposer:
|
|
180
|
+
"""Start a new `AlgokitComposer` transaction group"""
|
|
181
|
+
return AlgokitComposer(
|
|
182
|
+
algod=self.client.algod,
|
|
183
|
+
get_signer=lambda addr: self.account.get_signer(addr),
|
|
184
|
+
get_suggested_params=self.get_suggested_params,
|
|
185
|
+
default_validity_window=self._default_validity_window,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def send(self) -> AlgorandClientSendMethods:
|
|
190
|
+
"""Methods for sending a transaction and waiting for confirmation"""
|
|
191
|
+
return AlgorandClientSendMethods(
|
|
192
|
+
payment=lambda params: self._unwrap_single_send_result(self.new_group().add_payment(params).execute()),
|
|
193
|
+
asset_create=lambda params: self._unwrap_single_send_result(
|
|
194
|
+
self.new_group().add_asset_create(params).execute()
|
|
195
|
+
),
|
|
196
|
+
asset_config=lambda params: self._unwrap_single_send_result(
|
|
197
|
+
self.new_group().add_asset_config(params).execute()
|
|
198
|
+
),
|
|
199
|
+
asset_freeze=lambda params: self._unwrap_single_send_result(
|
|
200
|
+
self.new_group().add_asset_freeze(params).execute()
|
|
201
|
+
),
|
|
202
|
+
asset_destroy=lambda params: self._unwrap_single_send_result(
|
|
203
|
+
self.new_group().add_asset_destroy(params).execute()
|
|
204
|
+
),
|
|
205
|
+
asset_transfer=lambda params: self._unwrap_single_send_result(
|
|
206
|
+
self.new_group().add_asset_transfer(params).execute()
|
|
207
|
+
),
|
|
208
|
+
app_call=lambda params: self._unwrap_single_send_result(self.new_group().add_app_call(params).execute()),
|
|
209
|
+
online_key_reg=lambda params: self._unwrap_single_send_result(
|
|
210
|
+
self.new_group().add_online_key_reg(params).execute()
|
|
211
|
+
),
|
|
212
|
+
method_call=lambda params: self._unwrap_single_send_result(
|
|
213
|
+
self.new_group().add_method_call(params).execute()
|
|
214
|
+
),
|
|
215
|
+
asset_opt_in=lambda params: self._unwrap_single_send_result(
|
|
216
|
+
self.new_group().add_asset_opt_in(params).execute()
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def transactions(self) -> AlgorandClientTransactionMethods:
|
|
222
|
+
"""Methods for building transactions"""
|
|
223
|
+
|
|
224
|
+
return AlgorandClientTransactionMethods(
|
|
225
|
+
payment=lambda params: self.new_group().add_payment(params).build_group()[0].txn,
|
|
226
|
+
asset_create=lambda params: self.new_group().add_asset_create(params).build_group()[0].txn,
|
|
227
|
+
asset_config=lambda params: self.new_group().add_asset_config(params).build_group()[0].txn,
|
|
228
|
+
asset_freeze=lambda params: self.new_group().add_asset_freeze(params).build_group()[0].txn,
|
|
229
|
+
asset_destroy=lambda params: self.new_group().add_asset_destroy(params).build_group()[0].txn,
|
|
230
|
+
asset_transfer=lambda params: self.new_group().add_asset_transfer(params).build_group()[0].txn,
|
|
231
|
+
app_call=lambda params: self.new_group().add_app_call(params).build_group()[0].txn,
|
|
232
|
+
online_key_reg=lambda params: self.new_group().add_online_key_reg(params).build_group()[0].txn,
|
|
233
|
+
method_call=lambda params: [txn.txn for txn in self.new_group().add_method_call(params).build_group()],
|
|
234
|
+
asset_opt_in=lambda params: self.new_group().add_asset_opt_in(params).build_group()[0].txn,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
@staticmethod
|
|
238
|
+
def default_local_net() -> "AlgorandClient":
|
|
239
|
+
"""
|
|
240
|
+
Returns an `AlgorandClient` pointing at default LocalNet ports and API token.
|
|
241
|
+
|
|
242
|
+
:return: The `AlgorandClient`
|
|
243
|
+
"""
|
|
244
|
+
return AlgorandClient(
|
|
245
|
+
AlgoClientConfigs(
|
|
246
|
+
algod_config=get_default_localnet_config("algod"),
|
|
247
|
+
indexer_config=get_default_localnet_config("indexer"),
|
|
248
|
+
kmd_config=get_default_localnet_config("kmd"),
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def test_net() -> "AlgorandClient":
|
|
254
|
+
"""
|
|
255
|
+
Returns an `AlgorandClient` pointing at TestNet using AlgoNode.
|
|
256
|
+
|
|
257
|
+
:return: The `AlgorandClient`
|
|
258
|
+
"""
|
|
259
|
+
return AlgorandClient(
|
|
260
|
+
AlgoClientConfigs(
|
|
261
|
+
algod_config=get_algonode_config("testnet", "algod", ""),
|
|
262
|
+
indexer_config=get_algonode_config("testnet", "indexer", ""),
|
|
263
|
+
kmd_config=None,
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def main_net() -> "AlgorandClient":
|
|
269
|
+
"""
|
|
270
|
+
Returns an `AlgorandClient` pointing at MainNet using AlgoNode.
|
|
271
|
+
|
|
272
|
+
:return: The `AlgorandClient`
|
|
273
|
+
"""
|
|
274
|
+
return AlgorandClient(
|
|
275
|
+
AlgoClientConfigs(
|
|
276
|
+
algod_config=get_algonode_config("mainnet", "algod", ""),
|
|
277
|
+
indexer_config=get_algonode_config("mainnet", "indexer", ""),
|
|
278
|
+
kmd_config=None,
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def from_clients(clients: AlgoSdkClients) -> "AlgorandClient":
|
|
284
|
+
"""
|
|
285
|
+
Returns an `AlgorandClient` pointing to the given client(s).
|
|
286
|
+
|
|
287
|
+
:param clients: The clients to use
|
|
288
|
+
:return: The `AlgorandClient`
|
|
289
|
+
"""
|
|
290
|
+
return AlgorandClient(clients)
|
|
291
|
+
|
|
292
|
+
@staticmethod
|
|
293
|
+
def from_environment() -> "AlgorandClient":
|
|
294
|
+
"""
|
|
295
|
+
Returns an `AlgorandClient` loading the configuration from environment variables.
|
|
296
|
+
|
|
297
|
+
Retrieve configurations from environment variables when defined or get defaults.
|
|
298
|
+
|
|
299
|
+
Expects to be called from a Python environment.
|
|
300
|
+
|
|
301
|
+
:return: The `AlgorandClient`
|
|
302
|
+
"""
|
|
303
|
+
return AlgorandClient(
|
|
304
|
+
AlgoSdkClients(
|
|
305
|
+
algod=get_algod_client(),
|
|
306
|
+
kmd=get_kmd_client(),
|
|
307
|
+
indexer=get_indexer_client(),
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
@staticmethod
|
|
312
|
+
def from_config(config: AlgoClientConfigs) -> "AlgorandClient":
|
|
313
|
+
"""
|
|
314
|
+
Returns an `AlgorandClient` from the given config.
|
|
315
|
+
|
|
316
|
+
:param config: The config to use
|
|
317
|
+
:return: The `AlgorandClient`
|
|
318
|
+
"""
|
|
319
|
+
return AlgorandClient(config)
|