"""
SEP: 0002
Title: Federation protocol
Author: stellar.org
Status: Final
Created: 2017-10-30
Updated: 2019-10-10
Version 1.1.0
"""
from typing import Dict, Optional
from .. import AiohttpClient
from ..client.base_async_client import BaseAsyncClient
from ..client.base_sync_client import BaseSyncClient
from ..client.requests_client import RequestsClient
from ..client.response import Response
from ..exceptions import ValueError
from ..type_checked import type_checked
from .exceptions import (
BadFederationResponseError,
FederationServerNotFoundError,
InvalidFederationAddress,
)
from .stellar_toml import fetch_stellar_toml, fetch_stellar_toml_async
SEPARATOR = "*"
FEDERATION_SERVER_KEY = "FEDERATION_SERVER"
__all__ = [
"FederationRecord",
"resolve_stellar_address",
"resolve_stellar_address_async",
"resolve_account_id",
"resolve_account_id_async",
]
[docs]@type_checked
class FederationRecord:
def __init__(
self,
account_id: str,
stellar_address: str,
memo_type: Optional[str],
memo: Optional[str],
) -> None:
"""The :class:`FederationRecord`, which represents record in federation server.
:param account_id: Stellar public key / account ID
:param stellar_address: Stellar address
:param memo_type: Type of memo to attach to transaction, one of ``text``, ``id`` or ``hash``
:param memo: value of memo to attach to transaction, for ``hash`` this should be base64-encoded.
This field should always be of type ``string`` (even when `memo_type` is equal ``id``) to support parsing
value in languages that don't support big numbers.
"""
self.account_id: str = account_id
self.stellar_address: str = stellar_address
self.memo_type: Optional[str] = memo_type
self.memo: Optional[str] = memo
def __str__(self):
return (
f"<FederationRecord [account_id={self.account_id}, stellar_address={self.stellar_address}, "
f"memo_type={self.memo_type}, memo={self.memo}]>"
)
def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
return NotImplemented
return (
self.account_id == other.account_id
and self.stellar_address == other.stellar_address
and self.memo_type == other.memo_type
and self.memo == other.memo
)
[docs]@type_checked
def resolve_stellar_address(
stellar_address: str,
client: BaseSyncClient = None,
federation_url: str = None,
use_http: bool = False,
) -> FederationRecord:
"""Get the federation record if the user was found for a given Stellar address.
:param stellar_address: address Stellar address (ex. ``"bob*stellar.org"``).
:param client: Http Client used to send the request.
:param federation_url: The federation server URL (ex. ``"https://stellar.org/federation"``),
if you don't set this value, we will try to get it from `stellar_address`.
:param use_http: Specifies whether the request should go over plain HTTP vs HTTPS.
Note it is recommend that you **always** use HTTPS.
:return: Federation record.
"""
if not client:
client = RequestsClient()
parts = _split_stellar_address(stellar_address)
domain = parts["domain"]
if federation_url is None:
federation_url = fetch_stellar_toml(domain, use_http=use_http).get( # type: ignore[union-attr]
FEDERATION_SERVER_KEY
)
if federation_url is None:
raise FederationServerNotFoundError(
f"Unable to find federation server at {domain}."
)
raw_resp = client.get(federation_url, {"type": "name", "q": stellar_address})
return _handle_raw_response(raw_resp, stellar_address=stellar_address)
[docs]@type_checked
async def resolve_stellar_address_async(
stellar_address: str,
client: BaseAsyncClient = None,
federation_url: str = None,
use_http: bool = False,
) -> FederationRecord:
"""Get the federation record if the user was found for a given Stellar address.
:param stellar_address: address Stellar address (ex. ``"bob*stellar.org"``).
:param client: Http Client used to send the request.
:param federation_url: The federation server URL (ex. ``"https://stellar.org/federation"``),
if you don't set this value, we will try to get it from `stellar_address`.
:param use_http: Specifies whether the request should go over plain HTTP vs HTTPS.
Note it is recommend that you **always** use HTTPS.
:return: Federation record.
"""
if not client:
client = AiohttpClient()
parts = _split_stellar_address(stellar_address)
domain = parts["domain"]
if federation_url is None:
federation_url = (
await fetch_stellar_toml_async(domain, client=client, use_http=use_http)
).get(FEDERATION_SERVER_KEY)
if federation_url is None:
raise FederationServerNotFoundError(
f"Unable to find federation server at {domain}."
)
raw_resp = await client.get(federation_url, {"type": "name", "q": stellar_address})
return _handle_raw_response(raw_resp, stellar_address=stellar_address)
[docs]@type_checked
def resolve_account_id(
account_id: str,
domain: str = None,
federation_url: str = None,
client: BaseSyncClient = None,
use_http: bool = False,
) -> FederationRecord:
"""Given an account ID, get their federation record if the user was found
:param account_id: Account ID (ex. ``"GBYNR2QJXLBCBTRN44MRORCMI4YO7FZPFBCNOKTOBCAAFC7KC3LNPRYS"``)
:param domain: Get `federation_url` from the domain, you don't need to set this value if `federation_url` is set.
:param federation_url: The federation server URL (ex. ``"https://stellar.org/federation"``).
:param client: Http Client used to send the request.
:param use_http: Specifies whether the request should go over plain HTTP vs HTTPS.
Note it is recommend that you **always** use HTTPS.
:return: Federation record.
"""
if domain is None and federation_url is None:
raise ValueError("You should provide either `domain` or `federation_url`.")
if not client:
client = RequestsClient()
if domain is not None:
federation_url = fetch_stellar_toml(domain, client, use_http).get( # type: ignore[union-attr]
FEDERATION_SERVER_KEY
)
if federation_url is None:
raise FederationServerNotFoundError(
f"Unable to find federation server at {domain}."
)
assert federation_url is not None
raw_resp = client.get(federation_url, {"type": "id", "q": account_id})
return _handle_raw_response(raw_resp, account_id=account_id)
[docs]@type_checked
async def resolve_account_id_async(
account_id: str,
domain: str = None,
federation_url: str = None,
client: BaseAsyncClient = None,
use_http: bool = False,
) -> FederationRecord:
"""Given an account ID, get their federation record if the user was found
:param account_id: Account ID (ex. ``"GBYNR2QJXLBCBTRN44MRORCMI4YO7FZPFBCNOKTOBCAAFC7KC3LNPRYS"``)
:param domain: Get `federation_url` from the domain, you don't need to set this value if `federation_url` is set.
:param federation_url: The federation server URL (ex. ``"https://stellar.org/federation"``).
:param client: Http Client used to send the request.
:param use_http: Specifies whether the request should go over plain HTTP vs HTTPS.
Note it is recommend that you **always** use HTTPS.
:return: Federation record.
"""
if domain is None and federation_url is None:
raise ValueError("You should provide either `domain` or `federation_url`.")
if not client:
client = AiohttpClient()
if domain is not None:
federation_url = (await fetch_stellar_toml_async(domain, client, use_http)).get(
FEDERATION_SERVER_KEY
)
if federation_url is None:
raise FederationServerNotFoundError(
f"Unable to find federation server at {domain}."
)
assert federation_url is not None
raw_resp = await client.get(federation_url, {"type": "id", "q": account_id})
return _handle_raw_response(raw_resp, account_id=account_id)
@type_checked
def _handle_raw_response(
raw_resp: Response, stellar_address=None, account_id=None
) -> FederationRecord:
if not 200 <= raw_resp.status_code < 300:
raise BadFederationResponseError(raw_resp)
data = raw_resp.json()
account_id = account_id or data.get("account_id")
stellar_address = stellar_address or data.get("stellar_address")
memo_type = data.get("memo_type")
memo = data.get("memo")
return FederationRecord(
account_id=account_id,
stellar_address=stellar_address,
memo_type=memo_type,
memo=memo,
)
@type_checked
def _split_stellar_address(address: str) -> Dict[str, str]:
parts = address.split(SEPARATOR)
if len(parts) != 2:
raise InvalidFederationAddress(
"Address should be a valid address, such as `bob*stellar.org`"
)
name, domain = parts
return {"name": name, "domain": domain}