Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: improved error handling, snapshot tests for dispenser command #311

Merged
merged 9 commits into from
Sep 19, 2023
37 changes: 22 additions & 15 deletions src/algokit/cli/dispenser.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class DispenserAssetName(enum.IntEnum):

DEFAULT_CI_TOKEN_FILENAME = "algokit_ci_token.txt"

NOT_AUTHENTICATED_MESSAGE = "Please login first by running `algokit dispenser login` command"


def _handle_ci_token(output_mode: str, output_filename: str, token_data: dict) -> None:
if output_mode == OutputMode.STDOUT.value:
Expand Down Expand Up @@ -123,7 +125,7 @@ def logout_command() -> None:
)
def login_command(*, ci: bool, output_mode: str, output_filename: str) -> None:
if not ci and is_authenticated():
logger.info("Already logged in")
logger.info("You are already logged in")
return

try:
Expand Down Expand Up @@ -156,7 +158,7 @@ def login_command(*, ci: bool, output_mode: str, output_filename: str) -> None:
)
def fund_command(*, receiver: str, amount: int, whole_units: bool) -> None:
if not is_authenticated():
logger.error("Please login first")
logger.error(NOT_AUTHENTICATED_MESSAGE)
return

default_asset = DISPENSER_ASSETS[DispenserAssetName.ALGO]
Expand All @@ -173,22 +175,29 @@ def fund_command(*, receiver: str, amount: int, whole_units: bool) -> None:
except Exception as e:
logger.error(f"Error: {e}")
else:
logger.info(response.json()["message"])
response_body = response.json()
processed_amount = (
response_body["amount"] / (10**default_asset.decimals) if whole_units else response_body["amount"]
)
asset_description = default_asset.description if whole_units else f"μ{default_asset.description}"
logger.info(
f'Successfully funded {processed_amount} {asset_description}. Browse transaction at https://testnet.algoexplorer.io/tx/{response_body["txID"]}'
)


@dispenser_group.command("refund", help="Refund ALGOs back to the dispenser wallet address.")
@click.option("--txID", "-t", "tx_id", required=True, help="Transaction ID of your refund operation.")
def refund_command(*, tx_id: str) -> None:
if not is_authenticated():
logger.error("Please login first")
logger.error(NOT_AUTHENTICATED_MESSAGE)
return

try:
response = process_dispenser_request(url_suffix="refund", data={"refundTransactionID": tx_id})
process_dispenser_request(url_suffix="refund", data={"refundTransactionID": tx_id})
except Exception as e:
logger.error(f"Error: {e}")
else:
logger.info(response.json()["message"])
logger.info("Successfully processed refund transaction")


@dispenser_group.command("limit", help="Get information about current fund limit on your account. Resets daily.")
Expand All @@ -201,19 +210,17 @@ def refund_command(*, tx_id: str) -> None:
)
def get_fund_limit(*, whole_units: bool) -> None:
if not is_authenticated():
logger.error("Please login first")
logger.error(NOT_AUTHENTICATED_MESSAGE)
return

default_asset = DISPENSER_ASSETS[DispenserAssetName.ALGO]
try:
response = process_dispenser_request(
url_suffix=f"fund/{DISPENSER_ASSETS[DispenserAssetName.ALGO].asset_id}/limit", data={}, method="GET"
)
response = process_dispenser_request(url_suffix=f"fund/{default_asset.asset_id}/limit", data={}, method="GET")
except Exception as e:
logger.error(f"Error: {e}")
else:
response_amount = response.json()["amount"]
amount = response_amount
if whole_units:
amount = amount / (10 ** DISPENSER_ASSETS[DispenserAssetName.ALGO].decimals)
logger.debug(f"Converted response microAlgos to Algos: {amount}")
logger.info(f"Remaining daily fund limit: {amount}")
processed_amount = response_amount / (10**default_asset.decimals) if whole_units else response_amount
asset_description = default_asset.description if whole_units else f"μ{default_asset.description}"

logger.info(f"Remaining daily fund limit: {processed_amount} {asset_description}")
39 changes: 36 additions & 3 deletions src/algokit/core/dispenser.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import base64
import contextlib
import logging
import os
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
from typing import Any, ClassVar

Expand All @@ -27,8 +29,8 @@


class DispenserApiAudiences(str, Enum):
USER = "staging-dispenser-api-user"
CI = "staging-dispenser-api-ci"
USER = "api-staging-dispenser-user"
CI = "api-staging-dispenser-ci"


@dataclass
Expand Down Expand Up @@ -60,6 +62,21 @@ class AuthConfig:
}


class APIErrorCode:
DISPENSER_OUT_OF_FUNDS = "dispenser_out_of_funds"
FORBIDDEN = "forbidden"
FUND_LIMIT_EXCEEDED = "fund_limit_exceeded"
DISPENSER_ERROR = "dispenser_error"
MISSING_PARAMETERS = "missing_params"
AUTHORIZATION_ERROR = "authorization_error"
REPUTATION_REFRESH_FAILED = "reputation_refresh_failed"
TXN_EXPIRED = "txn_expired"
TXN_INVALID = "txn_invalid"
TXN_ALREADY_PROCESSED = "txn_already_processed"
INVALID_ASSET = "invalid_asset"
UNEXPECTED_ERROR = "unexpected_error"


def _get_dispenser_credential(key: str) -> str:
"""
Get dispenser account credentials from the keyring.
Expand Down Expand Up @@ -174,6 +191,12 @@ def _request_device_code(api_audience: DispenserApiAudiences, custom_scopes: str
return data


def _get_hours_until_reset(resets_at: str) -> float:
now_utc = datetime.now(timezone.utc)
reset_date = datetime.strptime(resets_at, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc)
return round((reset_date - now_utc).total_seconds() / 3600, 1)


def request_token(api_audience: DispenserApiAudiences, device_code: str) -> dict[str, Any]:
"""
Request OAuth tokens.
Expand Down Expand Up @@ -219,8 +242,18 @@ def process_dispenser_request(*, url_suffix: str, data: dict | None = None, meth

except httpx.HTTPStatusError as err:
error_message = f"Error processing dispenser API request: {err.response.status_code}"
error_response = None
with contextlib.suppress(Exception):
error_response = err.response.json()

if error_response and error_response.get("code") == APIErrorCode.FUND_LIMIT_EXCEEDED:
hours_until_reset = _get_hours_until_reset(error_response.get("resetsAt"))
error_message = (
"Limit exceeded. "
f"Try again in ~{hours_until_reset} hours if your request doesn't exceed the daily limit."
)

if err.response.status_code == httpx.codes.BAD_REQUEST:
elif err.response.status_code == httpx.codes.BAD_REQUEST:
error_message = err.response.json()["message"]

raise Exception(error_message) from err
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ERROR: Error: Limit exceeded. Try again in ~4.0 hours if your request doesn't exceed the daily limit.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Usage: algokit dispenser fund [OPTIONS]
Try 'algokit dispenser fund -h' for help.

Error: Missing option '--receiver' / '-r'.
aorumbayev marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ERROR: Please login first by running `algokit dispenser login` command
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DEBUG: Converted algos to microAlgos: 1000000
DEBUG: HTTP Request: POST https://snapshottest.dispenser.com/fund/0 "HTTP/1.1 200 OK"
Successfully funded 1.0 Algo. Browse transaction at https://testnet.algoexplorer.io/tx/dummy_tx_id
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DEBUG: Using CI access token over keyring credentials
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DEBUG: Converted algos to microAlgos: 1000000
DEBUG: Using CI access token over keyring credentials
DEBUG: HTTP Request: POST https://snapshottest.dispenser.com/fund/0 "HTTP/1.1 200 OK"
Successfully funded 1.0 Algo. Browse transaction at https://testnet.algoexplorer.io/tx/dummy_tx_id
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DEBUG: Error processing dispenser API request: Unable to process limit request
ERROR: Error: Unable to process limit request
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ERROR: Please login first by running `algokit dispenser login` command
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DEBUG: HTTP Request: GET https://snapshottest.dispenser.com/fund/0/limit "HTTP/1.1 200 OK"
Remaining daily fund limit: 1000000 μAlgo
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DEBUG: HTTP Request: GET https://snapshottest.dispenser.com/fund/0/limit "HTTP/1.1 200 OK"
Remaining daily fund limit: 1.0 Algo
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DEBUG: Using CI access token over keyring credentials
DEBUG: HTTP Request: GET https://snapshottest.dispenser.com/fund/0/limit "HTTP/1.1 200 OK"
Remaining daily fund limit: 1000000 μAlgo
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DEBUG: Using CI access token over keyring credentials
DEBUG: HTTP Request: GET https://snapshottest.dispenser.com/fund/0/limit "HTTP/1.1 200 OK"
Remaining daily fund limit: 1.0 Algo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
You are already logged in
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DEBUG: HTTP Request: POST https://dispenser-staging.eu.auth0.com/oauth/device/code "HTTP/1.1 200 OK"
Navigate to: https://example.com/device
Confirm code: user_code
DEBUG: HTTP Request: POST https://dispenser-staging.eu.auth0.com/oauth/token "HTTP/1.1 200 OK"
DEBUG: HTTP Request: POST https://dispenser-staging.eu.auth0.com/oauth/token "HTTP/1.1 200 OK"
WARNING: Authentication cancelled. Timeout reached after 5 minutes of inactivity.
Error: Error obtaining auth token
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DEBUG: Access token is expired. Attempting to refresh the token...
WARNING: Failed to refresh the access token. Please authenticate first before proceeding with this command.
aorumbayev marked this conversation as resolved.
Show resolved Hide resolved
Login successful
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DEBUG: Access token is expired. Attempting to refresh the token...
DEBUG: HTTP Request: POST https://dispenser-staging.eu.auth0.com/oauth/token "HTTP/1.1 200 OK"
You are already logged in
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
DEBUG: HTTP Request: POST https://dispenser-staging.eu.auth0.com/oauth/device/code "HTTP/1.1 200 OK"
Navigate to: https://example.com/device
Confirm code: user_code
DEBUG: HTTP Request: POST https://dispenser-staging.eu.auth0.com/oauth/token "HTTP/1.1 200 OK"
WARNING: Your CI access token has been saved to `algokit_ci_token.txt`.
Please ensure you keep this file safe or remove after copying the token!
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
DEBUG: HTTP Request: POST https://dispenser-staging.eu.auth0.com/oauth/device/code "HTTP/1.1 200 OK"
Navigate to: https://example.com/device
Confirm code: user_code
DEBUG: HTTP Request: POST https://dispenser-staging.eu.auth0.com/oauth/token "HTTP/1.1 200 OK"
WARNING: Your CI access token has been saved to `custom_file.txt`.
Please ensure you keep this file safe or remove after copying the token!
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
DEBUG: HTTP Request: POST https://dispenser-staging.eu.auth0.com/oauth/device/code "HTTP/1.1 200 OK"
Navigate to: https://example.com/device
Confirm code: user_code
DEBUG: HTTP Request: POST https://dispenser-staging.eu.auth0.com/oauth/token "HTTP/1.1 200 OK"

ALGOKIT_DISPENSER_ACCESS_TOKEN (valid for 30 days):

access_token

WARNING: Your CI access token has been printed to stdout.
Please ensure you keep this token safe!
If needed, clear your terminal history after copying the token!
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
DEBUG: HTTP Request: POST https://dispenser-staging.eu.auth0.com/oauth/device/code "HTTP/1.1 200 OK"
Navigate to: https://example.com/device
Confirm code: user_code
DEBUG: HTTP Request: POST https://dispenser-staging.eu.auth0.com/oauth/token "HTTP/1.1 200 OK"
Login successful
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
WARNING: Already logged out
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DEBUG: Error logging out An unexpected error occurred: Error response
Error: Error logging out
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DEBUG: HTTP Request: POST https://dispenser-staging.eu.auth0.com/oauth/revoke "HTTP/1.1 200 OK"
DEBUG: Token revoked successfully
Logout successful
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DEBUG: Error processing dispenser API request: Transaction was already processed
ERROR: Error: Transaction was already processed
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Usage: algokit dispenser refund [OPTIONS]
Try 'algokit dispenser refund -h' for help.

Error: Missing option '--txID' / '-t'.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ERROR: Please login first by running `algokit dispenser login` command
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DEBUG: HTTP Request: POST https://snapshottest.dispenser.com/refund "HTTP/1.1 200 OK"
Successfully processed refund transaction
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DEBUG: Using CI access token over keyring credentials
DEBUG: HTTP Request: POST https://snapshottest.dispenser.com/refund "HTTP/1.1 200 OK"
Successfully processed refund transaction
Loading