Skip to content

Commit

Permalink
feat: dispenser fund command aliasing support (#325)
Browse files Browse the repository at this point in the history
  • Loading branch information
aorumbayev authored Oct 11, 2023
1 parent cbc052a commit 9bb14aa
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 41 deletions.
2 changes: 1 addition & 1 deletion docs/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ algokit dispenser fund [OPTIONS]


### -r, --receiver <receiver>
**Required** Receiver address to fund with TestNet ALGOs.
**Required** Address or alias of the receiver to fund with TestNet ALGOs.


### -a, --amount <amount>
Expand Down
2 changes: 1 addition & 1 deletion docs/features/dispenser.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ $ algokit dispenser fund [OPTIONS]
This command funds your wallet address with TestNet ALGOs.
Options

- `--receiver`, -r: Receiver address to fund with TestNet ALGOs. This option is required.
- `--receiver`, -r: Receiver [alias](./tasks/wallet.md#add) or address to fund with TestNet ALGOs. This option is required.
- `--amount`, -a: Amount to fund. Defaults to microAlgos. This option is required.
- `--whole-units`: Use whole units (Algos) instead of smallest divisible units (microAlgos). Disabled by default.

Expand Down
18 changes: 15 additions & 3 deletions src/algokit/cli/dispenser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import click

from algokit.cli.tasks.utils import get_address
from algokit.core.dispenser import (
DISPENSER_ACCESS_TOKEN_KEY,
DispenserApiAudiences,
Expand Down Expand Up @@ -147,20 +148,31 @@ def login_command(*, ci: bool, output_mode: str, output_filename: str) -> None:


@dispenser_group.command("fund", help="Fund your wallet address with TestNet ALGOs.")
@click.option("--receiver", "-r", required=True, help="Receiver address to fund with TestNet ALGOs.")
@click.option("--amount", "-a", required=True, help="Amount to fund. Defaults to microAlgos.", default=1000000)
@click.option(
"--receiver",
"-r",
required=True,
help="Address or alias of the receiver to fund with TestNet ALGOs.",
type=click.STRING,
)
@click.option(
"--amount", "-a", required=True, help="Amount to fund. Defaults to microAlgos.", default=1000000, type=click.INT
)
@click.option(
"--whole-units",
"whole_units",
is_flag=True,
help="Use whole units (Algos) instead of smallest divisible units (microAlgos). Disabled by default.",
default=False,
type=click.BOOL,
)
def fund_command(*, receiver: str, amount: int, whole_units: bool) -> None:
if not is_authenticated():
logger.error(NOT_AUTHENTICATED_MESSAGE)
return

receiver_address = get_address(receiver)

default_asset = DISPENSER_ASSETS[DispenserAssetName.ALGO]
if whole_units:
amount = amount * (10**default_asset.decimals)
Expand All @@ -169,7 +181,7 @@ def fund_command(*, receiver: str, amount: int, whole_units: bool) -> None:
try:
response = process_dispenser_request(
url_suffix=f"fund/{DISPENSER_ASSETS[DispenserAssetName.ALGO].asset_id}",
data={"receiver": receiver, "amount": amount, "assetID": default_asset.asset_id},
data={"receiver": receiver_address, "amount": amount, "assetID": default_asset.asset_id},
method="POST",
)
except Exception as e:
Expand Down
3 changes: 3 additions & 0 deletions src/algokit/cli/tasks/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ def get_address(address: str) -> str:
validate_address(parsed_address)
return parsed_address
except click.ClickException as ex:
if len(parsed_address) == algosdk.constants.address_len:
raise click.ClickException(f"`{parsed_address}` is an invalid account address") from ex

alias_data = get_alias(parsed_address)

if not alias_data:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Error: `TZXGUW6DZ27OBB4QSGZKTYFEABCO3R7XWAXECEV73DTF3VOBNNJNAHZJJY` is an invalid account address
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DEBUG: `abc` does not exist
Error: Alias `abc` alias does not exist.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DEBUG: HTTP Request: POST https://snapshottest.dispenser.com/fund/0 "HTTP/1.1 200 OK"
Successfully funded 1000000 μAlgo. Browse transaction at https://testnet.algoexplorer.io/tx/dummy_tx_id
96 changes: 60 additions & 36 deletions tests/dispenser/test_dispenser.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import typing
import json
from pathlib import Path

import click
Expand All @@ -15,6 +15,7 @@
APIErrorCode,
AuthConfig,
)
from algokit.core.tasks.wallet import WALLET_ALIASES_KEYRING_USERNAME
from approvaltests.namer import NamerFactory
from pytest_httpx import HTTPXMock
from pytest_mock import MockerFixture
Expand All @@ -28,35 +29,6 @@ def _mock_api_base_url(mocker: MockerFixture) -> None:
mocker.patch("algokit.core.dispenser.ApiConfig.BASE_URL", "https://snapshottest.dispenser.com")


@pytest.fixture()
def mock_keyring(mocker: MockerFixture) -> typing.Generator[dict[str, str | None], None, None]:
credentials: dict[str, str | None] = {
DISPENSER_KEYRING_ID_TOKEN_KEY: None,
DISPENSER_KEYRING_ACCESS_TOKEN_KEY: None,
DISPENSER_KEYRING_REFRESH_TOKEN_KEY: None,
DISPENSER_KEYRING_USER_ID_KEY: None,
}

def _get_password(namespace: str, key: str) -> str | None: # noqa: ARG001
return credentials[key]

def _set_password(namespace: str, key: str, password: str) -> None: # noqa: ARG001
credentials[key] = password

def _delete_password(namespace: str, key: str) -> None: # noqa: ARG001
credentials[key] = None

mocker.patch("keyring.get_password", side_effect=_get_password)
mocker.patch("keyring.set_password", side_effect=_set_password)
mocker.patch("keyring.delete_password", side_effect=_delete_password)

yield credentials

# Teardown step: reset the credentials
for key in credentials:
credentials[key] = None


def _set_mock_keyring_credentials(
mock_keyring: dict, id_token: str, access_token: str, refresh_token: str, user_id: str
) -> None:
Expand Down Expand Up @@ -160,10 +132,7 @@ def test_logout_command_success(

# Assert
assert result.exit_code == 0
assert mock_keyring[DISPENSER_KEYRING_ID_TOKEN_KEY] is None
assert mock_keyring[DISPENSER_KEYRING_ACCESS_TOKEN_KEY] is None
assert mock_keyring[DISPENSER_KEYRING_REFRESH_TOKEN_KEY] is None
assert mock_keyring[DISPENSER_KEYRING_USER_ID_KEY] is None
assert not mock_keyring
verify(result.output)

def test_logout_command_revoke_exception(
Expand Down Expand Up @@ -385,7 +354,7 @@ def test_fund_command_success(
mocker.patch("algokit.cli.dispenser.is_authenticated", return_value=True)
algo_asset = DISPENSER_ASSETS[DispenserAssetName.ALGO]
amount = 1 if use_whole_units else int(1e6)
receiver = "A" * 58
receiver = "TZXGUW6DZ27OBB4QSGZKTYFEABCO3R7XWAXECEV73DTFLVOBNNJNAHZJJY"
httpx_mock.add_response(
url=f"{ApiConfig.BASE_URL}/fund/{algo_asset.asset_id}",
method="POST",
Expand Down Expand Up @@ -432,7 +401,7 @@ def test_fund_command_http_error(
)

# Act
result = invoke("dispenser fund -r abc -a 123")
result = invoke("dispenser fund -r TZXGUW6DZ27OBB4QSGZKTYFEABCO3R7XWAXECEV73DTFLVOBNNJNAHZJJY -a 123")

# Assert
assert result.exit_code == 0
Expand All @@ -452,6 +421,61 @@ def test_fund_command_not_authenticated(
assert result.exit_code == 0
verify(result.output)

def test_fund_command_from_alias_successful(
self,
mocker: MockerFixture,
mock_keyring: dict[str, str | None],
httpx_mock: HTTPXMock,
) -> None:
# Arrange
alias_name = "test_alias"
_set_mock_keyring_credentials(mock_keyring, "id_token", "access_token", "refresh_token", "user_id")
mock_keyring[alias_name] = json.dumps(
{
"alias": alias_name,
"address": "TZXGUW6DZ27OBB4QSGZKTYFEABCO3R7XWAXECEV73DTFLVOBNNJNAHZJJY",
"private_key": None,
}
)
mock_keyring[WALLET_ALIASES_KEYRING_USERNAME] = json.dumps([alias_name])
mocker.patch("algokit.cli.dispenser.is_authenticated", return_value=True)
httpx_mock.add_response(
url=f"{ApiConfig.BASE_URL}/fund/{DISPENSER_ASSETS[DispenserAssetName.ALGO].asset_id}",
method="POST",
json={"amount": int(1e6), "txID": "dummy_tx_id"},
)

# Act
result = invoke("dispenser fund -r test_alias -a 123")

# Assert
assert result.exit_code == 0
verify(result.output)

def test_fund_command_address_invalid(self, mocker: MockerFixture, mock_keyring: dict[str, str | None]) -> None:
# Arrange
mocker.patch("algokit.cli.dispenser.is_authenticated", return_value=True)
_set_mock_keyring_credentials(mock_keyring, "id_token", "access_token", "refresh_token", "user_id")

# Act
result = invoke("dispenser fund -r TZXGUW6DZ27OBB4QSGZKTYFEABCO3R7XWAXECEV73DTF3VOBNNJNAHZJJY -a 123")

# Assert
assert result.exit_code == 1
verify(result.output)

def test_fund_command_alias_invalid(self, mocker: MockerFixture, mock_keyring: dict[str, str | None]) -> None:
# Arrange
mocker.patch("algokit.cli.dispenser.is_authenticated", return_value=True)
_set_mock_keyring_credentials(mock_keyring, "id_token", "access_token", "refresh_token", "user_id")

# Act
result = invoke("dispenser fund -r abc -a 123")

# Assert
assert result.exit_code == 1
verify(result.output)


class TestRefundCommand:
def test_refund_command_invalid_args(
Expand Down

0 comments on commit 9bb14aa

Please sign in to comment.