mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 10:34:20 +01:00
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:
@@ -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
|
||||
|
||||
@@ -66,4 +66,4 @@ NOSTR_RELAYS = env.list(
|
||||
)
|
||||
|
||||
MAX_ORDER = 64
|
||||
VERSION = "0.9.2"
|
||||
VERSION = "0.9.3"
|
||||
|
||||
Submodule cashu/nostr updated: 2872fe3c24...59ddc1b5e0
@@ -1,3 +1,3 @@
|
||||
from .cli import cli
|
||||
from .cli.cli import cli
|
||||
|
||||
cli()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
125
cashu/wallet/cli/nostr.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
2
setup.py
2
setup.py
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user