Skip to content

Commit

Permalink
feat: nfd domains lookups (#328)
Browse files Browse the repository at this point in the history
* feat: nfd domains lookups

* chore: pr comments

* docs: regen docs

* chore: mypy tweaks
  • Loading branch information
aorumbayev authored Oct 13, 2023
1 parent 54b8a5e commit 69d1f0b
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 14 deletions.
57 changes: 44 additions & 13 deletions docs/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,49 +104,54 @@
- [Options](#options-17)
- [-f, --file ](#-f---file--1)
- [-n, --name ](#-n---name--1)
- [send](#send)
- [nfd-lookup](#nfd-lookup)
- [Options](#options-18)
- [-o, --output ](#-o---output--2)
- [Arguments](#arguments-5)
- [VALUE](#value)
- [send](#send)
- [Options](#options-19)
- [-f, --file ](#-f---file--2)
- [-t, --transaction ](#-t---transaction-)
- [-n, --network ](#-n---network-)
- [sign](#sign)
- [Options](#options-19)
- [Options](#options-20)
- [-a, --account ](#-a---account-)
- [-f, --file ](#-f---file--3)
- [-t, --transaction ](#-t---transaction--1)
- [-o, --output ](#-o---output--2)
- [-o, --output ](#-o---output--3)
- [--force](#--force-1)
- [transfer](#transfer)
- [Options](#options-20)
- [Options](#options-21)
- [-s, --sender ](#-s---sender-)
- [-r, --receiver ](#-r---receiver--1)
- [--asset, --id ](#--asset---id-)
- [-a, --amount ](#-a---amount--1)
- [--whole-units](#--whole-units-2)
- [-n, --network ](#-n---network--1)
- [vanity-address](#vanity-address)
- [Options](#options-21)
- [Options](#options-22)
- [-m, --match ](#-m---match-)
- [-o, --output ](#-o---output--3)
- [-o, --output ](#-o---output--4)
- [-a, --alias ](#-a---alias-)
- [--file-path ](#--file-path-)
- [-f, --force](#-f---force)
- [Arguments](#arguments-5)
- [Arguments](#arguments-6)
- [KEYWORD](#keyword)
- [wallet](#wallet)
- [Options](#options-22)
- [Options](#options-23)
- [-a, --address ](#-a---address-)
- [-m, --mnemonic](#-m---mnemonic)
- [-f, --force](#-f---force-1)
- [Arguments](#arguments-6)
- [ALIAS_NAME](#alias_name)
- [Arguments](#arguments-7)
- [ALIAS_NAME](#alias_name)
- [Arguments](#arguments-8)
- [ALIAS](#alias)
- [Options](#options-23)
- [Options](#options-24)
- [-f, --force](#-f---force-2)
- [Arguments](#arguments-8)
- [Arguments](#arguments-9)
- [ALIAS](#alias-1)
- [Options](#options-24)
- [Options](#options-25)
- [-f, --force](#-f---force-3)

# algokit
Expand Down Expand Up @@ -739,6 +744,32 @@ algokit task ipfs upload [OPTIONS]
### -n, --name <name>
Human readable name for this upload, for use in file listings.

### nfd-lookup

Perform a lookup via NFD domain or address, returning the associated address or domain respectively.

```shell
algokit task nfd-lookup [OPTIONS] VALUE
```

### Options


### -o, --output <output>
Output format for NFD API response. Defaults to address|domain resolved.


* **Options**

full | tiny | address


### Arguments


### VALUE
Required argument

### send

Send a signed transaction to the given network.
Expand Down
2 changes: 1 addition & 1 deletion docs/features/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ AlgoKit Tasks are a collection of handy tasks that can be used to perform variou
- Opt-in or opt-out of Algorand Assets - Coming soon!
- [Signing transactions](./tasks/sign.md) - Sign goal clerk compatible Algorand transactions.
- [Sending transactions](./tasks/send.md) - Send signed goal clerk compatible Algorand transactions.
- NFD lookups - Coming soon!
- [NFD lookups](./tasks/nfd.md) - Perform a lookup via NFD domain or address, returning the associated address or domain respectively using the AlgoKit CLI.
- [IPFS uploads](./tasks/ipfs.md) - Upload files to IPFS.
- ARC19 asset minting - Coming soon!
41 changes: 41 additions & 0 deletions docs/features/tasks/nfd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# AlgoKit Task NFD Lookup

The AlgoKit NFD Lookup feature allows you to perform a lookup via NFD domain or address, returning the associated address or domain respectively using the AlgoKit CLI. The feature is powered by [NFDomains MainNet API](https://api-docs.nf.domains/).

## Usage

Available commands and possible usage as follows:

```bash
$ ~ algokit task nfd-lookup
Usage: algokit task nfd-lookup [OPTIONS] VALUE

Perform a lookup via NFD domain or address, returning the associated address or domain respectively.

Options:
-o, --output [full|tiny|address] Output format for NFD API response. Defaults to address|domain resolved.
-h, --help Show this message and exit.
```

## Options

- `VALUE`: Specifies the NFD domain or Algorand address to lookup. This argument is required.
- `--output, -o [full|tiny|address]`: Specifies the output format for NFD API response. Defaults to address|domain resolved.

> When using the `full` and `tiny` output formats, please be aware that these match the [views in get requests of the NFD API](https://api-docs.nf.domains/quick-start#views-in-get-requests). The `address` output format, which is used by default, refers to the respective domain name or address resolved and outputs it as a string (if found).
## Example

To perform a lookup, you can use the nfd-lookup command as follows:

```bash
$ algokit task nfd-lookup {NFD_DOMAIN_OR_ALGORAND_ADDRESS}
```

This will perform a lookup and return the associated address or domain. If you want to specify the output format, you can use the --output flag:

```bash
$ algokit task nfd-lookup {NFD_DOMAIN_OR_ALGORAND_ADDRESS} --output full
```

If the lookup is successful, the result will be output to the console in a JSON format.
2 changes: 2 additions & 0 deletions src/algokit/cli/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import click

from algokit.cli.tasks.ipfs import ipfs_group
from algokit.cli.tasks.nfd import nfd_lookup
from algokit.cli.tasks.send_transaction import send
from algokit.cli.tasks.sign_transaction import sign
from algokit.cli.tasks.transfer import transfer
Expand All @@ -23,3 +24,4 @@ def task_group() -> None:
task_group.add_command(sign)
task_group.add_command(send)
task_group.add_command(ipfs_group)
task_group.add_command(nfd_lookup)
52 changes: 52 additions & 0 deletions src/algokit/cli/tasks/nfd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import logging

import click

from algokit.cli.tasks.utils import validate_address
from algokit.core.tasks.nfd import NFDMatchType, nfd_lookup_by_address, nfd_lookup_by_domain

logger = logging.getLogger(__name__)


def is_nfd(value: str) -> bool:
return value.endswith(".algo")


def is_algorand_address(value: str) -> bool:
try:
validate_address(value)
return True
except Exception:
return False


@click.command(
name="nfd-lookup",
help="Perform a lookup via NFD domain or address, returning the associated address or domain respectively.",
)
@click.argument(
"value",
type=click.STRING,
)
@click.option(
"--output",
"-o",
required=False,
default=NFDMatchType.ADDRESS.value,
type=click.Choice([e.value for e in NFDMatchType]),
help="Output format for NFD API response. Defaults to address|domain resolved.",
)
def nfd_lookup(
value: str,
output: str,
) -> None:
if not is_nfd(value) and not is_algorand_address(value):
raise click.ClickException("Invalid input. Must be either a valid NFD domain or an Algorand address.")

try:
if is_nfd(value):
click.echo(nfd_lookup_by_domain(value, NFDMatchType(output)))
elif is_algorand_address(value):
click.echo(nfd_lookup_by_address(value, NFDMatchType(output)))
except Exception as err:
raise click.ClickException(str(err)) from err
101 changes: 101 additions & 0 deletions src/algokit/core/tasks/nfd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import json
import logging
from enum import Enum

import httpx

logger = logging.getLogger(__name__)

NF_DOMAINS_API_URL = "https://api.nf.domains"


class NFDMatchType(Enum):
FULL = "full"
TINY = "tiny"
ADDRESS = "address"


def _process_get_request(url: str) -> dict:
response = httpx.get(url)

try:
response.raise_for_status()
data = response.json()
if not isinstance(data, dict):
raise ValueError("Response JSON is not a dictionary")
return data
except httpx.HTTPStatusError as err:
logger.debug(f"Error response: {err.response}")

if err.response.status_code == httpx.codes.NOT_FOUND:
raise Exception("Not found!") from err
if err.response.status_code == httpx.codes.BAD_REQUEST:
raise Exception(f"Invalid request: {err.response.text}") from err
if err.response.status_code == httpx.codes.UNAUTHORIZED:
raise Exception(f"Unauthorized to access NFD API: {err.response.text}") from err
if err.response.status_code == httpx.codes.FORBIDDEN:
raise Exception(f"Forbidden to access NFD API: {err.response.text}") from err
if err.response.status_code == httpx.codes.TOO_MANY_REQUESTS:
raise Exception(f"Too many requests to NFD API: {err.response.text}") from err

raise Exception(
f'NFD lookup failed with status code {err.response.status_code} and message "{err.response.text}"'
) from err


def nfd_lookup_by_address(address: str, view: NFDMatchType) -> str:
"""
Perform a lookup on an API to retrieve information about a given address.
Args:
address (str): The address to perform the lookup on.
view (NFDMatchType): The type of view to retrieve from the API.
It can be one of the following: "full", "tiny", or "address".
Returns:
str: If the view is "address", returns the name associated with the address as a string.
If the view is not "address", returns the JSON response from the API as a string with an indentation of 2.
Raises:
Exception: If the content from the API is not a dictionary, raises an exception with the unexpected response.
"""

view_type = "thumbnail" if view.value == NFDMatchType.ADDRESS.value else view.value
url = f"{NF_DOMAINS_API_URL}/nfd/lookup?address={address}&view={view_type}&allowUnverified=false"
content = _process_get_request(url)
if isinstance(content, dict):
if view.value == NFDMatchType.ADDRESS.value:
return str(content[address]["name"])
else:
return json.dumps(content, indent=2)

raise Exception(f"Unexpected response from NFD API: {content}")


def nfd_lookup_by_domain(domain: str, view: NFDMatchType) -> str:
"""
Performs a lookup on a given domain using the NF Domains API.
Args:
domain (str): The domain to be looked up.
view (NFDMatchType): The type of information to retrieve.
It can be one of the following: NFDMatchType.FULL, NFDMatchType.TINY, or NFDMatchType.ADDRESS.
Returns:
str: If the view is NFDMatchType.ADDRESS, returns the owner of the domain as a string.
If the view is not NFDMatchType.ADDRESS, returns the response JSON stringified with indentation.
Raises:
Exception: If the response from the NF Domains API is not a dictionary.
"""

view_type = "brief" if view.value == NFDMatchType.ADDRESS.value else view.value
url = f"{NF_DOMAINS_API_URL}/nfd/{domain}?view={view_type}&poll=false"
content = _process_get_request(url)
if isinstance(content, dict):
if view == NFDMatchType.ADDRESS:
return str(content["owner"])
else:
return json.dumps(content, indent=2)

raise Exception(f"Unexpected response from NFD API: {content}")
77 changes: 77 additions & 0 deletions tests/tasks/test_nfd_lookup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import algosdk
from pytest_httpx import HTTPXMock

from tests.utils.approvals import verify
from tests.utils.click_invoker import invoke


def test_nfd_lookup_by_domain_success(httpx_mock: HTTPXMock) -> None:
# Arrange
httpx_mock.add_response(
url="https://api.nf.domains/nfd/dummy.algo?view=brief&poll=false",
json={
"name": "dummy.algo",
"owner": "A" * 58,
"depositAccount": "A" * 58,
"properties": {},
},
)

# Act
result = invoke("task nfd-lookup dummy.algo")

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


def test_nfd_lookup_by_address_success(httpx_mock: HTTPXMock) -> None:
# Arrange
_, dummy_wallet = algosdk.account.generate_account() # type: ignore[no-untyped-call]
httpx_mock.add_response(
url=f"https://api.nf.domains/nfd/lookup?address={dummy_wallet}&view=thumbnail&allowUnverified=false",
json={
dummy_wallet: {
"appID": 222222222,
"state": "owned",
"timeChanged": "2022-02-02",
"depositAccount": "A" * 58,
"name": "dummy.algo",
"owner": "A" * 58,
"properties": {},
"caAlgo": ["A" * 58],
}
},
)

# Act
result = invoke(f"task nfd-lookup {dummy_wallet}")

# Assert
assert result.exit_code == 0
verify(result.output.replace(dummy_wallet, "A" * 58))


def test_nfd_lookup_error(httpx_mock: HTTPXMock) -> None:
# Arrange
httpx_mock.add_response(
url="https://api.nf.domains/nfd/dummy.algo?view=brief&poll=false",
status_code=400,
json={"message": "Invalid request"},
)

# Act
result = invoke("task nfd-lookup dummy.algo")

# Assert
assert result.exit_code == 1
assert "Invalid request" in result.output


def test_nfd_lookup_invalid_input() -> None:
# Act
result = invoke("task nfd-lookup dummy")

# Assert
assert result.exit_code == 1
assert "Invalid input. Must be either a valid NFD domain or an Algorand address." in result.output
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DEBUG: HTTP Request: GET https://api.nf.domains/nfd/lookup?address=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&view=thumbnail&allowUnverified=false "HTTP/1.1 200 OK"
dummy.algo
Loading

0 comments on commit 69d1f0b

Please sign in to comment.