Skip to content

Commit

Permalink
test: adding snapshot tests
Browse files Browse the repository at this point in the history
  • Loading branch information
aorumbayev committed Oct 17, 2023
1 parent 95de91c commit f285eea
Show file tree
Hide file tree
Showing 16 changed files with 584 additions and 47 deletions.
95 changes: 78 additions & 17 deletions src/algokit/cli/tasks/mint.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,45 @@
from pathlib import Path

import click
from algosdk.error import AlgodHTTPError
from algosdk.util import algos_to_microalgos

from algokit.cli.tasks.utils import (
get_account_with_private_key,
get_asset_explorer_url,
get_transaction_explorer_url,
load_algod_client,
validate_balance,
)
from algokit.core.tasks.ipfs import (
Web3StorageBadRequestError,
Web3StorageForbiddenError,
Web3StorageHttpError,
Web3StorageInternalServerError,
Web3StorageUnauthorizedError,
get_web3_storage_api_key,
)
from algokit.core.tasks.ipfs import get_web3_storage_api_key
from algokit.core.tasks.mint.mint import mint_token
from algokit.core.tasks.mint.models import TokenMetadata

logger = logging.getLogger(__name__)

MAX_UNIT_NAME_BYTE_LENGTH = 8
MAX_ASSET_NAME_BYTE_LENGTH = 32
ASSET_MINTING_MBR = 0.2 # Algos, 0.1 for base account, 0.1 for asset creation


def _validate_supply(total: int, decimals: int) -> None:
# Validate total and decimals
"""
Validate the total supply and decimal places of a token.
Args:
total (int): The total supply of the token.
decimals (int): The number of decimal places for the token.
Raises:
click.ClickException: If the validation fails.
"""
if not (total == 1 or (total % 10 == 0 and total != 0)):
raise click.ClickException("Total must be 1 or a power of 10 larger than 1 (10, 100, 1000, ...).")
if not ((total == 1 and decimals == 0) or (total != 1 and decimals == int(math.log10(total)))):
Expand All @@ -32,6 +52,18 @@ def _validate_supply(total: int, decimals: int) -> None:


def _validate_unit_name(context: click.Context, param: click.Parameter, value: str) -> str:
"""
Validate the unit name by checking if its byte length is less than or equal to a predefined maximum value.
Args:
context (click.Context): The click context.
param (click.Parameter): The click parameter.
value (str): The value of the parameter.
Returns:
str: The value of the parameter if it passes the validation.
"""

if len(value.encode("utf-8")) <= MAX_UNIT_NAME_BYTE_LENGTH:
return value
else:
Expand All @@ -41,6 +73,18 @@ def _validate_unit_name(context: click.Context, param: click.Parameter, value: s


def _validate_asset_name(context: click.Context, param: click.Parameter, value: str) -> str:
"""
Validate the asset name by checking if its byte length is less than or equal to a predefined maximum value.
Args:
context (click.Context): The click context.
param (click.Parameter): The click parameter.
value (str): The value of the parameter.
Returns:
str: The value of the parameter if it passes the validation.
"""

if len(value.encode("utf-8")) <= MAX_ASSET_NAME_BYTE_LENGTH:
return value
else:
Expand Down Expand Up @@ -166,24 +210,41 @@ def mint( # noqa: PLR0913
raise click.ClickException("You are not logged in! Please login using `algokit ipfs login`.")

client = load_algod_client(network)
validate_balance(
client, creator_account, 0, algos_to_microalgos(ASSET_MINTING_MBR) # type: ignore[no-untyped-call]
)

token_metadata = TokenMetadata.from_json_file(token_metadata_path)
if not token_metadata_path:
token_metadata.name = asset_name
token_metadata.decimals = decimals
try:
asset_id, txn_id = mint_token(
client=client,
api_key=web3_storage_api_key,
creator_account=creator_account,
token_metadata=token_metadata,
image_path=image_path,
unit_name=unit_name,
asset_name=asset_name,
mutable=mutable,
total=total,
)

asset_id, txn_id = mint_token(
client=client,
api_key=web3_storage_api_key,
creator_account=creator_account,
token_metadata=token_metadata,
image_path=image_path,
unit_name=unit_name,
asset_name=asset_name,
mutable=mutable,
total=total,
)

click.echo("\nSuccessfully minted the asset!")
click.echo(f"Browse your asset at: {get_asset_explorer_url(asset_id, network)}")
click.echo(f"Check transaction status at: {get_transaction_explorer_url(txn_id, network)}")
click.echo("\nSuccessfully minted the asset!")
click.echo(f"Browse your asset at: {get_asset_explorer_url(asset_id, network)}")
click.echo(f"Check transaction status at: {get_transaction_explorer_url(txn_id, network)}")
except (
Web3StorageBadRequestError,
Web3StorageUnauthorizedError,
Web3StorageForbiddenError,
Web3StorageInternalServerError,
Web3StorageHttpError,
) as ex:
logger.debug(ex)
raise click.ClickException(repr(ex)) from ex
except AlgodHTTPError as ex:
raise click.ClickException(str(ex)) from ex
except Exception as ex:
logger.debug(ex, exc_info=True)
raise click.ClickException("Failed to mint the asset!") from ex
3 changes: 2 additions & 1 deletion src/algokit/cli/tasks/transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ def transfer( # noqa: PLR0913

# Validate inputs
validate_address(receiver_address)
validate_balance(sender_account, receiver_address, asset_id, amount, algod_client)
validate_balance(algod_client, sender_account, asset_id, amount)
validate_balance(algod_client, receiver_address, asset_id)

# Transfer algos or assets depending on asset_id
txn_response: PaymentTxn | AssetTransferTxn | None = None
Expand Down
59 changes: 31 additions & 28 deletions src/algokit/cli/tasks/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,34 @@
get_algonode_config,
get_default_localnet_config,
)
from algosdk.util import microalgos_to_algos

from algokit.core.tasks.wallet import get_alias

logger = logging.getLogger(__name__)


def _validate_asset_balance(sender_account_info: dict, receiver_account_info: dict, asset_id: int, amount: int) -> None:
sender_asset_record = next(
(asset for asset in sender_account_info.get("assets", []) if asset["asset-id"] == asset_id), None
)
receiver_asset_record = next(
(asset for asset in receiver_account_info.get("assets", []) if asset["asset-id"] == asset_id), None
)
if not sender_asset_record:
raise click.ClickException("Sender is not opted into the asset")
if sender_asset_record["amount"] < amount:
raise click.ClickException("Insufficient asset balance in sender account")
if not receiver_asset_record:
raise click.ClickException("Receiver is not opted into the asset")
def _validate_asset_balance(account_info: dict, asset_id: int, decimals: int, amount: int = 0) -> None:
asset_record = next((asset for asset in account_info.get("assets", []) if asset["asset-id"] == asset_id), None)

if not asset_record:
raise click.ClickException("Account is not opted into the asset")

def _validate_algo_balance(sender_account_info: dict, amount: int) -> None:
if sender_account_info.get("amount", 0) < amount:
raise click.ClickException("Insufficient Algos balance in sender account")
if amount > 0 and asset_record["amount"] < amount:
required = amount / 10**decimals
available = asset_record["amount"] / 10**decimals
raise click.ClickException(
f"Insufficient asset balance in account, required: {required}, available: {available}"
)


def _validate_algo_balance(account_info: dict, amount: int) -> None:
if account_info.get("amount", 0) < amount:
required = microalgos_to_algos(amount) # type: ignore[no-untyped-call]
available = microalgos_to_algos(account_info.get("amount", 0)) # type: ignore[no-untyped-call]
raise click.ClickException(
f"Insufficient Algos balance in account, required: {required} Algos, available: {available} Algos"
)


def get_private_key_from_mnemonic() -> str:
Expand Down Expand Up @@ -127,33 +131,32 @@ def get_asset_decimals(asset_id: int, algod_client: algosdk.v2client.algod.Algod


def validate_balance(
sender: Account, receiver: str, asset_id: int, amount: int, algod_client: algosdk.v2client.algod.AlgodClient
algod_client: algosdk.v2client.algod.AlgodClient, account: Account | str, asset_id: int, amount: int = 0
) -> None:
"""
Validates the balance of a sender's account before transferring assets or Algos to a receiver's account.
Validates the balance of an account before an operation.
Args:
sender (Account): The sender's account object.
receiver (str): The receiver's account address.
asset_id (int): The ID of the asset to be transferred (0 for Algos).
amount (int): The amount of Algos or asset to be transferred.
algod_client (algosdk.v2client.algod.AlgodClient): The AlgodClient object for
interacting with the Algorand blockchain.
account (Account | str): The account object.
asset_id (int): The ID of the asset to be checked (0 for Algos).
amount (int): The amount of Algos or asset for the operation. Defaults to 0 implying opt-in check only.
Raises:
click.ClickException: If any validation check fails.
"""
address = account.address if isinstance(account, Account) else account
account_info = algod_client.account_info(address)

sender_account_info = algod_client.account_info(sender.address)
receiver_account_info = algod_client.account_info(receiver)

if not isinstance(sender_account_info, dict) or not isinstance(receiver_account_info, dict):
if not isinstance(account_info, dict):
raise click.ClickException("Invalid account info response")

if asset_id == 0:
_validate_algo_balance(sender_account_info, amount)
_validate_algo_balance(account_info, amount)
else:
_validate_asset_balance(sender_account_info, receiver_account_info, asset_id, amount)
decimals = get_asset_decimals(asset_id, algod_client)
_validate_asset_balance(account_info, asset_id, decimals, amount)


def validate_address(address: str) -> None:
Expand Down
82 changes: 82 additions & 0 deletions src/algokit/core/tasks/mint/mint.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@


def _reserve_address_from_cid(cid: str) -> str:
"""
Returns the reserve address associated with a given CID (Content Identifier).
Args:
cid (str): The CID for which the reserve address needs to be determined.
Returns:
str: The reserve address associated with the given CID.
"""

# Workaround to fix `multiformats` package issue, remove first two bytes before using `encode_address`.
# Initial fix using `py-multiformats-cid` and `multihash.decode` was dropped due to PEP 517 incompatibility.
digest = CID.decode(cid).digest[2:]
Expand All @@ -29,6 +39,19 @@ def _reserve_address_from_cid(cid: str) -> str:


def _create_url_from_cid(cid: str) -> str:
"""
Creates an ARC19 asset template URL based on the given CID (Content Identifier).
Args:
cid (str): The CID for which the URL needs to be created.
Returns:
str: The URL created based on the given CID.
Raises:
AssertionError: If the constructed URL does not match the expected format.
"""

cid_object = CID.decode(cid)
version = cid_object.version
codec = cid_object.codec.name
Expand All @@ -43,13 +66,31 @@ def _create_url_from_cid(cid: str) -> str:


def _file_integrity(filename: pathlib.Path) -> str:
"""
Calculate the SHA-256 hash of a file to ensure its integrity.
Args:
filename (pathlib.Path): The path to the file for which the integrity needs to be calculated.
Returns:
str: The integrity of the file in the format "sha-256<hash>".
"""
with filename.open("rb") as f:
file_bytes = f.read() # read entire file as bytes
readable_hash = hashlib.sha256(file_bytes).hexdigest()
return "sha-256" + readable_hash


def _file_mimetype(filename: pathlib.Path) -> str:
"""
Returns the MIME type of a file based on its extension.
Args:
filename (pathlib.Path): The path to the file.
Returns:
str: The MIME type of the file.
"""
extension = pathlib.Path(filename).suffix
return mimetypes.types_map[extension]

Expand All @@ -60,6 +101,20 @@ def _create_asset_txn(
token_metadata: TokenMetadata,
use_metadata_hash: bool = True,
) -> transaction.AssetConfigTxn:
"""
Create an instance of the AssetConfigTxn class by setting the parameters and metadata
for the asset configuration transaction.
Args:
asset_config_params (AssetConfigTxnParams): An instance of the AssetConfigTxnParams class
that contains the parameters for the asset configuration transaction.
token_metadata (TokenMetadata): An instance of the TokenMetadata class that contains the metadata for the asset.
use_metadata_hash (bool, optional): A boolean flag indicating whether to use the metadata hash
in the asset configuration transaction. Defaults to True.
Returns:
AssetConfigTxn: An instance of the AssetConfigTxn class representing the asset configuration transaction.
"""
json_metadata = token_metadata.to_json()
metadata = json.loads(json_metadata)

Expand Down Expand Up @@ -99,6 +154,32 @@ def mint_token( # noqa: PLR0913
image_path: pathlib.Path | None = None,
decimals: int | None = 0,
) -> tuple[int, str]:
"""
Mint a new token on the Algorand blockchain.
Args:
client (algod.AlgodClient): An instance of the `algod.AlgodClient` class representing the Algorand node.
api_key (str): A string representing the API key for accessing the Algorand network.
creator_account (Account): An instance of the `Account` class representing the account that
will create the token.
asset_name (str): A string representing the name of the token.
unit_name (str): A string representing the unit name of the token.
total (int): An integer representing the total supply of the token.
token_metadata (TokenMetadata): An instance of the `TokenMetadata` class representing the metadata of the token.
mutable (bool): A boolean indicating whether the token is mutable or not.
image_path (pathlib.Path | None, optional): A `pathlib.Path` object representing the path to the
image file associated with the token. Defaults to None.
decimals (int | None, optional): An integer representing the number of decimal places for the token.
Defaults to 0.
Returns:
tuple[int, str]: A tuple containing the asset index and transaction ID of the minted token.
Raises:
ValueError: If the token name in the metadata JSON does not match the provided asset name.
ValueError: If the decimals in the metadata JSON does not match the provided decimals amount.
"""

if not token_metadata.name or token_metadata.name != asset_name:
raise ValueError("Token name in metadata JSON must match CLI argument providing token name!")

Expand Down Expand Up @@ -131,6 +212,7 @@ def mint_token( # noqa: PLR0913
decimals=decimals,
)

logger.debug(f"Asset config params: {asset_config_params.to_json()}")
asset_config_txn = _create_asset_txn(
asset_config_params=asset_config_params,
token_metadata=token_metadata,
Expand Down
Loading

0 comments on commit f285eea

Please sign in to comment.