feat: create invoice to join

This commit is contained in:
Vlad Stan 2023-02-10 17:20:55 +02:00
parent db3ad2e32f
commit 8678090e7b
5 changed files with 176 additions and 8 deletions

13
crud.py
View file

@ -61,6 +61,17 @@ async def get_relay(user_id: str, relay_id: str) -> Optional[NostrRelay]:
return NostrRelay.from_row(row) if row else None return NostrRelay.from_row(row) if row else None
async def get_relay_by_id(relay_id: str) -> Optional[NostrRelay]:
"""Note: it does not require `user_id`. Can read any relay. Use it with care."""
row = await db.fetchone(
"""SELECT * FROM nostrrelay.relays WHERE id = ?""",
(
relay_id,
),
)
return NostrRelay.from_row(row) if row else None
async def get_relays(user_id: str) -> List[NostrRelay]: async def get_relays(user_id: str) -> List[NostrRelay]:
rows = await db.fetchall( rows = await db.fetchall(
@ -100,7 +111,7 @@ async def get_public_relay(relay_id: str) -> Optional[dict]:
"description": relay.description, "description": relay.description,
"pubkey": relay.pubkey, "pubkey": relay.pubkey,
"contact": relay.contact, "contact": relay.contact,
"config": dict(RelayPublicSpec(**dict(relay.config))) "config": RelayPublicSpec(**dict(relay.config)).dict(by_alias=True)
} }

19
helpers.py Normal file
View file

@ -0,0 +1,19 @@
from bech32 import bech32_decode, convertbits
def normalize_public_key(pubkey: str) -> str:
if pubkey.startswith('npub1'):
_, decoded_data = bech32_decode(pubkey)
if not decoded_data:
raise ValueError("Public Key is not valid npub")
decoded_data_bits = convertbits(decoded_data, 5, 8, False)
if not decoded_data_bits:
raise ValueError("Public Key is not valid npub")
return bytes(decoded_data_bits).hex()
#check if valid hex
if len(pubkey) != 64:
raise ValueError("Public Key is not valid hex")
int(pubkey, 16)
return pubkey

View file

@ -97,6 +97,10 @@ class NostrRelay(BaseModel):
config: "RelaySpec" = RelaySpec() config: "RelaySpec" = RelaySpec()
@property
def is_free_to_join(self):
return not self.config.is_paid_relay or self.config.cost_to_join == 0
@classmethod @classmethod
def from_row(cls, row: Row) -> "NostrRelay": def from_row(cls, row: Row) -> "NostrRelay":
relay = cls(**dict(row)) relay = cls(**dict(row))
@ -290,3 +294,8 @@ class NostrFilter(BaseModel):
values += [self.until] values += [self.until]
return inner_joins, where, values return inner_joins, where, values
class RelayJoin(BaseModel):
relay_id: str
pubkey: str

View file

@ -8,7 +8,55 @@
<div class="col-12 col-md-2 q-gutter-y-md"></div> <div class="col-12 col-md-2 q-gutter-y-md"></div>
<div class="col-12 col-md-6 q-gutter-y-md q-pa-xl"> <div class="col-12 col-md-6 q-gutter-y-md q-pa-xl">
<q-card> <q-card>
<q-card-section> </q-card-section> <q-card-section>
<h4 v-text="relay.name" class="q-my-none"></h4>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<span class="text-bold">Public Key:</span>
<q-input
filled
dense
v-model.trim="pubkey"
type="text"
label="User Public Key"
></q-input>
</q-card-section>
<q-card-section v-if="relay.config.isPaidRelay">
<q-btn @click="payToJoin" unelevated color="primary float-right"
>Pay to join</q-btn
>
<span class="text-bold">Cost to join: </span>
<span v-text="relay.config.costToJoin"></span>
<q-badge color="orange">
<span>sats</span>
</q-badge>
</q-card-section>
<q-card-section v-else>
<q-badge color="yellow" text-color="black">
This is a free relay
</q-badge>
</q-card-section>
<q-card-section v-if="joinInvoice">
<q-expansion-item
group="join-invoice"
label="Join Invoice"
:content-inset-level="0.5"
default-opened
>
<div class="q-ma-xl">
<q-responsive :ratio="1" class="q-ma-xl q-mx-md">
<qrcode
:value="'lightning:'+joinInvoice"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</div>
</q-expansion-item>
</q-card-section>
</q-card> </q-card>
</div> </div>
<div class="col-12 col-md-4 q-gutter-y-md q-pa-xl"> <div class="col-12 col-md-4 q-gutter-y-md q-pa-xl">
@ -17,7 +65,7 @@
<q-expansion-item <q-expansion-item
group="extras" group="extras"
icon="sensors" icon="sensors"
:label="relay.name" label="Relay Specs"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-separator class="q-mt-md"></q-separator> <q-separator class="q-mt-md"></q-separator>
@ -94,10 +142,40 @@
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
return { return {
relay: JSON.parse('{{relay | tojson | safe}}') relay: JSON.parse('{{relay | tojson | safe}}'),
pubkey: '',
joinInvoice: ''
} }
}, },
methods: {} methods: {
payToJoin: async function () {
if (!this.pubkey) {
this.$q.notify({
timeout: 5000,
type: 'warning',
message: 'Public key is missing'
})
return
}
try {
const {data} = await LNbits.api.request(
'PUT',
'/nostrrelay/api/v1/join',
'',
{
relay_id: this.relay.id,
pubkey: this.pubkey
}
)
this.joinInvoice = data.invoice
} catch (error) {
LNbits.utils.notifyApiError(error)
}
}
},
created: function () {
console.log('### created', this.relay)
}
}) })
</script> </script>
{% endblock %} {% endblock %}

View file

@ -3,10 +3,10 @@ from typing import List, Optional
from fastapi import Depends, WebSocket from fastapi import Depends, WebSocket
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from fastapi.responses import JSONResponse
from loguru import logger from loguru import logger
from pydantic.types import UUID4 from pydantic.types import UUID4
from lnbits.core.services import create_invoice
from lnbits.decorators import ( from lnbits.decorators import (
WalletTypeInfo, WalletTypeInfo,
check_admin, check_admin,
@ -21,12 +21,13 @@ from .crud import (
create_relay, create_relay,
delete_all_events, delete_all_events,
delete_relay, delete_relay,
get_public_relay,
get_relay, get_relay,
get_relay_by_id,
get_relays, get_relays,
update_relay, update_relay,
) )
from .models import NostrRelay from .helpers import normalize_public_key
from .models import NostrRelay, RelayJoin
client_manager = NostrClientManager() client_manager = NostrClientManager()
@ -151,3 +152,53 @@ async def api_delete_relay(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot delete relay", detail="Cannot delete relay",
) )
@nostrrelay_ext.put("/api/v1/join")
async def api_pay_to_join(
data: RelayJoin
):
try:
pubkey = normalize_public_key(data.pubkey)
relay = await get_relay_by_id(data.relay_id)
if not relay:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Relay not found",
)
if relay.is_free_to_join:
raise ValueError("Relay is free to join")
_, payment_request = await create_invoice(
wallet_id=relay.config.wallet,
amount=int(relay.config.cost_to_join),
memo=f"Pubkey '{data.pubkey}' wants to join {relay.id}",
extra={
"tag": "nostrrely",
"action": "join",
"relay": relay.id,
"pubkey": pubkey
},
)
print("### payment_request", payment_request)
return {
"invoice": payment_request
}
except ValueError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except HTTPException as ex:
raise ex
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot create invoice for client to join",
)