diff --git a/models.py b/models.py index 346741e..0d8e3dc 100644 --- a/models.py +++ b/models.py @@ -2,8 +2,8 @@ from sqlite3 import Row from typing import List, Optional, Tuple -from fastapi.param_functions import Query from lnbits.db import FilterModel, FromRowModel +from lnbits.utils.exchange_rates import fiat_amount_as_satoshis from pydantic import BaseModel from .helpers import format_amount, is_ws_url, normalize_identifier, validate_pub_key @@ -13,6 +13,64 @@ class CustomCost(BaseModel): bracket: int amount: float + def validate_data(self): + assert self.bracket >= 0, "Bracket must be positive." + assert self.amount >= 0, "Custom cost must be positive." + + +class PriceData(BaseModel): + currency: str + price: float + discount: float = 0 + referer_bonus: float = 0 + + reason: str + + async def price_sats(self) -> float: + if self.price == 0: + return 0 + if self.currency == "sats": + return self.price + return await fiat_amount_as_satoshis(self.price, self.currency) + + async def discount_sats(self) -> float: + if self.discount == 0: + return 0 + if self.currency == "sats": + return self.discount + return await fiat_amount_as_satoshis(self.discount, self.currency) + + async def referer_bonus_sats(self) -> float: + if self.referer_bonus == 0: + return 0 + if self.currency == "sats": + return self.referer_bonus + return await fiat_amount_as_satoshis(self.referer_bonus, self.currency) + + +class Promotion(BaseModel): + code: str = "" + buyer_discount_percent: float + referer_bonus_percent: float + selected_referer: Optional[str] = None + + def validate_data(self): + assert ( + 0 <= self.buyer_discount_percent <= 100 + ), f"Discount percent for '{self.code}' must be between 0 and 100." + assert ( + 0 <= self.referer_bonus_percent <= 100 + ), f"Referer percent for '{self.code}' must be between 0 and 100." + assert self.buyer_discount_percent + self.referer_bonus_percent <= 100, ( + f"Discount and Referer for '{self.code}'" " must be less than 100%." + ) + + +class PromoCodeStatus(BaseModel): + buyer_discount: Optional[float] = None + allow_referer: bool = False + referer: Optional[str] = None + class RotateAddressData(BaseModel): secret: str @@ -45,30 +103,127 @@ class CreateAddressData(BaseModel): pubkey: str = "" years: int = 1 relays: Optional[List[str]] = None + promo_code: Optional[str] = None + referer: Optional[str] = None create_invoice: bool = False + def normalize(self): + self.local_part = self.local_part.strip() + self.pubkey = self.pubkey.strip() + if self.relays: + self.relays = [r.strip() for r in self.relays] + + if self.promo_code: + self.promo_code = self.promo_code.strip() + if "@" in self.promo_code: + elements = self.promo_code.rsplit("@") + self.promo_code = elements[0] + self.referer = elements[1] + + if self.referer: + self.referer = self.referer.strip() + class DomainCostConfig(BaseModel): max_years: int = 1 - enable_custom_cost: bool = False char_count_cost: List[CustomCost] = [] rank_cost: List[CustomCost] = [] + promotions: List[Promotion] = [] + + def apply_promo_code( + self, amount: float, promo_code: Optional[str] = None + ) -> Tuple[float, float]: + if promo_code is None: + return 0, 0 + promotion = next((p for p in self.promotions if p.code == promo_code), None) + if not promotion: + return 0, 0 + + discount = amount * (promotion.buyer_discount_percent / 100) + referer_bonus = amount * (promotion.referer_bonus_percent / 100) + return round(discount, 2), round(referer_bonus, 2) + + def get_promotion(self, promo_code: Optional[str] = None) -> Optional[Promotion]: + if promo_code is None: + return None + return next((p for p in self.promotions if p.code == promo_code), None) + + def promo_code_buyer_discount(self, promo_code: Optional[str] = None) -> float: + promotion = self.get_promotion(promo_code) + if not promotion: + return 0 + return promotion.buyer_discount_percent + + def promo_code_referer( + self, promo_code: Optional[str] = None, default_referer: Optional[str] = None + ) -> Optional[str]: + promotion = self.get_promotion(promo_code) + if not promotion: + return None + if promotion.referer_bonus_percent == 0: + return None + if promotion.selected_referer: + return promotion.selected_referer + + return default_referer + + def promo_code_allows_referer(self, promo_code: Optional[str] = None) -> bool: + promotion = self.get_promotion(promo_code) + if not promotion: + return False + + return promotion.referer_bonus_percent > 0 and not promotion.selected_referer + + def promo_code_status(self, promo_code: Optional[str] = None) -> PromoCodeStatus: + return PromoCodeStatus( + buyer_discount=self.promo_code_buyer_discount(promo_code), + allow_referer=self.promo_code_allows_referer(promo_code), + referer=self.promo_code_referer(promo_code), + ) + + def validate_data(self): + for cost in self.char_count_cost: + cost.validate_data() + + for cost in self.rank_cost: + cost.validate_data() + + assert ( + 1 < self.max_years < 100 + ), "Maximum allowed years must be between 1 and 100." + promo_codes = [] + for promo in self.promotions: + promo.validate_data() + assert ( + promo.code not in promo_codes + ), f"Duplicate promo code: '{promo.code}'." + promo_codes.append(promo.code) class CreateDomainData(BaseModel): wallet: str currency: str - cost: float = Query(..., ge=0.01) + cost: float domain: str cost_config: Optional[DomainCostConfig] = None + def validate_data(self): + assert self.cost >= 0, "Domain cost must be positive." + if self.cost_config: + self.cost_config.validate_data() + class EditDomainData(BaseModel): id: str currency: str - cost: float = Query(..., ge=0.01) + cost: float cost_config: Optional[DomainCostConfig] = None + def validate_data(self): + assert self.cost >= 0, "Domain cost must be positive." + if self.cost_config: + self.cost_config.validate_data() + @classmethod def from_row(cls, row: Row) -> "EditDomainData": return cls(**dict(row)) @@ -99,33 +254,43 @@ class Domain(PublicDomain): cost_config: DomainCostConfig = DomainCostConfig() time: int - def price_for_identifier( - self, identifier: str, years: int, rank: Optional[int] = None - ) -> Tuple[float, str]: + async def price_for_identifier( + self, + identifier: str, + years: int, + rank: Optional[int] = None, + promo_code: Optional[str] = None, + ) -> PriceData: assert ( 1 <= years <= self.cost_config.max_years ), f"Number of years must be between '1' and '{self.cost_config.max_years}'." identifier = normalize_identifier(identifier) - max_amount = self.cost - reason = "" - if not self.cost_config.enable_custom_cost: - return max_amount * years, reason + max_amount, reason = self.cost, "" for char_cost in self.cost_config.char_count_cost: if len(identifier) <= char_cost.bracket and max_amount < char_cost.amount: max_amount = char_cost.amount reason = f"{len(identifier)} characters" - if not rank: - return max_amount * years, reason - - for rank_cost in self.cost_config.rank_cost: - if rank <= rank_cost.bracket and max_amount < rank_cost.amount: - max_amount = rank_cost.amount - reason = f"Top {rank_cost.bracket} identifier" - - return max_amount * years, reason + if rank: + for rank_cost in self.cost_config.rank_cost: + if rank <= rank_cost.bracket and max_amount < rank_cost.amount: + max_amount = rank_cost.amount + reason = f"Top {rank_cost.bracket} identifier" + + full_price = max_amount * years + discount, referer_bonus = self.cost_config.apply_promo_code( + full_price, promo_code + ) + + return PriceData( + currency=self.currency, + price=full_price - discount, + discount=discount, + referer_bonus=referer_bonus, + reason=reason, + ) def public_data(self): data = dict(PublicDomain(**dict(self))) @@ -156,6 +321,8 @@ class AddressConfig(BaseModel): reimburse_payment_hash: Optional[str] = None activated_by_owner: bool = False years: int = 1 + promo_code: Optional[str] = None + referer: Optional[str] = None max_years: int = 1 relays: List[str] = [] @@ -175,6 +342,8 @@ class Address(FromRowModel): config: AddressConfig = AddressConfig() + promo_code_status: PromoCodeStatus = PromoCodeStatus() + @property def has_pubkey(self): return self.pubkey != "" diff --git a/services.py b/services.py index 4fb780f..b0f973b 100644 --- a/services.py +++ b/services.py @@ -1,10 +1,9 @@ -from typing import List, Optional +from typing import List, Optional, Tuple import httpx from lnbits.core.crud import get_standalone_payment, get_user -from lnbits.core.services import create_invoice +from lnbits.core.services import create_invoice, pay_invoice from lnbits.db import Filters, Page -from lnbits.utils.exchange_rates import fiat_amount_as_satoshis from loguru import logger from .crud import ( @@ -36,6 +35,7 @@ AddressStatus, CreateAddressData, Domain, + PriceData, ) @@ -82,68 +82,62 @@ async def get_user_addresses_paginated( async def get_identifier_status( - domain: Domain, identifier: str, years: int + domain: Domain, identifier: str, years: int, promo_code: Optional[str] = None ) -> AddressStatus: identifier = normalize_identifier(identifier) address = await get_active_address_by_local_part(domain.id, identifier) if address: return AddressStatus(identifier=identifier, available=False) - rank = None - if domain.cost_config.enable_custom_cost: - identifier_ranking = await get_identifier_ranking(identifier) - rank = identifier_ranking.rank if identifier_ranking else None + price_data = await get_identifier_price_data(domain, identifier, years, promo_code) - if rank == 0: + if not price_data: return AddressStatus(identifier=identifier, available=False) - price, reason = domain.price_for_identifier(identifier, years, rank) - - price_in_sats = ( - price - if domain.currency == "sats" - else await fiat_amount_as_satoshis(price, domain.currency) - ) - return AddressStatus( identifier=identifier, available=True, - price=price, - price_in_sats=price_in_sats, - price_reason=reason, + price=price_data.price, + price_in_sats=await price_data.price_sats(), + price_reason=price_data.reason, currency=domain.currency, ) +async def get_identifier_price_data( + domain: Domain, identifier: str, years: int, promo_code: Optional[str] = None +) -> Optional[PriceData]: + identifier_ranking = await get_identifier_ranking(identifier) + rank = identifier_ranking.rank if identifier_ranking else None + + if rank == 0: + return None + + return await domain.price_for_identifier(identifier, years, rank, promo_code) + + async def request_user_address( domain: Domain, address_data: CreateAddressData, wallet_id: str, user_id: str, ): - address = await create_address(domain, address_data, wallet_id, user_id) + address = await create_address( + domain, address_data, wallet_id, user_id, address_data.promo_code + ) assert ( address.config.price_in_sats ), f"Cannot compute price for '{address_data.local_part}'." + payment_hash, payment_request = None, None if address_data.create_invoice: - # in case the user pays, but the identifier is no longer available - payment_hash, payment_request = await create_invoice( - wallet_id=domain.wallet, - amount=int(address.config.price_in_sats), - memo=f"Payment of {address.config.price} {address.config.currency} " - f"for NIP-05 for {address_data.local_part}@{domain.domain}", - extra={ - "tag": "nostrnip5", - "domain_id": domain.id, - "address_id": address.id, - "action": "activate", - "reimburse_wallet_id": wallet_id, - }, + payment_hash, payment_request = await create_invoice_for_identifier( + domain, address, wallet_id ) - else: - payment_hash, payment_request = None, None + address.promo_code_status = domain.cost_config.promo_code_status( + address_data.promo_code + ) resp = { "payment_hash": payment_hash, "payment_request": payment_request, @@ -153,11 +147,44 @@ async def request_user_address( return resp +async def create_invoice_for_identifier( + domain: Domain, + address: Address, + reimburse_wallet_id: str, +) -> Tuple[str, str]: + price_data = await get_identifier_price_data( + domain, address.local_part, address.config.years, address.config.promo_code + ) + assert price_data, f"Cannot compute price for '{address.local_part}'." + price_in_sats = await price_data.price_sats() + discount_sats = await price_data.discount_sats() + referer_bonus_sats = await price_data.referer_bonus_sats() + + payment_hash, payment_request = await create_invoice( + wallet_id=domain.wallet, + amount=int(price_in_sats), + memo=f"Payment of {address.config.price} {address.config.currency} " + f"for NIP-05 {address.local_part}@{domain.domain}", + extra={ + "tag": "nostrnip5", + "domain_id": domain.id, + "address_id": address.id, + "action": "activate", + "reimburse_wallet_id": reimburse_wallet_id, + "discount_sats": int(discount_sats), + "referer": address.config.referer, + "referer_bonus_sats": int(referer_bonus_sats), + }, + ) + return payment_hash, payment_request + + async def create_address( domain: Domain, data: CreateAddressData, wallet_id: Optional[str] = None, user_id: Optional[str] = None, + promo_code: Optional[str] = None, ) -> Address: identifier = normalize_identifier(data.local_part) @@ -165,26 +192,24 @@ async def create_address( if data.pubkey != "": data.pubkey = validate_pub_key(data.pubkey) - identifier_status = await get_identifier_status(domain, identifier, data.years) - - assert identifier_status.available, f"Identifier '{identifier}' not available." - assert identifier_status.price, f"Cannot compute price for '{identifier}'." + owner_id = owner_id_from_user_id(user_id) + addresss = await get_address_for_owner(owner_id, domain.id, identifier) - price_in_sats = ( - identifier_status.price - if domain.currency == "sats" - else await fiat_amount_as_satoshis(identifier_status.price, domain.currency) + promo_code = promo_code or (addresss.config.promo_code if addresss else None) + identifier_status = await get_identifier_status( + domain, identifier, data.years, promo_code ) - assert price_in_sats, f"Cannot compute price for '{identifier}'." - owner_id = owner_id_from_user_id(user_id) - addresss = await get_address_for_owner(owner_id, domain.id, identifier) + assert identifier_status.available, f"Identifier '{identifier}' not available." + assert identifier_status.price, f"Cannot compute price for '{identifier}'." config = addresss.config if addresss else AddressConfig() config.price = identifier_status.price - config.price_in_sats = price_in_sats + config.price_in_sats = identifier_status.price_in_sats config.currency = domain.currency config.years = data.years + config.promo_code = data.promo_code + config.referer = domain.cost_config.promo_code_referer(promo_code, data.referer) config.max_years = domain.cost_config.max_years config.ln_address.wallet = wallet_id or "" @@ -231,7 +256,7 @@ async def get_valid_addresses_for_owner( if not domain: continue status = await get_identifier_status( - domain, address.local_part, address.config.years + domain, address.local_part, address.config.years, address.config.promo_code ) if status.available: @@ -243,11 +268,48 @@ async def get_valid_addresses_for_owner( continue address.config.currency = domain.currency + address.promo_code_status = domain.cost_config.promo_code_status( + address.config.promo_code + ) valid_addresses.append(address) return valid_addresses +async def pay_referer_for_promo_code(address: Address, referer: str, bonus_sats: int): + try: + assert bonus_sats > 0, f"Bonus amount negative: '{bonus_sats}'." + + domain = await get_domain_by_id(address.domain_id) + assert domain, f"Missing domain for '{address.local_part}'." + + referer_address = await get_active_address_by_local_part( + address.domain_id, referer + ) + assert referer_address, f"Missing address for referer '{referer}'." + referer_wallet = referer_address.config.ln_address.wallet + assert referer_wallet, f"Missing wallet for referer '{referer}'." + + _, payment_request = await create_invoice( + wallet_id=referer_wallet, + amount=bonus_sats, + memo=f"Referer bonus of {bonus_sats} sats to '{referer}' " + f"from NIP-05 {address.local_part}@{domain.domain}", + extra={ + "tag": "nostrnip5", + "domain_id": domain.id, + "address_id": address.id, + "action": "referer_bonus", + }, + ) + + await pay_invoice(wallet_id=domain.wallet, payment_request=payment_request) + + except Exception as exc: + logger.warning(f"Failed to pay referer for '{referer}'.") + logger.warning(exc) + + async def check_address_payment(domain_id: str, payment_hash: str) -> bool: payment = await get_standalone_payment(payment_hash, incoming=True) if not payment: diff --git a/tasks.py b/tasks.py index 938aed4..fce2720 100644 --- a/tasks.py +++ b/tasks.py @@ -6,7 +6,7 @@ from .crud import get_address, update_address from .models import Address -from .services import activate_address, update_ln_address +from .services import activate_address, pay_referer_for_promo_code, update_ln_address async def wait_for_paid_invoices(): @@ -61,6 +61,7 @@ async def _activate_address(payment: Payment, address: Address): ) if activated: await _create_ln_address(payment, address) + await _pay_promo_code(payment, address) else: await _update_reimburse_data(payment, address) @@ -83,8 +84,25 @@ async def _create_ln_address(payment: Payment, address: Address): f" '{address.local_part} ({address.id}')." ) return - address.config.ln_address.wallet = wallet - await update_ln_address(address) + try: + address.config.ln_address.wallet = wallet + await update_ln_address(address) + except Exception as exc: + logger.warning(exc) + + +async def _pay_promo_code(payment: Payment, address: Address): + referer = payment.extra.get("referer") + if not referer: + return + referer_bonus_sats = payment.extra.get("referer_bonus_sats") + if not referer_bonus_sats or not isinstance(referer_bonus_sats, int): + logger.warning( + f"Found referer but no bonus specified for '{address.local_part}'." + ) + return + + await pay_referer_for_promo_code(address, referer, int(referer_bonus_sats)) async def _reimburse_payment(address: Address): diff --git a/templates/nostrnip5/domain.html b/templates/nostrnip5/domain.html new file mode 100644 index 0000000..26bc4e6 --- /dev/null +++ b/templates/nostrnip5/domain.html @@ -0,0 +1,537 @@ +{% extends "public.html" %} {% block toolbar_title %} Edit domain "{{ +domain.domain }}" {% endblock %} {% from "macros.jinja" import window_vars with +context %} {% block page %} + +
+
+ +
+ + +
+ Update Domain +
+
+
+ +
+
+ +
+
+ +
+ + {% raw %} +
+ +
+ + + + + +
+
+ + + + + + + + + +
+
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+ + {% endraw %} +
+ +
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/templates/nostrnip5/index.html b/templates/nostrnip5/index.html index b7a2d15..f66142a 100644 --- a/templates/nostrnip5/index.html +++ b/templates/nostrnip5/index.html @@ -101,7 +101,9 @@
Domains
size="xs" icon="edit" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" - @click="editDomain(props.row.id)" + type="a" + :href="'domain/' + props.row.id" + target="_blank" > @@ -281,148 +283,16 @@
hint="Maximum number of years a user can have an address before renewing." > - -
- - - - - - - - -
-
- -
-
- -
-
- -
-
-
-
-
- -
-
-
-
- -
-
- -
-
- -
-
- -
-
-
-
-
- -
-
-
-
-
-
-
-
- Update Domain - Create Domain - Cancel -
+ Create Domain + Cancel @@ -994,7 +864,6 @@
Settings
this.formDialog.data = { cost_config: { max_years: 1, - enable_custom_cost: false, char_count_cost: [], rank_cost: [] } @@ -1117,10 +986,7 @@
Settings
LNbits.utils.notifyApiError(error) }) }, - editDomain: function (domain_id) { - this.formDialog.show = true - this.formDialog.data = _.findWhere(this.domains, {id: domain_id}) - }, + deleteDomain: function (domain_id) { var self = this var domain = _.findWhere(this.domains, {id: domain_id}) @@ -1395,28 +1261,7 @@
Settings
LNbits.utils.notifyApiError(error) }) }, - addCharCountCost: function () { - this.formDialog.data.cost_config.char_count_cost.push({ - bracket: 0, - amount: 1 - }) - }, - removeCharCountCost: function (index) { - if (index < this.formDialog.data.cost_config.char_count_cost.length) { - this.formDialog.data.cost_config.char_count_cost.splice(index, 1) - } - }, - addRankCost: function () { - this.formDialog.data.cost_config.rank_cost.push({ - bracket: 0, - amount: 1 - }) - }, - removeRankCost: function (index) { - if (index < this.formDialog.data.cost_config.rank_cost.length) { - this.formDialog.data.cost_config.rank_cost.splice(index, 1) - } - }, + domainNameFromId: function (domainId) { const domain = this.domains.find(d => d.id === domainId) || {} return domain.domain || '' diff --git a/views.py b/views.py index 5a964d3..3f7ee3f 100644 --- a/views.py +++ b/views.py @@ -33,6 +33,19 @@ async def index(request: Request, user: User = Depends(check_user_exists)): ) +@nostrnip5_generic_router.get("/domain/{domain_id}", response_class=HTMLResponse) +async def domain_details( + request: Request, domain_id: str, user: User = Depends(check_user_exists) +): + domain = await get_domain_by_id(domain_id) + if not domain: + raise HTTPException(HTTPStatus.NOT_FOUND, "Domain does not exist.") + return nostrnip5_renderer().TemplateResponse( + "nostrnip5/domain.html", + {"request": request, "domain": domain.dict(), "user": user.dict()}, + ) + + @nostrnip5_generic_router.get("/signup/{domain_id}", response_class=HTMLResponse) async def signup( request: Request, diff --git a/views_api.py b/views_api.py index 7a345cc..a12392c 100644 --- a/views_api.py +++ b/views_api.py @@ -17,6 +17,7 @@ require_admin_key, ) from lnbits.helpers import generate_filter_params_openapi +from lnbits.utils.cache import cache from loguru import logger from starlette.exceptions import HTTPException @@ -106,7 +107,7 @@ async def api_get_domains(domain_id: str, w: WalletTypeInfo = Depends(get_key_ty async def api_create_domain( data: CreateDomainData, wallet: WalletTypeInfo = Depends(require_admin_key) ): - + data.validate_data() return await create_domain_internal(wallet_id=wallet.wallet.id, data=data) @@ -115,7 +116,7 @@ async def api_create_domain( async def api_update_domain( data: EditDomainData, wallet: WalletTypeInfo = Depends(require_admin_key) ): - + data.validate_data() return await update_domain_internal(wallet_id=wallet.wallet.id, data=data) @@ -146,16 +147,24 @@ async def api_get_nostr_json( if not name: return {"names": {}, "relays": {}} + cached_nip5 = cache.get(f"{domain_id}/{name}") + if cached_nip5: + return cached_nip5 + address = await get_active_address_by_local_part(domain_id, name) if not address: return {"names": {}, "relays": {}} - return { + nip5 = { "names": {address.local_part: address.pubkey}, "relays": {address.pubkey: address.config.relays}, } + cache.set(f"{domain_id}/{name}", nip5, 60) + + return nip5 + @http_try_except @nostrnip5_api_router.get( @@ -254,6 +263,7 @@ async def api_activate_address( assert domain, "Domain does not exist." active_address = await activate_address(domain_id, address_id) + cache.pop(f"{domain_id}/{active_address.local_part}") return await update_ln_address(active_address) @@ -325,6 +335,8 @@ async def api_update_address( address.config.relays = data.relays await update_address(domain_id, address.id, pubkey=pubkey, config=address.config) + cache.pop(f"{domain_id}/{address.local_part}") + return address @@ -337,6 +349,8 @@ async def api_request_address( domain_id: str, w: WalletTypeInfo = Depends(require_admin_key), ): + address_data.normalize() + # make sure the domain belongs to the user domain = await get_domain(domain_id, w.wallet.id) assert domain, "Domain does not exist." @@ -417,6 +431,8 @@ async def api_rotate_user_address( await update_address(domain_id, address_id, pubkey=data.pubkey) + cache.pop(f"{domain_id}/{address.local_part}") + return True @@ -447,7 +463,10 @@ async def api_update_user_address( pubkey = data.pubkey if data.pubkey else address.pubkey if data.relays: address.config.relays = data.relays - await update_address(domain_id, address.id, pubkey=pubkey, config=address.config) + address = await update_address( + domain_id, address.id, pubkey=pubkey, config=address.config + ) + cache.pop(f"{domain_id}/{address.local_part}") return address @@ -465,6 +484,8 @@ async def api_request_user_address( if not user_id: raise HTTPException(HTTPStatus.UNAUTHORIZED) + address_data.normalize() + # make sure the address belongs to the user domain = await get_domain_by_id(address_data.domain_id) assert domain, "Domain does not exist." @@ -486,6 +507,7 @@ async def api_request_public_user_address( user_id: Optional[str] = Depends(optional_user_id), ): + address_data.normalize() # make sure the address belongs to the user domain = await get_domain_by_id(address_data.domain_id) assert domain, "Domain does not exist."