Nostr-p2nip5 (#110)

* move cli

* set_requests decorator

* fix wrapper

* refactor nostr.py

* ignore coroutine unpack error

* nostr lib 0.8

* make format
This commit is contained in:
calle
2023-02-25 12:27:33 +01:00
committed by GitHub
parent 5ec2c3604e
commit 46eeb2c36e
11 changed files with 175 additions and 127 deletions

View File

@@ -115,7 +115,7 @@ cashu info
Returns:
```bash
Version: 0.9.2
Version: 0.9.3
Debug: False
Cashu dir: /home/user/.cashu
Wallet: wallet

View File

@@ -66,4 +66,4 @@ NOSTR_RELAYS = env.list(
)
MAX_ORDER = 64
VERSION = "0.9.2"
VERSION = "0.9.3"

View File

@@ -1,3 +1,3 @@
from .cli import cli
from .cli.cli import cli
cli()

View File

@@ -5,7 +5,6 @@ import base64
import json
import os
import sys
import threading
import time
from datetime import datetime
from functools import wraps
@@ -39,12 +38,9 @@ from cashu.nostr.nostr.client.client import NostrClient
from cashu.tor.tor import TorProxy
from cashu.wallet import migrations
from cashu.wallet.crud import (
get_keyset,
get_lightning_invoices,
get_nostr_last_check_timestamp,
get_reserved_proofs,
get_unused_locks,
set_nostr_last_check_timestamp,
)
from cashu.wallet.wallet import Wallet as Wallet
@@ -52,12 +48,11 @@ from .cli_helpers import (
get_mint_wallet,
print_mint_balances,
proofs_to_serialized_tokenv2,
receive_nostr,
redeem_multimint,
send_nostr,
token_from_lnbits_link,
verify_mints,
)
from .nostr import receive_nostr, send_nostr
async def init_wallet(wallet: Wallet):
@@ -144,7 +139,7 @@ async def pay(ctx: Context, invoice: str, yes: bool):
if wallet.available_balance < amount:
print("Error: Balance too low.")
return
_, send_proofs = await wallet.split_to_send(wallet.proofs, amount)
_, send_proofs = await wallet.split_to_send(wallet.proofs, amount) # type: ignore
await wallet.pay_lightning(send_proofs, invoice)
wallet.status()

View File

@@ -1,7 +1,4 @@
import asyncio
import os
import threading
import time
import urllib.parse
from typing import List
@@ -12,14 +9,7 @@ from loguru import logger
from cashu.core.base import Proof, TokenV2, TokenV2Mint, WalletKeyset
from cashu.core.helpers import sum_proofs
from cashu.core.settings import CASHU_DIR, MINT_URL, NOSTR_PRIVATE_KEY, NOSTR_RELAYS
from cashu.nostr.nostr.client.client import NostrClient
from cashu.nostr.nostr.event import Event
from cashu.nostr.nostr.key import PublicKey
from cashu.wallet.crud import (
get_keyset,
get_nostr_last_check_timestamp,
set_nostr_last_check_timestamp,
)
from cashu.wallet.crud import get_keyset
from cashu.wallet.wallet import Wallet as Wallet
@@ -212,81 +202,3 @@ async def proofs_to_serialized_tokenv2(wallet, proofs: List[Proof], url: str):
token.mints.append(TokenV2Mint(url=url, ids=keysets))
token_serialized = await wallet._serialize_token_base64(token)
return token_serialized
async def send_nostr(ctx: Context, amount: int, pubkey: str, verbose: bool, yes: bool):
"""
Sends tokens via nostr.
"""
# load a wallet for the chosen mint
wallet = await get_mint_wallet(ctx)
await wallet.load_proofs()
_, send_proofs = await wallet.split_to_send(
wallet.proofs, amount, set_reserved=True
)
token = await wallet.serialize_proofs(send_proofs)
print("")
print(token)
if not yes:
print("")
click.confirm(
f"Send {amount} sat to nostr pubkey {pubkey}?",
abort=True,
default=True,
)
# we only use ephemeral private keys for sending
client = NostrClient(relays=NOSTR_RELAYS)
if verbose:
print(f"Your ephemeral nostr private key: {client.private_key.bech32()}")
if pubkey.startswith("npub"):
pubkey_to = PublicKey().from_npub(pubkey)
else:
pubkey_to = PublicKey(bytes.fromhex(pubkey))
client.dm(token, pubkey_to)
print(f"Token sent to {pubkey}")
client.close()
async def receive_nostr(ctx: Context, verbose: bool):
if NOSTR_PRIVATE_KEY is None:
print(
"Warning: No nostr private key set! You don't have NOSTR_PRIVATE_KEY set in your .env file. I will create a random private key for this session but I will not remember it."
)
print("")
client = NostrClient(private_key=NOSTR_PRIVATE_KEY, relays=NOSTR_RELAYS)
print(f"Your nostr public key: {client.public_key.bech32()}")
if verbose:
print(f"Your nostr private key (do not share!): {client.private_key.bech32()}")
await asyncio.sleep(2)
def get_token_callback(event: Event, decrypted_content):
if verbose:
print(
f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}"
)
try:
# call the receive method
from cashu.wallet.cli import receive
asyncio.run(receive(ctx, decrypted_content, ""))
except Exception as e:
pass
# determine timestamp of last check so we don't scan all historical DMs
wallet: Wallet = ctx.obj["WALLET"]
last_check = await get_nostr_last_check_timestamp(db=wallet.db)
if last_check:
last_check -= 60 * 60 # 1 hour tolerance
await set_nostr_last_check_timestamp(timestamp=int(time.time()), db=wallet.db)
t = threading.Thread(
target=client.get_dm,
args=(client.public_key, get_token_callback, {"since": last_check}),
name="Nostr DM",
)
t.start()

125
cashu/wallet/cli/nostr.py Normal file
View File

@@ -0,0 +1,125 @@
import asyncio
import threading
import time
import click
from click import Context
from requests.exceptions import ConnectionError
from cashu.core.settings import NOSTR_PRIVATE_KEY, NOSTR_RELAYS
from cashu.nostr.nostr.client.client import NostrClient
from cashu.nostr.nostr.event import Event
from cashu.nostr.nostr.key import PublicKey
from cashu.wallet.cli.cli_helpers import get_mint_wallet
from cashu.wallet.crud import (
get_nostr_last_check_timestamp,
set_nostr_last_check_timestamp,
)
from cashu.wallet.wallet import Wallet
async def nip5_to_pubkey(wallet: Wallet, address: str):
"""
Retrieves the nostr public key of a NIP-05 identifier.
"""
# we will be using the requests session from the wallet
await wallet._init_s()
# now we can use it
user, host = address.split("@")
resp_dict = {}
try:
resp = wallet.s.get(
f"https://{host}/.well-known/nostr.json?name={user}",
)
resp.raise_for_status()
except ConnectionError:
raise Exception(f"Could not connect to {host}")
except Exception as e:
raise e
resp_dict = resp.json()
assert "names" in resp_dict, Exception(f"did not receive any names from {host}")
assert user in resp_dict["names"], Exception(f"{user}@{host} not found")
pubkey = resp_dict["names"][user]
return pubkey
async def send_nostr(ctx: Context, amount: int, pubkey: str, verbose: bool, yes: bool):
"""
Sends tokens via nostr.
"""
# load a wallet for the chosen mint
wallet = await get_mint_wallet(ctx)
if "@" in pubkey:
pubkey = await nip5_to_pubkey(wallet, pubkey)
await wallet.load_proofs()
_, send_proofs = await wallet.split_to_send(
wallet.proofs, amount, set_reserved=True
)
token = await wallet.serialize_proofs(send_proofs)
if pubkey.startswith("npub"):
pubkey_to = PublicKey().from_npub(pubkey)
else:
pubkey_to = PublicKey(bytes.fromhex(pubkey))
print("")
print(token)
if not yes:
print("")
click.confirm(
f"Send {amount} sat to {pubkey_to.bech32()}?",
abort=True,
default=True,
)
client = NostrClient(private_key=NOSTR_PRIVATE_KEY, relays=NOSTR_RELAYS)
if verbose and not NOSTR_PRIVATE_KEY:
# we generated a random key if none was present
print(f"Your nostr private key: {client.private_key.bech32()}")
client.dm(token, pubkey_to)
print(f"Token sent to {pubkey_to.bech32()}")
client.close()
async def receive_nostr(ctx: Context, verbose: bool):
if NOSTR_PRIVATE_KEY is None:
print(
"Warning: No nostr private key set! You don't have NOSTR_PRIVATE_KEY set in your .env file. I will create a random private key for this session but I will not remember it."
)
print("")
client = NostrClient(private_key=NOSTR_PRIVATE_KEY, relays=NOSTR_RELAYS)
print(f"Your nostr public key: {client.public_key.bech32()}")
if verbose:
print(f"Your nostr private key (do not share!): {client.private_key.bech32()}")
await asyncio.sleep(2)
def get_token_callback(event: Event, decrypted_content):
if verbose:
print(
f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}"
)
try:
# call the receive method
from cashu.wallet.cli.cli import receive
asyncio.run(receive(ctx, decrypted_content, ""))
except Exception as e:
pass
# determine timestamp of last check so we don't scan all historical DMs
wallet: Wallet = ctx.obj["WALLET"]
last_check = await get_nostr_last_check_timestamp(db=wallet.db)
if last_check:
last_check -= 60 * 60 # 1 hour tolerance
await set_nostr_last_check_timestamp(timestamp=int(time.time()), db=wallet.db)
t = threading.Thread(
target=client.get_dm,
args=(client.public_key, get_token_callback, {"since": last_check}),
name="Nostr DM",
)
t.start()

View File

@@ -59,20 +59,17 @@ from cashu.wallet.crud import (
)
class LedgerAPI:
keys: Dict[int, PublicKey]
keyset: str
tor: TorProxy
db: Database
def async_set_requests(func):
"""
Decorator that wraps around any async class method of LedgerAPI that makes
API calls. Sets some HTTP headers and starts a Tor instance if none is
already running and and sets local proxy to use it.
"""
def __init__(self, url):
self.url = url
def _set_requests(self):
s = requests.Session()
s.headers.update({"Client-version": VERSION})
async def wrapper(self, *args, **kwargs):
self.s.headers.update({"Client-version": VERSION})
if DEBUG:
s.verify = False
self.s.verify = False
socks_host, socks_port = None, None
if TOR and TorProxy().check_platform():
self.tor = TorProxy(timeout=True)
@@ -86,9 +83,28 @@ class LedgerAPI:
"http": f"socks5://{socks_host}:{socks_port}",
"https": f"socks5://{socks_host}:{socks_port}",
}
s.proxies.update(proxies)
s.headers.update({"User-Agent": scrts.token_urlsafe(8)})
return s
self.s.proxies.update(proxies)
self.s.headers.update({"User-Agent": scrts.token_urlsafe(8)})
return await func(self, *args, **kwargs)
return wrapper
class LedgerAPI:
keys: Dict[int, PublicKey]
keyset: str
tor: TorProxy
db: Database
s: requests.Session
def __init__(self, url):
self.url = url
self.s = requests.Session()
@async_set_requests
async def _init_s(self):
"""Dummy function that can be called from outside to use LedgerAPI.s"""
return
def _construct_proofs(
self, promises: List[BlindedSignature], secrets: List[str], rs: List[str]
@@ -193,8 +209,8 @@ class LedgerAPI:
ENDPOINTS
"""
@async_set_requests
async def _get_keys(self, url: str):
self.s = self._set_requests()
resp = self.s.get(
url + "/keys",
)
@@ -208,11 +224,11 @@ class LedgerAPI:
keyset = WalletKeyset(public_keys=keyset_keys, mint_url=url)
return keyset
@async_set_requests
async def _get_keyset(self, url: str, keyset_id: str):
"""
keyset_id is base64, needs to be urlsafe-encoded.
"""
self.s = self._set_requests()
keyset_id_urlsafe = keyset_id.replace("+", "-").replace("/", "_")
resp = self.s.get(
url + f"/keys/{keyset_id_urlsafe}",
@@ -227,8 +243,8 @@ class LedgerAPI:
keyset = WalletKeyset(public_keys=keyset_keys, mint_url=url)
return keyset
@async_set_requests
async def _get_keyset_ids(self, url: str):
self.s = self._set_requests()
resp = self.s.get(
url + "/keysets",
)
@@ -238,9 +254,9 @@ class LedgerAPI:
assert len(keysets.keysets), Exception("did not receive any keysets")
return keysets.dict()
@async_set_requests
def request_mint(self, amount):
"""Requests a mint from the server and returns Lightning invoice."""
self.s = self._set_requests()
resp = self.s.get(self.url + "/mint", params={"amount": amount})
resp.raise_for_status()
return_dict = resp.json()
@@ -248,13 +264,13 @@ class LedgerAPI:
mint_response = GetMintResponse.parse_obj(return_dict)
return Invoice(amount=amount, pr=mint_response.pr, hash=mint_response.hash)
@async_set_requests
async def mint(self, amounts, payment_hash=None):
"""Mints new coins and returns a proof of promise."""
secrets = [self._generate_secret() for s in range(len(amounts))]
await self._check_used_secrets(secrets)
outputs, rs = self._construct_outputs(amounts, secrets)
outputs_payload = PostMintRequest(outputs=outputs)
self.s = self._set_requests()
resp = self.s.post(
self.url + "/mint",
json=outputs_payload.dict(),
@@ -271,6 +287,7 @@ class LedgerAPI:
return self._construct_proofs(promises, secrets, rs)
@async_set_requests
async def split(self, proofs, amount, scnd_secret: Optional[str] = None):
"""Consume proofs and create new promises based on amount split.
If scnd_secret is None, random secrets will be generated for the tokens to keep (frst_outputs)
@@ -315,7 +332,6 @@ class LedgerAPI:
"proofs": {i: proofs_include for i in range(len(proofs))},
}
self.s = self._set_requests()
resp = self.s.post(
self.url + "/split",
json=split_payload.dict(include=_splitrequest_include_fields(proofs)), # type: ignore
@@ -336,6 +352,7 @@ class LedgerAPI:
return frst_proofs, scnd_proofs
@async_set_requests
async def check_spendable(self, proofs: List[Proof]):
"""
Cheks whether the secrets in proofs are already spent or not and returns a list of booleans.
@@ -348,7 +365,6 @@ class LedgerAPI:
"proofs": {i: {"secret"} for i in range(len(proofs))},
}
self.s = self._set_requests()
resp = self.s.post(
self.url + "/check",
json=payload.dict(include=_check_spendable_include_fields(proofs)), # type: ignore
@@ -359,10 +375,10 @@ class LedgerAPI:
spendable = CheckSpendableResponse.parse_obj(return_dict)
return spendable
@async_set_requests
async def check_fees(self, payment_request: str):
"""Checks whether the Lightning payment is internal."""
payload = CheckFeesRequest(pr=payment_request)
self.s = self._set_requests()
resp = self.s.post(
self.url + "/checkfees",
json=payload.dict(),
@@ -372,6 +388,7 @@ class LedgerAPI:
self.raise_on_error(return_dict)
return return_dict
@async_set_requests
async def pay_lightning(self, proofs: List[Proof], invoice: str):
"""
Accepts proofs and a lightning invoice to pay in exchange.
@@ -387,7 +404,6 @@ class LedgerAPI:
"proofs": {i: proofs_include for i in range(len(proofs))},
}
self.s = self._set_requests()
resp = self.s.post(
self.url + "/melt",
json=payload.dict(include=_meltrequest_include_fields(proofs)), # type: ignore

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "cashu"
version = "0.9.2"
version = "0.9.3"
description = "Ecash wallet and mint."
authors = ["calle <callebtc@protonmail.com>"]
license = "MIT"

View File

@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli:cli"]}
setuptools.setup(
name="cashu",
version="0.9.2",
version="0.9.3",
description="Ecash wallet and mint for Bitcoin Lightning",
long_description=long_description,
long_description_content_type="text/markdown",

View File

@@ -7,7 +7,7 @@ from click.testing import CliRunner
from cashu.core.migrations import migrate_databases
from cashu.core.settings import VERSION
from cashu.wallet import migrations
from cashu.wallet.cli import cli
from cashu.wallet.cli.cli import cli
from cashu.wallet.wallet import Wallet
from tests.conftest import SERVER_ENDPOINT, mint