Skip to content

Commit

Permalink
feat: task opt in and opt out (#55)
Browse files Browse the repository at this point in the history
* feat: adding opt in

* fix: add opt in to init file

* fix: fix opt in access

* feat: opt-out function

* fix: fix imports

* fix: fixing Account import

* fix: fix opt out and add opt in for multiple assets

* fix: addressing pr reviews

* fix: fix test

* tests: added new tests and fixed other tests

* fix: fixing mypy and ruff errors

* fix: fix transfer method

* fix: fix unnecessary argument

* docs: add docs

* refactor: refactoring based on pr reviews

* chore: update urllib3 for pip audit

---------

Co-authored-by: inaie ignacio <[email protected]>
  • Loading branch information
negar-abbasi and inaie-makerx authored Oct 18, 2023
1 parent 992cb35 commit 3cf9238
Show file tree
Hide file tree
Showing 6 changed files with 366 additions and 26 deletions.
6 changes: 3 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/algokit_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
MethodHints,
OnCompleteActionName,
)
from algokit_utils.asset import opt_in, opt_out
from algokit_utils.deploy import (
DELETABLE_TEMPLATE_NAME,
NOTE_PREFIX,
Expand Down Expand Up @@ -178,4 +179,6 @@
"transfer",
"TransferAssetParameters",
"transfer_asset",
"opt_in",
"opt_out",
]
168 changes: 168 additions & 0 deletions src/algokit_utils/asset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import logging
from typing import TYPE_CHECKING

from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionWithSigner
from algosdk.constants import TX_GROUP_LIMIT
from algosdk.transaction import AssetTransferTxn

if TYPE_CHECKING:
from algosdk.v2client.algod import AlgodClient

from enum import Enum, auto

from algokit_utils.models import Account

__all__ = ["opt_in", "opt_out"]
logger = logging.getLogger(__name__)


class ValidationType(Enum):
OPTIN = auto()
OPTOUT = auto()


def _ensure_account_is_valid(algod_client: "AlgodClient", account: Account) -> None:
try:
algod_client.account_info(account.address)
except Exception as err:
error_message = f"Account address{account.address} does not exist"
logger.debug(error_message)
raise err


def _ensure_asset_balance_conditions(
algod_client: "AlgodClient", account: Account, asset_ids: list, validation_type: ValidationType
) -> None:
invalid_asset_ids = []
account_info = algod_client.account_info(account.address)
account_assets = account_info.get("assets", []) # type: ignore # noqa: PGH003
for asset_id in asset_ids:
asset_exists_in_account_info = any(asset["asset-id"] == asset_id for asset in account_assets)
if validation_type == ValidationType.OPTIN:
if asset_exists_in_account_info:
logger.debug(f"Asset {asset_id} is already opted in for account {account.address}")
invalid_asset_ids.append(asset_id)

elif validation_type == ValidationType.OPTOUT:
if not account_assets or not asset_exists_in_account_info:
logger.debug(f"Account {account.address} does not have asset {asset_id}")
invalid_asset_ids.append(asset_id)
else:
asset_balance = next((asset["amount"] for asset in account_assets if asset["asset-id"] == asset_id), 0)
if asset_balance != 0:
logger.debug(f"Asset {asset_id} balance is not zero")
invalid_asset_ids.append(asset_id)

if len(invalid_asset_ids) > 0:
action = "opted out" if validation_type == ValidationType.OPTOUT else "opted in"
condition_message = (
"their amount is zero and that the account has"
if validation_type == ValidationType.OPTOUT
else "they are valid and that the account has not"
)

error_message = (
f"Assets {invalid_asset_ids} cannot be {action}. Ensure that "
f"{condition_message} previously opted into them."
)
raise ValueError(error_message)


def opt_in(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]:
"""
Opt-in to a list of assets on the Algorand blockchain. Before an account can receive a specific asset,
it must `opt-in` to receive it. An opt-in transaction places an asset holding of 0 into the account and increases
its minimum balance by [100,000 microAlgos](https://developer.algorand.org/docs/get-details/asa/#assets-overview).
Args:
algod_client (AlgodClient): An instance of the AlgodClient class from the algosdk library.
account (Account): An instance of the Account class representing the account that wants to opt-in to the assets.
asset_ids (list[int]): A list of integers representing the asset IDs to opt-in to.
Returns:
dict[int, str]: A dictionary where the keys are the asset IDs and the values
are the transaction IDs for opting-in to each asset.
"""
_ensure_account_is_valid(algod_client, account)
_ensure_asset_balance_conditions(algod_client, account, asset_ids, ValidationType.OPTIN)
suggested_params = algod_client.suggested_params()
result = {}
for i in range(0, len(asset_ids), TX_GROUP_LIMIT):
atc = AtomicTransactionComposer()
chunk = asset_ids[i : i + TX_GROUP_LIMIT]
for asset_id in chunk:
asset = algod_client.asset_info(asset_id)
xfer_txn = AssetTransferTxn(
sp=suggested_params,
sender=account.address,
receiver=account.address,
close_assets_to=None,
revocation_target=None,
amt=0,
note=f"opt in asset id ${asset_id}",
index=asset["index"], # type: ignore # noqa: PGH003
rekey_to=None,
)

transaction_with_signer = TransactionWithSigner(
txn=xfer_txn,
signer=account.signer,
)
atc.add_transaction(transaction_with_signer)
atc.execute(algod_client, 4)

for index, asset_id in enumerate(chunk):
result[asset_id] = atc.tx_ids[index]

return result


def opt_out(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]:
"""
Opt out from a list of Algorand Standard Assets (ASAs) by transferring them back to their creators.
The account also recovers the Minimum Balance Requirement for the asset (100,000 microAlgos)
The `optOut` function manages the opt-out process, permitting the account to discontinue holding a group of assets.
It's essential to note that an account can only opt_out of an asset if its balance of that asset is zero.
Args:
algod_client (AlgodClient): An instance of the AlgodClient class from the `algosdk` library.
account (Account): An instance of the Account class that holds the private key and address for an account.
asset_ids (list[int]): A list of integers representing the asset IDs of the ASAs to opt out from.
Returns:
dict[int, str]: A dictionary where the keys are the asset IDs and the values are the transaction IDs of
the executed transactions.
"""
_ensure_account_is_valid(algod_client, account)
_ensure_asset_balance_conditions(algod_client, account, asset_ids, ValidationType.OPTOUT)
suggested_params = algod_client.suggested_params()
result = {}
for i in range(0, len(asset_ids), TX_GROUP_LIMIT):
atc = AtomicTransactionComposer()
chunk = asset_ids[i : i + TX_GROUP_LIMIT]
for asset_id in chunk:
asset = algod_client.asset_info(asset_id)
asset_creator = asset["params"]["creator"] # type: ignore # noqa: PGH003
xfer_txn = AssetTransferTxn(
sp=suggested_params,
sender=account.address,
receiver=account.address,
close_assets_to=asset_creator,
revocation_target=None,
amt=0,
note=f"opt out asset id ${asset_id}",
index=asset["index"], # type: ignore # noqa: PGH003
rekey_to=None,
)

transaction_with_signer = TransactionWithSigner(
txn=xfer_txn,
signer=account.signer,
)
atc.add_transaction(transaction_with_signer)
atc.execute(algod_client, 4)

for index, asset_id in enumerate(chunk):
result[asset_id] = atc.tx_ids[index]

return result
18 changes: 1 addition & 17 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@
ApplicationClient,
ApplicationSpecification,
EnsureBalanceParameters,
TransferAssetParameters,
ensure_funded,
get_account,
get_algod_client,
get_indexer_client,
get_kmd_client_from_algod_client,
replace_template_variables,
transfer_asset,
)
from dotenv import load_dotenv

Expand Down Expand Up @@ -201,20 +199,7 @@ def generate_test_asset(algod_client: "AlgodClient", sender: Account, total: int
raise ValueError("Unexpected response from pending_transaction_info")


def opt_in(algod_client: "AlgodClient", account: Account, asset_id: int) -> None:
transfer_asset(
algod_client,
TransferAssetParameters(
from_account=account,
to_address=account.address,
asset_id=asset_id,
amount=0,
note=f"Opt in asset id ${asset_id}",
),
)


def assure_funds_and_opt_in(algod_client: "AlgodClient", account: Account, asset_id: int) -> None:
def assure_funds(algod_client: "AlgodClient", account: Account) -> None:
ensure_funded(
algod_client,
EnsureBalanceParameters(
Expand All @@ -223,4 +208,3 @@ def assure_funds_and_opt_in(algod_client: "AlgodClient", account: Account, asset
min_funding_increment_micro_algos=1,
),
)
opt_in(algod_client=algod_client, account=account, asset_id=asset_id)
Loading

1 comment on commit 3cf9238

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
src/algokit_utils
   _ensure_funded.py69199%99
   _transfer.py62395%13, 76–77
   account.py851385%14–17, 61–65, 96, 109, 136, 139, 183
   application_client.py5519683%53–54, 111, 128, 179, 184, 213, 325, 330–331, 333, 335, 402, 411, 420, 470, 478, 487, 531, 539, 548, 592, 600, 609, 661, 669, 678, 720, 728, 737, 797, 812, 830–833, 909, 949, 961, 974, 1016, 1076–1082, 1086–1091, 1093, 1129, 1136, 1247, 1279, 1333–1335, 1337, 1347–1404, 1415–1420, 1440–1443
   application_specification.py971189%92, 94, 193–202, 206
   asset.py79594%9, 27–30
   config.py17759%13–18, 21–22
   deploy.py4552395%30–33, 168, 172–173, 190, 246, 402, 413–421, 438–441, 451, 459, 652–653, 677
   dispenser_api.py821285%112–113, 117–120, 155–157, 176–178
   logic_error.py38295%6, 30
   models.py126794%45, 50–52, 61–62, 125
   network_clients.py66395%89–90, 121
TOTAL173918389% 

Tests Skipped Failures Errors Time
190 0 💤 0 ❌ 0 🔥 2m 8s ⏱️

Please sign in to comment.