algokit-utils 2.4.0b1__py3-none-any.whl → 3.0.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/__init__.py +23 -181
- algokit_utils/_debugging.py +89 -45
- algokit_utils/_legacy_v2/__init__.py +177 -0
- algokit_utils/{_ensure_funded.py → _legacy_v2/_ensure_funded.py} +21 -24
- algokit_utils/{_transfer.py → _legacy_v2/_transfer.py} +26 -23
- algokit_utils/_legacy_v2/account.py +203 -0
- algokit_utils/_legacy_v2/application_client.py +1472 -0
- algokit_utils/_legacy_v2/application_specification.py +21 -0
- algokit_utils/_legacy_v2/asset.py +168 -0
- algokit_utils/_legacy_v2/common.py +28 -0
- algokit_utils/_legacy_v2/deploy.py +822 -0
- algokit_utils/_legacy_v2/logic_error.py +14 -0
- algokit_utils/{models.py → _legacy_v2/models.py} +16 -45
- algokit_utils/_legacy_v2/network_clients.py +144 -0
- algokit_utils/account.py +12 -183
- algokit_utils/accounts/__init__.py +2 -0
- algokit_utils/accounts/account_manager.py +912 -0
- algokit_utils/accounts/kmd_account_manager.py +161 -0
- algokit_utils/algorand.py +359 -0
- algokit_utils/application_client.py +9 -1447
- algokit_utils/application_specification.py +39 -197
- algokit_utils/applications/__init__.py +7 -0
- algokit_utils/applications/abi.py +275 -0
- algokit_utils/applications/app_client.py +2108 -0
- algokit_utils/applications/app_deployer.py +725 -0
- algokit_utils/applications/app_factory.py +1134 -0
- algokit_utils/applications/app_manager.py +578 -0
- algokit_utils/applications/app_spec/__init__.py +2 -0
- algokit_utils/applications/app_spec/arc32.py +207 -0
- algokit_utils/applications/app_spec/arc56.py +989 -0
- algokit_utils/applications/enums.py +40 -0
- algokit_utils/asset.py +32 -168
- algokit_utils/assets/__init__.py +1 -0
- algokit_utils/assets/asset_manager.py +336 -0
- algokit_utils/beta/_utils.py +36 -0
- algokit_utils/beta/account_manager.py +4 -195
- algokit_utils/beta/algorand_client.py +4 -314
- algokit_utils/beta/client_manager.py +5 -74
- algokit_utils/beta/composer.py +5 -712
- algokit_utils/clients/__init__.py +2 -0
- algokit_utils/clients/client_manager.py +738 -0
- algokit_utils/clients/dispenser_api_client.py +224 -0
- algokit_utils/common.py +8 -26
- algokit_utils/config.py +76 -29
- algokit_utils/deploy.py +7 -894
- algokit_utils/dispenser_api.py +8 -176
- algokit_utils/errors/__init__.py +1 -0
- algokit_utils/errors/logic_error.py +121 -0
- algokit_utils/logic_error.py +7 -82
- algokit_utils/models/__init__.py +8 -0
- algokit_utils/models/account.py +217 -0
- algokit_utils/models/amount.py +200 -0
- algokit_utils/models/application.py +91 -0
- algokit_utils/models/network.py +29 -0
- algokit_utils/models/simulate.py +11 -0
- algokit_utils/models/state.py +68 -0
- algokit_utils/models/transaction.py +100 -0
- algokit_utils/network_clients.py +7 -128
- algokit_utils/protocols/__init__.py +2 -0
- algokit_utils/protocols/account.py +22 -0
- algokit_utils/protocols/typed_clients.py +108 -0
- algokit_utils/transactions/__init__.py +3 -0
- algokit_utils/transactions/transaction_composer.py +2499 -0
- algokit_utils/transactions/transaction_creator.py +688 -0
- algokit_utils/transactions/transaction_sender.py +1219 -0
- {algokit_utils-2.4.0b1.dist-info → algokit_utils-3.0.0.dist-info}/METADATA +11 -7
- algokit_utils-3.0.0.dist-info/RECORD +70 -0
- {algokit_utils-2.4.0b1.dist-info → algokit_utils-3.0.0.dist-info}/WHEEL +1 -1
- algokit_utils-2.4.0b1.dist-info/RECORD +0 -24
- {algokit_utils-2.4.0b1.dist-info → algokit_utils-3.0.0.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import overload
|
|
5
|
+
|
|
6
|
+
import algosdk
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
__all__ = ["ALGORAND_MIN_TX_FEE", "AlgoAmount", "algo", "micro_algo", "transaction_fees"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AlgoAmount:
|
|
13
|
+
"""Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbers.
|
|
14
|
+
|
|
15
|
+
:example:
|
|
16
|
+
>>> amount = AlgoAmount(algo=1)
|
|
17
|
+
>>> amount = AlgoAmount.from_algo(1)
|
|
18
|
+
>>> amount = AlgoAmount(micro_algo=1_000_000)
|
|
19
|
+
>>> amount = AlgoAmount.from_micro_algo(1_000_000)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@overload
|
|
23
|
+
def __init__(self, *, micro_algo: int) -> None: ...
|
|
24
|
+
|
|
25
|
+
@overload
|
|
26
|
+
def __init__(self, *, algo: int | Decimal) -> None: ...
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
micro_algo: int | None = None,
|
|
32
|
+
algo: int | Decimal | None = None,
|
|
33
|
+
):
|
|
34
|
+
if micro_algo is None and algo is None:
|
|
35
|
+
raise ValueError("No amount provided")
|
|
36
|
+
|
|
37
|
+
if micro_algo is not None:
|
|
38
|
+
self.amount_in_micro_algo = int(micro_algo)
|
|
39
|
+
elif algo is not None:
|
|
40
|
+
self.amount_in_micro_algo = int(algo * algosdk.constants.MICROALGOS_TO_ALGOS_RATIO)
|
|
41
|
+
else:
|
|
42
|
+
raise ValueError("Invalid amount provided")
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def micro_algo(self) -> int:
|
|
46
|
+
"""Return the amount as a number in µAlgo.
|
|
47
|
+
|
|
48
|
+
:returns: The amount in µAlgo.
|
|
49
|
+
"""
|
|
50
|
+
return self.amount_in_micro_algo
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def algo(self) -> Decimal:
|
|
54
|
+
"""Return the amount as a number in Algo.
|
|
55
|
+
|
|
56
|
+
:returns: The amount in Algo.
|
|
57
|
+
"""
|
|
58
|
+
return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return]
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def from_algo(amount: int | Decimal) -> AlgoAmount:
|
|
62
|
+
"""Create an AlgoAmount object representing the given number of Algo.
|
|
63
|
+
|
|
64
|
+
:param amount: The amount in Algo.
|
|
65
|
+
:returns: An AlgoAmount instance.
|
|
66
|
+
|
|
67
|
+
:example:
|
|
68
|
+
>>> amount = AlgoAmount.from_algo(1)
|
|
69
|
+
"""
|
|
70
|
+
return AlgoAmount(algo=amount)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def from_micro_algo(amount: int) -> AlgoAmount:
|
|
74
|
+
"""Create an AlgoAmount object representing the given number of µAlgo.
|
|
75
|
+
|
|
76
|
+
:param amount: The amount in µAlgo.
|
|
77
|
+
:returns: An AlgoAmount instance.
|
|
78
|
+
|
|
79
|
+
:example:
|
|
80
|
+
>>> amount = AlgoAmount.from_micro_algo(1_000_000)
|
|
81
|
+
"""
|
|
82
|
+
return AlgoAmount(micro_algo=amount)
|
|
83
|
+
|
|
84
|
+
def __str__(self) -> str:
|
|
85
|
+
return f"{self.micro_algo:,} µALGO"
|
|
86
|
+
|
|
87
|
+
def __int__(self) -> int:
|
|
88
|
+
return self.micro_algo
|
|
89
|
+
|
|
90
|
+
def __add__(self, other: AlgoAmount) -> AlgoAmount:
|
|
91
|
+
if isinstance(other, AlgoAmount):
|
|
92
|
+
total_micro_algos = self.micro_algo + other.micro_algo
|
|
93
|
+
else:
|
|
94
|
+
raise TypeError(f"Unsupported operand type(s) for +: 'AlgoAmount' and '{type(other).__name__}'")
|
|
95
|
+
return AlgoAmount.from_micro_algo(total_micro_algos)
|
|
96
|
+
|
|
97
|
+
def __radd__(self, other: AlgoAmount) -> AlgoAmount:
|
|
98
|
+
return self.__add__(other)
|
|
99
|
+
|
|
100
|
+
def __iadd__(self, other: AlgoAmount) -> Self:
|
|
101
|
+
if isinstance(other, AlgoAmount):
|
|
102
|
+
self.amount_in_micro_algo += other.micro_algo
|
|
103
|
+
else:
|
|
104
|
+
raise TypeError(f"Unsupported operand type(s) for +: 'AlgoAmount' and '{type(other).__name__}'")
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def __eq__(self, other: object) -> bool:
|
|
108
|
+
if isinstance(other, AlgoAmount):
|
|
109
|
+
return self.amount_in_micro_algo == other.amount_in_micro_algo
|
|
110
|
+
elif isinstance(other, int):
|
|
111
|
+
return self.amount_in_micro_algo == int(other)
|
|
112
|
+
raise TypeError(f"Unsupported operand type(s) for ==: 'AlgoAmount' and '{type(other).__name__}'")
|
|
113
|
+
|
|
114
|
+
def __ne__(self, other: object) -> bool:
|
|
115
|
+
if isinstance(other, AlgoAmount):
|
|
116
|
+
return self.amount_in_micro_algo != other.amount_in_micro_algo
|
|
117
|
+
elif isinstance(other, int):
|
|
118
|
+
return self.amount_in_micro_algo != int(other)
|
|
119
|
+
raise TypeError(f"Unsupported operand type(s) for !=: 'AlgoAmount' and '{type(other).__name__}'")
|
|
120
|
+
|
|
121
|
+
def __lt__(self, other: object) -> bool:
|
|
122
|
+
if isinstance(other, AlgoAmount):
|
|
123
|
+
return self.amount_in_micro_algo < other.amount_in_micro_algo
|
|
124
|
+
elif isinstance(other, int):
|
|
125
|
+
return self.amount_in_micro_algo < int(other)
|
|
126
|
+
raise TypeError(f"Unsupported operand type(s) for <: 'AlgoAmount' and '{type(other).__name__}'")
|
|
127
|
+
|
|
128
|
+
def __le__(self, other: object) -> bool:
|
|
129
|
+
if isinstance(other, AlgoAmount):
|
|
130
|
+
return self.amount_in_micro_algo <= other.amount_in_micro_algo
|
|
131
|
+
elif isinstance(other, int):
|
|
132
|
+
return self.amount_in_micro_algo <= int(other)
|
|
133
|
+
raise TypeError(f"Unsupported operand type(s) for <=: 'AlgoAmount' and '{type(other).__name__}'")
|
|
134
|
+
|
|
135
|
+
def __gt__(self, other: object) -> bool:
|
|
136
|
+
if isinstance(other, AlgoAmount):
|
|
137
|
+
return self.amount_in_micro_algo > other.amount_in_micro_algo
|
|
138
|
+
elif isinstance(other, int):
|
|
139
|
+
return self.amount_in_micro_algo > int(other)
|
|
140
|
+
raise TypeError(f"Unsupported operand type(s) for >: 'AlgoAmount' and '{type(other).__name__}'")
|
|
141
|
+
|
|
142
|
+
def __ge__(self, other: object) -> bool:
|
|
143
|
+
if isinstance(other, AlgoAmount):
|
|
144
|
+
return self.amount_in_micro_algo >= other.amount_in_micro_algo
|
|
145
|
+
elif isinstance(other, int):
|
|
146
|
+
return self.amount_in_micro_algo >= int(other)
|
|
147
|
+
raise TypeError(f"Unsupported operand type(s) for >=: 'AlgoAmount' and '{type(other).__name__}'")
|
|
148
|
+
|
|
149
|
+
def __sub__(self, other: AlgoAmount) -> AlgoAmount:
|
|
150
|
+
if isinstance(other, AlgoAmount):
|
|
151
|
+
total_micro_algos = self.micro_algo - other.micro_algo
|
|
152
|
+
else:
|
|
153
|
+
raise TypeError(f"Unsupported operand type(s) for -: 'AlgoAmount' and '{type(other).__name__}'")
|
|
154
|
+
return AlgoAmount.from_micro_algo(total_micro_algos)
|
|
155
|
+
|
|
156
|
+
def __rsub__(self, other: int) -> AlgoAmount:
|
|
157
|
+
if isinstance(other, (int)):
|
|
158
|
+
total_micro_algos = int(other) - self.micro_algo
|
|
159
|
+
return AlgoAmount.from_micro_algo(total_micro_algos)
|
|
160
|
+
raise TypeError(f"Unsupported operand type(s) for -: '{type(other).__name__}' and 'AlgoAmount'")
|
|
161
|
+
|
|
162
|
+
def __isub__(self, other: AlgoAmount) -> Self:
|
|
163
|
+
if isinstance(other, AlgoAmount):
|
|
164
|
+
self.amount_in_micro_algo -= other.micro_algo
|
|
165
|
+
else:
|
|
166
|
+
raise TypeError(f"Unsupported operand type(s) for -: 'AlgoAmount' and '{type(other).__name__}'")
|
|
167
|
+
return self
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# Helper functions
|
|
171
|
+
def algo(algo: int) -> AlgoAmount:
|
|
172
|
+
"""Create an AlgoAmount object representing the given number of Algo.
|
|
173
|
+
|
|
174
|
+
:param algo: The number of Algo to create an AlgoAmount object for.
|
|
175
|
+
:return: An AlgoAmount object representing the given number of Algo.
|
|
176
|
+
"""
|
|
177
|
+
return AlgoAmount.from_algo(algo)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def micro_algo(micro_algo: int) -> AlgoAmount:
|
|
181
|
+
"""Create an AlgoAmount object representing the given number of µAlgo.
|
|
182
|
+
|
|
183
|
+
:param micro_algo: The number of µAlgo to create an AlgoAmount object for.
|
|
184
|
+
:return: An AlgoAmount object representing the given number of µAlgo.
|
|
185
|
+
"""
|
|
186
|
+
return AlgoAmount.from_micro_algo(micro_algo)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
ALGORAND_MIN_TX_FEE = micro_algo(1_000)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def transaction_fees(number_of_transactions: int) -> AlgoAmount:
|
|
193
|
+
"""Calculate the total transaction fees for a given number of transactions.
|
|
194
|
+
|
|
195
|
+
:param number_of_transactions: The number of transactions to calculate the fees for.
|
|
196
|
+
:return: The total transaction fees.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
total_micro_algos = number_of_transactions * ALGORAND_MIN_TX_FEE.micro_algo
|
|
200
|
+
return AlgoAmount.from_micro_algo(total_micro_algos)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import algosdk
|
|
5
|
+
from algosdk.source_map import SourceMap
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AppCompilationResult",
|
|
12
|
+
"AppInformation",
|
|
13
|
+
"AppSourceMaps",
|
|
14
|
+
"AppState",
|
|
15
|
+
"CompiledTeal",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(kw_only=True, frozen=True)
|
|
20
|
+
class AppState:
|
|
21
|
+
key_raw: bytes
|
|
22
|
+
"""The key of the state as raw bytes"""
|
|
23
|
+
key_base64: str
|
|
24
|
+
"""The key of the state"""
|
|
25
|
+
value_raw: bytes | None
|
|
26
|
+
"""The value of the state as raw bytes"""
|
|
27
|
+
value_base64: str | None
|
|
28
|
+
"""The value of the state as base64 encoded string"""
|
|
29
|
+
value: str | int
|
|
30
|
+
"""The value of the state as a string or integer"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(kw_only=True, frozen=True)
|
|
34
|
+
class AppInformation:
|
|
35
|
+
app_id: int
|
|
36
|
+
"""The ID of the application"""
|
|
37
|
+
app_address: str
|
|
38
|
+
"""The address of the application"""
|
|
39
|
+
approval_program: bytes
|
|
40
|
+
"""The approval program"""
|
|
41
|
+
clear_state_program: bytes
|
|
42
|
+
"""The clear state program"""
|
|
43
|
+
creator: str
|
|
44
|
+
"""The creator of the application"""
|
|
45
|
+
global_state: dict[str, AppState]
|
|
46
|
+
"""The global state of the application"""
|
|
47
|
+
local_ints: int
|
|
48
|
+
"""The number of local ints"""
|
|
49
|
+
local_byte_slices: int
|
|
50
|
+
"""The number of local byte slices"""
|
|
51
|
+
global_ints: int
|
|
52
|
+
"""The number of global ints"""
|
|
53
|
+
global_byte_slices: int
|
|
54
|
+
"""The number of global byte slices"""
|
|
55
|
+
extra_program_pages: int | None
|
|
56
|
+
"""The number of extra program pages"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(kw_only=True, frozen=True)
|
|
60
|
+
class CompiledTeal:
|
|
61
|
+
"""The compiled teal code"""
|
|
62
|
+
|
|
63
|
+
teal: str
|
|
64
|
+
"""The teal code"""
|
|
65
|
+
compiled: str
|
|
66
|
+
"""The compiled teal code"""
|
|
67
|
+
compiled_hash: str
|
|
68
|
+
"""The compiled hash"""
|
|
69
|
+
compiled_base64_to_bytes: bytes
|
|
70
|
+
"""The compiled base64 to bytes"""
|
|
71
|
+
source_map: algosdk.source_map.SourceMap | None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(kw_only=True, frozen=True)
|
|
75
|
+
class AppCompilationResult:
|
|
76
|
+
"""The compiled teal code"""
|
|
77
|
+
|
|
78
|
+
compiled_approval: CompiledTeal
|
|
79
|
+
"""The compiled approval program"""
|
|
80
|
+
compiled_clear: CompiledTeal
|
|
81
|
+
"""The compiled clear state program"""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(kw_only=True, frozen=True)
|
|
85
|
+
class AppSourceMaps:
|
|
86
|
+
"""The source maps for the application"""
|
|
87
|
+
|
|
88
|
+
approval_source_map: SourceMap | None = None
|
|
89
|
+
"""The source map for the approval program"""
|
|
90
|
+
clear_source_map: SourceMap | None = None
|
|
91
|
+
"""The source map for the clear state program"""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"AlgoClientConfigs",
|
|
5
|
+
"AlgoClientNetworkConfig",
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclasses.dataclass
|
|
10
|
+
class AlgoClientNetworkConfig:
|
|
11
|
+
"""Connection details for connecting to an {py:class}`algosdk.v2client.algod.AlgodClient` or
|
|
12
|
+
{py:class}`algosdk.v2client.indexer.IndexerClient`"""
|
|
13
|
+
|
|
14
|
+
server: str
|
|
15
|
+
"""URL for the service e.g. `http://localhost` or `https://testnet-api.algonode.cloud`"""
|
|
16
|
+
token: str | None = None
|
|
17
|
+
"""API Token to authenticate with the service e.g '4001' or '8980'"""
|
|
18
|
+
port: str | int | None = None
|
|
19
|
+
|
|
20
|
+
def full_url(self) -> str:
|
|
21
|
+
"""Returns the full URL for the service"""
|
|
22
|
+
return f"{self.server.rstrip('/')}{f':{self.port}' if self.port else ''}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclasses.dataclass
|
|
26
|
+
class AlgoClientConfigs:
|
|
27
|
+
algod_config: AlgoClientNetworkConfig
|
|
28
|
+
indexer_config: AlgoClientNetworkConfig | None
|
|
29
|
+
kmd_config: AlgoClientNetworkConfig | None
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
from typing import TypeAlias
|
|
6
|
+
|
|
7
|
+
from algosdk.atomic_transaction_composer import AccountTransactionSigner
|
|
8
|
+
from algosdk.box_reference import BoxReference as AlgosdkBoxReference
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"BoxIdentifier",
|
|
12
|
+
"BoxName",
|
|
13
|
+
"BoxReference",
|
|
14
|
+
"BoxValue",
|
|
15
|
+
"DataTypeFlag",
|
|
16
|
+
"TealTemplateParams",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(kw_only=True, frozen=True)
|
|
21
|
+
class BoxName:
|
|
22
|
+
"""The name of the box"""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
"""The name of the box as a string"""
|
|
26
|
+
name_raw: bytes
|
|
27
|
+
"""The name of the box as raw bytes"""
|
|
28
|
+
name_base64: str
|
|
29
|
+
"""The name of the box as a base64 encoded string"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(kw_only=True, frozen=True)
|
|
33
|
+
class BoxValue:
|
|
34
|
+
"""The value of the box"""
|
|
35
|
+
|
|
36
|
+
name: BoxName
|
|
37
|
+
"""The name of the box"""
|
|
38
|
+
value: bytes
|
|
39
|
+
"""The value of the box as raw bytes"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DataTypeFlag(IntEnum):
|
|
43
|
+
BYTES = 1
|
|
44
|
+
UINT = 2
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
TealTemplateParams: TypeAlias = Mapping[str, str | int | bytes] | dict[str, str | int | bytes]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
BoxIdentifier: TypeAlias = str | bytes | AccountTransactionSigner
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BoxReference(AlgosdkBoxReference):
|
|
54
|
+
def __init__(self, app_id: int, name: bytes | str):
|
|
55
|
+
super().__init__(app_index=app_id, name=self._b64_decode(name))
|
|
56
|
+
|
|
57
|
+
def __eq__(self, other: object) -> bool:
|
|
58
|
+
if isinstance(other, (BoxReference | AlgosdkBoxReference)):
|
|
59
|
+
return self.app_index == other.app_index and self.name == other.name
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
def _b64_decode(self, value: str | bytes) -> bytes:
|
|
63
|
+
if isinstance(value, str):
|
|
64
|
+
try:
|
|
65
|
+
return base64.b64decode(value)
|
|
66
|
+
except Exception:
|
|
67
|
+
return value.encode("utf-8")
|
|
68
|
+
return value
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from typing import Any, Literal, TypedDict, TypeVar
|
|
2
|
+
|
|
3
|
+
import algosdk
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"Arc2TransactionNote",
|
|
7
|
+
"BaseArc2Note",
|
|
8
|
+
"JsonFormatArc2Note",
|
|
9
|
+
"SendParams",
|
|
10
|
+
"StringFormatArc2Note",
|
|
11
|
+
"TransactionNote",
|
|
12
|
+
"TransactionNoteData",
|
|
13
|
+
"TransactionWrapper",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Define specific types for different formats
|
|
18
|
+
class BaseArc2Note(TypedDict):
|
|
19
|
+
"""Base ARC-0002 transaction note structure"""
|
|
20
|
+
|
|
21
|
+
dapp_name: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StringFormatArc2Note(BaseArc2Note):
|
|
25
|
+
"""ARC-0002 note for string-based formats (m/b/u)"""
|
|
26
|
+
|
|
27
|
+
format: Literal["m", "b", "u"]
|
|
28
|
+
data: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class JsonFormatArc2Note(BaseArc2Note):
|
|
32
|
+
"""ARC-0002 note for JSON format"""
|
|
33
|
+
|
|
34
|
+
format: Literal["j"]
|
|
35
|
+
data: str | dict[str, Any] | list[Any] | int | None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Combined type for all valid ARC-0002 notes
|
|
39
|
+
# See: https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md
|
|
40
|
+
Arc2TransactionNote = StringFormatArc2Note | JsonFormatArc2Note
|
|
41
|
+
|
|
42
|
+
TransactionNoteData = str | None | int | list[Any] | dict[str, Any]
|
|
43
|
+
TransactionNote = bytes | TransactionNoteData | Arc2TransactionNote
|
|
44
|
+
|
|
45
|
+
TxnTypeT = TypeVar("TxnTypeT", bound=algosdk.transaction.Transaction)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TransactionWrapper:
|
|
49
|
+
"""Wrapper around algosdk.transaction.Transaction with optional property validators"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, transaction: algosdk.transaction.Transaction) -> None:
|
|
52
|
+
self._raw = transaction
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def raw(self) -> algosdk.transaction.Transaction:
|
|
56
|
+
return self._raw
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def payment(self) -> algosdk.transaction.PaymentTxn:
|
|
60
|
+
return self._return_if_type(
|
|
61
|
+
algosdk.transaction.PaymentTxn,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def keyreg(self) -> algosdk.transaction.KeyregTxn:
|
|
66
|
+
return self._return_if_type(algosdk.transaction.KeyregTxn)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def asset_config(self) -> algosdk.transaction.AssetConfigTxn:
|
|
70
|
+
return self._return_if_type(algosdk.transaction.AssetConfigTxn)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def asset_transfer(self) -> algosdk.transaction.AssetTransferTxn:
|
|
74
|
+
return self._return_if_type(algosdk.transaction.AssetTransferTxn)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def asset_freeze(self) -> algosdk.transaction.AssetFreezeTxn:
|
|
78
|
+
return self._return_if_type(algosdk.transaction.AssetFreezeTxn)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def application_call(self) -> algosdk.transaction.ApplicationCallTxn:
|
|
82
|
+
return self._return_if_type(algosdk.transaction.ApplicationCallTxn)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def state_proof(self) -> algosdk.transaction.StateProofTxn:
|
|
86
|
+
return self._return_if_type(algosdk.transaction.StateProofTxn)
|
|
87
|
+
|
|
88
|
+
def _return_if_type(self, txn_type: type[TxnTypeT]) -> TxnTypeT:
|
|
89
|
+
if isinstance(self._raw, txn_type):
|
|
90
|
+
return self._raw
|
|
91
|
+
raise ValueError(f"Transaction is not of type {txn_type.__name__}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SendParams(TypedDict, total=False):
|
|
95
|
+
"""Parameters for sending a transaction"""
|
|
96
|
+
|
|
97
|
+
max_rounds_to_wait: int | None
|
|
98
|
+
suppress_log: bool | None
|
|
99
|
+
populate_app_call_resources: bool | None
|
|
100
|
+
cover_app_call_inner_transaction_fees: bool | None
|
algokit_utils/network_clients.py
CHANGED
|
@@ -1,130 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import os
|
|
3
|
-
from typing import Literal
|
|
4
|
-
from urllib import parse
|
|
1
|
+
import warnings
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
warnings.warn(
|
|
4
|
+
"The legacy v2 network clients module is deprecated and will be removed in a future version.",
|
|
5
|
+
DeprecationWarning,
|
|
6
|
+
stacklevel=2,
|
|
7
|
+
)
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
"AlgoClientConfig",
|
|
12
|
-
"get_algod_client",
|
|
13
|
-
"get_algonode_config",
|
|
14
|
-
"get_default_localnet_config",
|
|
15
|
-
"get_indexer_client",
|
|
16
|
-
"get_kmd_client_from_algod_client",
|
|
17
|
-
"is_localnet",
|
|
18
|
-
"is_mainnet",
|
|
19
|
-
"is_testnet",
|
|
20
|
-
"AlgoClientConfigs",
|
|
21
|
-
"get_kmd_client",
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@dataclasses.dataclass
|
|
26
|
-
class AlgoClientConfig:
|
|
27
|
-
"""Connection details for connecting to an {py:class}`algosdk.v2client.algod.AlgodClient` or
|
|
28
|
-
{py:class}`algosdk.v2client.indexer.IndexerClient`"""
|
|
29
|
-
|
|
30
|
-
server: str
|
|
31
|
-
"""URL for the service e.g. `http://localhost:4001` or `https://testnet-api.algonode.cloud`"""
|
|
32
|
-
token: str
|
|
33
|
-
"""API Token to authenticate with the service"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@dataclasses.dataclass
|
|
37
|
-
class AlgoClientConfigs:
|
|
38
|
-
algod_config: AlgoClientConfig
|
|
39
|
-
indexer_config: AlgoClientConfig
|
|
40
|
-
kmd_config: AlgoClientConfig | None
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def get_default_localnet_config(config: Literal["algod", "indexer", "kmd"]) -> AlgoClientConfig:
|
|
44
|
-
"""Returns the client configuration to point to the default LocalNet"""
|
|
45
|
-
port = {"algod": 4001, "indexer": 8980, "kmd": 4002}[config]
|
|
46
|
-
return AlgoClientConfig(server=f"http://localhost:{port}", token="a" * 64)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def get_algonode_config(
|
|
50
|
-
network: Literal["testnet", "mainnet"], config: Literal["algod", "indexer"], token: str
|
|
51
|
-
) -> AlgoClientConfig:
|
|
52
|
-
client = "api" if config == "algod" else "idx"
|
|
53
|
-
return AlgoClientConfig(
|
|
54
|
-
server=f"https://{network}-{client}.algonode.cloud",
|
|
55
|
-
token=token,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient:
|
|
60
|
-
"""Returns an {py:class}`algosdk.v2client.algod.AlgodClient` from `config` or environment
|
|
61
|
-
|
|
62
|
-
If no configuration provided will use environment variables `ALGOD_SERVER`, `ALGOD_PORT` and `ALGOD_TOKEN`"""
|
|
63
|
-
config = config or _get_config_from_environment("ALGOD")
|
|
64
|
-
headers = {"X-Algo-API-Token": config.token}
|
|
65
|
-
return AlgodClient(config.token, config.server, headers)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient:
|
|
69
|
-
"""Returns an {py:class}`algosdk.kmd.KMDClient` from `config` or environment
|
|
70
|
-
|
|
71
|
-
If no configuration provided will use environment variables `KMD_SERVER`, `KMD_PORT` and `KMD_TOKEN`"""
|
|
72
|
-
config = config or _get_config_from_environment("KMD")
|
|
73
|
-
return KMDClient(config.token, config.server) # type: ignore[no-untyped-call]
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient:
|
|
77
|
-
"""Returns an {py:class}`algosdk.v2client.indexer.IndexerClient` from `config` or environment.
|
|
78
|
-
|
|
79
|
-
If no configuration provided will use environment variables `INDEXER_SERVER`, `INDEXER_PORT` and `INDEXER_TOKEN`"""
|
|
80
|
-
config = config or _get_config_from_environment("INDEXER")
|
|
81
|
-
headers = {"X-Indexer-API-Token": config.token}
|
|
82
|
-
return IndexerClient(config.token, config.server, headers) # type: ignore[no-untyped-call]
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def is_localnet(client: AlgodClient) -> bool:
|
|
86
|
-
"""Returns True if client genesis is `devnet-v1` or `sandnet-v1`"""
|
|
87
|
-
params = client.suggested_params()
|
|
88
|
-
return params.gen in ["devnet-v1", "sandnet-v1", "dockernet-v1"]
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def is_mainnet(client: AlgodClient) -> bool:
|
|
92
|
-
"""Returns True if client genesis is `mainnet-v1`"""
|
|
93
|
-
params = client.suggested_params()
|
|
94
|
-
return params.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"]
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def is_testnet(client: AlgodClient) -> bool:
|
|
98
|
-
"""Returns True if client genesis is `testnet-v1`"""
|
|
99
|
-
params = client.suggested_params()
|
|
100
|
-
return params.gen in ["testnet-v1.0", "testnet-v1", "testnet"]
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def get_kmd_client_from_algod_client(client: AlgodClient) -> KMDClient:
|
|
104
|
-
"""Returns an {py:class}`algosdk.kmd.KMDClient` from supplied `client`
|
|
105
|
-
|
|
106
|
-
Will use the same address as provided `client` but on port specified by `KMD_PORT` environment variable,
|
|
107
|
-
or 4002 by default"""
|
|
108
|
-
# We can only use Kmd on the LocalNet otherwise it's not exposed so this makes some assumptions
|
|
109
|
-
# (e.g. same token and server as algod and port 4002 by default)
|
|
110
|
-
port = os.getenv("KMD_PORT", "4002")
|
|
111
|
-
server = _replace_kmd_port(client.algod_address, port)
|
|
112
|
-
return KMDClient(client.algod_token, server) # type: ignore[no-untyped-call]
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _replace_kmd_port(address: str, port: str) -> str:
|
|
116
|
-
parsed_algod = parse.urlparse(address)
|
|
117
|
-
kmd_host = parsed_algod.netloc.split(":", maxsplit=1)[0] + f":{port}"
|
|
118
|
-
kmd_parsed = parsed_algod._replace(netloc=kmd_host)
|
|
119
|
-
return parse.urlunparse(kmd_parsed)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def _get_config_from_environment(environment_prefix: str) -> AlgoClientConfig:
|
|
123
|
-
server = os.getenv(f"{environment_prefix}_SERVER")
|
|
124
|
-
if server is None:
|
|
125
|
-
raise Exception(f"Server environment variable not set: {environment_prefix}_SERVER")
|
|
126
|
-
port = os.getenv(f"{environment_prefix}_PORT")
|
|
127
|
-
if port:
|
|
128
|
-
parsed = parse.urlparse(server)
|
|
129
|
-
server = parsed._replace(netloc=f"{parsed.hostname}:{port}").geturl()
|
|
130
|
-
return AlgoClientConfig(server, os.getenv(f"{environment_prefix}_TOKEN", ""))
|
|
9
|
+
from algokit_utils._legacy_v2.network_clients import * # noqa: F403, E402
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Protocol, runtime_checkable
|
|
2
|
+
|
|
3
|
+
from algosdk.atomic_transaction_composer import TransactionSigner
|
|
4
|
+
|
|
5
|
+
__all__ = ["TransactionSignerAccountProtocol"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@runtime_checkable
|
|
9
|
+
class TransactionSignerAccountProtocol(Protocol):
|
|
10
|
+
"""An account that has a transaction signer.
|
|
11
|
+
Implemented by SigningAccount, LogicSigAccount, MultiSigAccount and TransactionSignerAccount abstractions.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def address(self) -> str:
|
|
16
|
+
"""The address of the account."""
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def signer(self) -> TransactionSigner:
|
|
21
|
+
"""The transaction signer for the account."""
|
|
22
|
+
...
|