-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathasset_manager.py
320 lines (275 loc) · 13.8 KB
/
asset_manager.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
from collections.abc import Callable
from dataclasses import dataclass
import algosdk
from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner
from algosdk.v2client import algod
from algokit_utils.models.account import SigningAccount
from algokit_utils.models.amount import AlgoAmount
from algokit_utils.models.transaction import SendParams
from algokit_utils.transactions.transaction_composer import (
AssetOptInParams,
AssetOptOutParams,
TransactionComposer,
)
__all__ = ["AccountAssetInformation", "AssetInformation", "AssetManager", "BulkAssetOptInOutResult"]
@dataclass(kw_only=True, frozen=True)
class AccountAssetInformation:
"""Information about an account's holding of a particular asset.
:ivar asset_id: The ID of the asset
:ivar balance: The amount of the asset held by the account
:ivar frozen: Whether the asset is frozen for this account
:ivar round: The round this information was retrieved at
"""
asset_id: int
balance: int
frozen: bool
round: int
@dataclass(kw_only=True, frozen=True)
class AssetInformation:
"""Information about an Algorand Standard Asset (ASA).
:ivar asset_id: The ID of the asset
:ivar creator: The address of the account that created the asset
:ivar total: The total amount of the smallest divisible units that were created of the asset
:ivar decimals: The amount of decimal places the asset was created with
:ivar default_frozen: Whether the asset was frozen by default for all accounts, defaults to None
:ivar manager: The address of the optional account that can manage the configuration of the asset and destroy it,
defaults to None
:ivar reserve: The address of the optional account that holds the reserve (uncirculated supply) units of the asset,
defaults to None
:ivar freeze: The address of the optional account that can be used to freeze or unfreeze holdings of this asset,
defaults to None
:ivar clawback: The address of the optional account that can clawback holdings of this asset from any account,
defaults to None
:ivar unit_name: The optional name of the unit of this asset (e.g. ticker name), defaults to None
:ivar unit_name_b64: The optional name of the unit of this asset as bytes, defaults to None
:ivar asset_name: The optional name of the asset, defaults to None
:ivar asset_name_b64: The optional name of the asset as bytes, defaults to None
:ivar url: Optional URL where more information about the asset can be retrieved, defaults to None
:ivar url_b64: Optional URL where more information about the asset can be retrieved as bytes, defaults to None
:ivar metadata_hash: 32-byte hash of some metadata that is relevant to the asset and/or asset holders,
defaults to None
"""
asset_id: int
creator: str
total: int
decimals: int
default_frozen: bool | None = None
manager: str | None = None
reserve: str | None = None
freeze: str | None = None
clawback: str | None = None
unit_name: str | None = None
unit_name_b64: bytes | None = None
asset_name: str | None = None
asset_name_b64: bytes | None = None
url: str | None = None
url_b64: bytes | None = None
metadata_hash: bytes | None = None
@dataclass(kw_only=True, frozen=True)
class BulkAssetOptInOutResult:
"""Result from performing a bulk opt-in or bulk opt-out for an account against a series of assets.
:ivar asset_id: The ID of the asset opted into / out of
:ivar transaction_id: The transaction ID of the resulting opt in / out
"""
asset_id: int
transaction_id: str
class AssetManager:
"""A manager for Algorand Standard Assets (ASAs).
:param algod_client: An algod client
:param new_group: A function that creates a new TransactionComposer transaction group
"""
def __init__(self, algod_client: algod.AlgodClient, new_group: Callable[[], TransactionComposer]):
self._algod = algod_client
self._new_group = new_group
def get_by_id(self, asset_id: int) -> AssetInformation:
"""Returns the current asset information for the asset with the given ID.
:param asset_id: The ID of the asset
:return: The asset information
"""
asset = self._algod.asset_info(asset_id)
assert isinstance(asset, dict)
params = asset["params"]
return AssetInformation(
asset_id=asset_id,
total=params["total"],
decimals=params["decimals"],
asset_name=params.get("name"),
asset_name_b64=params.get("name-b64"),
unit_name=params.get("unit-name"),
unit_name_b64=params.get("unit-name-b64"),
url=params.get("url"),
url_b64=params.get("url-b64"),
creator=params["creator"],
manager=params.get("manager"),
clawback=params.get("clawback"),
freeze=params.get("freeze"),
reserve=params.get("reserve"),
default_frozen=params.get("default-frozen"),
metadata_hash=params.get("metadata-hash"),
)
def get_account_information(
self, sender: str | SigningAccount | TransactionSigner, asset_id: int
) -> AccountAssetInformation:
"""Returns the given sender account's asset holding for a given asset.
:param sender: The address of the sender/account to look up
:param asset_id: The ID of the asset to return a holding for
:return: The account asset holding information
"""
address = self._get_address_from_sender(sender)
info = self._algod.account_asset_info(address, asset_id)
assert isinstance(info, dict)
return AccountAssetInformation(
asset_id=asset_id,
balance=info["asset-holding"]["amount"],
frozen=info["asset-holding"]["is-frozen"],
round=info["round"],
)
def bulk_opt_in( # noqa: PLR0913
self,
account: str,
asset_ids: list[int],
signer: TransactionSigner | None = None,
rekey_to: str | None = None,
note: bytes | None = None,
lease: bytes | None = None,
static_fee: AlgoAmount | None = None,
extra_fee: AlgoAmount | None = None,
max_fee: AlgoAmount | None = None,
validity_window: int | None = None,
first_valid_round: int | None = None,
last_valid_round: int | None = None,
send_params: SendParams | None = None,
) -> list[BulkAssetOptInOutResult]:
"""Opt an account in to a list of Algorand Standard Assets.
:param account: The account to opt-in
:param asset_ids: The list of asset IDs to opt-in to
:param signer: The signer to use for the transaction, defaults to None
:param rekey_to: The address to rekey the account to, defaults to None
:param note: The note to include in the transaction, defaults to None
:param lease: The lease to include in the transaction, defaults to None
:param static_fee: The static fee to include in the transaction, defaults to None
:param extra_fee: The extra fee to include in the transaction, defaults to None
:param max_fee: The maximum fee to include in the transaction, defaults to None
:param validity_window: The validity window to include in the transaction, defaults to None
:param first_valid_round: The first valid round to include in the transaction, defaults to None
:param last_valid_round: The last valid round to include in the transaction, defaults to None
:param send_params: The send parameters to use for the transaction, defaults to None
:return: An array of records matching asset ID to transaction ID of the opt in
"""
results: list[BulkAssetOptInOutResult] = []
sender = self._get_address_from_sender(account)
for asset_group in _chunk_array(asset_ids, algosdk.constants.TX_GROUP_LIMIT):
composer = self._new_group()
for asset_id in asset_group:
params = AssetOptInParams(
sender=sender,
asset_id=asset_id,
signer=signer,
rekey_to=rekey_to,
note=note,
lease=lease,
static_fee=static_fee,
extra_fee=extra_fee,
max_fee=max_fee,
validity_window=validity_window,
first_valid_round=first_valid_round,
last_valid_round=last_valid_round,
)
composer.add_asset_opt_in(params)
result = composer.send(send_params)
for i, asset_id in enumerate(asset_group):
results.append(BulkAssetOptInOutResult(asset_id=asset_id, transaction_id=result.tx_ids[i]))
return results
def bulk_opt_out( # noqa: C901, PLR0913
self,
*,
account: str,
asset_ids: list[int],
ensure_zero_balance: bool = True,
signer: TransactionSigner | None = None,
rekey_to: str | None = None,
note: bytes | None = None,
lease: bytes | None = None,
static_fee: AlgoAmount | None = None,
extra_fee: AlgoAmount | None = None,
max_fee: AlgoAmount | None = None,
validity_window: int | None = None,
first_valid_round: int | None = None,
last_valid_round: int | None = None,
send_params: SendParams | None = None,
) -> list[BulkAssetOptInOutResult]:
"""Opt an account out of a list of Algorand Standard Assets.
:param account: The account to opt-out
:param asset_ids: The list of asset IDs to opt-out of
:param ensure_zero_balance: Whether to check if the account has a zero balance first, defaults to True
:param signer: The signer to use for the transaction, defaults to None
:param rekey_to: The address to rekey the account to, defaults to None
:param note: The note to include in the transaction, defaults to None
:param lease: The lease to include in the transaction, defaults to None
:param static_fee: The static fee to include in the transaction, defaults to None
:param extra_fee: The extra fee to include in the transaction, defaults to None
:param max_fee: The maximum fee to include in the transaction, defaults to None
:param validity_window: The validity window to include in the transaction, defaults to None
:param first_valid_round: The first valid round to include in the transaction, defaults to None
:param last_valid_round: The last valid round to include in the transaction, defaults to None
:param send_params: The send parameters to use for the transaction, defaults to None
:raises ValueError: If ensure_zero_balance is True and account has non-zero balance or is not opted in
:return: An array of records matching asset ID to transaction ID of the opt out
"""
results: list[BulkAssetOptInOutResult] = []
sender = self._get_address_from_sender(account)
for asset_group in _chunk_array(asset_ids, algosdk.constants.TX_GROUP_LIMIT):
composer = self._new_group()
not_opted_in_asset_ids: list[int] = []
non_zero_balance_asset_ids: list[int] = []
if ensure_zero_balance:
for asset_id in asset_group:
try:
account_asset_info = self.get_account_information(sender, asset_id)
if account_asset_info.balance != 0:
non_zero_balance_asset_ids.append(asset_id)
except Exception:
not_opted_in_asset_ids.append(asset_id)
if not_opted_in_asset_ids or non_zero_balance_asset_ids:
error_message = f"Account {sender}"
if not_opted_in_asset_ids:
error_message += f" is not opted-in to Asset(s) {', '.join(map(str, not_opted_in_asset_ids))}"
if non_zero_balance_asset_ids:
error_message += (
f" has non-zero balance for Asset(s) {', '.join(map(str, non_zero_balance_asset_ids))}"
)
error_message += "; can't opt-out."
raise ValueError(error_message)
for asset_id in asset_group:
asset_info = self.get_by_id(asset_id)
params = AssetOptOutParams(
sender=sender,
asset_id=asset_id,
creator=asset_info.creator,
signer=signer,
rekey_to=rekey_to,
note=note,
lease=lease,
static_fee=static_fee,
extra_fee=extra_fee,
max_fee=max_fee,
validity_window=validity_window,
first_valid_round=first_valid_round,
last_valid_round=last_valid_round,
)
composer.add_asset_opt_out(params)
result = composer.send(send_params)
for i, asset_id in enumerate(asset_group):
results.append(BulkAssetOptInOutResult(asset_id=asset_id, transaction_id=result.tx_ids[i]))
return results
@staticmethod
def _get_address_from_sender(sender: str | SigningAccount | TransactionSigner) -> str:
if isinstance(sender, str):
return sender
if isinstance(sender, SigningAccount):
return sender.address
if isinstance(sender, AccountTransactionSigner):
return str(algosdk.account.address_from_private_key(sender.private_key))
raise ValueError(f"Unsupported sender type: {type(sender)}")
def _chunk_array(array: list, size: int) -> list[list]:
return [array[i : i + size] for i in range(0, len(array), size)]