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

feat: free nip5 #23

Merged
merged 2 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ async def get_addresses_for_owner(owner_id: str) -> List[Address]:
return [Address.from_row(row) for row in rows]


async def get_free_addresses_for_owner(owner_id: str, domain_id: str) -> List[Address]:
rows = await db.fetchall(
"SELECT * FROM nostrnip5.addresses"
" WHERE owner_id = ? and domain_id = ? AND is_free = true",
(owner_id, domain_id),
)

return [Address.from_row(row) for row in rows]


async def get_all_addresses(wallet_ids: Union[str, List[str]]) -> List[Address]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
Expand Down
12 changes: 12 additions & 0 deletions migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,15 @@ async def m007_add_cost_extra_column_to_addresses(db):
"ALTER TABLE nostrnip5.addresses ADD COLUMN "
"reimburse_amount REAL NOT NULL DEFAULT 0"
)


async def m008_add_is_free_column(db):
"""
Adds is_free flag for addresses.
"""
await db.execute(
"""
ALTER TABLE nostrnip5.addresses
ADD COLUMN is_free BOOLEAN NOT NULL DEFAULT false
"""
)
2 changes: 2 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ class Address(FromRowModel):
local_part: str
pubkey: str
active: bool
is_free: bool
time: int
reimburse_amount: int = 0
expires_at: Optional[float]
Expand All @@ -358,6 +359,7 @@ def from_row(cls, row: Row) -> "Address":

class AddressStatus(BaseModel):
identifier: str
free_identifier_number: Optional[str] = None
available: bool = False
price: Optional[float] = None
price_in_sats: Optional[float] = None
Expand Down
51 changes: 44 additions & 7 deletions services.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from random import randint
from typing import List, Optional, Tuple

import httpx
Expand All @@ -19,6 +20,7 @@
get_all_addresses_paginated,
get_domain_by_id,
get_domains,
get_free_addresses_for_owner,
get_identifier_ranking,
get_settings,
update_address,
Expand Down Expand Up @@ -116,6 +118,25 @@ async def get_identifier_price_data(
return await domain.price_for_identifier(identifier, years, rank, promo_code)


async def get_next_free_identifier(domain_id: str, identifier: str):
free_identifier_number = str(randint(0, 999999)).zfill(6)
free_identifier = identifier + "." + free_identifier_number
active_address = await get_active_address_by_local_part(domain_id, free_identifier)
if not active_address:
return free_identifier_number
return await get_next_free_identifier(domain_id, identifier)


async def get_user_free_identifier(
user_id: str, domain_id: str, identifier: str
) -> Optional[str]:
owner = owner_id_from_user_id(user_id)
free_addresses = await get_free_addresses_for_owner(owner, domain_id)
if free_addresses:
return None
return await get_next_free_identifier(domain_id, identifier)


async def request_user_address(
domain: Domain,
address_data: CreateAddressData,
Expand All @@ -125,6 +146,11 @@ async def request_user_address(
address = await create_address(
domain, address_data, wallet_id, user_id, address_data.promo_code
)
if is_free_identifier(address.local_part):
await activate_address(domain.id, address.id)
address = await update_address(domain.id, address.id, is_free=True)
return dict(address)

assert (
address.config.price_in_sats
), f"Cannot compute price for '{address_data.local_part}'."
Expand Down Expand Up @@ -179,6 +205,18 @@ async def create_invoice_for_identifier(
return payment_hash, payment_request


def is_free_identifier(identifier: str) -> bool:
"Local Part without the suffix added for free addresses."
if len(identifier) < 7:
return False
if identifier[-7] != ".":
return False
if not identifier[-6:].isdigit():
return False

return True


async def create_address(
domain: Domain,
data: CreateAddressData,
Expand All @@ -193,17 +231,16 @@ async def create_address(
data.pubkey = validate_pub_key(data.pubkey)

owner_id = owner_id_from_user_id(user_id)
addresss = await get_address_for_owner(owner_id, domain.id, identifier)
address = await get_address_for_owner(owner_id, domain.id, identifier)

promo_code = promo_code or (addresss.config.promo_code if addresss else None)
promo_code = promo_code or (address.config.promo_code if address else None)
identifier_status = await get_identifier_status(
domain, identifier, data.years, promo_code
)

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 = address.config if address else AddressConfig()
config.price = identifier_status.price
config.price_in_sats = identifier_status.price_in_sats
config.currency = domain.currency
Expand All @@ -213,10 +250,10 @@ async def create_address(
config.max_years = domain.cost_config.max_years
config.ln_address.wallet = wallet_id or ""

if addresss:
assert not addresss.active, f"Identifier '{data.local_part}' already activated."
if address:
assert not address.active, f"Identifier '{data.local_part}' already activated."
address = await update_address(
domain.id, addresss.id, config=config, pubkey=data.pubkey
domain.id, address.id, config=config, pubkey=data.pubkey
)
else:
address = await create_address_internal(data, owner_id, config=config)
Expand Down
16 changes: 14 additions & 2 deletions views_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@
check_address_payment,
create_address,
get_identifier_status,
get_next_free_identifier,
get_reimburse_wallet_id,
get_user_addresses,
get_user_addresses_paginated,
get_user_domains,
get_user_free_identifier,
get_valid_addresses_for_owner,
refresh_buckets,
request_user_address,
Expand Down Expand Up @@ -171,7 +173,10 @@ async def api_get_nostr_json(
"/api/v1/domain/{domain_id}/search", status_code=HTTPStatus.OK
)
async def api_search_identifier(
domain_id: str, q: Optional[str] = None, years: Optional[int] = None
domain_id: str,
q: Optional[str] = None,
years: Optional[int] = None,
user_id: Optional[str] = Depends(optional_user_id),
) -> AddressStatus:

if not q:
Expand All @@ -180,7 +185,14 @@ async def api_search_identifier(
domain = await get_domain_by_id(domain_id)
assert domain, "Unknown domain id."

return await get_identifier_status(domain, q, years or 1)
address_status = await get_identifier_status(domain, q, years or 1)
address_status.free_identifier_number = (
await get_user_free_identifier(user_id, domain.id, address_status.identifier)
if user_id
else await get_next_free_identifier(domain.id, address_status.identifier)
)

return address_status


@http_try_except
Expand Down
Loading