mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-24 03:54:21 +01:00
merge
This commit is contained in:
15
.env.example
15
.env.example
@@ -8,6 +8,13 @@ CASHU_DIR=~/.cashu
|
||||
MINT_HOST=127.0.0.1
|
||||
MINT_PORT=3338
|
||||
|
||||
# use builtin tor, this overrides SOCKS_HOST and SOCKS_PORT
|
||||
TOR=TRUE
|
||||
|
||||
# use custom tor proxy, use with TOR=false
|
||||
SOCKS_HOST=localhost
|
||||
SOCKS_PORT=9050
|
||||
|
||||
# MINT
|
||||
|
||||
MINT_PRIVATE_KEY=supersecretprivatekey
|
||||
@@ -22,4 +29,10 @@ LIGHTNING_FEE_PERCENT=1.0
|
||||
LIGHTNING_RESERVE_FEE_MIN=4000
|
||||
|
||||
LNBITS_ENDPOINT=https://legend.lnbits.com
|
||||
LNBITS_KEY=yourkeyasdasdasd
|
||||
LNBITS_KEY=yourkeyasdasdasd
|
||||
|
||||
# NOSTR
|
||||
# nostr private key to which to receive tokens to
|
||||
NOSTR_PRIVATE_KEY=hex_nostrprivatekey_here
|
||||
# nostr relays (comma separated list)
|
||||
NOSTR_RELAYS="wss://nostr-pub.wellorder.net"
|
||||
|
||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
custom: https://legend.lnbits.com/tipjar/794
|
||||
9
.github/codecov.yml
vendored
Normal file
9
.github/codecov.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
coverage:
|
||||
status:
|
||||
patch: off
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
# adjust accordingly based on how flaky your tests are
|
||||
# this allows a 10% drop from the previous base commit coverage
|
||||
threshold: 10%
|
||||
@@ -1,4 +1,4 @@
|
||||
name: formatting
|
||||
name: checks
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
poetry:
|
||||
formatting:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -29,3 +29,25 @@ jobs:
|
||||
run: poetry run black --check .
|
||||
- name: Check isort
|
||||
run: poetry run isort --profile black --check-only .
|
||||
linting:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Set up Poetry ${{ matrix.poetry-version }}
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Install packages
|
||||
run: poetry install --with dev
|
||||
- name: Setup mypy
|
||||
run: yes | poetry run mypy cashu --install-types || true
|
||||
- name: Run mypy
|
||||
run: poetry run mypy cashu --ignore-missing
|
||||
9
.github/workflows/tests.yml
vendored
9
.github/workflows/tests.yml
vendored
@@ -4,9 +4,10 @@ on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
poetry:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
steps:
|
||||
@@ -22,6 +23,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --with dev
|
||||
shell: bash
|
||||
- name: Run mint
|
||||
env:
|
||||
LIGHTNING: False
|
||||
@@ -35,5 +37,8 @@ jobs:
|
||||
LIGHTNING: False
|
||||
MINT_HOST: localhost
|
||||
MINT_PORT: 3338
|
||||
TOR: False
|
||||
run: |
|
||||
poetry run pytest tests
|
||||
poetry run pytest tests --cov-report xml --cov cashu
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "cashu/nostr"]
|
||||
path = cashu/nostr
|
||||
url = https://github.com/callebtc/python-nostr/
|
||||
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal file
@@ -0,0 +1,2 @@
|
||||
include requirements.txt
|
||||
recursive-include cashu/tor *
|
||||
123
README.md
123
README.md
@@ -1,8 +1,9 @@
|
||||
# cashu
|
||||
|
||||
**Cashu is a Chaumian Ecash wallet and mint with Bitcoin Lightning support.**
|
||||
**Cashu is a Chaumian Ecash wallet and mint for Bitcoin Lightning.**
|
||||
|
||||
<a href="https://pypi.org/project/cashu/"><img alt="Release" src="https://img.shields.io/pypi/v/cashu?color=black"></a> <a href="https://pepy.tech/project/cashu"> <img alt="Downloads" src="https://pepy.tech/badge/cashu"></a> <a href="https://app.codecov.io/gh/callebtc/cashu"><img alt="Coverage" src="https://img.shields.io/codecov/c/gh/callebtc/cashu"></a>
|
||||
|
||||
<a href="https://pypi.org/project/cashu/"><img alt="Release" src="https://img.shields.io/pypi/v/cashu?color=black"></a>
|
||||
|
||||
*Disclaimer: The author is NOT a cryptographer and this work has not been reviewed. This means that there is very likely a fatal flaw somewhere. Cashu is still experimental and not production-ready.*
|
||||
|
||||
@@ -17,6 +18,16 @@ Cashu is an Ecash implementation based on David Wagner's variant of Chaumian bli
|
||||
<a href="#running-a-mint">Run a mint</a>
|
||||
</p>
|
||||
|
||||
### Feature overview
|
||||
|
||||
- Full Bitcoin Lightning support
|
||||
- Standalone CLI wallet and mint server
|
||||
- Mint library includable into other Python projects
|
||||
- PostgreSQL and SQLite database support
|
||||
- Builtin Tor for hiding IPs for wallet and mint interactions
|
||||
- Multimint wallet for tokens from different mints
|
||||
- Send tokens to nostr pubkeys
|
||||
|
||||
## Cashu client protocol
|
||||
There are ongoing efforts to implement alternative Cashu clients that use the same protocol such as a [Cashu Javascript wallet](https://github.com/motorina0/cashu-js-wallet). If you are interested in helping with Cashu development, please see the [docs](docs/) for the notation and conventions used.
|
||||
|
||||
@@ -29,7 +40,7 @@ pip install cashu
|
||||
|
||||
To update Cashu, use `pip install cashu -U`.
|
||||
|
||||
If you have problems running the command above on Ubuntu, run `sudo apt install -y pip pkg-config`.
|
||||
If you have problems running the command above on Ubuntu, run `sudo apt install -y pip pkg-config` and `pip install wheel`. On macOS, you might have to run `pip install wheel` and `brew install pkg-config`.
|
||||
|
||||
You can skip the entire next section about Poetry and jump right to [Using Cashu](#using-cashu).
|
||||
|
||||
@@ -39,8 +50,8 @@ These steps help you install Python via pyenv and Poetry. If you already have Po
|
||||
#### Poetry: Prerequisites
|
||||
|
||||
```bash
|
||||
sudo apt install -y build-essential pkg-config libffi-dev libpq-dev zlib1g-dev libssl-dev python3-dev
|
||||
# on mac: brew install postgres
|
||||
# on ubuntu:
|
||||
sudo apt install -y build-essential pkg-config libffi-dev libpq-dev zlib1g-dev libssl-dev python3-dev libsqlite3-dev ncurses-dev libbz2-dev libreadline-dev lzma-dev
|
||||
|
||||
# install python using pyenv
|
||||
curl https://pyenv.run | bash
|
||||
@@ -59,7 +70,7 @@ source ~/.bashrc
|
||||
#### Poetry: Install Cashu
|
||||
```bash
|
||||
# install cashu
|
||||
git clone https://github.com/callebtc/cashu.git
|
||||
git clone https://github.com/callebtc/cashu.git --recurse-submodules
|
||||
cd cashu
|
||||
pyenv local 3.9.13
|
||||
poetry install
|
||||
@@ -89,8 +100,7 @@ vim .env
|
||||
To use the wallet with the [public test mint](#test-instance), you need to change the appropriate entries in the `.env` file.
|
||||
|
||||
#### Test instance
|
||||
*Warning: this instance is just for demonstration only. Currently, only Lightning deposits work but not withdrawals. The server could vanish at any moment so consider any Satoshis you deposit a donation. I will add Lightning withdrawals soon so unless someone comes up with a huge inflation bug, you might be able to claim them back at a later point in time.*
|
||||
|
||||
*Warning: this instance is just for demonstration only. The server could vanish at any moment so consider any Satoshis you deposit a donation.*
|
||||
|
||||
Change the appropriate `.env` file settings to
|
||||
```bash
|
||||
@@ -99,41 +109,39 @@ MINT_PORT=3338
|
||||
```
|
||||
|
||||
# Using Cashu
|
||||
|
||||
#### Request a mint
|
||||
|
||||
This command will return a Lightning invoice and a payment hash. You have to pay the invoice before you can receive the tokens. Note: Minting tokens involves two steps: requesting a mint, and actually minting tokens (see below).
|
||||
|
||||
```bash
|
||||
cashu mint 420
|
||||
cashu info
|
||||
```
|
||||
|
||||
Returns:
|
||||
```bash
|
||||
Pay this invoice to mint 420 sat:
|
||||
Invoice: lnbc4200n1p3nfk7zsp522g8wlsx9cvmhtyuyuae48nvreew9x9f8kkqhd2v2umrdtwl2ysspp5w2w6jvcnz4ftcwsxtad5kv3yev62pcp5cvq42dqqrmwtr2k6mk8qdq4vdshx6r4ypjx2ur0wd5hgxqyjw5qcqpjrzjqfe5jlwxmwt4sa4s8mqjqp8qtreqant6mqwwkts46dtawvncjwvhczurxgqqvvgqqqqqqqqnqqqqqzgqyg9qyysgqzaus4lsfs3zzk4ehdzrkxzv8ryu2yxppxyjrune3nks2dgrnua6nv7lsztmyjaf96xp569tf7rxdmfud5q45zmr4xue5hjple6xhcrcpfmveag
|
||||
|
||||
After paying the invoice, run this command:
|
||||
cashu mint 420 --hash 729da933131552bc3a065f5b4b3224cb34a0e034c3015534001edcb1aadadd8e
|
||||
Version: 0.7.0
|
||||
Debug: False
|
||||
Cashu dir: /home/user/.cashu
|
||||
Wallet: wallet
|
||||
Mint URL: https://8333.space:3338
|
||||
```
|
||||
|
||||
#### Mint tokens
|
||||
After paying the invoice, copy the `hash` value from above and add it to the command
|
||||
```bash
|
||||
cashu mint 420 --hash 729da933131552bc3a065f5b4b3224cb34a0e034c3015534001edcb1aadadd8e
|
||||
```
|
||||
You should see your balance update accordingly:
|
||||
```bash
|
||||
Balance: 0 sat (Available: 0 sat in 0 tokens)
|
||||
Balance: 420 sat (Available: 420 sat in 4 tokens)
|
||||
```
|
||||
|
||||
Available tokens here means those tokens that have not been reserved for sending.
|
||||
|
||||
#### Check balance
|
||||
```bash
|
||||
cashu balance
|
||||
```
|
||||
|
||||
#### Generate a Lightning invoice
|
||||
|
||||
This command will return a Lightning invoice that you need to pay to mint new ecash tokens.
|
||||
|
||||
```bash
|
||||
cashu invoice 420
|
||||
```
|
||||
|
||||
The client will check every few seconds if the invoice has been paid. If you abort this step but still pay the invoice, you can use the command `cashu invoice <amount> --hash <hash>`.
|
||||
|
||||
#### Pay a Lightning invoice
|
||||
```bash
|
||||
cashu pay lnbc120n1p3jfmdapp5r9jz...
|
||||
```
|
||||
|
||||
#### Send tokens
|
||||
To send tokens to another user, enter
|
||||
```bash
|
||||
@@ -141,12 +149,12 @@ cashu send 69
|
||||
```
|
||||
You should see the encoded token. Copy the token and send it to another user such as via email or a messenger. The token looks like this:
|
||||
```bash
|
||||
W3siYW1vdW50IjogMSwgIkMiOiB7IngiOiAzMzg0Mzg0NDYzNzAwMTY1NDA2MTQxMDY3Mzg1MDg5MjA2MTU2NjQxMjM4Nzg5MDE4NzAzODg0NjAwNDUzNTAwNzY3...
|
||||
W3siYW1vdW50IjogMSwgIkMiOiB7IngiOi...
|
||||
```
|
||||
|
||||
You can now see that your available balance has dropped by the amount that you reserved for sending if you enter `cashu balance`:
|
||||
```bash
|
||||
Balance: 420 sat (Available: 351 sat in 7 tokens)
|
||||
Balance: 420 sat
|
||||
```
|
||||
|
||||
#### Receive tokens
|
||||
@@ -156,53 +164,10 @@ cashu receive W3siYW1vdW50IjogMSwgIkMiOi...
|
||||
```
|
||||
You should see the balance increase:
|
||||
```bash
|
||||
Balance: 0 sat (Available: 0 sat in 0 tokens)
|
||||
Balance: 69 sat (Available: 69 sat in 3 tokens)
|
||||
Balance: 0 sat
|
||||
Balance: 69 sat
|
||||
```
|
||||
|
||||
#### Burn tokens
|
||||
The sending user needs to burn (invalidate) their tokens from above, otherwise they will try to double spend them (which won't work because the server keeps a list of all spent tokens):
|
||||
```bash
|
||||
cashu burn W3siYW1vdW50IjogMSwgIkMiOi...
|
||||
```
|
||||
Returns:
|
||||
```bash
|
||||
Balance: 420 sat (Available: 351 sat in 7 tokens)
|
||||
Balance: 351 sat (Available: 351 sat in 7 tokens)
|
||||
```
|
||||
Use `cashu burn -a` to burn all used tokens or `cashu burn -f` to force a spent recheck on all tokens and burn them is they are used. This command is safe to use, it won't burn unspent tokens.
|
||||
|
||||
#### Check pending tokens
|
||||
```bash
|
||||
cashu pending
|
||||
```
|
||||
Returns
|
||||
```bash
|
||||
Amount: 64 sat Sent: 2022-09-28 06:53:03 ID: 33025ade-3efa-11ed-9096-16a10f0dbf61
|
||||
|
||||
W3siYW1vdW50Ijog...
|
||||
|
||||
Amount: 64 sat Sent: 2022-09-28 06:57:25 ID: cf588354-3efa-11ed-b5ec-16a10f0dbf61
|
||||
|
||||
W3siYW1vdW50Ijog...
|
||||
|
||||
Amount: 128 sat Sent: 2022-09-28 09:57:43 ID: fef371fa-3f13-11ed-b31a-16a10f0dbf61
|
||||
|
||||
W3siYW1vdW50Ij...
|
||||
|
||||
Balance: 1234 sat (Available: 1234 sat in 7 tokens)
|
||||
```
|
||||
You can either burn these tokens manually when the receiver has redeemed them, or you can receive them yourself if you want to cancel a pending payment.
|
||||
|
||||
#### Pay a Lightning invoice
|
||||
```bash
|
||||
cashu pay lnbc120n1p3jfmdapp5r9jz...
|
||||
```
|
||||
Returns:
|
||||
```bash
|
||||
Balance: 351 sat (Available: 351 sat in 7 tokens)
|
||||
Balance: 339 sat (Available: 339 sat in 8 tokens)
|
||||
```
|
||||
|
||||
# Running a mint
|
||||
This command runs the mint on your local computer. Skip this step if you want to use the [public test mint](#test-instance) instead.
|
||||
|
||||
@@ -2,27 +2,32 @@
|
||||
|
||||
"""
|
||||
Implementation of https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406
|
||||
Alice:
|
||||
|
||||
Bob (Mint):
|
||||
A = a*G
|
||||
return A
|
||||
Bob:
|
||||
Y = hash_to_point(secret_message)
|
||||
|
||||
Alice (Client):
|
||||
Y = hash_to_curve(secret_message)
|
||||
r = random blinding factor
|
||||
B'= Y + r*G
|
||||
return B'
|
||||
Alice:
|
||||
|
||||
Bob:
|
||||
C' = a*B'
|
||||
(= a*Y + a*r*G)
|
||||
return C'
|
||||
Bob:
|
||||
|
||||
Alice:
|
||||
C = C' - r*A
|
||||
(= C' - a*r*G)
|
||||
(= a*Y)
|
||||
return C, secret_message
|
||||
Alice:
|
||||
Y = hash_to_point(secret_message)
|
||||
|
||||
Bob:
|
||||
Y = hash_to_curve(secret_message)
|
||||
C == a*Y
|
||||
If true, C must have originated from Alice
|
||||
If true, C must have originated from Bob
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
@@ -30,29 +35,26 @@ import hashlib
|
||||
from secp256k1 import PrivateKey, PublicKey
|
||||
|
||||
|
||||
def hash_to_point(secret_msg):
|
||||
"""Generates x coordinate from the message hash and checks if the point lies on the curve.
|
||||
If it does not, it tries computing again a new x coordinate from the hash of the coordinate."""
|
||||
def hash_to_curve(message: bytes):
|
||||
"""Generates a point from the message hash and checks if the point lies on the curve.
|
||||
If it does not, it tries computing a new point from the hash."""
|
||||
point = None
|
||||
msg = secret_msg
|
||||
msg_to_hash = message
|
||||
while point is None:
|
||||
_hash = hashlib.sha256(msg).hexdigest().encode("utf-8")
|
||||
try:
|
||||
# We construct compressed pub which has x coordinate encoded with even y
|
||||
_hash = list(_hash[:33]) # take the 33 bytes and get a list of bytes
|
||||
_hash[0] = 0x02 # set first byte to represent even y coord
|
||||
_hash = bytes(_hash)
|
||||
point = PublicKey(_hash, raw=True)
|
||||
_hash = hashlib.sha256(msg_to_hash).digest()
|
||||
point = PublicKey(b"\x02" + _hash, raw=True)
|
||||
except:
|
||||
msg = _hash
|
||||
|
||||
msg_to_hash = _hash
|
||||
return point
|
||||
|
||||
|
||||
def step1_alice(secret_msg):
|
||||
secret_msg = secret_msg.encode("utf-8")
|
||||
Y = hash_to_point(secret_msg)
|
||||
r = PrivateKey()
|
||||
def step1_alice(secret_msg: str, blinding_factor: bytes = None):
|
||||
Y = hash_to_curve(secret_msg.encode("utf-8"))
|
||||
if blinding_factor:
|
||||
r = PrivateKey(privkey=blinding_factor, raw=True)
|
||||
else:
|
||||
r = PrivateKey()
|
||||
B_ = Y + r.pubkey
|
||||
return B_, r
|
||||
|
||||
@@ -68,7 +70,7 @@ def step3_alice(C_, r, A):
|
||||
|
||||
|
||||
def verify(a, C, secret_msg):
|
||||
Y = hash_to_point(secret_msg.encode("utf-8"))
|
||||
Y = hash_to_curve(secret_msg.encode("utf-8"))
|
||||
return C == Y.mult(a)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from sqlite3 import Row
|
||||
from typing import Any, Dict, List, Union
|
||||
from typing import Any, Dict, List, Optional, TypedDict, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -12,52 +12,19 @@ class P2SHScript(BaseModel):
|
||||
signature: str
|
||||
address: Union[str, None] = None
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
return cls(
|
||||
address=row[0],
|
||||
script=row[1],
|
||||
signature=row[2],
|
||||
used=row[3],
|
||||
)
|
||||
|
||||
|
||||
class Proof(BaseModel):
|
||||
id: str = ""
|
||||
amount: int
|
||||
id: Union[
|
||||
None, str
|
||||
] = "" # NOTE: None for backwards compatibility of old clients < 0.3
|
||||
amount: int = 0
|
||||
secret: str = ""
|
||||
C: str
|
||||
C: str = ""
|
||||
script: Union[P2SHScript, None] = None
|
||||
reserved: bool = False # whether this proof is reserved for sending
|
||||
send_id: str = "" # unique ID of send attempt
|
||||
time_created: str = ""
|
||||
time_reserved: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
return cls(
|
||||
amount=row[0],
|
||||
C=row[1],
|
||||
secret=row[2],
|
||||
reserved=row[3] or False,
|
||||
send_id=row[4] or "",
|
||||
time_created=row[5] or "",
|
||||
time_reserved=row[6] or "",
|
||||
id=row[7] or "",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict):
|
||||
assert "amount" in d, "no amount in proof"
|
||||
return cls(
|
||||
amount=d.get("amount"),
|
||||
C=d.get("C"),
|
||||
secret=d.get("secret") or "",
|
||||
reserved=d.get("reserved") or False,
|
||||
send_id=d.get("send_id") or "",
|
||||
time_created=d.get("time_created") or "",
|
||||
time_reserved=d.get("time_reserved") or "",
|
||||
)
|
||||
reserved: Union[None, bool] = False # whether this proof is reserved for sending
|
||||
send_id: Union[None, str] = "" # unique ID of send attempt
|
||||
time_created: Union[None, str] = ""
|
||||
time_reserved: Union[None, str] = ""
|
||||
|
||||
def to_dict(self):
|
||||
return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C)
|
||||
@@ -81,21 +48,15 @@ class Proofs(BaseModel):
|
||||
class Invoice(BaseModel):
|
||||
amount: int
|
||||
pr: str
|
||||
hash: str
|
||||
issued: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
return cls(
|
||||
amount=int(row[0]),
|
||||
pr=str(row[1]),
|
||||
hash=str(row[2]),
|
||||
issued=bool(row[3]),
|
||||
)
|
||||
hash: Union[None, str] = None
|
||||
preimage: Union[str, None] = None
|
||||
issued: Union[None, bool] = False
|
||||
paid: Union[None, bool] = False
|
||||
time_created: Union[None, str, int, float] = ""
|
||||
time_paid: Union[None, str, int, float] = ""
|
||||
|
||||
|
||||
class BlindedMessage(BaseModel):
|
||||
id: str = ""
|
||||
amount: int
|
||||
B_: str
|
||||
|
||||
@@ -105,14 +66,6 @@ class BlindedSignature(BaseModel):
|
||||
amount: int
|
||||
C_: str
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict):
|
||||
return cls(
|
||||
id=d.get("id"),
|
||||
amount=d["amount"],
|
||||
C_=d["C_"],
|
||||
)
|
||||
|
||||
|
||||
class MintRequest(BaseModel):
|
||||
blinded_messages: List[BlindedMessage] = []
|
||||
@@ -166,7 +119,6 @@ class CheckFeesResponse(BaseModel):
|
||||
|
||||
class MeltRequest(BaseModel):
|
||||
proofs: List[Proof]
|
||||
amount: int = None # deprecated
|
||||
invoice: str
|
||||
|
||||
|
||||
@@ -175,16 +127,6 @@ class KeyBase(BaseModel):
|
||||
amount: int
|
||||
pubkey: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
if row is None:
|
||||
return cls
|
||||
return cls(
|
||||
id=row[0],
|
||||
amount=int(row[1]),
|
||||
pubkey=row[2],
|
||||
)
|
||||
|
||||
|
||||
class WalletKeyset:
|
||||
id: str
|
||||
@@ -215,29 +157,17 @@ class WalletKeyset:
|
||||
self.public_keys = pubkeys
|
||||
self.id = derive_keyset_id(self.public_keys)
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
if row is None:
|
||||
return cls
|
||||
return cls(
|
||||
id=row[0],
|
||||
mint_url=row[1],
|
||||
valid_from=row[2],
|
||||
valid_to=row[3],
|
||||
first_seen=row[4],
|
||||
active=row[5],
|
||||
)
|
||||
|
||||
|
||||
class MintKeyset:
|
||||
id: str
|
||||
derivation_path: str
|
||||
private_keys: Dict[int, PrivateKey]
|
||||
public_keys: Dict[int, PublicKey] = None
|
||||
public_keys: Dict[int, PublicKey] = {}
|
||||
valid_from: Union[str, None] = None
|
||||
valid_to: Union[str, None] = None
|
||||
first_seen: Union[str, None] = None
|
||||
active: bool = True
|
||||
version: Union[str, None] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -246,8 +176,9 @@ class MintKeyset:
|
||||
valid_to=None,
|
||||
first_seen=None,
|
||||
active=None,
|
||||
seed: Union[None, str] = None,
|
||||
derivation_path: str = "0",
|
||||
seed: str = "",
|
||||
derivation_path: str = "",
|
||||
version: str = "",
|
||||
):
|
||||
self.derivation_path = derivation_path
|
||||
self.id = id
|
||||
@@ -255,28 +186,17 @@ class MintKeyset:
|
||||
self.valid_to = valid_to
|
||||
self.first_seen = first_seen
|
||||
self.active = active
|
||||
self.version = version
|
||||
# generate keys from seed
|
||||
if seed:
|
||||
self.generate_keys(seed)
|
||||
|
||||
def generate_keys(self, seed):
|
||||
"""Generates keys of a keyset from a seed."""
|
||||
self.private_keys = derive_keys(seed, self.derivation_path)
|
||||
self.public_keys = derive_pubkeys(self.private_keys)
|
||||
self.id = derive_keyset_id(self.public_keys)
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
if row is None:
|
||||
return cls
|
||||
return cls(
|
||||
id=row[0],
|
||||
derivation_path=row[1],
|
||||
valid_from=row[2],
|
||||
valid_to=row[3],
|
||||
first_seen=row[4],
|
||||
active=row[5],
|
||||
)
|
||||
|
||||
def get_keybase(self):
|
||||
return {
|
||||
k: KeyBase(id=self.id, amount=k, pubkey=v.serialize().hex())
|
||||
@@ -292,3 +212,19 @@ class MintKeysets:
|
||||
|
||||
def get_ids(self):
|
||||
return [k for k, _ in self.keysets.items()]
|
||||
|
||||
|
||||
class TokenMintJson(BaseModel):
|
||||
url: str
|
||||
ks: List[str]
|
||||
|
||||
|
||||
class TokenJson(BaseModel):
|
||||
tokens: List[Proof]
|
||||
mints: Optional[Dict[str, TokenMintJson]] = None
|
||||
|
||||
def to_dict(self):
|
||||
return dict(
|
||||
tokens=[p.to_dict() for p in self.tokens],
|
||||
mints={k: v.dict() for k, v in self.mints.items()}, # type: ignore
|
||||
)
|
||||
|
||||
@@ -5,6 +5,14 @@ from typing import Dict, List
|
||||
from cashu.core.secp import PrivateKey, PublicKey
|
||||
from cashu.core.settings import MAX_ORDER
|
||||
|
||||
# entropy = bytes([random.getrandbits(8) for i in range(16)])
|
||||
# mnemonic = bip39.mnemonic_from_bytes(entropy)
|
||||
# seed = bip39.mnemonic_to_seed(mnemonic)
|
||||
# root = bip32.HDKey.from_seed(seed, version=NETWORKS["main"]["xprv"])
|
||||
|
||||
# bip44_xprv = root.derive("m/44h/1h/0h")
|
||||
# bip44_xpub = bip44_xprv.to_public()
|
||||
|
||||
|
||||
def derive_keys(master_key: str, derivation_path: str = ""):
|
||||
"""
|
||||
@@ -27,9 +35,11 @@ def derive_pubkeys(keys: Dict[int, PrivateKey]):
|
||||
return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]}
|
||||
|
||||
|
||||
def derive_keyset_id(keys: Dict[str, PublicKey]):
|
||||
def derive_keyset_id(keys: Dict[int, PublicKey]):
|
||||
"""Deterministic derivation keyset_id from set of public keys."""
|
||||
pubkeys_concat = "".join([p.serialize().hex() for _, p in keys.items()])
|
||||
# sort public keys by amount
|
||||
sorted_keys = dict(sorted(keys.items()))
|
||||
pubkeys_concat = "".join([p.serialize().hex() for _, p in sorted_keys.items()])
|
||||
return base64.b64encode(
|
||||
hashlib.sha256((pubkeys_concat).encode("utf-8")).digest()
|
||||
).decode()[:12]
|
||||
|
||||
31
cashu/core/legacy.py
Normal file
31
cashu/core/legacy.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import hashlib
|
||||
|
||||
from secp256k1 import PublicKey
|
||||
|
||||
|
||||
def hash_to_point_pre_0_3_3(secret_msg):
|
||||
"""
|
||||
NOTE: Clients pre 0.3.3 used a different hash_to_curve
|
||||
|
||||
Generates x coordinate from the message hash and checks if the point lies on the curve.
|
||||
If it does not, it tries computing again a new x coordinate from the hash of the coordinate.
|
||||
"""
|
||||
point = None
|
||||
msg = secret_msg
|
||||
while point is None:
|
||||
_hash = hashlib.sha256(msg).hexdigest().encode("utf-8")
|
||||
try:
|
||||
# We construct compressed pub which has x coordinate encoded with even y
|
||||
_hash = list(_hash[:33]) # take the 33 bytes and get a list of bytes
|
||||
_hash[0] = 0x02 # set first byte to represent even y coord
|
||||
_hash = bytes(_hash)
|
||||
point = PublicKey(_hash, raw=True)
|
||||
except:
|
||||
msg = _hash
|
||||
|
||||
return point
|
||||
|
||||
|
||||
def verify_pre_0_3_3(a, C, secret_msg):
|
||||
Y = hash_to_point_pre_0_3_3(secret_msg.encode("utf-8"))
|
||||
return C == Y.mult(a)
|
||||
@@ -3,13 +3,17 @@ import re
|
||||
from cashu.core.db import COCKROACH, POSTGRES, SQLITE, Database
|
||||
|
||||
|
||||
def table_with_schema(db, table: str):
|
||||
return f"{db.references_schema if db.schema else ''}{table}"
|
||||
|
||||
|
||||
async def migrate_databases(db: Database, migrations_module):
|
||||
"""Creates the necessary databases if they don't exist already; or migrates them."""
|
||||
|
||||
async def set_migration_version(conn, db_name, version):
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO dbversions (db, version) VALUES (?, ?)
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'dbversions')} (db, version) VALUES (?, ?)
|
||||
ON CONFLICT (db) DO UPDATE SET version = ?
|
||||
""",
|
||||
(db_name, version, version),
|
||||
@@ -18,7 +22,7 @@ async def migrate_databases(db: Database, migrations_module):
|
||||
async def run_migration(db, migrations_module):
|
||||
db_name = migrations_module.__name__.split(".")[-2]
|
||||
for key, migrate in migrations_module.__dict__.items():
|
||||
match = match = matcher.match(key)
|
||||
match = matcher.match(key)
|
||||
if match:
|
||||
version = int(match.group(1))
|
||||
if version > current_versions.get(db_name, 0):
|
||||
@@ -33,17 +37,19 @@ async def migrate_databases(db: Database, migrations_module):
|
||||
async with db.connect() as conn:
|
||||
if conn.type == SQLITE:
|
||||
exists = await conn.fetchone(
|
||||
"SELECT * FROM sqlite_master WHERE type='table' AND name='dbversions'"
|
||||
f"SELECT * FROM sqlite_master WHERE type='table' AND name='{table_with_schema(db, 'dbversions')}'"
|
||||
)
|
||||
elif conn.type in {POSTGRES, COCKROACH}:
|
||||
exists = await conn.fetchone(
|
||||
"SELECT * FROM information_schema.tables WHERE table_name = 'dbversions'"
|
||||
f"SELECT * FROM information_schema.tables WHERE table_name = '{table_with_schema(db, 'dbversions')}'"
|
||||
)
|
||||
|
||||
if not exists:
|
||||
await migrations_module.m000_create_migrations_table(conn)
|
||||
|
||||
rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall()
|
||||
rows = await (
|
||||
await conn.execute(f"SELECT * FROM {table_with_schema(db, 'dbversions')}")
|
||||
).fetchall()
|
||||
current_versions = {row["db"]: row["version"] for row in rows}
|
||||
matcher = re.compile(r"^m(\d\d\d)_")
|
||||
await run_migration(conn, migrations_module)
|
||||
await run_migration(db, migrations_module)
|
||||
|
||||
@@ -7,14 +7,15 @@ from environs import Env # type: ignore
|
||||
|
||||
env = Env()
|
||||
|
||||
ENV_FILE: Union[str, None] = os.path.join(str(Path.home()), ".cashu", ".env")
|
||||
# env file: default to current dir, else home dir
|
||||
ENV_FILE = os.path.join(os.getcwd(), ".env")
|
||||
if not os.path.isfile(ENV_FILE):
|
||||
ENV_FILE = os.path.join(os.getcwd(), ".env")
|
||||
ENV_FILE = os.path.join(str(Path.home()), ".cashu", ".env")
|
||||
if os.path.isfile(ENV_FILE):
|
||||
env.read_env(ENV_FILE)
|
||||
else:
|
||||
ENV_FILE = None
|
||||
env.read_env()
|
||||
ENV_FILE = ""
|
||||
env.read_env(recurse=False)
|
||||
|
||||
DEBUG = env.bool("DEBUG", default=False)
|
||||
if not DEBUG:
|
||||
@@ -24,6 +25,11 @@ CASHU_DIR = env.str("CASHU_DIR", default=os.path.join(str(Path.home()), ".cashu"
|
||||
CASHU_DIR = CASHU_DIR.replace("~", str(Path.home()))
|
||||
assert len(CASHU_DIR), "CASHU_DIR not defined"
|
||||
|
||||
TOR = env.bool("TOR", default=True)
|
||||
|
||||
SOCKS_HOST = env.str("SOCKS_HOST", default=None)
|
||||
SOCKS_PORT = env.int("SOCKS_PORT", default=9050)
|
||||
|
||||
LIGHTNING = env.bool("LIGHTNING", default=True)
|
||||
LIGHTNING_FEE_PERCENT = env.float("LIGHTNING_FEE_PERCENT", default=1.0)
|
||||
assert LIGHTNING_FEE_PERCENT >= 0, "LIGHTNING_FEE_PERCENT must be at least 0"
|
||||
@@ -47,5 +53,8 @@ if not MINT_URL:
|
||||
LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None)
|
||||
LNBITS_KEY = env.str("LNBITS_KEY", default=None)
|
||||
|
||||
NOSTR_PRIVATE_KEY = env.str("NOSTR_PRIVATE_KEY", default=None)
|
||||
NOSTR_RELAYS = env.list("NOSTR_RELAYS", default=["wss://nostr-pub.wellorder.net"])
|
||||
|
||||
MAX_ORDER = 64
|
||||
VERSION = "0.3.2"
|
||||
VERSION = "0.7.0"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from cashu.lightning.lnbits import LNbitsWallet
|
||||
# from cashu.lightning.lnbits import LNbitsWallet
|
||||
|
||||
WALLET = LNbitsWallet()
|
||||
# WALLET = LNbitsWallet()
|
||||
|
||||
@@ -79,9 +79,9 @@ class Wallet(ABC):
|
||||
) -> Coroutine[None, None, PaymentStatus]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
pass
|
||||
# @abstractmethod
|
||||
# def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
# pass
|
||||
|
||||
|
||||
class Unsupported(Exception):
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
from os import getenv
|
||||
from typing import AsyncGenerator, Dict, Optional
|
||||
from typing import Dict, Optional
|
||||
|
||||
import requests
|
||||
|
||||
@@ -133,26 +130,26 @@ class LNbitsWallet(Wallet):
|
||||
|
||||
return PaymentStatus(data["paid"], data["details"]["fee"], data["preimage"])
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
url = f"{self.endpoint}/api/v1/payments/sse"
|
||||
# async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
# url = f"{self.endpoint}/api/v1/payments/sse"
|
||||
|
||||
while True:
|
||||
try:
|
||||
async with requests.stream("GET", url) as r:
|
||||
async for line in r.aiter_lines():
|
||||
if line.startswith("data:"):
|
||||
try:
|
||||
data = json.loads(line[5:])
|
||||
except json.decoder.JSONDecodeError:
|
||||
continue
|
||||
# while True:
|
||||
# try:
|
||||
# async with requests.stream("GET", url) as r:
|
||||
# async for line in r.aiter_lines():
|
||||
# if line.startswith("data:"):
|
||||
# try:
|
||||
# data = json.loads(line[5:])
|
||||
# except json.decoder.JSONDecodeError:
|
||||
# continue
|
||||
|
||||
if type(data) is not dict:
|
||||
continue
|
||||
# if type(data) is not dict:
|
||||
# continue
|
||||
|
||||
yield data["payment_hash"] # payment_hash
|
||||
# yield data["payment_hash"] # payment_hash
|
||||
|
||||
except:
|
||||
pass
|
||||
# except:
|
||||
# pass
|
||||
|
||||
print("lost connection to lnbits /payments/sse, retrying in 5 seconds")
|
||||
await asyncio.sleep(5)
|
||||
# print("lost connection to lnbits /payments/sse, retrying in 5 seconds")
|
||||
# await asyncio.sleep(5)
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
from cashu.core.settings import MINT_PRIVATE_KEY
|
||||
from cashu.mint.ledger import Ledger
|
||||
|
||||
ledger = Ledger(MINT_PRIVATE_KEY, "data/mint")
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from fastapi import FastAPI
|
||||
from loguru import logger
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
from cashu.core.settings import DEBUG, VERSION
|
||||
from cashu.lightning import WALLET
|
||||
from cashu.mint.migrations import m001_initial
|
||||
|
||||
from . import ledger
|
||||
from .router import router
|
||||
from .startup import load_ledger
|
||||
from .startup import start_mint_init
|
||||
|
||||
# from starlette_context import context
|
||||
# from starlette_context.middleware import RawContextMiddleware
|
||||
|
||||
|
||||
# class CustomHeaderMiddleware(BaseHTTPMiddleware):
|
||||
# """
|
||||
# Middleware for starlette that can set the context from request headers
|
||||
# """
|
||||
|
||||
# async def dispatch(self, request, call_next):
|
||||
# context["client-version"] = request.headers.get("Client-version")
|
||||
# response = await call_next(request)
|
||||
# return response
|
||||
|
||||
|
||||
def create_app(config_object="core.settings") -> FastAPI:
|
||||
@@ -49,6 +61,13 @@ def create_app(config_object="core.settings") -> FastAPI:
|
||||
|
||||
configure_logger()
|
||||
|
||||
# middleware = [
|
||||
# Middleware(
|
||||
# RawContextMiddleware,
|
||||
# ),
|
||||
# Middleware(CustomHeaderMiddleware),
|
||||
# ]
|
||||
|
||||
app = FastAPI(
|
||||
title="Cashu Mint",
|
||||
description="Ecash wallet and mint with Bitcoin Lightning support.",
|
||||
@@ -57,8 +76,8 @@ def create_app(config_object="core.settings") -> FastAPI:
|
||||
"name": "MIT License",
|
||||
"url": "https://raw.githubusercontent.com/callebtc/cashu/main/LICENSE",
|
||||
},
|
||||
# middleware=middleware,
|
||||
)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@@ -68,5 +87,5 @@ app.include_router(router=router)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_load_ledger():
|
||||
await load_ledger()
|
||||
async def startup_mint():
|
||||
await start_mint_init()
|
||||
|
||||
@@ -1,20 +1,74 @@
|
||||
from typing import Optional
|
||||
import time
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from cashu.core.base import Invoice, MintKeyset, Proof
|
||||
from cashu.core.db import Connection, Database
|
||||
from cashu.core.migrations import table_with_schema
|
||||
|
||||
|
||||
class LedgerCrud:
|
||||
"""
|
||||
Database interface for Cashu mint.
|
||||
|
||||
This class needs to be overloaded by any app that imports the Cashu mint.
|
||||
"""
|
||||
|
||||
async def get_keyset(*args, **kwags):
|
||||
|
||||
return await get_keyset(*args, **kwags)
|
||||
|
||||
async def get_lightning_invoice(*args, **kwags):
|
||||
|
||||
return await get_lightning_invoice(*args, **kwags)
|
||||
|
||||
async def get_proofs_used(*args, **kwags):
|
||||
|
||||
return await get_proofs_used(*args, **kwags)
|
||||
|
||||
async def invalidate_proof(*args, **kwags):
|
||||
|
||||
return await invalidate_proof(*args, **kwags)
|
||||
|
||||
async def get_proofs_pending(*args, **kwags):
|
||||
|
||||
return await get_proofs_pending(*args, **kwags)
|
||||
|
||||
async def set_proof_pending(*args, **kwags):
|
||||
|
||||
return await set_proof_pending(*args, **kwags)
|
||||
|
||||
async def unset_proof_pending(*args, **kwags):
|
||||
|
||||
return await unset_proof_pending(*args, **kwags)
|
||||
|
||||
async def store_keyset(*args, **kwags):
|
||||
|
||||
return await store_keyset(*args, **kwags)
|
||||
|
||||
async def store_lightning_invoice(*args, **kwags):
|
||||
|
||||
return await store_lightning_invoice(*args, **kwags)
|
||||
|
||||
async def store_promise(*args, **kwags):
|
||||
|
||||
return await store_promise(*args, **kwags)
|
||||
|
||||
async def update_lightning_invoice(*args, **kwags):
|
||||
|
||||
return await update_lightning_invoice(*args, **kwags)
|
||||
|
||||
|
||||
async def store_promise(
|
||||
db: Database,
|
||||
amount: int,
|
||||
B_: str,
|
||||
C_: str,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO promises
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'promises')}
|
||||
(amount, B_b, C_b)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
@@ -32,23 +86,23 @@ async def get_proofs_used(
|
||||
):
|
||||
|
||||
rows = await (conn or db).fetchall(
|
||||
"""
|
||||
SELECT secret from proofs_used
|
||||
f"""
|
||||
SELECT secret from {table_with_schema(db, 'proofs_used')}
|
||||
"""
|
||||
)
|
||||
return [row[0] for row in rows]
|
||||
|
||||
|
||||
async def invalidate_proof(
|
||||
proof: Proof,
|
||||
db: Database,
|
||||
proof: Proof,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
# we add the proof and secret to the used list
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO proofs_used
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'proofs_used')}
|
||||
(amount, C, secret)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
@@ -60,15 +114,64 @@ async def invalidate_proof(
|
||||
)
|
||||
|
||||
|
||||
async def store_lightning_invoice(
|
||||
invoice: Invoice,
|
||||
async def get_proofs_pending(
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
rows = await (conn or db).fetchall(
|
||||
f"""
|
||||
SELECT * from {table_with_schema(db, 'proofs_pending')}
|
||||
"""
|
||||
)
|
||||
return [Proof(**r) for r in rows]
|
||||
|
||||
|
||||
async def set_proof_pending(
|
||||
db: Database,
|
||||
proof: Proof,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
# we add the proof and secret to the used list
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'proofs_pending')}
|
||||
(amount, C, secret)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(
|
||||
proof.amount,
|
||||
str(proof.C),
|
||||
str(proof.secret),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def unset_proof_pending(
|
||||
proof: Proof,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO invoices
|
||||
f"""
|
||||
DELETE FROM {table_with_schema(db, 'proofs_pending')}
|
||||
WHERE secret = ?
|
||||
""",
|
||||
(str(proof["secret"]),),
|
||||
)
|
||||
|
||||
|
||||
async def store_lightning_invoice(
|
||||
db: Database,
|
||||
invoice: Invoice,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'invoices')}
|
||||
(amount, pr, hash, issued)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
@@ -82,29 +185,29 @@ async def store_lightning_invoice(
|
||||
|
||||
|
||||
async def get_lightning_invoice(
|
||||
hash: str,
|
||||
db: Database,
|
||||
hash: str,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
row = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * from invoices
|
||||
f"""
|
||||
SELECT * from {table_with_schema(db, 'invoices')}
|
||||
WHERE hash = ?
|
||||
""",
|
||||
(hash,),
|
||||
)
|
||||
return Invoice.from_row(row)
|
||||
return Invoice(**row)
|
||||
|
||||
|
||||
async def update_lightning_invoice(
|
||||
db: Database,
|
||||
hash: str,
|
||||
issued: bool,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
await (conn or db).execute(
|
||||
"UPDATE invoices SET issued = ? WHERE hash = ?",
|
||||
f"UPDATE {table_with_schema(db, 'invoices')} SET issued = ? WHERE hash = ?",
|
||||
(
|
||||
issued,
|
||||
hash,
|
||||
@@ -113,37 +216,37 @@ async def update_lightning_invoice(
|
||||
|
||||
|
||||
async def store_keyset(
|
||||
db: Database,
|
||||
keyset: MintKeyset,
|
||||
mint_url: str = None,
|
||||
db: Database = None,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO keysets
|
||||
(id, derivation_path, valid_from, valid_to, first_seen, active)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
await (conn or db).execute( # type: ignore
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'keysets')}
|
||||
(id, derivation_path, valid_from, valid_to, first_seen, active, version)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
keyset.id,
|
||||
keyset.derivation_path,
|
||||
keyset.valid_from,
|
||||
keyset.valid_to,
|
||||
keyset.first_seen,
|
||||
keyset.valid_from or db.timestamp_now,
|
||||
keyset.valid_to or db.timestamp_now,
|
||||
keyset.first_seen or db.timestamp_now,
|
||||
True,
|
||||
keyset.version,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def get_keyset(
|
||||
db: Database,
|
||||
id: str = None,
|
||||
derivation_path: str = None,
|
||||
db: Database = None,
|
||||
derivation_path: str = "",
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
clauses = []
|
||||
values = []
|
||||
values: List[Any] = []
|
||||
clauses.append("active = ?")
|
||||
values.append(True)
|
||||
if id:
|
||||
@@ -156,11 +259,11 @@ async def get_keyset(
|
||||
if clauses:
|
||||
where = f"WHERE {' AND '.join(clauses)}"
|
||||
|
||||
rows = await (conn or db).fetchall(
|
||||
rows = await (conn or db).fetchall( # type: ignore
|
||||
f"""
|
||||
SELECT * from keysets
|
||||
SELECT * from {table_with_schema(db, 'keysets')}
|
||||
{where}
|
||||
""",
|
||||
tuple(values),
|
||||
)
|
||||
return [MintKeyset.from_row(row) for row in rows]
|
||||
return [MintKeyset(**row) for row in rows]
|
||||
|
||||
@@ -9,6 +9,7 @@ from loguru import logger
|
||||
|
||||
import cashu.core.b_dhke as b_dhke
|
||||
import cashu.core.bolt11 as bolt11
|
||||
import cashu.core.legacy as legacy
|
||||
from cashu.core.base import (
|
||||
BlindedMessage,
|
||||
BlindedSignature,
|
||||
@@ -17,102 +18,129 @@ from cashu.core.base import (
|
||||
MintKeysets,
|
||||
Proof,
|
||||
)
|
||||
from cashu.core.crypto import derive_keys, derive_keyset_id, derive_pubkeys
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.helpers import fee_reserve, sum_proofs
|
||||
from cashu.core.script import verify_script
|
||||
from cashu.core.secp import PublicKey
|
||||
from cashu.core.settings import LIGHTNING, MAX_ORDER
|
||||
from cashu.core.settings import LIGHTNING, MAX_ORDER, VERSION
|
||||
from cashu.core.split import amount_split
|
||||
from cashu.lightning import WALLET
|
||||
from cashu.mint.crud import (
|
||||
get_keyset,
|
||||
get_lightning_invoice,
|
||||
get_proofs_used,
|
||||
invalidate_proof,
|
||||
store_keyset,
|
||||
store_lightning_invoice,
|
||||
store_promise,
|
||||
update_lightning_invoice,
|
||||
)
|
||||
from cashu.mint.crud import LedgerCrud
|
||||
|
||||
# from starlette_context import context
|
||||
|
||||
|
||||
class Ledger:
|
||||
def __init__(self, secret_key: str, db: str, derivation_path=""):
|
||||
def __init__(
|
||||
self,
|
||||
db: Database,
|
||||
seed: str,
|
||||
derivation_path="",
|
||||
crud=LedgerCrud,
|
||||
lightning=None,
|
||||
):
|
||||
self.proofs_used: Set[str] = set()
|
||||
self.master_key = secret_key
|
||||
self.master_key = seed
|
||||
self.derivation_path = derivation_path
|
||||
self.db: Database = Database("mint", db)
|
||||
|
||||
self.db = db
|
||||
self.crud = crud
|
||||
self.lightning = lightning
|
||||
|
||||
async def load_used_proofs(self):
|
||||
"""Load all used proofs from database."""
|
||||
self.proofs_used = set(await get_proofs_used(db=self.db))
|
||||
proofs_used = await self.crud.get_proofs_used(db=self.db)
|
||||
self.proofs_used = set(proofs_used)
|
||||
|
||||
async def init_keysets(self):
|
||||
"""Loads all past keysets and stores the active one if not already in db"""
|
||||
# generate current keyset from seed and current derivation path
|
||||
self.keyset = MintKeyset(
|
||||
seed=self.master_key, derivation_path=self.derivation_path
|
||||
async def load_keyset(self, derivation_path, autosave=True):
|
||||
"""Load current keyset keyset or generate new one."""
|
||||
keyset = MintKeyset(
|
||||
seed=self.master_key, derivation_path=derivation_path, version=VERSION
|
||||
)
|
||||
# check if current keyset is stored in db and store if not
|
||||
current_keyset_local: List[MintKeyset] = await get_keyset(
|
||||
id=self.keyset.id, db=self.db
|
||||
logger.trace(f"Loading keyset {keyset.id} from db.")
|
||||
tmp_keyset_local: List[MintKeyset] = await self.crud.get_keyset(
|
||||
id=keyset.id, db=self.db
|
||||
)
|
||||
if not len(current_keyset_local):
|
||||
logger.debug(f"Storing keyset {self.keyset.id}")
|
||||
await store_keyset(keyset=self.keyset, db=self.db)
|
||||
if not len(tmp_keyset_local) and autosave:
|
||||
logger.trace(f"Storing keyset {keyset.id}.")
|
||||
await self.crud.store_keyset(keyset=keyset, db=self.db)
|
||||
|
||||
# store the new keyset in the current keysets
|
||||
self.keysets.keysets[keyset.id] = keyset
|
||||
return keyset
|
||||
|
||||
async def init_keysets(self, autosave=True):
|
||||
"""Loads all keysets from db."""
|
||||
# load all past keysets from db
|
||||
# this needs two steps because the types of tmp_keysets and the argument of MintKeysets() are different
|
||||
tmp_keysets: List[MintKeyset] = await get_keyset(db=self.db)
|
||||
tmp_keysets: List[MintKeyset] = await self.crud.get_keyset(db=self.db)
|
||||
self.keysets = MintKeysets(tmp_keysets)
|
||||
logger.debug(f"Keysets {self.keysets.keysets}")
|
||||
logger.trace(f"Loading {len(self.keysets.keysets)} keysets form db.")
|
||||
# generate all derived keys from stored derivation paths of past keysets
|
||||
for _, v in self.keysets.keysets.items():
|
||||
logger.trace(f"Generating keys for keyset {v.id}")
|
||||
v.generate_keys(self.master_key)
|
||||
# load the current keyset
|
||||
self.keyset = await self.load_keyset(self.derivation_path, autosave)
|
||||
|
||||
if len(self.keysets.keysets):
|
||||
logger.debug(f"Loaded {len(self.keysets.keysets)} keysets from db.")
|
||||
|
||||
async def _generate_promises(self, amounts: List[int], B_s: List[str]):
|
||||
async def _generate_promises(
|
||||
self, B_s: List[BlindedMessage], keyset: MintKeyset = None
|
||||
):
|
||||
"""Generates promises that sum to the given amount."""
|
||||
return [
|
||||
await self._generate_promise(amount, PublicKey(bytes.fromhex(B_), raw=True))
|
||||
for (amount, B_) in zip(amounts, B_s)
|
||||
await self._generate_promise(
|
||||
b.amount, PublicKey(bytes.fromhex(b.B_), raw=True), keyset
|
||||
)
|
||||
for b in B_s
|
||||
]
|
||||
|
||||
async def _generate_promise(self, amount: int, B_: PublicKey):
|
||||
async def _generate_promise(
|
||||
self, amount: int, B_: PublicKey, keyset: MintKeyset = None
|
||||
):
|
||||
"""Generates a promise for given amount and returns a pair (amount, C')."""
|
||||
secret_key = self.keyset.private_keys[amount] # Get the correct key
|
||||
C_ = b_dhke.step2_bob(B_, secret_key)
|
||||
await store_promise(
|
||||
amount, B_=B_.serialize().hex(), C_=C_.serialize().hex(), db=self.db
|
||||
keyset = keyset if keyset else self.keyset
|
||||
private_key_amount = keyset.private_keys[amount]
|
||||
C_ = b_dhke.step2_bob(B_, private_key_amount)
|
||||
await self.crud.store_promise(
|
||||
amount=amount, B_=B_.serialize().hex(), C_=C_.serialize().hex(), db=self.db
|
||||
)
|
||||
return BlindedSignature(amount=amount, C_=C_.serialize().hex())
|
||||
return BlindedSignature(id=keyset.id, amount=amount, C_=C_.serialize().hex())
|
||||
|
||||
def _check_spendable(self, proof: Proof):
|
||||
"""Checks whether the proof was already spent."""
|
||||
return not proof.secret in self.proofs_used
|
||||
|
||||
def _verify_secret_criteria(self, proof: Proof):
|
||||
"""Verifies that a secret is present"""
|
||||
"""Verifies that a secret is present and is not too long (DOS prevention)."""
|
||||
if proof.secret is None or proof.secret == "":
|
||||
raise Exception("no secret in proof.")
|
||||
if len(proof.secret) > 64:
|
||||
raise Exception("secret too long.")
|
||||
return True
|
||||
|
||||
def _verify_proof(self, proof: Proof):
|
||||
def _verify_proof_bdhke(self, proof: Proof):
|
||||
"""Verifies that the proof of promise was issued by this ledger."""
|
||||
if not self._check_spendable(proof):
|
||||
raise Exception(f"tokens already spent. Secret: {proof.secret}")
|
||||
# if no keyset id is given in proof, assume the current one
|
||||
if not proof.id:
|
||||
secret_key = self.keyset.private_keys[proof.amount]
|
||||
private_key_amount = self.keyset.private_keys[proof.amount]
|
||||
else:
|
||||
# use the appropriate active keyset for this proof.id
|
||||
secret_key = self.keysets.keysets[proof.id].private_keys[proof.amount]
|
||||
private_key_amount = self.keysets.keysets[proof.id].private_keys[
|
||||
proof.amount
|
||||
]
|
||||
|
||||
C = PublicKey(bytes.fromhex(proof.C), raw=True)
|
||||
return b_dhke.verify(secret_key, C, proof.secret)
|
||||
|
||||
# backwards compatibility with old hash_to_curve < 0.4.0
|
||||
try:
|
||||
ret = legacy.verify_pre_0_3_3(private_key_amount, C, proof.secret)
|
||||
if ret:
|
||||
return ret
|
||||
except:
|
||||
pass
|
||||
|
||||
return b_dhke.verify(private_key_amount, C, proof.secret)
|
||||
|
||||
def _verify_script(self, idx: int, proof: Proof):
|
||||
"""
|
||||
@@ -153,10 +181,13 @@ class Ledger:
|
||||
given = [o.amount for o in outputs]
|
||||
return given == expected
|
||||
|
||||
def _verify_no_duplicates(self, proofs: List[Proof], outputs: List[BlindedMessage]):
|
||||
def _verify_no_duplicate_proofs(self, proofs: List[Proof]):
|
||||
secrets = [p.secret for p in proofs]
|
||||
if len(secrets) != len(list(set(secrets))):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _verify_no_duplicate_outputs(self, outputs: List[BlindedMessage]):
|
||||
B_s = [od.B_ for od in outputs]
|
||||
if len(B_s) != len(list(set(B_s))):
|
||||
return False
|
||||
@@ -185,67 +216,132 @@ class Ledger:
|
||||
sum_outputs = sum(self._verify_amount(p.amount) for p in outs)
|
||||
assert sum_outputs - sum_inputs == 0
|
||||
|
||||
def _get_output_split(self, amount: int):
|
||||
"""Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8]."""
|
||||
self._verify_amount(amount)
|
||||
bits_amt = bin(amount)[::-1][:-2]
|
||||
rv = []
|
||||
for (pos, bit) in enumerate(bits_amt):
|
||||
if bit == "1":
|
||||
rv.append(2**pos)
|
||||
return rv
|
||||
|
||||
async def _request_lightning_invoice(self, amount: int):
|
||||
"""Returns an invoice from the Lightning backend."""
|
||||
error, balance = await WALLET.status()
|
||||
error, balance = await self.lightning.status()
|
||||
if error:
|
||||
raise Exception(f"Lightning wallet not responding: {error}")
|
||||
ok, checking_id, payment_request, error_message = await WALLET.create_invoice(
|
||||
amount, "cashu deposit"
|
||||
)
|
||||
(
|
||||
ok,
|
||||
checking_id,
|
||||
payment_request,
|
||||
error_message,
|
||||
) = await self.lightning.create_invoice(amount, "cashu deposit")
|
||||
return payment_request, checking_id
|
||||
|
||||
async def _check_lightning_invoice(self, amounts, payment_hash: str):
|
||||
"""Checks with the Lightning backend whether an invoice with this payment_hash was paid."""
|
||||
invoice: Invoice = await get_lightning_invoice(payment_hash, db=self.db)
|
||||
async def _check_lightning_invoice(self, amount: int, payment_hash: str):
|
||||
"""
|
||||
Checks with the Lightning backend whether an invoice with this payment_hash was paid.
|
||||
Raises exception if invoice is unpaid.
|
||||
"""
|
||||
invoice: Invoice = await self.crud.get_lightning_invoice(
|
||||
hash=payment_hash, db=self.db
|
||||
)
|
||||
if invoice is None:
|
||||
raise Exception("invoice not found.")
|
||||
if invoice.issued:
|
||||
raise Exception("tokens already issued for this invoice.")
|
||||
total_requested = sum(amounts)
|
||||
if total_requested > invoice.amount:
|
||||
raise Exception(
|
||||
f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}"
|
||||
|
||||
# set this invoice as issued
|
||||
await self.crud.update_lightning_invoice(
|
||||
hash=payment_hash, issued=True, db=self.db
|
||||
)
|
||||
|
||||
try:
|
||||
if amount > invoice.amount:
|
||||
raise Exception(
|
||||
f"requested amount too high: {amount}. Invoice amount: {invoice.amount}"
|
||||
)
|
||||
|
||||
status = await self.lightning.get_invoice_status(payment_hash)
|
||||
if status.paid:
|
||||
return status.paid
|
||||
else:
|
||||
raise Exception("Lightning invoice not paid yet.")
|
||||
except Exception as e:
|
||||
# unset issued
|
||||
await self.crud.update_lightning_invoice(
|
||||
hash=payment_hash, issued=False, db=self.db
|
||||
)
|
||||
status = await WALLET.get_invoice_status(payment_hash)
|
||||
if status.paid:
|
||||
await update_lightning_invoice(payment_hash, issued=True, db=self.db)
|
||||
return status.paid
|
||||
raise e
|
||||
|
||||
async def _pay_lightning_invoice(self, invoice: str, fees_msat: int):
|
||||
"""Returns an invoice from the Lightning backend."""
|
||||
error, _ = await WALLET.status()
|
||||
error, _ = await self.lightning.status()
|
||||
if error:
|
||||
raise Exception(f"Lightning wallet not responding: {error}")
|
||||
ok, checking_id, fee_msat, preimage, error_message = await WALLET.pay_invoice(
|
||||
invoice, fee_limit_msat=fees_msat
|
||||
)
|
||||
(
|
||||
ok,
|
||||
checking_id,
|
||||
fee_msat,
|
||||
preimage,
|
||||
error_message,
|
||||
) = await self.lightning.pay_invoice(invoice, fee_limit_msat=fees_msat)
|
||||
return ok, preimage
|
||||
|
||||
async def _invalidate_proofs(self, proofs: List[Proof]):
|
||||
"""Adds secrets of proofs to the list of knwon secrets and stores them in the db."""
|
||||
"""
|
||||
Adds secrets of proofs to the list of known secrets and stores them in the db.
|
||||
Removes proofs from pending table.
|
||||
"""
|
||||
# Mark proofs as used and prepare new promises
|
||||
proof_msgs = set([p.secret for p in proofs])
|
||||
self.proofs_used |= proof_msgs
|
||||
# store in db
|
||||
for p in proofs:
|
||||
await invalidate_proof(p, db=self.db)
|
||||
await self.crud.invalidate_proof(proof=p, db=self.db)
|
||||
|
||||
def _serialize_pubkeys(self):
|
||||
"""Returns public keys for possible amounts."""
|
||||
return {a: p.serialize().hex() for a, p in self.keyset.public_keys.items()}
|
||||
async def _set_proofs_pending(self, proofs: List[Proof]):
|
||||
"""
|
||||
If none of the proofs is in the pending table (_validate_proofs_pending), adds proofs to
|
||||
the list of pending proofs or removes them. Used as a mutex for proofs.
|
||||
"""
|
||||
# first we check whether these proofs are pending aready
|
||||
await self._validate_proofs_pending(proofs)
|
||||
for p in proofs:
|
||||
try:
|
||||
await self.crud.set_proof_pending(proof=p, db=self.db)
|
||||
except:
|
||||
raise Exception("proofs already pending.")
|
||||
|
||||
async def _unset_proofs_pending(self, proofs: List[Proof]):
|
||||
"""Deletes proofs from pending table."""
|
||||
# we try: except: this block in order to avoid that any errors here
|
||||
# could block the _invalidate_proofs() call that happens afterwards.
|
||||
try:
|
||||
for p in proofs:
|
||||
await self.crud.unset_proof_pending(proof=p, db=self.db)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
async def _validate_proofs_pending(self, proofs: List[Proof]):
|
||||
"""Checks if any of the provided proofs is in the pending proofs table. Raises exception for at least one match."""
|
||||
proofs_pending = await self.crud.get_proofs_pending(db=self.db)
|
||||
for p in proofs:
|
||||
for pp in proofs_pending:
|
||||
if p.secret == pp.secret:
|
||||
raise Exception("proofs are pending.")
|
||||
|
||||
async def _verify_proofs(self, proofs: List[Proof]):
|
||||
"""Checks a series of criteria for the verification of proofs."""
|
||||
# Verify scripts
|
||||
if not all([self._verify_script(i, p) for i, p in enumerate(proofs)]):
|
||||
raise Exception("script validation failed.")
|
||||
# Verify secret criteria
|
||||
if not all([self._verify_secret_criteria(p) for p in proofs]):
|
||||
raise Exception("secrets do not match criteria.")
|
||||
# verify that only unique proofs were used
|
||||
if not self._verify_no_duplicate_proofs(proofs):
|
||||
raise Exception("duplicate proofs.")
|
||||
# Verify proofs
|
||||
if not all([self._verify_proof_bdhke(p) for p in proofs]):
|
||||
raise Exception("could not verify proofs.")
|
||||
|
||||
# Public methods
|
||||
def get_keyset(self):
|
||||
return self._serialize_pubkeys()
|
||||
def get_keyset(self, keyset_id: str = None):
|
||||
keyset = self.keysets.keysets[keyset_id] if keyset_id else self.keyset
|
||||
return {a: p.serialize().hex() for a, p in keyset.public_keys.items()}
|
||||
|
||||
async def request_mint(self, amount):
|
||||
"""Returns Lightning invoice and stores it in the db."""
|
||||
@@ -255,46 +351,63 @@ class Ledger:
|
||||
)
|
||||
if not payment_request or not checking_id:
|
||||
raise Exception(f"Could not create Lightning invoice.")
|
||||
await store_lightning_invoice(invoice, db=self.db)
|
||||
await self.crud.store_lightning_invoice(invoice=invoice, db=self.db)
|
||||
return payment_request, checking_id
|
||||
|
||||
async def mint(self, B_s: List[PublicKey], amounts: List[int], payment_hash=None):
|
||||
async def mint(
|
||||
self,
|
||||
B_s: List[BlindedMessage],
|
||||
payment_hash=None,
|
||||
keyset: MintKeyset = None,
|
||||
):
|
||||
"""Mints a promise for coins for B_."""
|
||||
amounts = [b.amount for b in B_s]
|
||||
amount = sum(amounts)
|
||||
# check if lightning invoice was paid
|
||||
if LIGHTNING:
|
||||
if not payment_hash:
|
||||
raise Exception("no payment_hash provided.")
|
||||
try:
|
||||
paid = await self._check_lightning_invoice(amounts, payment_hash)
|
||||
paid = await self._check_lightning_invoice(amount, payment_hash)
|
||||
except Exception as e:
|
||||
raise Exception("could not check invoice: " + str(e))
|
||||
if not paid:
|
||||
raise Exception("Lightning invoice not paid yet.")
|
||||
raise e
|
||||
|
||||
for amount in amounts:
|
||||
if amount not in [2**i for i in range(MAX_ORDER)]:
|
||||
raise Exception(f"Can only mint amounts up to {2**MAX_ORDER}.")
|
||||
|
||||
promises = [
|
||||
await self._generate_promise(amount, B_) for B_, amount in zip(B_s, amounts)
|
||||
]
|
||||
promises = await self._generate_promises(B_s, keyset)
|
||||
return promises
|
||||
|
||||
async def melt(self, proofs: List[Proof], invoice: str):
|
||||
"""Invalidates proofs and pays a Lightning invoice."""
|
||||
# Verify proofs
|
||||
if not all([self._verify_proof(p) for p in proofs]):
|
||||
raise Exception("could not verify proofs.")
|
||||
|
||||
total_provided = sum_proofs(proofs)
|
||||
invoice_obj = bolt11.decode(invoice)
|
||||
amount = math.ceil(invoice_obj.amount_msat / 1000)
|
||||
fees_msat = await self.check_fees(invoice)
|
||||
assert total_provided >= amount + fees_msat / 1000, Exception(
|
||||
"provided proofs not enough for Lightning payment."
|
||||
)
|
||||
# validate and set proofs as pending
|
||||
await self._set_proofs_pending(proofs)
|
||||
|
||||
try:
|
||||
await self._verify_proofs(proofs)
|
||||
|
||||
total_provided = sum_proofs(proofs)
|
||||
invoice_obj = bolt11.decode(invoice)
|
||||
amount = math.ceil(invoice_obj.amount_msat / 1000)
|
||||
fees_msat = await self.check_fees(invoice)
|
||||
assert total_provided >= amount + fees_msat / 1000, Exception(
|
||||
"provided proofs not enough for Lightning payment."
|
||||
)
|
||||
|
||||
if LIGHTNING:
|
||||
status, preimage = await self._pay_lightning_invoice(invoice, fees_msat)
|
||||
else:
|
||||
status, preimage = True, "preimage"
|
||||
if status == True:
|
||||
await self._invalidate_proofs(proofs)
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
# delete proofs from pending list
|
||||
await self._unset_proofs_pending(proofs)
|
||||
|
||||
status, preimage = await self._pay_lightning_invoice(invoice, fees_msat)
|
||||
if status == True:
|
||||
await self._invalidate_proofs(proofs)
|
||||
return status, preimage
|
||||
|
||||
async def check_spendable(self, proofs: List[Proof]):
|
||||
@@ -307,49 +420,62 @@ class Ledger:
|
||||
amount = math.ceil(decoded_invoice.amount_msat / 1000)
|
||||
# hack: check if it's internal, if it exists, it will return paid = False,
|
||||
# if id does not exist (not internal), it returns paid = None
|
||||
paid = await WALLET.get_invoice_status(decoded_invoice.payment_hash)
|
||||
internal = paid.paid == False
|
||||
if LIGHTNING:
|
||||
paid = await self.lightning.get_invoice_status(decoded_invoice.payment_hash)
|
||||
internal = paid.paid == False
|
||||
else:
|
||||
internal = True
|
||||
fees_msat = fee_reserve(amount * 1000, internal)
|
||||
return fees_msat
|
||||
|
||||
async def split(
|
||||
self, proofs: List[Proof], amount: int, outputs: List[BlindedMessage]
|
||||
self,
|
||||
proofs: List[Proof],
|
||||
amount: int,
|
||||
outputs: List[BlindedMessage],
|
||||
keyset: MintKeyset = None,
|
||||
):
|
||||
"""Consumes proofs and prepares new promises based on the amount split."""
|
||||
|
||||
# set proofs as pending
|
||||
await self._set_proofs_pending(proofs)
|
||||
|
||||
total = sum_proofs(proofs)
|
||||
|
||||
# verify that amount is kosher
|
||||
self._verify_split_amount(amount)
|
||||
# verify overspending attempt
|
||||
if amount > total:
|
||||
raise Exception("split amount is higher than the total sum.")
|
||||
try:
|
||||
# verify that amount is kosher
|
||||
self._verify_split_amount(amount)
|
||||
# verify overspending attempt
|
||||
if amount > total:
|
||||
raise Exception("split amount is higher than the total sum.")
|
||||
|
||||
# Verify scripts
|
||||
if not all([self._verify_script(i, p) for i, p in enumerate(proofs)]):
|
||||
raise Exception("script verification failed.")
|
||||
# Verify secret criteria
|
||||
if not all([self._verify_secret_criteria(p) for p in proofs]):
|
||||
raise Exception("secrets do not match criteria.")
|
||||
# verify that only unique proofs and outputs were used
|
||||
if not self._verify_no_duplicates(proofs, outputs):
|
||||
raise Exception("duplicate proofs or promises.")
|
||||
# verify that outputs have the correct amount
|
||||
if not self._verify_outputs(total, amount, outputs):
|
||||
raise Exception("split of promises is not as expected.")
|
||||
# Verify proofs
|
||||
if not all([self._verify_proof(p) for p in proofs]):
|
||||
raise Exception("could not verify proofs.")
|
||||
await self._verify_proofs(proofs)
|
||||
|
||||
# verify that only unique outputs were used
|
||||
if not self._verify_no_duplicate_outputs(outputs):
|
||||
raise Exception("duplicate promises.")
|
||||
# verify that outputs have the correct amount
|
||||
if not self._verify_outputs(total, amount, outputs):
|
||||
raise Exception("split of promises is not as expected.")
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
# delete proofs from pending list
|
||||
await self._unset_proofs_pending(proofs)
|
||||
|
||||
# Mark proofs as used and prepare new promises
|
||||
await self._invalidate_proofs(proofs)
|
||||
|
||||
# split outputs according to amount
|
||||
outs_fst = amount_split(total - amount)
|
||||
outs_snd = amount_split(amount)
|
||||
B_fst = [od.B_ for od in outputs[: len(outs_fst)]]
|
||||
B_snd = [od.B_ for od in outputs[len(outs_fst) :]]
|
||||
B_fst = [od for od in outputs[: len(outs_fst)]]
|
||||
B_snd = [od for od in outputs[len(outs_fst) :]]
|
||||
|
||||
# generate promises
|
||||
prom_fst, prom_snd = await self._generate_promises(
|
||||
outs_fst, B_fst
|
||||
), await self._generate_promises(outs_snd, B_snd)
|
||||
B_fst, keyset
|
||||
), await self._generate_promises(B_snd, keyset)
|
||||
|
||||
# verify amounts in produced proofs
|
||||
self._verify_equation_balanced(proofs, prom_fst + prom_snd)
|
||||
return prom_fst, prom_snd
|
||||
|
||||
@@ -22,7 +22,8 @@ def main(
|
||||
ssl_keyfile: str = None,
|
||||
ssl_certfile: str = None,
|
||||
):
|
||||
"""Launched with `poetry run mint` at root level"""
|
||||
"""This routine starts the uvicorn server if the Cashu mint is
|
||||
launched with `poetry run mint` at root level"""
|
||||
# this beautiful beast parses all command line arguments and passes them to the uvicorn server
|
||||
d = dict()
|
||||
for a in ctx.args:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.migrations import table_with_schema
|
||||
|
||||
|
||||
async def m000_create_migrations_table(db):
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS dbversions (
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'dbversions')} (
|
||||
db TEXT PRIMARY KEY,
|
||||
version INT NOT NULL
|
||||
)
|
||||
@@ -14,8 +15,8 @@ async def m000_create_migrations_table(db):
|
||||
|
||||
async def m001_initial(db: Database):
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS promises (
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'promises')} (
|
||||
amount INTEGER NOT NULL,
|
||||
B_b TEXT NOT NULL,
|
||||
C_b TEXT NOT NULL,
|
||||
@@ -27,8 +28,8 @@ async def m001_initial(db: Database):
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS proofs_used (
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used')} (
|
||||
amount INTEGER NOT NULL,
|
||||
C TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
@@ -40,8 +41,8 @@ async def m001_initial(db: Database):
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS invoices (
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'invoices')} (
|
||||
amount INTEGER NOT NULL,
|
||||
pr TEXT NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
@@ -53,38 +54,38 @@ async def m001_initial(db: Database):
|
||||
"""
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE VIEW IF NOT EXISTS balance_issued AS
|
||||
SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
||||
SELECT SUM(amount) AS s
|
||||
FROM promises
|
||||
WHERE amount > 0
|
||||
);
|
||||
"""
|
||||
)
|
||||
# await db.execute(
|
||||
# f"""
|
||||
# CREATE VIEW {table_with_schema(db, 'balance_issued')} AS
|
||||
# SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
||||
# SELECT SUM(amount) AS s
|
||||
# FROM {table_with_schema(db, 'promises')}
|
||||
# WHERE amount > 0
|
||||
# );
|
||||
# """
|
||||
# )
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE VIEW IF NOT EXISTS balance_used AS
|
||||
SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
||||
SELECT SUM(amount) AS s
|
||||
FROM proofs_used
|
||||
WHERE amount > 0
|
||||
);
|
||||
"""
|
||||
)
|
||||
# await db.execute(
|
||||
# f"""
|
||||
# CREATE VIEW {table_with_schema(db, 'balance_used')} AS
|
||||
# SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
||||
# SELECT SUM(amount) AS s
|
||||
# FROM {table_with_schema(db, 'proofs_used')}
|
||||
# WHERE amount > 0
|
||||
# );
|
||||
# """
|
||||
# )
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE VIEW IF NOT EXISTS balance AS
|
||||
SELECT s_issued - s_used AS balance FROM (
|
||||
SELECT bi.balance AS s_issued, bu.balance AS s_used
|
||||
FROM balance_issued bi
|
||||
CROSS JOIN balance_used bu
|
||||
);
|
||||
"""
|
||||
)
|
||||
# await db.execute(
|
||||
# f"""
|
||||
# CREATE VIEW {table_with_schema(db, 'balance')} AS
|
||||
# SELECT s_issued - s_used AS balance FROM (
|
||||
# SELECT bi.balance AS s_issued, bu.balance AS s_used
|
||||
# FROM {table_with_schema(db, 'balance_issued')} bi
|
||||
# CROSS JOIN {table_with_schema(db, 'balance_used')} bu
|
||||
# );
|
||||
# """
|
||||
# )
|
||||
|
||||
|
||||
async def m003_mint_keysets(db: Database):
|
||||
@@ -93,12 +94,12 @@ async def m003_mint_keysets(db: Database):
|
||||
"""
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS keysets (
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'keysets')} (
|
||||
id TEXT NOT NULL,
|
||||
derivation_path TEXT,
|
||||
valid_from TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
valid_to TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
first_seen TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
valid_from TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
valid_to TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
first_seen TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
active BOOL DEFAULT TRUE,
|
||||
|
||||
UNIQUE (derivation_path)
|
||||
@@ -108,7 +109,7 @@ async def m003_mint_keysets(db: Database):
|
||||
)
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS mint_pubkeys (
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'mint_pubkeys')} (
|
||||
id TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
pubkey TEXT NOT NULL,
|
||||
@@ -118,3 +119,30 @@ async def m003_mint_keysets(db: Database):
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m004_keysets_add_version(db: Database):
|
||||
"""
|
||||
Column that remembers with which version
|
||||
"""
|
||||
await db.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN version TEXT"
|
||||
)
|
||||
|
||||
|
||||
async def m005_pending_proofs_table(db: Database) -> None:
|
||||
"""
|
||||
Store pending proofs.
|
||||
"""
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending')} (
|
||||
amount INTEGER NOT NULL,
|
||||
C TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
|
||||
UNIQUE (secret)
|
||||
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from typing import Union
|
||||
from typing import Dict, List, Union
|
||||
|
||||
from fastapi import APIRouter
|
||||
from secp256k1 import PublicKey
|
||||
|
||||
from cashu.core.base import (
|
||||
BlindedSignature,
|
||||
CheckFeesRequest,
|
||||
CheckFeesResponse,
|
||||
CheckRequest,
|
||||
@@ -15,25 +16,38 @@ from cashu.core.base import (
|
||||
SplitRequest,
|
||||
)
|
||||
from cashu.core.errors import CashuError
|
||||
from cashu.mint import ledger
|
||||
from cashu.mint.startup import ledger
|
||||
|
||||
router: APIRouter = APIRouter()
|
||||
|
||||
|
||||
@router.get("/keys")
|
||||
def keys():
|
||||
"""Get the public keys of the mint"""
|
||||
return ledger.get_keyset()
|
||||
async def keys() -> dict[int, str]:
|
||||
"""Get the public keys of the mint of the newest keyset"""
|
||||
keyset = ledger.get_keyset()
|
||||
return keyset
|
||||
|
||||
|
||||
@router.get("/keys/{idBase64Urlsafe}")
|
||||
async def keyset_keys(idBase64Urlsafe: str) -> dict[int, str]:
|
||||
"""
|
||||
Get the public keys of the mint of a specificy keyset id.
|
||||
The id is encoded in base64_urlsafe and needs to be converted back to
|
||||
normal base64 before it can be processed.
|
||||
"""
|
||||
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
|
||||
keyset = ledger.get_keyset(keyset_id=id)
|
||||
return keyset
|
||||
|
||||
|
||||
@router.get("/keysets")
|
||||
def keysets():
|
||||
async def keysets() -> dict[str, list[str]]:
|
||||
"""Get all active keysets of the mint"""
|
||||
return {"keysets": ledger.keysets.get_ids()}
|
||||
|
||||
|
||||
@router.get("/mint")
|
||||
async def request_mint(amount: int = 0):
|
||||
async def request_mint(amount: int = 0) -> GetMintResponse:
|
||||
"""
|
||||
Request minting of new tokens. The mint responds with a Lightning invoice.
|
||||
This endpoint can be used for a Lightning invoice UX flow.
|
||||
@@ -48,29 +62,25 @@ async def request_mint(amount: int = 0):
|
||||
|
||||
@router.post("/mint")
|
||||
async def mint(
|
||||
payloads: MintRequest,
|
||||
bolt11: Union[str, None] = None,
|
||||
mint_request: MintRequest,
|
||||
payment_hash: Union[str, None] = None,
|
||||
):
|
||||
) -> Union[List[BlindedSignature], CashuError]:
|
||||
"""
|
||||
Requests the minting of tokens belonging to a paid payment request.
|
||||
|
||||
Call this endpoint after `GET /mint`.
|
||||
"""
|
||||
amounts = []
|
||||
B_s = []
|
||||
for payload in payloads.blinded_messages:
|
||||
amounts.append(payload.amount)
|
||||
B_s.append(PublicKey(bytes.fromhex(payload.B_), raw=True))
|
||||
try:
|
||||
promises = await ledger.mint(B_s, amounts, payment_hash=payment_hash)
|
||||
promises = await ledger.mint(
|
||||
mint_request.blinded_messages, payment_hash=payment_hash
|
||||
)
|
||||
return promises
|
||||
except Exception as exc:
|
||||
return CashuError(error=str(exc))
|
||||
|
||||
|
||||
@router.post("/melt")
|
||||
async def melt(payload: MeltRequest):
|
||||
async def melt(payload: MeltRequest) -> GetMeltResponse:
|
||||
"""
|
||||
Requests tokens to be destroyed and sent out via Lightning.
|
||||
"""
|
||||
@@ -80,13 +90,13 @@ async def melt(payload: MeltRequest):
|
||||
|
||||
|
||||
@router.post("/check")
|
||||
async def check_spendable(payload: CheckRequest):
|
||||
async def check_spendable(payload: CheckRequest) -> Dict[int, bool]:
|
||||
"""Check whether a secret has been spent already or not."""
|
||||
return await ledger.check_spendable(payload.proofs)
|
||||
|
||||
|
||||
@router.post("/checkfees")
|
||||
async def check_fees(payload: CheckFeesRequest):
|
||||
async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse:
|
||||
"""
|
||||
Responds with the fees necessary to pay a Lightning invoice.
|
||||
Used by wallets for figuring out the fees they need to supply.
|
||||
@@ -97,20 +107,26 @@ async def check_fees(payload: CheckFeesRequest):
|
||||
|
||||
|
||||
@router.post("/split")
|
||||
async def split(payload: SplitRequest):
|
||||
async def split(
|
||||
payload: SplitRequest,
|
||||
) -> Union[CashuError, PostSplitResponse]:
|
||||
"""
|
||||
Requetst a set of tokens with amount "total" to be split into two
|
||||
newly minted sets with amount "split" and "total-split".
|
||||
"""
|
||||
proofs = payload.proofs
|
||||
amount = payload.amount
|
||||
|
||||
# NOTE: backwards compatibility with clients < v0.2.2
|
||||
outputs = payload.outputs.blinded_messages if payload.outputs else None
|
||||
|
||||
assert outputs, Exception("no outputs provided.")
|
||||
try:
|
||||
split_return = await ledger.split(proofs, amount, outputs)
|
||||
except Exception as exc:
|
||||
return CashuError(error=str(exc))
|
||||
if not split_return:
|
||||
return {"error": "there was a problem with the split."}
|
||||
return CashuError(error="there was an error with the split")
|
||||
frst_promises, scnd_promises = split_return
|
||||
resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises)
|
||||
return resp
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
# startup routine of the standalone app. These are the steps that need
|
||||
# to be taken by external apps importing the cashu mint.
|
||||
|
||||
import asyncio
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.settings import CASHU_DIR, LIGHTNING
|
||||
from cashu.lightning import WALLET
|
||||
from cashu.core.settings import CASHU_DIR, LIGHTNING, MINT_PRIVATE_KEY
|
||||
from cashu.lightning.lnbits import LNbitsWallet
|
||||
from cashu.mint import migrations
|
||||
from cashu.mint.ledger import Ledger
|
||||
|
||||
from . import ledger
|
||||
ledger = Ledger(
|
||||
db=Database("mint", "data/mint"),
|
||||
seed=MINT_PRIVATE_KEY,
|
||||
# seed="asd",
|
||||
derivation_path="0/0/0/0",
|
||||
lightning=LNbitsWallet() if LIGHTNING else None,
|
||||
)
|
||||
|
||||
|
||||
async def load_ledger():
|
||||
async def start_mint_init():
|
||||
|
||||
await migrate_databases(ledger.db, migrations)
|
||||
# await asyncio.wait([m001_initial(ledger.db)])
|
||||
await ledger.load_used_proofs()
|
||||
await ledger.init_keysets()
|
||||
|
||||
if LIGHTNING:
|
||||
error_message, balance = await WALLET.status()
|
||||
error_message, balance = await ledger.lightning.status()
|
||||
if error_message:
|
||||
logger.warning(
|
||||
f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
|
||||
f"The backend for {ledger.lightning.__class__.__name__} isn't working properly: '{error_message}'",
|
||||
RuntimeWarning,
|
||||
)
|
||||
logger.info(f"Lightning balance: {balance} sat")
|
||||
|
||||
1
cashu/nostr
Submodule
1
cashu/nostr
Submodule
Submodule cashu/nostr added at d7fb45f6a1
1609
cashu/tor/LICENCE_tor
Executable file
1609
cashu/tor/LICENCE_tor
Executable file
File diff suppressed because it is too large
Load Diff
BIN
cashu/wallet/wallet_live/.DS_Store → cashu/tor/bundle/.DS_Store
vendored
Normal file → Executable file
BIN
cashu/wallet/wallet_live/.DS_Store → cashu/tor/bundle/.DS_Store
vendored
Normal file → Executable file
Binary file not shown.
389
cashu/tor/bundle/linux/LICENSE
Executable file
389
cashu/tor/bundle/linux/LICENSE
Executable file
@@ -0,0 +1,389 @@
|
||||
This file contains the license for Tor,
|
||||
a free software project to provide anonymity on the Internet.
|
||||
|
||||
It also lists the licenses for other components used by Tor.
|
||||
|
||||
For more information about Tor, see https://www.torproject.org/.
|
||||
|
||||
If you got this file as a part of a larger bundle,
|
||||
there may be other license terms that you should be aware of.
|
||||
|
||||
===============================================================================
|
||||
Tor is distributed under the "3-clause BSD" license, a commonly used
|
||||
software license that means Tor is both free software and open source:
|
||||
|
||||
Copyright (c) 2001-2004, Roger Dingledine
|
||||
Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson
|
||||
Copyright (c) 2007-2019, The Tor Project, Inc.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
* Neither the names of the copyright owners nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
===============================================================================
|
||||
src/ext/strlcat.c and src/ext/strlcpy.c by Todd C. Miller are licensed
|
||||
under the following license:
|
||||
|
||||
* Copyright (c) 1998 Todd C. Miller <Todd.Miller@courtesan.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 3. The name of the author may not be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
* THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
===============================================================================
|
||||
src/ext/tor_queue.h is licensed under the following license:
|
||||
|
||||
* Copyright (c) 1991, 1993
|
||||
* The Regents of the University of California. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of the University nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
|
||||
===============================================================================
|
||||
src/ext/csiphash.c is licensed under the following license:
|
||||
|
||||
Copyright (c) 2013 Marek Majkowski <marek@popcount.org>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
===============================================================================
|
||||
Trunnel is distributed under this license:
|
||||
|
||||
Copyright 2014 The Tor Project, Inc.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
* Neither the names of the copyright owners nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
===============================================================================
|
||||
getdelim.c is distributed under this license:
|
||||
|
||||
Copyright (c) 2011 The NetBSD Foundation, Inc.
|
||||
All rights reserved.
|
||||
|
||||
This code is derived from software contributed to The NetBSD Foundation
|
||||
by Christos Zoulas.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
|
||||
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
===============================================================================
|
||||
src/config/geoip and src/config/geoip6:
|
||||
|
||||
These files are based on the IPFire Location Database. For more
|
||||
information, see https://location.ipfire.org/.
|
||||
|
||||
The data is distributed under a creative commons "BY-SA 4.0" license.
|
||||
|
||||
Find the full license terms at:
|
||||
https://creativecommons.org/licenses/by-sa/4.0/
|
||||
|
||||
===============================================================================
|
||||
m4/pc_from_ucontext.m4 is available under the following license. Note that
|
||||
it is *not* built into the Tor software.
|
||||
|
||||
Copyright (c) 2005, Google Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
===============================================================================
|
||||
m4/pkg.m4 is available under the following license. Note that
|
||||
it is *not* built into the Tor software.
|
||||
|
||||
pkg.m4 - Macros to locate and utilise pkg-config. -*- Autoconf -*-
|
||||
serial 1 (pkg-config-0.24)
|
||||
|
||||
Copyright © 2004 Scott James Remnant <scott@netsplit.com>.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
|
||||
As a special exception to the GNU General Public License, if you
|
||||
distribute this file as part of a program that contains a
|
||||
configuration script generated by Autoconf, you may include it under
|
||||
the same distribution terms that you use for the rest of that program.
|
||||
===============================================================================
|
||||
src/ext/readpassphrase.[ch] are distributed under this license:
|
||||
|
||||
Copyright (c) 2000-2002, 2007 Todd C. Miller <Todd.Miller@courtesan.com>
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
Sponsored in part by the Defense Advanced Research Projects
|
||||
Agency (DARPA) and Air Force Research Laboratory, Air Force
|
||||
Materiel Command, USAF, under agreement number F39502-99-1-0512.
|
||||
|
||||
===============================================================================
|
||||
src/ext/mulodi4.c is distributed under this license:
|
||||
|
||||
=========================================================================
|
||||
compiler_rt License
|
||||
=========================================================================
|
||||
|
||||
The compiler_rt library is dual licensed under both the
|
||||
University of Illinois "BSD-Like" license and the MIT license.
|
||||
As a user of this code you may choose to use it under either
|
||||
license. As a contributor, you agree to allow your code to be
|
||||
used under both.
|
||||
|
||||
Full text of the relevant licenses is included below.
|
||||
|
||||
=========================================================================
|
||||
|
||||
University of Illinois/NCSA
|
||||
Open Source License
|
||||
|
||||
Copyright (c) 2009-2016 by the contributors listed in CREDITS.TXT
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Developed by:
|
||||
|
||||
LLVM Team
|
||||
|
||||
University of Illinois at Urbana-Champaign
|
||||
|
||||
http://llvm.org
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal with the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
* Redistributions of source code must retain the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimers.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimers in the documentation and/or other materials
|
||||
provided with the distribution.
|
||||
|
||||
* Neither the names of the LLVM Team, University of Illinois
|
||||
at Urbana-Champaign, nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this
|
||||
Software without specific prior written permission.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS WITH THE SOFTWARE.
|
||||
|
||||
=========================================================================
|
||||
|
||||
Copyright (c) 2009-2015 by the contributors listed in CREDITS.TXT
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
=========================================================================
|
||||
Copyrights and Licenses for Third Party Software Distributed with LLVM:
|
||||
=========================================================================
|
||||
|
||||
The LLVM software contains code written by third parties. Such
|
||||
software will have its own individual LICENSE.TXT file in the
|
||||
directory in which it appears. This file will describe the
|
||||
copyrights, license, and restrictions which apply to that code.
|
||||
|
||||
The disclaimer of warranty in the University of Illinois Open
|
||||
Source License applies to all code in the LLVM Distribution, and
|
||||
nothing in any of the other licenses gives permission to use the
|
||||
names of the LLVM Team or the University of Illinois to endorse
|
||||
or promote products derived from this Software.
|
||||
|
||||
===============================================================================
|
||||
If you got Tor as a static binary with OpenSSL included, then you should know:
|
||||
"This product includes software developed by the OpenSSL Project
|
||||
for use in the OpenSSL Toolkit (http://www.openssl.org/)"
|
||||
===============================================================================
|
||||
BIN
cashu/tor/bundle/linux/libcrypto.so.1.1
Executable file
BIN
cashu/tor/bundle/linux/libcrypto.so.1.1
Executable file
Binary file not shown.
BIN
cashu/tor/bundle/linux/libevent-2.1.so.7
Executable file
BIN
cashu/tor/bundle/linux/libevent-2.1.so.7
Executable file
Binary file not shown.
BIN
cashu/tor/bundle/linux/libssl.so.1.1
Executable file
BIN
cashu/tor/bundle/linux/libssl.so.1.1
Executable file
Binary file not shown.
BIN
cashu/tor/bundle/linux/libstdc++/libstdc++.so.6
Executable file
BIN
cashu/tor/bundle/linux/libstdc++/libstdc++.so.6
Executable file
Binary file not shown.
BIN
cashu/tor/bundle/linux/tor
Executable file
BIN
cashu/tor/bundle/linux/tor
Executable file
Binary file not shown.
BIN
cashu/tor/bundle/mac/libevent-2.1.7.dylib
Executable file
BIN
cashu/tor/bundle/mac/libevent-2.1.7.dylib
Executable file
Binary file not shown.
BIN
cashu/tor/bundle/mac/tor
Executable file
BIN
cashu/tor/bundle/mac/tor
Executable file
Binary file not shown.
34
cashu/tor/timeout.py
Executable file
34
cashu/tor/timeout.py
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def main():
|
||||
assert len(sys.argv) > 2, "Usage: timeout.py [seconds] [command...]"
|
||||
# cmd = " ".join(sys.argv[2:]) # for with shell=True
|
||||
cmd = sys.argv[2:]
|
||||
timeout = int(sys.argv[1])
|
||||
assert timeout > 0, "timeout (in seconds) must be a positive integer."
|
||||
start_time = time.time()
|
||||
|
||||
pro = subprocess.Popen(cmd, shell=False)
|
||||
|
||||
while time.time() < start_time + timeout + 1:
|
||||
time.sleep(1)
|
||||
pro.terminate()
|
||||
pro.wait()
|
||||
pro.kill()
|
||||
pro.wait()
|
||||
|
||||
# we kill the child processes as well (tor.py and tor) just to be sure
|
||||
os.kill(pro.pid + 1, 15)
|
||||
os.kill(pro.pid + 1, 9)
|
||||
|
||||
os.kill(pro.pid + 2, 15)
|
||||
os.kill(pro.pid + 2, 9)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
177
cashu/tor/tor.py
Executable file
177
cashu/tor/tor.py
Executable file
@@ -0,0 +1,177 @@
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class TorProxy:
|
||||
def __init__(self, timeout=False):
|
||||
self.base_path = pathlib.Path(__file__).parent.resolve()
|
||||
self.platform = platform.system()
|
||||
self.timeout = 60 * 60 if timeout else 0 # seconds
|
||||
self.tor_proc = None
|
||||
self.pid_file = os.path.join(self.base_path, "tor.pid")
|
||||
self.tor_pid = None
|
||||
self.startup_finished = True
|
||||
self.tor_running = self.is_running()
|
||||
|
||||
@classmethod
|
||||
def check_platform(cls):
|
||||
if platform.system() == "Linux":
|
||||
if platform.machine() != "x86_64":
|
||||
logger.debug("Builtin Tor not supported on this platform.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def log_status(self):
|
||||
logger.debug(f"Tor binary path: {self.tor_path()}")
|
||||
logger.debug(f"Tor config path: {self.tor_config_path()}")
|
||||
logger.debug(f"Tor running: {self.tor_running}")
|
||||
logger.debug(
|
||||
f"Tor port open: {self.is_port_open()}",
|
||||
)
|
||||
logger.debug(f"Tor PID in tor.pid: {self.read_pid()}")
|
||||
logger.debug(f"Tor PID running: {self.signal_pid(self.read_pid())}")
|
||||
|
||||
def run_daemon(self, verbose=False):
|
||||
if not self.check_platform() or self.tor_running:
|
||||
return
|
||||
self.log_status()
|
||||
logger.debug("Starting Tor")
|
||||
cmd = [f"{self.tor_path()}", "--defaults-torrc", f"{self.tor_config_path()}"]
|
||||
if self.timeout:
|
||||
logger.debug(f"Starting tor with timeout {self.timeout}s")
|
||||
cmd = [
|
||||
sys.executable,
|
||||
os.path.join(self.base_path, "timeout.py"),
|
||||
f"{self.timeout}",
|
||||
] + cmd
|
||||
env = dict(os.environ)
|
||||
if platform.system() == "Linux":
|
||||
env["LD_LIBRARY_PATH"] = os.path.dirname(self.tor_path())
|
||||
elif platform.system() == "Darwin":
|
||||
env["DYLD_LIBRARY_PATH"] = os.path.dirname(self.tor_path())
|
||||
self.tor_proc = subprocess.Popen(
|
||||
cmd,
|
||||
env=env,
|
||||
shell=False,
|
||||
close_fds=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=True,
|
||||
)
|
||||
logger.debug("Running tor daemon with pid {}".format(self.tor_proc.pid))
|
||||
with open(self.pid_file, "w", encoding="utf-8") as f:
|
||||
f.write(str(self.tor_proc.pid))
|
||||
|
||||
self.wait_until_startup(verbose=verbose)
|
||||
|
||||
def stop_daemon(self, pid=None):
|
||||
pid = pid or self.tor_proc.pid if self.tor_proc else None
|
||||
if self.tor_proc and pid:
|
||||
self.signal_pid(pid, 15) # sigterm
|
||||
time.sleep(5)
|
||||
self.signal_pid(pid, 9) # sigkill
|
||||
|
||||
if os.path.exists(self.pid_file):
|
||||
os.remove(self.pid_file)
|
||||
|
||||
def tor_path(self):
|
||||
PATHS = {
|
||||
"Windows": os.path.join(self.base_path, "bundle", "win", "Tor", "tor.exe"),
|
||||
"Linux": os.path.join(self.base_path, "bundle", "linux", "tor"),
|
||||
"Darwin": os.path.join(self.base_path, "bundle", "mac", "tor"),
|
||||
}
|
||||
# make sure that file has correct permissions
|
||||
try:
|
||||
logger.debug(f"Setting permissions of {PATHS[platform.system()]} to 755")
|
||||
os.chmod(PATHS[platform.system()], 0o755)
|
||||
except:
|
||||
logger.debug("Exception: could not set permissions of Tor binary")
|
||||
return PATHS[platform.system()]
|
||||
|
||||
def tor_config_path(self):
|
||||
return os.path.join(self.base_path, "torrc")
|
||||
|
||||
def is_running(self):
|
||||
# another tor proxy is running
|
||||
if not self.is_port_open():
|
||||
return False
|
||||
# our tor proxy running from a previous session
|
||||
if self.signal_pid(self.read_pid()):
|
||||
return True
|
||||
# current attached process running
|
||||
return self.tor_proc and self.tor_proc.poll() is None
|
||||
|
||||
def wait_until_startup(self, verbose=False):
|
||||
if not self.check_platform():
|
||||
return
|
||||
if self.is_port_open():
|
||||
return
|
||||
if self.tor_proc is None:
|
||||
raise Exception("Tor proxy not attached.")
|
||||
if not self.tor_proc.stdout:
|
||||
raise Exception("could not get tor stdout.")
|
||||
if verbose:
|
||||
print("Starting Tor...", end="", flush=True)
|
||||
for line in self.tor_proc.stdout:
|
||||
# print(line)
|
||||
if verbose:
|
||||
print(".", end="", flush=True)
|
||||
if "Bootstrapped 100%" in str(line):
|
||||
if verbose:
|
||||
print("done", flush=True)
|
||||
break
|
||||
# tor is ready
|
||||
self.startup_finished = True
|
||||
return
|
||||
|
||||
def is_port_open(self):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
location = ("127.0.0.1", 9050)
|
||||
try:
|
||||
s.connect(location)
|
||||
s.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def read_pid(self):
|
||||
if not os.path.isfile(self.pid_file):
|
||||
return None
|
||||
with open(self.pid_file, "r") as f:
|
||||
pid = f.readlines()
|
||||
# check if pid is valid
|
||||
if len(pid) == 0 or not int(pid[0]) > 0:
|
||||
return None
|
||||
return pid[0]
|
||||
|
||||
def signal_pid(self, pid, signal=0):
|
||||
"""
|
||||
Checks whether a process with pid is running (signal 0 is not a kill signal!)
|
||||
or stops (signal 15) or kills it (signal 9).
|
||||
"""
|
||||
if not pid:
|
||||
return False
|
||||
if not int(pid) > 0:
|
||||
return False
|
||||
pid = int(pid)
|
||||
try:
|
||||
os.kill(pid, signal)
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
tor = TorProxy(timeout=True)
|
||||
tor.run_daemon(verbose=True)
|
||||
# time.sleep(5)
|
||||
# logger.debug("Killing Tor")
|
||||
# tor.stop_daemon()
|
||||
254
cashu/tor/torrc
Executable file
254
cashu/tor/torrc
Executable file
@@ -0,0 +1,254 @@
|
||||
## Configuration file for a typical Tor user
|
||||
## Last updated 28 February 2019 for Tor 0.3.5.1-alpha.
|
||||
## (may or may not work for much older or much newer versions of Tor.)
|
||||
##
|
||||
## Lines that begin with "## " try to explain what's going on. Lines
|
||||
## that begin with just "#" are disabled commands: you can enable them
|
||||
## by removing the "#" symbol.
|
||||
##
|
||||
## See 'man tor', or https://www.torproject.org/docs/tor-manual.html,
|
||||
## for more options you can use in this file.
|
||||
##
|
||||
## Tor will look for this file in various places based on your platform:
|
||||
## https://www.torproject.org/docs/faq#torrc
|
||||
|
||||
## Tor opens a SOCKS proxy on port 9050 by default -- even if you don't
|
||||
## configure one below. Set "SOCKSPort 0" if you plan to run Tor only
|
||||
## as a relay, and not make any local application connections yourself.
|
||||
SOCKSPort 9050 # Default: Bind to localhost:9050 for local connections.
|
||||
#SOCKSPort 192.168.0.1:9100 # Bind to this address:port too.
|
||||
|
||||
## Entry policies to allow/deny SOCKS requests based on IP address.
|
||||
## First entry that matches wins. If no SOCKSPolicy is set, we accept
|
||||
## all (and only) requests that reach a SOCKSPort. Untrusted users who
|
||||
## can access your SOCKSPort may be able to learn about the connections
|
||||
## you make.
|
||||
#SOCKSPolicy accept 192.168.0.0/16
|
||||
#SOCKSPolicy accept6 FC00::/7
|
||||
#SOCKSPolicy reject *
|
||||
SOCKSPolicy accept 127.0.0.1
|
||||
|
||||
## Logs go to stdout at level "notice" unless redirected by something
|
||||
## else, like one of the below lines. You can have as many Log lines as
|
||||
## you want.
|
||||
##
|
||||
## We advise using "notice" in most cases, since anything more verbose
|
||||
## may provide sensitive information to an attacker who obtains the logs.
|
||||
##
|
||||
## Send all messages of level 'notice' or higher to /usr/local/var/log/tor/notices.log
|
||||
#Log notice file /usr/local/var/log/tor/notices.log
|
||||
## Send every possible message to /usr/local/var/log/tor/debug.log
|
||||
#Log debug file /usr/local/var/log/tor/debug.log
|
||||
## Use the system log instead of Tor's logfiles
|
||||
#Log notice syslog
|
||||
## To send all messages to stderr:
|
||||
#Log debug stderr
|
||||
|
||||
## Uncomment this to start the process in the background... or use
|
||||
## --runasdaemon 1 on the command line. This is ignored on Windows;
|
||||
## see the FAQ entry if you want Tor to run as an NT service.
|
||||
#RunAsDaemon 1
|
||||
|
||||
## The directory for keeping all the keys/etc. By default, we store
|
||||
## things in $HOME/.tor on Unix, and in Application Data\tor on Windows.
|
||||
#DataDirectory /usr/local/var/lib/tor
|
||||
|
||||
## The port on which Tor will listen for local connections from Tor
|
||||
## controller applications, as documented in control-spec.txt.
|
||||
ControlPort 9051
|
||||
## If you enable the controlport, be sure to enable one of these
|
||||
## authentication methods, to prevent attackers from accessing it.
|
||||
HashedControlPassword 16:3F85DAF2A2A34032603235343E19ABBE3CB6BF03F1443984F21EEE749F
|
||||
#CookieAuthentication 1
|
||||
|
||||
############### This section is just for location-hidden services ###
|
||||
|
||||
## Once you have configured a hidden service, you can look at the
|
||||
## contents of the file ".../hidden_service/hostname" for the address
|
||||
## to tell people.
|
||||
##
|
||||
## HiddenServicePort x y:z says to redirect requests on port x to the
|
||||
## address y:z.
|
||||
|
||||
#HiddenServiceDir /usr/local/var/lib/tor/hidden_service/
|
||||
#HiddenServicePort 80 127.0.0.1:80
|
||||
|
||||
#HiddenServiceDir /usr/local/var/lib/tor/other_hidden_service/
|
||||
#HiddenServicePort 80 127.0.0.1:80
|
||||
#HiddenServicePort 22 127.0.0.1:22
|
||||
|
||||
################ This section is just for relays #####################
|
||||
#
|
||||
## See https://www.torproject.org/docs/tor-doc-relay for details.
|
||||
|
||||
## Required: what port to advertise for incoming Tor connections.
|
||||
#ORPort 9001
|
||||
## If you want to listen on a port other than the one advertised in
|
||||
## ORPort (e.g. to advertise 443 but bind to 9090), you can do it as
|
||||
## follows. You'll need to do ipchains or other port forwarding
|
||||
## yourself to make this work.
|
||||
#ORPort 443 NoListen
|
||||
#ORPort 127.0.0.1:9090 NoAdvertise
|
||||
## If you want to listen on IPv6 your numeric address must be explictly
|
||||
## between square brackets as follows. You must also listen on IPv4.
|
||||
#ORPort [2001:DB8::1]:9050
|
||||
|
||||
## The IP address or full DNS name for incoming connections to your
|
||||
## relay. Leave commented out and Tor will guess.
|
||||
Address 1.1.1.1
|
||||
|
||||
## If you have multiple network interfaces, you can specify one for
|
||||
## outgoing traffic to use.
|
||||
## OutboundBindAddressExit will be used for all exit traffic, while
|
||||
## OutboundBindAddressOR will be used for all OR and Dir connections
|
||||
## (DNS connections ignore OutboundBindAddress).
|
||||
## If you do not wish to differentiate, use OutboundBindAddress to
|
||||
## specify the same address for both in a single line.
|
||||
#OutboundBindAddressExit 10.0.0.4
|
||||
#OutboundBindAddressOR 10.0.0.5
|
||||
|
||||
## A handle for your relay, so people don't have to refer to it by key.
|
||||
## Nicknames must be between 1 and 19 characters inclusive, and must
|
||||
## contain only the characters [a-zA-Z0-9].
|
||||
## If not set, "Unnamed" will be used.
|
||||
#Nickname ididnteditheconfig
|
||||
|
||||
## Define these to limit how much relayed traffic you will allow. Your
|
||||
## own traffic is still unthrottled. Note that RelayBandwidthRate must
|
||||
## be at least 75 kilobytes per second.
|
||||
## Note that units for these config options are bytes (per second), not
|
||||
## bits (per second), and that prefixes are binary prefixes, i.e. 2^10,
|
||||
## 2^20, etc.
|
||||
#RelayBandwidthRate 100 KBytes # Throttle traffic to 100KB/s (800Kbps)
|
||||
#RelayBandwidthBurst 200 KBytes # But allow bursts up to 200KB (1600Kb)
|
||||
|
||||
## Use these to restrict the maximum traffic per day, week, or month.
|
||||
## Note that this threshold applies separately to sent and received bytes,
|
||||
## not to their sum: setting "40 GB" may allow up to 80 GB total before
|
||||
## hibernating.
|
||||
##
|
||||
## Set a maximum of 40 gigabytes each way per period.
|
||||
#AccountingMax 40 GBytes
|
||||
## Each period starts daily at midnight (AccountingMax is per day)
|
||||
#AccountingStart day 00:00
|
||||
## Each period starts on the 3rd of the month at 15:00 (AccountingMax
|
||||
## is per month)
|
||||
#AccountingStart month 3 15:00
|
||||
|
||||
## Administrative contact information for this relay or bridge. This line
|
||||
## can be used to contact you if your relay or bridge is misconfigured or
|
||||
## something else goes wrong. Note that we archive and publish all
|
||||
## descriptors containing these lines and that Google indexes them, so
|
||||
## spammers might also collect them. You may want to obscure the fact that
|
||||
## it's an email address and/or generate a new address for this purpose.
|
||||
##
|
||||
## If you are running multiple relays, you MUST set this option.
|
||||
##
|
||||
#ContactInfo Random Person <nobody AT example dot com>
|
||||
## You might also include your PGP or GPG fingerprint if you have one:
|
||||
#ContactInfo 0xFFFFFFFF Random Person <nobody AT example dot com>
|
||||
|
||||
## Uncomment this to mirror directory information for others. Please do
|
||||
## if you have enough bandwidth.
|
||||
#DirPort 9030 # what port to advertise for directory connections
|
||||
## If you want to listen on a port other than the one advertised in
|
||||
## DirPort (e.g. to advertise 80 but bind to 9091), you can do it as
|
||||
## follows. below too. You'll need to do ipchains or other port
|
||||
## forwarding yourself to make this work.
|
||||
#DirPort 80 NoListen
|
||||
#DirPort 127.0.0.1:9091 NoAdvertise
|
||||
## Uncomment to return an arbitrary blob of html on your DirPort. Now you
|
||||
## can explain what Tor is if anybody wonders why your IP address is
|
||||
## contacting them. See contrib/tor-exit-notice.html in Tor's source
|
||||
## distribution for a sample.
|
||||
#DirPortFrontPage /usr/local/etc/tor/tor-exit-notice.html
|
||||
|
||||
## Uncomment this if you run more than one Tor relay, and add the identity
|
||||
## key fingerprint of each Tor relay you control, even if they're on
|
||||
## different networks. You declare it here so Tor clients can avoid
|
||||
## using more than one of your relays in a single circuit. See
|
||||
## https://www.torproject.org/docs/faq#MultipleRelays
|
||||
## However, you should never include a bridge's fingerprint here, as it would
|
||||
## break its concealability and potentially reveal its IP/TCP address.
|
||||
##
|
||||
## If you are running multiple relays, you MUST set this option.
|
||||
##
|
||||
## Note: do not use MyFamily on bridge relays.
|
||||
#MyFamily $keyid,$keyid,...
|
||||
|
||||
## Uncomment this if you want your relay to be an exit, with the default
|
||||
## exit policy (or whatever exit policy you set below).
|
||||
## (If ReducedExitPolicy, ExitPolicy, or IPv6Exit are set, relays are exits.
|
||||
## If none of these options are set, relays are non-exits.)
|
||||
#ExitRelay 1
|
||||
|
||||
## Uncomment this if you want your relay to allow IPv6 exit traffic.
|
||||
## (Relays do not allow any exit traffic by default.)
|
||||
#IPv6Exit 1
|
||||
|
||||
## Uncomment this if you want your relay to be an exit, with a reduced set
|
||||
## of exit ports.
|
||||
#ReducedExitPolicy 1
|
||||
|
||||
## Uncomment these lines if you want your relay to be an exit, with the
|
||||
## specified set of exit IPs and ports.
|
||||
##
|
||||
## A comma-separated list of exit policies. They're considered first
|
||||
## to last, and the first match wins.
|
||||
##
|
||||
## If you want to allow the same ports on IPv4 and IPv6, write your rules
|
||||
## using accept/reject *. If you want to allow different ports on IPv4 and
|
||||
## IPv6, write your IPv6 rules using accept6/reject6 *6, and your IPv4 rules
|
||||
## using accept/reject *4.
|
||||
##
|
||||
## If you want to _replace_ the default exit policy, end this with either a
|
||||
## reject *:* or an accept *:*. Otherwise, you're _augmenting_ (prepending to)
|
||||
## the default exit policy. Leave commented to just use the default, which is
|
||||
## described in the man page or at
|
||||
## https://www.torproject.org/documentation.html
|
||||
##
|
||||
## Look at https://www.torproject.org/faq-abuse.html#TypicalAbuses
|
||||
## for issues you might encounter if you use the default exit policy.
|
||||
##
|
||||
## If certain IPs and ports are blocked externally, e.g. by your firewall,
|
||||
## you should update your exit policy to reflect this -- otherwise Tor
|
||||
## users will be told that those destinations are down.
|
||||
##
|
||||
## For security, by default Tor rejects connections to private (local)
|
||||
## networks, including to the configured primary public IPv4 and IPv6 addresses,
|
||||
## and any public IPv4 and IPv6 addresses on any interface on the relay.
|
||||
## See the man page entry for ExitPolicyRejectPrivate if you want to allow
|
||||
## "exit enclaving".
|
||||
##
|
||||
#ExitPolicy accept *:6660-6667,reject *:* # allow irc ports on IPv4 and IPv6 but no more
|
||||
#ExitPolicy accept *:119 # accept nntp ports on IPv4 and IPv6 as well as default exit policy
|
||||
#ExitPolicy accept *4:119 # accept nntp ports on IPv4 only as well as default exit policy
|
||||
#ExitPolicy accept6 *6:119 # accept nntp ports on IPv6 only as well as default exit policy
|
||||
#ExitPolicy reject *:* # no exits allowed
|
||||
|
||||
## Bridge relays (or "bridges") are Tor relays that aren't listed in the
|
||||
## main directory. Since there is no complete public list of them, even an
|
||||
## ISP that filters connections to all the known Tor relays probably
|
||||
## won't be able to block all the bridges. Also, websites won't treat you
|
||||
## differently because they won't know you're running Tor. If you can
|
||||
## be a real relay, please do; but if not, be a bridge!
|
||||
##
|
||||
## Warning: when running your Tor as a bridge, make sure than MyFamily is
|
||||
## NOT configured.
|
||||
#BridgeRelay 1
|
||||
## By default, Tor will advertise your bridge to users through various
|
||||
## mechanisms like https://bridges.torproject.org/. If you want to run
|
||||
## a private bridge, for example because you'll give out your bridge
|
||||
## address manually to your friends, uncomment this line:
|
||||
#PublishServerDescriptor 0
|
||||
|
||||
## Configuration options can be imported from files or folders using the %include
|
||||
## option with the value being a path. If the path is a file, the options from the
|
||||
## file will be parsed as if they were written where the %include option is. If
|
||||
## the path is a folder, all files on that folder will be parsed following lexical
|
||||
## order. Files starting with a dot are ignored. Files on subfolders are ignored.
|
||||
## The %include option can be used recursively.
|
||||
#%include /etc/torrc.d/
|
||||
#%include /etc/torrc.custom
|
||||
|
||||
#HTTPTunnelPort 8118
|
||||
@@ -1,3 +1,3 @@
|
||||
import sys
|
||||
|
||||
sys.tracebacklimit = None
|
||||
sys.tracebacklimit = None # type: ignore
|
||||
|
||||
492
cashu/wallet/cli.py
Executable file → Normal file
492
cashu/wallet/cli.py
Executable file → Normal file
@@ -3,30 +3,60 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from itertools import groupby
|
||||
from operator import itemgetter
|
||||
from os import listdir
|
||||
from os.path import isdir, join
|
||||
from typing import Dict, List
|
||||
|
||||
import click
|
||||
from loguru import logger
|
||||
|
||||
import cashu.core.bolt11 as bolt11
|
||||
from cashu.core.base import Proof
|
||||
from cashu.core.bolt11 import Invoice, decode
|
||||
from cashu.core.helpers import fee_reserve, sum_proofs
|
||||
from cashu.core.helpers import sum_proofs
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.settings import CASHU_DIR, DEBUG, ENV_FILE, LIGHTNING, MINT_URL, VERSION
|
||||
from cashu.core.settings import (
|
||||
CASHU_DIR,
|
||||
DEBUG,
|
||||
ENV_FILE,
|
||||
LIGHTNING,
|
||||
MINT_URL,
|
||||
NOSTR_PRIVATE_KEY,
|
||||
NOSTR_RELAYS,
|
||||
SOCKS_HOST,
|
||||
SOCKS_PORT,
|
||||
TOR,
|
||||
VERSION,
|
||||
)
|
||||
from cashu.nostr.nostr.client.client import NostrClient
|
||||
from cashu.nostr.nostr.event import Event
|
||||
from cashu.nostr.nostr.key import PublicKey
|
||||
from cashu.tor.tor import TorProxy
|
||||
from cashu.wallet import migrations
|
||||
from cashu.wallet.crud import get_reserved_proofs, get_unused_locks
|
||||
from cashu.wallet.crud import (
|
||||
get_keyset,
|
||||
get_lightning_invoices,
|
||||
get_reserved_proofs,
|
||||
get_unused_locks,
|
||||
)
|
||||
from cashu.wallet.wallet import Wallet as Wallet
|
||||
|
||||
from .cli_helpers import (
|
||||
get_mint_wallet,
|
||||
print_mint_balances,
|
||||
proofs_to_token,
|
||||
redeem_multimint,
|
||||
token_from_lnbits_link,
|
||||
verify_mints,
|
||||
)
|
||||
|
||||
|
||||
async def init_wallet(wallet: Wallet):
|
||||
"""Performs migrations and loads proofs from db."""
|
||||
@@ -52,9 +82,24 @@ class NaturalOrderGroup(click.Group):
|
||||
)
|
||||
@click.pass_context
|
||||
def cli(ctx, host: str, walletname: str):
|
||||
if TOR and not TorProxy().check_platform():
|
||||
error_str = "Your settings say TOR=true but the built-in Tor bundle is not supported on your system. You have two options: Either install Tor manually and set TOR=FALSE and SOCKS_HOST=localhost and SOCKS_PORT=9050 in your Cashu config (recommended). Or turn off Tor by setting TOR=false (not recommended). Cashu will not work until you edit your config file accordingly."
|
||||
error_str += "\n\n"
|
||||
if ENV_FILE:
|
||||
error_str += f"Edit your Cashu config file here: {ENV_FILE}"
|
||||
env_path = ENV_FILE
|
||||
else:
|
||||
error_str += (
|
||||
f"Ceate a new Cashu config file here: {os.path.join(CASHU_DIR, '.env')}"
|
||||
)
|
||||
env_path = os.path.join(CASHU_DIR, ".env")
|
||||
error_str += f'\n\nYou can turn off Tor with this command: echo "TOR=FALSE" >> {env_path}'
|
||||
raise Exception(error_str)
|
||||
|
||||
# configure logger
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="DEBUG" if DEBUG else "INFO")
|
||||
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["HOST"] = host
|
||||
ctx.obj["WALLET_NAME"] = walletname
|
||||
@@ -73,26 +118,55 @@ def coro(f):
|
||||
return wrapper
|
||||
|
||||
|
||||
@cli.command("pay", help="Pay Lightning invoice.")
|
||||
@click.argument("invoice", type=str)
|
||||
@click.option(
|
||||
"--yes", "-y", default=False, is_flag=True, help="Skip confirmation.", type=bool
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def pay(ctx, invoice: str, yes: bool):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
amount, fees = await wallet.get_pay_amount_with_fees(invoice)
|
||||
if not yes:
|
||||
click.confirm(
|
||||
f"Pay {amount - fees} sat ({amount} sat incl. fees)?",
|
||||
abort=True,
|
||||
default=True,
|
||||
)
|
||||
|
||||
print(f"Paying Lightning invoice ...")
|
||||
assert amount > 0, "amount is not positive"
|
||||
if wallet.available_balance < amount:
|
||||
print("Error: Balance too low.")
|
||||
return
|
||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, amount)
|
||||
await wallet.pay_lightning(send_proofs, invoice)
|
||||
wallet.status()
|
||||
|
||||
|
||||
@cli.command("invoice", help="Create Lighting invoice.")
|
||||
@click.argument("amount", type=int)
|
||||
@click.option("--hash", default="", help="Hash of the paid invoice.", type=str)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def mint(ctx, amount: int, hash: str):
|
||||
async def invoice(ctx, amount: int, hash: str):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
if not LIGHTNING:
|
||||
r = await wallet.mint(amount)
|
||||
elif amount and not hash:
|
||||
r = await wallet.request_mint(amount)
|
||||
if "pr" in r:
|
||||
invoice = await wallet.request_mint(amount)
|
||||
if invoice.pr:
|
||||
print(f"Pay invoice to mint {amount} sat:")
|
||||
print("")
|
||||
print(f"Invoice: {r['pr']}")
|
||||
print(f"Invoice: {invoice.pr}")
|
||||
print("")
|
||||
print(
|
||||
f"Execute this command if you abort the check:\ncashu invoice {amount} --hash {r['hash']}"
|
||||
f"Execute this command if you abort the check:\ncashu invoice {amount} --hash {invoice.hash}"
|
||||
)
|
||||
check_until = time.time() + 5 * 60 # check for five minutes
|
||||
print("")
|
||||
@@ -105,7 +179,7 @@ async def mint(ctx, amount: int, hash: str):
|
||||
while time.time() < check_until and not paid:
|
||||
time.sleep(3)
|
||||
try:
|
||||
await wallet.mint(amount, r["hash"])
|
||||
await wallet.mint(amount, invoice.hash)
|
||||
paid = True
|
||||
print(" Invoice paid.")
|
||||
except Exception as e:
|
||||
@@ -119,116 +193,296 @@ async def mint(ctx, amount: int, hash: str):
|
||||
return
|
||||
|
||||
|
||||
@cli.command("pay", help="Pay Lightning invoice.")
|
||||
@click.argument("invoice", type=str)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def pay(ctx, invoice: str):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
decoded_invoice: Invoice = bolt11.decode(invoice)
|
||||
# check if it's an internal payment
|
||||
fees = (await wallet.check_fees(invoice))["fee"]
|
||||
amount = math.ceil(
|
||||
(decoded_invoice.amount_msat + fees * 1000) / 1000
|
||||
) # 1% fee for Lightning
|
||||
print(
|
||||
f"Paying Lightning invoice of {decoded_invoice.amount_msat//1000} sat ({amount} sat incl. fees)"
|
||||
)
|
||||
assert amount > 0, "amount is not positive"
|
||||
if wallet.available_balance < amount:
|
||||
print("Error: Balance too low.")
|
||||
return
|
||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, amount)
|
||||
await wallet.pay_lightning(send_proofs, invoice)
|
||||
wallet.status()
|
||||
|
||||
|
||||
@cli.command("balance", help="Balance.")
|
||||
@click.option(
|
||||
"--verbose",
|
||||
"-v",
|
||||
default=False,
|
||||
is_flag=True,
|
||||
help="Show pending tokens as well.",
|
||||
type=bool,
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def balance(ctx):
|
||||
async def balance(ctx, verbose):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
keyset_balances = wallet.balance_per_keyset()
|
||||
if len(keyset_balances) > 1:
|
||||
print(f"You have balances in {len(keyset_balances)} keysets:")
|
||||
print("")
|
||||
for k, v in keyset_balances.items():
|
||||
print(
|
||||
f"Keyset: {k or 'undefined'} Balance: {v['balance']} sat (available: {v['available']} sat)"
|
||||
)
|
||||
print("")
|
||||
print(
|
||||
f"Balance: {wallet.balance} sat (available: {wallet.available_balance} sat in {len([p for p in wallet.proofs if not p.reserved])} tokens)"
|
||||
if verbose:
|
||||
# show balances per keyset
|
||||
keyset_balances = wallet.balance_per_keyset()
|
||||
if len(keyset_balances) > 1:
|
||||
print(f"You have balances in {len(keyset_balances)} keysets:")
|
||||
print("")
|
||||
for k, v in keyset_balances.items():
|
||||
print(
|
||||
f"Keyset: {k} - Balance: {v['available']} sat (pending: {v['balance']-v['available']} sat)"
|
||||
)
|
||||
print("")
|
||||
|
||||
await print_mint_balances(ctx, wallet)
|
||||
|
||||
if verbose:
|
||||
print(
|
||||
f"Balance: {wallet.available_balance} sat (pending: {wallet.balance-wallet.available_balance} sat) in {len([p for p in wallet.proofs if not p.reserved])} tokens"
|
||||
)
|
||||
else:
|
||||
print(f"Balance: {wallet.available_balance} sat")
|
||||
|
||||
|
||||
async def nostr_send(ctx, amount: int, pubkey: str, verbose: bool, yes: bool):
|
||||
"""
|
||||
Sends tokens via nostr.
|
||||
"""
|
||||
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.hex()}")
|
||||
await asyncio.sleep(1)
|
||||
client.dm(token, PublicKey(bytes.fromhex(pubkey)))
|
||||
print(f"Token sent to {pubkey}")
|
||||
client.close()
|
||||
|
||||
|
||||
@cli.command("send", help="Send coins.")
|
||||
@click.argument("amount", type=int)
|
||||
@click.option("--lock", "-l", default=None, help="Lock coins (P2SH).", type=str)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def send(ctx, amount: int, lock: str):
|
||||
async def send(ctx, amount: int, lock: str, legacy: bool):
|
||||
"""
|
||||
Prints token to send to stdout.
|
||||
"""
|
||||
if lock and len(lock) < 22:
|
||||
print("Error: lock has to be at least 22 characters long.")
|
||||
return
|
||||
p2sh = False
|
||||
if lock and len(lock.split("P2SH:")) == 2:
|
||||
p2sh = True
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, amount, lock)
|
||||
await wallet.set_reserved(send_proofs, reserved=True)
|
||||
coin = await wallet.serialize_proofs(
|
||||
send_proofs, hide_secrets=True if lock and not p2sh else False
|
||||
|
||||
wallet = await get_mint_wallet(ctx)
|
||||
await wallet.load_proofs()
|
||||
|
||||
_, send_proofs = await wallet.split_to_send(
|
||||
wallet.proofs, amount, lock, set_reserved=True
|
||||
)
|
||||
print(coin)
|
||||
token = await wallet.serialize_proofs(
|
||||
send_proofs,
|
||||
include_mints=True,
|
||||
)
|
||||
print(token)
|
||||
|
||||
if legacy:
|
||||
print("")
|
||||
print(
|
||||
"Legacy token without mint information for older clients. This token can only be be received by wallets who use the mint the token is issued from:"
|
||||
)
|
||||
print("")
|
||||
token = await wallet.serialize_proofs(
|
||||
send_proofs,
|
||||
legacy=True,
|
||||
)
|
||||
print(token)
|
||||
|
||||
wallet.status()
|
||||
|
||||
|
||||
@cli.command("receive", help="Receive coins.")
|
||||
@click.argument("coin", type=str)
|
||||
@click.option("--lock", "-l", default=None, help="Unlock coins.", type=str)
|
||||
@cli.command("send", help="Send tokens.")
|
||||
@click.argument("amount", type=int)
|
||||
@click.option(
|
||||
"--nostr",
|
||||
"-n",
|
||||
help="Send to nostr pubkey",
|
||||
type=str,
|
||||
)
|
||||
@click.option("--lock", "-l", default=None, help="Lock tokens (P2SH).", type=str)
|
||||
@click.option(
|
||||
"--legacy",
|
||||
default=False,
|
||||
is_flag=True,
|
||||
help="Print legacy token without mint information.",
|
||||
type=bool,
|
||||
)
|
||||
@click.option(
|
||||
"--verbose",
|
||||
"-v",
|
||||
default=False,
|
||||
is_flag=True,
|
||||
help="Show more information.",
|
||||
type=bool,
|
||||
)
|
||||
@click.option(
|
||||
"--yes", "-y", default=False, is_flag=True, help="Skip confirmation.", type=bool
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def receive(ctx, coin: str, lock: str):
|
||||
async def send_command(
|
||||
ctx,
|
||||
amount: int,
|
||||
nostr: str,
|
||||
lock: str,
|
||||
legacy: bool,
|
||||
verbose: bool,
|
||||
yes: bool,
|
||||
):
|
||||
if nostr is None:
|
||||
await send(ctx, amount, lock, legacy)
|
||||
else:
|
||||
await nostr_send(ctx, amount, nostr, verbose, yes)
|
||||
|
||||
|
||||
async def receive(ctx, token: str, lock: str):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
|
||||
# check for P2SH locks
|
||||
if lock:
|
||||
# load the script and signature of this address from the database
|
||||
assert len(lock.split("P2SH:")) == 2, Exception(
|
||||
"lock has wrong format. Expected P2SH:<address>."
|
||||
)
|
||||
address_split = lock.split("P2SH:")[1]
|
||||
|
||||
p2shscripts = await get_unused_locks(address_split, db=wallet.db)
|
||||
assert len(p2shscripts) == 1, Exception("lock not found.")
|
||||
script = p2shscripts[0].script
|
||||
signature = p2shscripts[0].signature
|
||||
script, signature = p2shscripts[0].script, p2shscripts[0].signature
|
||||
else:
|
||||
script, signature = None, None
|
||||
proofs = [Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(coin))]
|
||||
_, _ = await wallet.redeem(proofs, scnd_script=script, scnd_siganture=signature)
|
||||
|
||||
# deserialize token
|
||||
|
||||
# ----- backwards compatibility -----
|
||||
|
||||
# we support old tokens (< 0.7) without mint information and (W3siaWQ...)
|
||||
# new tokens (>= 0.7) with multiple mint support (eyJ0b2...)
|
||||
try:
|
||||
# backwards compatibility: tokens without mint information
|
||||
# supports tokens of the form W3siaWQiOiJH
|
||||
|
||||
# if it's an lnbits https:// link with a token as an argument, speacial treatment
|
||||
token, url = token_from_lnbits_link(token)
|
||||
|
||||
# assume W3siaWQiOiJH.. token
|
||||
# next line trows an error if the desirialization with the old format doesn't
|
||||
# work and we can assume it's the new format
|
||||
proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))]
|
||||
|
||||
# we take the proofs parsed from the old format token and produce a new format token with it
|
||||
token = await proofs_to_token(wallet, proofs, url)
|
||||
except:
|
||||
pass
|
||||
|
||||
# ----- receive token -----
|
||||
|
||||
# deserialize token
|
||||
dtoken = json.loads(base64.urlsafe_b64decode(token))
|
||||
|
||||
assert "tokens" in dtoken, Exception("no proofs in token")
|
||||
includes_mint_info: bool = "mints" in dtoken and dtoken.get("mints") is not None
|
||||
|
||||
# if there is a `mints` field in the token
|
||||
# we check whether the token has mints that we don't know yet
|
||||
# and ask the user if they want to trust the new mitns
|
||||
if includes_mint_info:
|
||||
# we ask the user to confirm any new mints the tokens may include
|
||||
await verify_mints(ctx, dtoken)
|
||||
# redeem tokens with new wallet instances
|
||||
await redeem_multimint(ctx, dtoken, script, signature)
|
||||
# reload main wallet so the balance updates
|
||||
await wallet.load_proofs()
|
||||
else:
|
||||
# no mint information present, we extract the proofs and use wallet's default mint
|
||||
proofs = [Proof(**p) for p in dtoken["tokens"]]
|
||||
_, _ = await wallet.redeem(proofs, script, signature)
|
||||
|
||||
wallet.status()
|
||||
|
||||
|
||||
@cli.command("burn", help="Burn spent coins.")
|
||||
@click.argument("coin", required=False, type=str)
|
||||
@click.option("--all", "-a", default=False, is_flag=True, help="Burn all spent coins.")
|
||||
async def receive_nostr(ctx, 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(privatekey_hex=NOSTR_PRIVATE_KEY, relays=NOSTR_RELAYS)
|
||||
print(f"Your nostr public key: {client.public_key.hex()}")
|
||||
if verbose:
|
||||
print(f"Your nostr private key (do not share!): {client.private_key.hex()}")
|
||||
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
|
||||
asyncio.run(receive(ctx, decrypted_content, ""))
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
t = threading.Thread(
|
||||
target=client.get_dm,
|
||||
args=(
|
||||
client.public_key,
|
||||
get_token_callback,
|
||||
),
|
||||
name="Nostr DM",
|
||||
)
|
||||
t.start()
|
||||
|
||||
|
||||
@cli.command("receive", help="Receive tokens.")
|
||||
@click.argument("token", type=str, default="")
|
||||
@click.option("--lock", "-l", default=None, help="Unlock tokens.", type=str)
|
||||
@click.option(
|
||||
"--force", "-f", default=False, is_flag=True, help="Force check on all coins."
|
||||
"--nostr", "-n", default=False, is_flag=True, help="Receive tokens via nostr."
|
||||
)
|
||||
@click.option(
|
||||
"--verbose",
|
||||
"-v",
|
||||
help="Display more information.",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
type=bool,
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def burn(ctx, coin: str, all: bool, force: bool):
|
||||
async def receive_cli(ctx, token: str, lock: str, nostr: bool, verbose: bool):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
wallet.status()
|
||||
if token:
|
||||
await receive(ctx, token, lock)
|
||||
elif nostr:
|
||||
await receive_nostr(ctx, verbose)
|
||||
else:
|
||||
print("Error: enter token or use the flag --nostr.")
|
||||
|
||||
|
||||
@cli.command("burn", help="Burn spent tokens.")
|
||||
@click.argument("token", required=False, type=str)
|
||||
@click.option("--all", "-a", default=False, is_flag=True, help="Burn all spent tokens.")
|
||||
@click.option(
|
||||
"--force", "-f", default=False, is_flag=True, help="Force check on all tokens."
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def burn(ctx, token: str, all: bool, force: bool):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
if not (all or coin or force) or (coin and all):
|
||||
if not (all or token or force) or (token and all):
|
||||
print(
|
||||
"Error: enter a coin or use --all to burn all pending coins or --force to check all coins."
|
||||
"Error: enter a token or use --all to burn all pending tokens or --force to check all tokens."
|
||||
)
|
||||
return
|
||||
if all:
|
||||
@@ -239,40 +493,36 @@ async def burn(ctx, coin: str, all: bool, force: bool):
|
||||
proofs = wallet.proofs
|
||||
else:
|
||||
# check only the specified ones
|
||||
proofs = [
|
||||
Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(coin))
|
||||
]
|
||||
proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))]
|
||||
wallet.status()
|
||||
await wallet.invalidate(proofs)
|
||||
wallet.status()
|
||||
|
||||
|
||||
@cli.command("pending", help="Show pending coins.")
|
||||
@cli.command("pending", help="Show pending tokens.")
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def pending(ctx):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
reserved_proofs = await get_reserved_proofs(wallet.db)
|
||||
if len(reserved_proofs):
|
||||
print(f"--------------------------\n")
|
||||
sorted_proofs = sorted(reserved_proofs, key=itemgetter("send_id"))
|
||||
sorted_proofs = sorted(reserved_proofs, key=itemgetter("send_id")) # type: ignore
|
||||
for i, (key, value) in enumerate(
|
||||
groupby(sorted_proofs, key=itemgetter("send_id"))
|
||||
):
|
||||
grouped_proofs = list(value)
|
||||
coin = await wallet.serialize_proofs(grouped_proofs)
|
||||
coin_hidden_secret = await wallet.serialize_proofs(
|
||||
grouped_proofs, hide_secrets=True
|
||||
)
|
||||
token = await wallet.serialize_proofs(grouped_proofs)
|
||||
token_hidden_secret = await wallet.serialize_proofs(grouped_proofs)
|
||||
reserved_date = datetime.utcfromtimestamp(
|
||||
int(grouped_proofs[0].time_reserved)
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(
|
||||
f"#{i} Amount: {sum_proofs(grouped_proofs)} sat Time: {reserved_date} ID: {key}\n"
|
||||
)
|
||||
print(f"With secret: {coin}\n\nSecretless: {coin_hidden_secret}\n")
|
||||
print(f"{token}\n")
|
||||
print(f"--------------------------\n")
|
||||
print("To remove all spent tokens use: cashu burn -a")
|
||||
wallet.status()
|
||||
|
||||
|
||||
@@ -284,16 +534,16 @@ async def lock(ctx):
|
||||
p2shscript = await wallet.create_p2sh_lock()
|
||||
txin_p2sh_address = p2shscript.address
|
||||
print("---- Pay to script hash (P2SH) ----\n")
|
||||
print("Use a lock to receive coins that only you can unlock.")
|
||||
print("Use a lock to receive tokens that only you can unlock.")
|
||||
print("")
|
||||
print(f"Public receiving lock: P2SH:{txin_p2sh_address}")
|
||||
print("")
|
||||
print(
|
||||
f"Anyone can send coins to this lock:\n\ncashu send <amount> --lock P2SH:{txin_p2sh_address}"
|
||||
f"Anyone can send tokens to this lock:\n\ncashu send <amount> --lock P2SH:{txin_p2sh_address}"
|
||||
)
|
||||
print("")
|
||||
print(
|
||||
f"Only you can receive coins from this lock:\n\ncashu receive <coin> --lock P2SH:{txin_p2sh_address}\n"
|
||||
f"Only you can receive tokens from this lock:\n\ncashu receive <token> --lock P2SH:{txin_p2sh_address}\n"
|
||||
)
|
||||
|
||||
|
||||
@@ -311,7 +561,7 @@ async def locks(ctx):
|
||||
print(f"Script: {l.script}")
|
||||
print(f"Signature: {l.signature}")
|
||||
print("")
|
||||
print(f"Receive: cashu receive <coin> --lock P2SH:{l.address}")
|
||||
print(f"Receive: cashu receive <token> --lock P2SH:{l.address}")
|
||||
print("")
|
||||
print(f"--------------------------\n")
|
||||
else:
|
||||
@@ -319,6 +569,41 @@ async def locks(ctx):
|
||||
return True
|
||||
|
||||
|
||||
@cli.command("invoices", help="List of all pending invoices.")
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def invoices(ctx):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
invoices = await get_lightning_invoices(db=wallet.db)
|
||||
if len(invoices):
|
||||
print("")
|
||||
print(f"--------------------------\n")
|
||||
for invoice in invoices:
|
||||
print(f"Paid: {invoice.paid}")
|
||||
print(f"Incoming: {invoice.amount > 0}")
|
||||
print(f"Amount: {abs(invoice.amount)}")
|
||||
if invoice.hash:
|
||||
print(f"Hash: {invoice.hash}")
|
||||
if invoice.preimage:
|
||||
print(f"Preimage: {invoice.preimage}")
|
||||
if invoice.time_created:
|
||||
d = datetime.utcfromtimestamp(
|
||||
int(float(invoice.time_created))
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"Created: {d}")
|
||||
if invoice.time_paid:
|
||||
d = datetime.utcfromtimestamp(int(float(invoice.time_paid))).strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
print(f"Paid: {d}")
|
||||
print("")
|
||||
print(f"Payment request: {invoice.pr}")
|
||||
print("")
|
||||
print(f"--------------------------\n")
|
||||
else:
|
||||
print("No invoices found.")
|
||||
|
||||
|
||||
@cli.command("wallets", help="List of all available wallets.")
|
||||
@click.pass_context
|
||||
@coro
|
||||
@@ -349,10 +634,19 @@ async def wallets(ctx):
|
||||
@coro
|
||||
async def info(ctx):
|
||||
print(f"Version: {VERSION}")
|
||||
print(f"Debug: {DEBUG}")
|
||||
print(f"Wallet: {ctx.obj['WALLET_NAME']}")
|
||||
if DEBUG:
|
||||
print(f"Debug: {DEBUG}")
|
||||
print(f"Cashu dir: {CASHU_DIR}")
|
||||
if ENV_FILE:
|
||||
print(f"Settings: {ENV_FILE}")
|
||||
print(f"Wallet: {ctx.obj['WALLET_NAME']}")
|
||||
print(f"Mint URL: {MINT_URL}")
|
||||
if TOR:
|
||||
print(f"Tor enabled: {TOR}")
|
||||
if NOSTR_PRIVATE_KEY:
|
||||
client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY, connect=False)
|
||||
print(f"Nostr public key: {client.public_key.hex()}")
|
||||
print(f"Nostr relays: {NOSTR_RELAYS}")
|
||||
if SOCKS_HOST:
|
||||
print(f"Socks proxy: {SOCKS_HOST}:{SOCKS_PORT}")
|
||||
print(f"Mint URL: {ctx.obj['HOST']}")
|
||||
return
|
||||
|
||||
172
cashu/wallet/cli_helpers.py
Normal file
172
cashu/wallet/cli_helpers.py
Normal file
@@ -0,0 +1,172 @@
|
||||
import os
|
||||
import urllib.parse
|
||||
|
||||
import click
|
||||
|
||||
from cashu.core.base import Proof, TokenJson, TokenMintJson, WalletKeyset
|
||||
from cashu.core.settings import CASHU_DIR, MINT_URL
|
||||
from cashu.wallet.crud import get_keyset
|
||||
from cashu.wallet.wallet import Wallet as Wallet
|
||||
|
||||
|
||||
async def verify_mints(ctx, dtoken):
|
||||
trust_token_mints = True
|
||||
for mint_id in dtoken.get("mints"):
|
||||
for keyset in set(dtoken["mints"][mint_id]["ks"]):
|
||||
mint_url = dtoken["mints"][mint_id]["url"]
|
||||
# init a temporary wallet object
|
||||
keyset_wallet = Wallet(
|
||||
mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"])
|
||||
)
|
||||
# make sure that this mint supports this keyset
|
||||
mint_keysets = await keyset_wallet._get_keysets(mint_url)
|
||||
assert keyset in mint_keysets["keysets"], "mint does not have this keyset."
|
||||
|
||||
# we validate the keyset id by fetching the keys from the mint
|
||||
mint_keyset = await keyset_wallet._get_keyset(mint_url, keyset)
|
||||
assert keyset == mint_keyset.id, Exception("keyset not valid.")
|
||||
|
||||
# we check the db whether we know this mint already and ask the user if not
|
||||
mint_keysets = await get_keyset(mint_url=mint_url, db=keyset_wallet.db)
|
||||
if mint_keysets is None:
|
||||
# we encountered a new mint and ask for a user confirmation
|
||||
trust_token_mints = False
|
||||
print("")
|
||||
print("Warning: Tokens are from a mint you don't know yet.")
|
||||
print("\n")
|
||||
print(f"Mint URL: {mint_url}")
|
||||
print(f"Mint keyset: {keyset}")
|
||||
print("\n")
|
||||
click.confirm(
|
||||
f"Do you trust this mint and want to receive the tokens?",
|
||||
abort=True,
|
||||
default=True,
|
||||
)
|
||||
trust_token_mints = True
|
||||
|
||||
assert trust_token_mints, Exception("Aborted!")
|
||||
|
||||
|
||||
async def redeem_multimint(ctx, dtoken, script, signature):
|
||||
# we get the mint information in the token and load the keys of each mint
|
||||
# we then redeem the tokens for each keyset individually
|
||||
for mint_id in dtoken.get("mints"):
|
||||
for keyset in set(dtoken["mints"][mint_id]["ks"]):
|
||||
mint_url = dtoken["mints"][mint_id]["url"]
|
||||
# init a temporary wallet object
|
||||
keyset_wallet = Wallet(
|
||||
mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"])
|
||||
)
|
||||
|
||||
# load the keys
|
||||
await keyset_wallet.load_mint(keyset_id=keyset)
|
||||
|
||||
# redeem proofs of this keyset
|
||||
redeem_proofs = [
|
||||
Proof(**p) for p in dtoken["tokens"] if Proof(**p).id == keyset
|
||||
]
|
||||
_, _ = await keyset_wallet.redeem(
|
||||
redeem_proofs, scnd_script=script, scnd_siganture=signature
|
||||
)
|
||||
|
||||
|
||||
async def print_mint_balances(ctx, wallet, show_mints=False):
|
||||
# get balances per mint
|
||||
mint_balances = await wallet.balance_per_minturl()
|
||||
|
||||
# if we have a balance on a non-default mint, we show its URL
|
||||
keysets = [k for k, v in wallet.balance_per_keyset().items()]
|
||||
for k in keysets:
|
||||
ks = await get_keyset(id=str(k), db=wallet.db)
|
||||
if ks and ks.mint_url != ctx.obj["HOST"]:
|
||||
show_mints = True
|
||||
|
||||
# or we have a balance on more than one mint
|
||||
# show balances per mint
|
||||
if len(mint_balances) > 1 or show_mints:
|
||||
print(f"You have balances in {len(mint_balances)} mints:")
|
||||
print("")
|
||||
for i, (k, v) in enumerate(mint_balances.items()):
|
||||
print(
|
||||
f"Mint {i+1}: Balance: {v['available']} sat (pending: {v['balance']-v['available']} sat) URL: {k}"
|
||||
)
|
||||
print("")
|
||||
|
||||
|
||||
async def get_mint_wallet(ctx):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
|
||||
mint_balances = await wallet.balance_per_minturl()
|
||||
|
||||
if len(mint_balances) > 1:
|
||||
await print_mint_balances(ctx, wallet, show_mints=True)
|
||||
|
||||
mint_nr_str = (
|
||||
input(f"Select mint [1-{len(mint_balances)}, press enter for default 1]: ")
|
||||
or "1"
|
||||
)
|
||||
if not mint_nr_str.isdigit():
|
||||
raise Exception("invalid input.")
|
||||
mint_nr = int(mint_nr_str)
|
||||
else:
|
||||
mint_nr = 1
|
||||
|
||||
mint_url = list(mint_balances.keys())[mint_nr - 1]
|
||||
|
||||
# load this mint_url into a wallet
|
||||
mint_wallet = Wallet(mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"]))
|
||||
mint_keysets: WalletKeyset = await get_keyset(mint_url=mint_url, db=mint_wallet.db) # type: ignore
|
||||
|
||||
# load the keys
|
||||
await mint_wallet.load_mint(keyset_id=mint_keysets.id)
|
||||
|
||||
return mint_wallet
|
||||
|
||||
|
||||
# LNbits token link parsing
|
||||
# can extract minut URL from LNbits token links like:
|
||||
# https://lnbits.server/cashu/wallet?mint_id=aMintId&recv_token=W3siaWQiOiJHY2...
|
||||
def token_from_lnbits_link(link):
|
||||
url, token = "", ""
|
||||
if len(link.split("&recv_token=")) == 2:
|
||||
# extract URL params
|
||||
params = urllib.parse.parse_qs(link.split("?")[1])
|
||||
# extract URL
|
||||
if "mint_id" in params:
|
||||
url = (
|
||||
link.split("?")[0].split("/wallet")[0]
|
||||
+ "/api/v1/"
|
||||
+ params["mint_id"][0]
|
||||
)
|
||||
# extract token
|
||||
token = params["recv_token"][0]
|
||||
return token, url
|
||||
else:
|
||||
return link, ""
|
||||
|
||||
|
||||
async def proofs_to_token(wallet, proofs, url: str):
|
||||
"""
|
||||
Ingests proofs and
|
||||
"""
|
||||
# and add url and keyset id to token
|
||||
token: TokenJson = await wallet._make_token(proofs, include_mints=False)
|
||||
token.mints = {}
|
||||
|
||||
# get keysets of proofs
|
||||
keysets = list(set([p.id for p in proofs]))
|
||||
assert keysets is not None, "no keysets"
|
||||
|
||||
# check whether we know the mint urls for these proofs
|
||||
for k in keysets:
|
||||
ks = await get_keyset(id=k, db=wallet.db)
|
||||
url = ks.mint_url if ks is not None else None
|
||||
|
||||
url = url or (
|
||||
input(f"Enter mint URL (press enter for default {MINT_URL}): ") or MINT_URL
|
||||
)
|
||||
|
||||
token.mints[url] = TokenMintJson(url=url, ks=keysets) # type: ignore
|
||||
token_serialized = await wallet._serialize_token_base64(token)
|
||||
return token_serialized
|
||||
@@ -1,7 +1,7 @@
|
||||
import time
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from cashu.core.base import KeyBase, P2SHScript, Proof, WalletKeyset
|
||||
from cashu.core.base import Invoice, KeyBase, P2SHScript, Proof, WalletKeyset
|
||||
from cashu.core.db import Connection, Database
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ async def get_proofs(
|
||||
SELECT * from proofs
|
||||
"""
|
||||
)
|
||||
return [Proof.from_row(r) for r in rows]
|
||||
return [Proof(**dict(r)) for r in rows]
|
||||
|
||||
|
||||
async def get_reserved_proofs(
|
||||
@@ -45,7 +45,7 @@ async def get_reserved_proofs(
|
||||
WHERE reserved
|
||||
"""
|
||||
)
|
||||
return [Proof.from_row(r) for r in rows]
|
||||
return [Proof(**r) for r in rows]
|
||||
|
||||
|
||||
async def invalidate_proof(
|
||||
@@ -93,7 +93,7 @@ async def update_proof_reserved(
|
||||
clauses.append("time_reserved = ?")
|
||||
values.append(int(time.time()))
|
||||
|
||||
await (conn or db).execute(
|
||||
await (conn or db).execute( # type: ignore
|
||||
f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = ?",
|
||||
(*values, str(proof.secret)),
|
||||
)
|
||||
@@ -155,14 +155,14 @@ async def get_unused_locks(
|
||||
if clause:
|
||||
where = f"WHERE {' AND '.join(clause)}"
|
||||
|
||||
rows = await (conn or db).fetchall(
|
||||
rows = await (conn or db).fetchall( # type: ignore
|
||||
f"""
|
||||
SELECT * from p2sh
|
||||
{where}
|
||||
""",
|
||||
tuple(args),
|
||||
)
|
||||
return [P2SHScript.from_row(r) for r in rows]
|
||||
return [P2SHScript(**r) for r in rows]
|
||||
|
||||
|
||||
async def update_p2sh_used(
|
||||
@@ -176,7 +176,7 @@ async def update_p2sh_used(
|
||||
clauses.append("used = ?")
|
||||
values.append(used)
|
||||
|
||||
await (conn or db).execute(
|
||||
await (conn or db).execute( # type: ignore
|
||||
f"UPDATE proofs SET {', '.join(clauses)} WHERE address = ?",
|
||||
(*values, str(p2sh.address)),
|
||||
)
|
||||
@@ -189,7 +189,7 @@ async def store_keyset(
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
await (conn or db).execute(
|
||||
await (conn or db).execute( # type: ignore
|
||||
"""
|
||||
INSERT INTO keysets
|
||||
(id, mint_url, valid_from, valid_to, first_seen, active)
|
||||
@@ -198,22 +198,22 @@ async def store_keyset(
|
||||
(
|
||||
keyset.id,
|
||||
mint_url or keyset.mint_url,
|
||||
keyset.valid_from,
|
||||
keyset.valid_to,
|
||||
keyset.first_seen,
|
||||
keyset.valid_from or int(time.time()),
|
||||
keyset.valid_to or int(time.time()),
|
||||
keyset.first_seen or int(time.time()),
|
||||
True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def get_keyset(
|
||||
id: str = None,
|
||||
mint_url: str = None,
|
||||
id: str = "",
|
||||
mint_url: str = "",
|
||||
db: Database = None,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
clauses = []
|
||||
values = []
|
||||
values: List[Any] = []
|
||||
clauses.append("active = ?")
|
||||
values.append(True)
|
||||
if id:
|
||||
@@ -226,11 +226,111 @@ async def get_keyset(
|
||||
if clauses:
|
||||
where = f"WHERE {' AND '.join(clauses)}"
|
||||
|
||||
row = await (conn or db).fetchone(
|
||||
row = await (conn or db).fetchone( # type: ignore
|
||||
f"""
|
||||
SELECT * from keysets
|
||||
{where}
|
||||
""",
|
||||
tuple(values),
|
||||
)
|
||||
return WalletKeyset.from_row(row) if row is not None else None
|
||||
return WalletKeyset(**row) if row is not None else None
|
||||
|
||||
|
||||
async def store_lightning_invoice(
|
||||
db: Database,
|
||||
invoice: Invoice,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO invoices
|
||||
(amount, pr, hash, preimage, paid, time_created, time_paid)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
invoice.amount,
|
||||
invoice.pr,
|
||||
invoice.hash,
|
||||
invoice.preimage,
|
||||
invoice.paid,
|
||||
invoice.time_created,
|
||||
invoice.time_paid,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def get_lightning_invoice(
|
||||
db: Database,
|
||||
hash: str = None,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
clauses = []
|
||||
values: List[Any] = []
|
||||
if hash:
|
||||
clauses.append("hash = ?")
|
||||
values.append(hash)
|
||||
|
||||
where = ""
|
||||
if clauses:
|
||||
where = f"WHERE {' AND '.join(clauses)}"
|
||||
|
||||
row = await (conn or db).fetchone(
|
||||
f"""
|
||||
SELECT * from invoices
|
||||
{where}
|
||||
""",
|
||||
tuple(values),
|
||||
)
|
||||
return Invoice(**row)
|
||||
|
||||
|
||||
async def get_lightning_invoices(
|
||||
db: Database,
|
||||
paid: bool = None,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
clauses: List[Any] = []
|
||||
values: List[Any] = []
|
||||
|
||||
if paid is not None:
|
||||
clauses.append("paid = ?")
|
||||
values.append(paid)
|
||||
|
||||
where = ""
|
||||
if clauses:
|
||||
where = f"WHERE {' AND '.join(clauses)}"
|
||||
|
||||
rows = await (conn or db).fetchall(
|
||||
f"""
|
||||
SELECT * from invoices
|
||||
{where}
|
||||
""",
|
||||
tuple(values),
|
||||
)
|
||||
return [Invoice(**r) for r in rows]
|
||||
|
||||
|
||||
async def update_lightning_invoice(
|
||||
db: Database,
|
||||
hash: str,
|
||||
paid: bool,
|
||||
time_paid: int = None,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
clauses = []
|
||||
values: List[Any] = []
|
||||
clauses.append("paid = ?")
|
||||
values.append(paid)
|
||||
|
||||
if time_paid:
|
||||
clauses.append("time_paid = ?")
|
||||
values.append(time_paid)
|
||||
|
||||
await (conn or db).execute(
|
||||
f"UPDATE invoices SET {', '.join(clauses)} WHERE hash = ?",
|
||||
(
|
||||
*values,
|
||||
hash,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -107,8 +107,8 @@ async def m005_wallet_keysets(db: Database):
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS keysets (
|
||||
id TEXT NOT NULL,
|
||||
mint_url TEXT NOT NULL,
|
||||
id TEXT,
|
||||
mint_url TEXT,
|
||||
valid_from TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
valid_to TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
first_seen TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
@@ -119,18 +119,28 @@ async def m005_wallet_keysets(db: Database):
|
||||
);
|
||||
"""
|
||||
)
|
||||
# await db.execute(
|
||||
# f"""
|
||||
# CREATE TABLE IF NOT EXISTS mint_pubkeys (
|
||||
# id TEXT NOT NULL,
|
||||
# amount INTEGER NOT NULL,
|
||||
# pubkey TEXT NOT NULL,
|
||||
|
||||
# UNIQUE (id, pubkey)
|
||||
|
||||
# );
|
||||
# """
|
||||
# )
|
||||
|
||||
await db.execute("ALTER TABLE proofs ADD COLUMN id TEXT")
|
||||
await db.execute("ALTER TABLE proofs_used ADD COLUMN id TEXT")
|
||||
|
||||
|
||||
async def m006_invoices(db: Database):
|
||||
"""
|
||||
Stores Lightning invoices.
|
||||
"""
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS invoices (
|
||||
amount INTEGER NOT NULL,
|
||||
pr TEXT NOT NULL,
|
||||
hash TEXT,
|
||||
preimage TEXT,
|
||||
paid BOOL DEFAULT FALSE,
|
||||
time_created TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
time_paid TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
|
||||
UNIQUE (hash)
|
||||
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
import base64
|
||||
import json
|
||||
import math
|
||||
import secrets as scrts
|
||||
import time
|
||||
import uuid
|
||||
from itertools import groupby
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import requests
|
||||
from loguru import logger
|
||||
|
||||
import cashu.core.b_dhke as b_dhke
|
||||
import cashu.core.bolt11 as bolt11
|
||||
from cashu.core.base import (
|
||||
BlindedMessage,
|
||||
BlindedSignature,
|
||||
CheckFeesRequest,
|
||||
CheckRequest,
|
||||
Invoice,
|
||||
MeltRequest,
|
||||
MintRequest,
|
||||
P2SHScript,
|
||||
Proof,
|
||||
SplitRequest,
|
||||
TokenJson,
|
||||
TokenMintJson,
|
||||
WalletKeyset,
|
||||
)
|
||||
from cashu.core.bolt11 import Invoice as InvoiceBolt11
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.helpers import sum_proofs
|
||||
from cashu.core.script import (
|
||||
@@ -30,52 +37,53 @@ from cashu.core.script import (
|
||||
step2_carol_sign_tx,
|
||||
)
|
||||
from cashu.core.secp import PublicKey
|
||||
from cashu.core.settings import DEBUG
|
||||
from cashu.core.settings import DEBUG, SOCKS_HOST, SOCKS_PORT, TOR, VERSION
|
||||
from cashu.core.split import amount_split
|
||||
from cashu.tor.tor import TorProxy
|
||||
from cashu.wallet.crud import (
|
||||
get_keyset,
|
||||
get_proofs,
|
||||
invalidate_proof,
|
||||
secret_used,
|
||||
store_keyset,
|
||||
store_lightning_invoice,
|
||||
store_p2sh,
|
||||
store_proof,
|
||||
update_lightning_invoice,
|
||||
update_proof_reserved,
|
||||
)
|
||||
|
||||
|
||||
class LedgerAPI:
|
||||
keys: Dict[int, str]
|
||||
keys: Dict[int, PublicKey]
|
||||
keyset: str
|
||||
tor: TorProxy
|
||||
db: Database
|
||||
|
||||
def __init__(self, url):
|
||||
self.url = url
|
||||
|
||||
async def _get_keys(self, url):
|
||||
resp = requests.get(url + "/keys").json()
|
||||
keys = resp
|
||||
assert len(keys), Exception("did not receive any keys")
|
||||
keyset_keys = {
|
||||
int(amt): PublicKey(bytes.fromhex(val), raw=True)
|
||||
for amt, val in keys.items()
|
||||
}
|
||||
keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url)
|
||||
return keyset
|
||||
def _set_requests(self):
|
||||
s = requests.Session()
|
||||
s.headers.update({"Client-version": VERSION})
|
||||
if DEBUG:
|
||||
s.verify = False
|
||||
socks_host, socks_port = None, None
|
||||
if TOR and TorProxy().check_platform():
|
||||
self.tor = TorProxy(timeout=True)
|
||||
self.tor.run_daemon(verbose=True)
|
||||
socks_host, socks_port = "localhost", 9050
|
||||
else:
|
||||
socks_host, socks_port = SOCKS_HOST, SOCKS_PORT
|
||||
|
||||
async def _get_keysets(self, url):
|
||||
keysets = requests.get(url + "/keysets").json()
|
||||
assert len(keysets), Exception("did not receive any keysets")
|
||||
return keysets
|
||||
|
||||
@staticmethod
|
||||
def _get_output_split(amount):
|
||||
"""Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8]."""
|
||||
bits_amt = bin(amount)[::-1][:-2]
|
||||
rv = []
|
||||
for (pos, bit) in enumerate(bits_amt):
|
||||
if bit == "1":
|
||||
rv.append(2**pos)
|
||||
return rv
|
||||
if socks_host and socks_port:
|
||||
proxies = {
|
||||
"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
|
||||
|
||||
def _construct_proofs(
|
||||
self, promises: List[BlindedSignature], secrets: List[str], rs: List[str]
|
||||
@@ -94,21 +102,42 @@ class LedgerAPI:
|
||||
proofs.append(proof)
|
||||
return proofs
|
||||
|
||||
@staticmethod
|
||||
def raise_on_error(resp_dict):
|
||||
if "error" in resp_dict:
|
||||
raise Exception("Mint Error: {}".format(resp_dict["error"]))
|
||||
|
||||
@staticmethod
|
||||
def _generate_secret(randombits=128):
|
||||
"""Returns base64 encoded random string."""
|
||||
return scrts.token_urlsafe(randombits // 8)
|
||||
|
||||
async def _load_mint(self):
|
||||
async def _load_mint(self, keyset_id: str = ""):
|
||||
"""
|
||||
Loads the current keys and the active keyset of the map.
|
||||
Loads the public keys of the mint. Either gets the keys for the specified
|
||||
`keyset_id` or loads the most recent one from the mint.
|
||||
Gets and the active keyset ids of the mint and stores in `self.keysets`.
|
||||
"""
|
||||
assert len(
|
||||
self.url
|
||||
), "Ledger not initialized correctly: mint URL not specified yet. "
|
||||
# get current keyset
|
||||
keyset = await self._get_keys(self.url)
|
||||
# get all active keysets
|
||||
|
||||
if keyset_id:
|
||||
# get requested keyset
|
||||
keyset = await self._get_keyset(self.url, keyset_id)
|
||||
else:
|
||||
# get current keyset
|
||||
keyset = await self._get_keys(self.url)
|
||||
|
||||
# store current keyset
|
||||
assert len(keyset.public_keys) > 0, "did not receive keys from mint."
|
||||
|
||||
# check if current keyset is in db
|
||||
keyset_local: Optional[WalletKeyset] = await get_keyset(keyset.id, db=self.db)
|
||||
if keyset_local is None:
|
||||
await store_keyset(keyset=keyset, db=self.db)
|
||||
|
||||
# get all active keysets of this mint
|
||||
mint_keysets = []
|
||||
try:
|
||||
keysets_resp = await self._get_keysets(self.url)
|
||||
@@ -118,26 +147,12 @@ class LedgerAPI:
|
||||
pass
|
||||
self.keysets = mint_keysets if len(mint_keysets) else [keyset.id]
|
||||
|
||||
# store current keyset
|
||||
assert len(keyset.public_keys) > 0, "did not receive keys from mint."
|
||||
|
||||
# check if current keyset is in db
|
||||
keyset_local: WalletKeyset = await get_keyset(keyset.id, db=self.db)
|
||||
if keyset_local is None:
|
||||
await store_keyset(keyset=keyset, db=self.db)
|
||||
|
||||
logger.debug(f"Mint keysets: {self.keysets}")
|
||||
logger.debug(f"Current mint keyset: {keyset.id}")
|
||||
|
||||
self.keys = keyset.public_keys
|
||||
self.keyset_id = keyset.id
|
||||
|
||||
def request_mint(self, amount):
|
||||
"""Requests a mint from the server and returns Lightning invoice."""
|
||||
r = requests.get(self.url + "/mint", params={"amount": amount})
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
@staticmethod
|
||||
def _construct_outputs(amounts: List[int], secrets: List[str]):
|
||||
"""Takes a list of amounts and secrets and returns outputs.
|
||||
@@ -167,29 +182,82 @@ class LedgerAPI:
|
||||
return [f"{secret}:{self._generate_secret()}" for i in range(n)]
|
||||
return [f"{i}:{secret}" for i in range(n)]
|
||||
|
||||
"""
|
||||
ENDPOINTS
|
||||
"""
|
||||
|
||||
async def _get_keys(self, url: str):
|
||||
self.s = self._set_requests()
|
||||
resp = self.s.get(
|
||||
url + "/keys",
|
||||
)
|
||||
resp.raise_for_status()
|
||||
keys = resp.json()
|
||||
assert len(keys), Exception("did not receive any keys")
|
||||
keyset_keys = {
|
||||
int(amt): PublicKey(bytes.fromhex(val), raw=True)
|
||||
for amt, val in keys.items()
|
||||
}
|
||||
keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url)
|
||||
return keyset
|
||||
|
||||
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}",
|
||||
)
|
||||
resp.raise_for_status()
|
||||
keys = resp.json()
|
||||
assert len(keys), Exception("did not receive any keys")
|
||||
keyset_keys = {
|
||||
int(amt): PublicKey(bytes.fromhex(val), raw=True)
|
||||
for amt, val in keys.items()
|
||||
}
|
||||
keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url)
|
||||
return keyset
|
||||
|
||||
async def _get_keysets(self, url: str):
|
||||
self.s = self._set_requests()
|
||||
resp = self.s.get(
|
||||
url + "/keysets",
|
||||
)
|
||||
resp.raise_for_status()
|
||||
keysets = resp.json()
|
||||
assert len(keysets), Exception("did not receive any keysets")
|
||||
return keysets
|
||||
|
||||
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()
|
||||
self.raise_on_error(return_dict)
|
||||
return Invoice(amount=amount, pr=return_dict["pr"], hash=return_dict["hash"])
|
||||
|
||||
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)
|
||||
payloads, rs = self._construct_outputs(amounts, secrets)
|
||||
|
||||
resp = requests.post(
|
||||
self.s = self._set_requests()
|
||||
resp = self.s.post(
|
||||
self.url + "/mint",
|
||||
json=payloads.dict(),
|
||||
params={"payment_hash": payment_hash},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
try:
|
||||
promises_list = resp.json()
|
||||
except:
|
||||
raise Exception("Unkown mint error.")
|
||||
if "error" in promises_list:
|
||||
raise Exception("Error: {}".format(promises_list["error"]))
|
||||
promises_list = resp.json()
|
||||
self.raise_on_error(promises_list)
|
||||
|
||||
promises = [BlindedSignature.from_dict(p) for p in promises_list]
|
||||
promises = [BlindedSignature(**p) for p in promises_list]
|
||||
return self._construct_proofs(promises, secrets, rs)
|
||||
|
||||
async def split(self, proofs, amount, scnd_secret: str = None):
|
||||
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)
|
||||
and the promises to send (scnd_outputs).
|
||||
@@ -223,6 +291,7 @@ class LedgerAPI:
|
||||
payloads, rs = self._construct_outputs(amounts, secrets)
|
||||
split_payload = SplitRequest(proofs=proofs, amount=amount, outputs=payloads)
|
||||
|
||||
# construct payload
|
||||
def _splitrequest_include_fields(proofs):
|
||||
"""strips away fields from the model that aren't necessary for the /split"""
|
||||
proofs_include = {"id", "amount", "secret", "C", "script"}
|
||||
@@ -232,19 +301,17 @@ class LedgerAPI:
|
||||
"proofs": {i: proofs_include for i in range(len(proofs))},
|
||||
}
|
||||
|
||||
resp = requests.post(
|
||||
self.s = self._set_requests()
|
||||
resp = self.s.post(
|
||||
self.url + "/split",
|
||||
json=split_payload.dict(include=_splitrequest_include_fields(proofs)),
|
||||
)
|
||||
resp.raise_for_status()
|
||||
try:
|
||||
promises_dict = resp.json()
|
||||
except:
|
||||
raise Exception("Unkown mint error.")
|
||||
if "error" in promises_dict:
|
||||
raise Exception("Mint Error: {}".format(promises_dict["error"]))
|
||||
promises_fst = [BlindedSignature.from_dict(p) for p in promises_dict["fst"]]
|
||||
promises_snd = [BlindedSignature.from_dict(p) for p in promises_dict["snd"]]
|
||||
promises_dict = resp.json()
|
||||
self.raise_on_error(promises_dict)
|
||||
|
||||
promises_fst = [BlindedSignature(**p) for p in promises_dict["fst"]]
|
||||
promises_snd = [BlindedSignature(**p) for p in promises_dict["snd"]]
|
||||
# Construct proofs from promises (i.e., unblind signatures)
|
||||
frst_proofs = self._construct_proofs(
|
||||
promises_fst, secrets[: len(promises_fst)], rs[: len(promises_fst)]
|
||||
@@ -256,29 +323,44 @@ class LedgerAPI:
|
||||
return frst_proofs, scnd_proofs
|
||||
|
||||
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.
|
||||
"""
|
||||
payload = CheckRequest(proofs=proofs)
|
||||
resp = requests.post(
|
||||
|
||||
def _check_spendable_include_fields(proofs):
|
||||
"""strips away fields from the model that aren't necessary for the /split"""
|
||||
return {
|
||||
"proofs": {i: {"secret"} for i in range(len(proofs))},
|
||||
}
|
||||
|
||||
self.s = self._set_requests()
|
||||
resp = self.s.post(
|
||||
self.url + "/check",
|
||||
json=payload.dict(),
|
||||
json=payload.dict(include=_check_spendable_include_fields(proofs)),
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return_dict = resp.json()
|
||||
|
||||
self.raise_on_error(return_dict)
|
||||
return return_dict
|
||||
|
||||
async def check_fees(self, payment_request: str):
|
||||
"""Checks whether the Lightning payment is internal."""
|
||||
payload = CheckFeesRequest(pr=payment_request)
|
||||
resp = requests.post(
|
||||
self.s = self._set_requests()
|
||||
resp = self.s.post(
|
||||
self.url + "/checkfees",
|
||||
json=payload.dict(),
|
||||
)
|
||||
resp.raise_for_status()
|
||||
|
||||
return_dict = resp.json()
|
||||
self.raise_on_error(return_dict)
|
||||
return return_dict
|
||||
|
||||
async def pay_lightning(self, proofs: List[Proof], invoice: str):
|
||||
"""
|
||||
Accepts proofs and a lightning invoice to pay in exchange.
|
||||
"""
|
||||
payload = MeltRequest(proofs=proofs, invoice=invoice)
|
||||
|
||||
def _meltequest_include_fields(proofs):
|
||||
@@ -290,13 +372,14 @@ class LedgerAPI:
|
||||
"proofs": {i: proofs_include for i in range(len(proofs))},
|
||||
}
|
||||
|
||||
resp = requests.post(
|
||||
self.s = self._set_requests()
|
||||
resp = self.s.post(
|
||||
self.url + "/melt",
|
||||
json=payload.dict(include=_meltequest_include_fields(proofs)),
|
||||
)
|
||||
resp.raise_for_status()
|
||||
|
||||
return_dict = resp.json()
|
||||
self.raise_on_error(return_dict)
|
||||
return return_dict
|
||||
|
||||
|
||||
@@ -309,8 +392,8 @@ class Wallet(LedgerAPI):
|
||||
self.proofs: List[Proof] = []
|
||||
self.name = name
|
||||
|
||||
async def load_mint(self):
|
||||
await super()._load_mint()
|
||||
async def load_mint(self, keyset_id: str = ""):
|
||||
await super()._load_mint(keyset_id)
|
||||
|
||||
async def load_proofs(self):
|
||||
self.proofs = await get_proofs(db=self.db)
|
||||
@@ -323,23 +406,42 @@ class Wallet(LedgerAPI):
|
||||
def _get_proofs_per_keyset(proofs: List[Proof]):
|
||||
return {key: list(group) for key, group in groupby(proofs, lambda p: p.id)}
|
||||
|
||||
async def request_mint(self, amount):
|
||||
return super().request_mint(amount)
|
||||
async def _get_proofs_per_minturl(self, proofs: List[Proof]):
|
||||
ret = {}
|
||||
for id in set([p.id for p in proofs]):
|
||||
if id is None:
|
||||
continue
|
||||
keyset: WalletKeyset = await get_keyset(id=id, db=self.db)
|
||||
if keyset.mint_url not in ret:
|
||||
ret[keyset.mint_url] = [p for p in proofs if p.id == id]
|
||||
else:
|
||||
ret[keyset.mint_url].extend([p for p in proofs if p.id == id])
|
||||
return ret
|
||||
|
||||
async def mint(self, amount: int, payment_hash: str = None):
|
||||
async def request_mint(self, amount):
|
||||
invoice = super().request_mint(amount)
|
||||
invoice.time_created = int(time.time())
|
||||
await store_lightning_invoice(db=self.db, invoice=invoice)
|
||||
return invoice
|
||||
|
||||
async def mint(self, amount: int, payment_hash: Optional[str] = None):
|
||||
split = amount_split(amount)
|
||||
proofs = await super().mint(split, payment_hash)
|
||||
if proofs == []:
|
||||
raise Exception("received no proofs.")
|
||||
await self._store_proofs(proofs)
|
||||
if payment_hash:
|
||||
await update_lightning_invoice(
|
||||
db=self.db, hash=payment_hash, paid=True, time_paid=int(time.time())
|
||||
)
|
||||
self.proofs += proofs
|
||||
return proofs
|
||||
|
||||
async def redeem(
|
||||
self,
|
||||
proofs: List[Proof],
|
||||
scnd_script: str = None,
|
||||
scnd_siganture: str = None,
|
||||
scnd_script: Optional[str] = None,
|
||||
scnd_siganture: Optional[str] = None,
|
||||
):
|
||||
if scnd_script and scnd_siganture:
|
||||
logger.debug(f"Unlock script: {scnd_script}")
|
||||
@@ -352,7 +454,7 @@ class Wallet(LedgerAPI):
|
||||
self,
|
||||
proofs: List[Proof],
|
||||
amount: int,
|
||||
scnd_secret: str = None,
|
||||
scnd_secret: Optional[str] = None,
|
||||
):
|
||||
assert len(proofs) > 0, ValueError("no proofs provided.")
|
||||
frst_proofs, scnd_proofs = await super().split(proofs, amount, scnd_secret)
|
||||
@@ -373,21 +475,79 @@ class Wallet(LedgerAPI):
|
||||
status = await super().pay_lightning(proofs, invoice)
|
||||
if status["paid"] == True:
|
||||
await self.invalidate(proofs)
|
||||
invoice_obj = Invoice(
|
||||
amount=-sum_proofs(proofs),
|
||||
pr=invoice,
|
||||
preimage=status.get("preimage"),
|
||||
paid=True,
|
||||
time_paid=time.time(),
|
||||
)
|
||||
await store_lightning_invoice(db=self.db, invoice=invoice_obj)
|
||||
else:
|
||||
raise Exception("could not pay invoice.")
|
||||
return status["paid"]
|
||||
|
||||
@staticmethod
|
||||
async def serialize_proofs(proofs: List[Proof], hide_secrets=False):
|
||||
if hide_secrets:
|
||||
proofs_serialized = [p.to_dict_no_secret() for p in proofs]
|
||||
else:
|
||||
proofs_serialized = [p.to_dict() for p in proofs]
|
||||
token = base64.urlsafe_b64encode(
|
||||
json.dumps(proofs_serialized).encode()
|
||||
).decode()
|
||||
async def _make_token(self, proofs: List[Proof], include_mints=True):
|
||||
"""
|
||||
Takes list of proofs and produces a TokenJson by looking up
|
||||
the keyset id and mint URLs from the database.
|
||||
"""
|
||||
# build token
|
||||
token = TokenJson(tokens=proofs)
|
||||
|
||||
# add mint information to the token, if requested
|
||||
if include_mints:
|
||||
# hold information about the mint
|
||||
mints: Dict[str, TokenMintJson] = dict()
|
||||
# iterate through all proofs and add their keyset to `mints`
|
||||
for proof in proofs:
|
||||
if proof.id:
|
||||
# load the keyset from the db
|
||||
keyset = await get_keyset(id=proof.id, db=self.db)
|
||||
if keyset and keyset.mint_url:
|
||||
# TODO: replace this with a mint pubkey
|
||||
placeholder_mint_id = keyset.mint_url
|
||||
if placeholder_mint_id not in mints:
|
||||
# mint information
|
||||
id = TokenMintJson(
|
||||
url=keyset.mint_url,
|
||||
ks=[keyset.id],
|
||||
)
|
||||
mints[placeholder_mint_id] = id
|
||||
else:
|
||||
# if a mint has multiple keysets, append to the existing list
|
||||
if keyset.id not in mints[placeholder_mint_id].ks:
|
||||
mints[placeholder_mint_id].ks.append(keyset.id)
|
||||
if len(mints) > 0:
|
||||
token.mints = mints
|
||||
return token
|
||||
|
||||
async def _serialize_token_base64(self, token: TokenJson):
|
||||
"""
|
||||
Takes a TokenJson and serializes it in urlsafe_base64.
|
||||
"""
|
||||
# encode the token as a base64 string
|
||||
token_base64 = base64.urlsafe_b64encode(
|
||||
json.dumps(token.to_dict()).encode()
|
||||
).decode()
|
||||
return token_base64
|
||||
|
||||
async def serialize_proofs(
|
||||
self, proofs: List[Proof], include_mints=True, legacy=False
|
||||
):
|
||||
"""
|
||||
Produces sharable token with proofs and mint information.
|
||||
"""
|
||||
|
||||
if legacy:
|
||||
proofs_serialized = [p.to_dict() for p in proofs]
|
||||
return base64.urlsafe_b64encode(
|
||||
json.dumps(proofs_serialized).encode()
|
||||
).decode()
|
||||
|
||||
token = await self._make_token(proofs, include_mints)
|
||||
return await self._serialize_token_base64(token)
|
||||
|
||||
async def _select_proofs_to_send(self, proofs: List[Proof], amount_to_send: int):
|
||||
"""
|
||||
Selects proofs that can be used with the current mint.
|
||||
@@ -408,16 +568,44 @@ class Wallet(LedgerAPI):
|
||||
send_proofs.append(sorted_proofs[len(send_proofs)])
|
||||
return send_proofs
|
||||
|
||||
async def split_to_send(self, proofs: List[Proof], amount, scnd_secret: str = None):
|
||||
async def get_pay_amount_with_fees(self, invoice: str):
|
||||
"""
|
||||
Decodes the amount from a Lightning invoice and returns the
|
||||
total amount (amount+fees) to be paid.
|
||||
"""
|
||||
decoded_invoice: InvoiceBolt11 = bolt11.decode(invoice)
|
||||
# check if it's an internal payment
|
||||
fees = int((await self.check_fees(invoice))["fee"])
|
||||
amount = math.ceil((decoded_invoice.amount_msat + fees * 1000) / 1000) # 1% fee
|
||||
return amount, fees
|
||||
|
||||
async def split_to_pay(self, invoice: str):
|
||||
"""
|
||||
Splits proofs such that a Lightning invoice can be paid.
|
||||
"""
|
||||
amount, _ = await self.get_pay_amount_with_fees(invoice)
|
||||
_, send_proofs = await self.split_to_send(self.proofs, amount)
|
||||
return send_proofs
|
||||
|
||||
async def split_to_send(
|
||||
self,
|
||||
proofs: List[Proof],
|
||||
amount,
|
||||
scnd_secret: Optional[str] = None,
|
||||
set_reserved: bool = False,
|
||||
):
|
||||
"""Like self.split but only considers non-reserved tokens."""
|
||||
if scnd_secret:
|
||||
logger.debug(f"Spending conditions: {scnd_secret}")
|
||||
spendable_proofs = await self._select_proofs_to_send(proofs, amount)
|
||||
if sum_proofs(spendable_proofs) < amount:
|
||||
raise Exception("balance too low.")
|
||||
return await self.split(
|
||||
keep_proofs, send_proofs = await self.split(
|
||||
[p for p in spendable_proofs if not p.reserved], amount, scnd_secret
|
||||
)
|
||||
if set_reserved:
|
||||
await self.set_reserved(send_proofs, reserved=True)
|
||||
return keep_proofs, send_proofs
|
||||
|
||||
async def set_reserved(self, proofs: List[Proof], reserved: bool):
|
||||
"""Mark a proof as reserved to avoid reuse or delete marking."""
|
||||
@@ -468,9 +656,10 @@ class Wallet(LedgerAPI):
|
||||
return sum_proofs([p for p in self.proofs if not p.reserved])
|
||||
|
||||
def status(self):
|
||||
print(
|
||||
f"Balance: {self.balance} sat (available: {self.available_balance} sat in {len([p for p in self.proofs if not p.reserved])} tokens)"
|
||||
)
|
||||
# print(
|
||||
# f"Balance: {self.balance} sat (available: {self.available_balance} sat in {len([p for p in self.proofs if not p.reserved])} tokens)"
|
||||
# )
|
||||
print(f"Balance: {self.available_balance} sat")
|
||||
|
||||
def balance_per_keyset(self):
|
||||
return {
|
||||
@@ -481,5 +670,16 @@ class Wallet(LedgerAPI):
|
||||
for key, proofs in self._get_proofs_per_keyset(self.proofs).items()
|
||||
}
|
||||
|
||||
async def balance_per_minturl(self):
|
||||
balances = await self._get_proofs_per_minturl(self.proofs)
|
||||
balances_return = {
|
||||
key: {
|
||||
"balance": sum_proofs(proofs),
|
||||
"available": sum_proofs([p for p in proofs if not p.reserved]),
|
||||
}
|
||||
for key, proofs in balances.items()
|
||||
}
|
||||
return dict(sorted(balances_return.items(), key=lambda item: item[1]["available"], reverse=True)) # type: ignore
|
||||
|
||||
def proof_amounts(self):
|
||||
return [p["amount"] for p in sorted(self.proofs, key=lambda p: p["amount"])]
|
||||
|
||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
# Notation
|
||||
|
||||
Sending user: `Alice`
|
||||
Receivung user: `Carol`
|
||||
Sending user: `Alice`<br>
|
||||
Receiving user: `Carol`<br>
|
||||
Mint: `Bob`
|
||||
|
||||
## Bob (mint)
|
||||
@@ -15,11 +15,11 @@ Mint: `Bob`
|
||||
- `T` blinded message
|
||||
- `Z` proof (unblinded signature)
|
||||
|
||||
# Blind Diffie-Hellmann key exchange (BDH)
|
||||
# Blind Diffie-Hellman key exchange (BDH)
|
||||
- Mint `Bob` publishes `K = kG`
|
||||
- `Alice` picks secret `x` and computes `Y = hash_to_point(x)`
|
||||
- `Alice` picks secret `x` and computes `Y = hash_to_curve(x)`
|
||||
- `Alice` sends to `Bob`: `T = Y + rG` with `r` being a random nonce
|
||||
- `Bob` sends back to `Alice` blinded key: `Q = kT` (these two steps are the DH key exchange)
|
||||
- `Alice` can calculate the unblinded key as `Q - rK = kY + krG - krG = kY = Z`
|
||||
- Alice can take the pair `(x, Z)` as a token and can send it to `Carol`.
|
||||
- `Carol` can send `(x, Z)` to `Bob` who then checks that `k*hash_to_point(x) == Z`, and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets.
|
||||
- `Carol` can send `(x, Z)` to `Bob` who then checks that `k*hash_to_curve(x) == Z`, and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets.
|
||||
|
||||
93
docs/specs/00.md
Normal file
93
docs/specs/00.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# NUT-0 - Notation and Models
|
||||
|
||||
Sending user: `Alice`
|
||||
Receiving user: `Carol`
|
||||
Mint: `Bob`
|
||||
|
||||
## Bob (mint)
|
||||
- `k` private key of mint (one for each amount)
|
||||
- `K` public key of mint
|
||||
- `Q` promise (blinded signature)
|
||||
|
||||
## Alice (user)
|
||||
- `x` random string (secret message), corresponds to point `Y` on curve
|
||||
- `r` private key (blinding factor)
|
||||
- `T` blinded message
|
||||
- `Z` proof (unblinded signature)
|
||||
|
||||
# Blind Diffie-Hellmann key exchange (BDHKE)
|
||||
- Mint `Bob` publishes `K = kG`
|
||||
- `Alice` picks secret `x` and computes `Y = hash_to_curve(x)`
|
||||
- `Alice` sends to `Bob`: `T = Y + rG` with `r` being a random nonce
|
||||
- `Bob` sends back to `Alice` blinded key: `Q = kT` (these two steps are the DH key exchange)
|
||||
- `Alice` can calculate the unblinded key as `Q - rK = kY + krG - krG = kY = Z`
|
||||
- Alice can take the pair `(x, Z)` as a token and can send it to `Carol`.
|
||||
- `Carol` can send `(x, Z)` to `Bob` who then checks that `k*hash_to_curve(x) == Z`, and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets.
|
||||
|
||||
## 0.1 - Models
|
||||
|
||||
### `BlindedMessage`
|
||||
A encrypted ("blinded") secret and an amount sent from `Alice` to `Bob`.
|
||||
|
||||
```json
|
||||
{
|
||||
"amount": int,
|
||||
"B_": str
|
||||
}
|
||||
```
|
||||
|
||||
### `BlindedSignature`
|
||||
A signature on the `BlindedMessage` sent from `Bob` to `Alice`.
|
||||
|
||||
```json
|
||||
{
|
||||
"amount": int,
|
||||
"C_": str,
|
||||
"id": str | None
|
||||
}
|
||||
```
|
||||
|
||||
### `Proof`
|
||||
A `Proof` is also called a `Token` and has the following form:
|
||||
|
||||
```json
|
||||
{
|
||||
"amount": int,
|
||||
"secret": str,
|
||||
"C": str,
|
||||
"id": None | str,
|
||||
"script": P2SHScript | None,
|
||||
}
|
||||
```
|
||||
|
||||
### `Proofs`
|
||||
A list of `Proof`'s. In general, this will be used for most operations instead of a single `Proof`. `Proofs` can be serialized (see Methods/Serialization [TODO: Link Serialization])
|
||||
|
||||
## 0.2 - Methods
|
||||
|
||||
### Serialization of `Proofs`
|
||||
To send and receive `Proofs`, wallets serialize them in a `base64_urlsafe` format.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "DSAl9nvvyfva",
|
||||
"amount": 8,
|
||||
"secret": "DbRKIya0etdwI5sFAN0AXQ",
|
||||
"C": "02df7f2fc29631b71a1db11c163b0b1cb40444aa2b3d253d43b68d77a72ed2d625"
|
||||
},
|
||||
{
|
||||
"id": "DSAl9nvvyfva",
|
||||
"amount": 16,
|
||||
"secret": "d_PPc5KpuAB2M60WYAW5-Q",
|
||||
"C": "0270e0a37f7a0b21eab43af751dd3c03f61f04c626c0448f603f1d1f5ae5a7d7e6"
|
||||
}
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```
|
||||
W3siaWQiOiAiRFNBbDludnZ5ZnZhIiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiRGJSS0l5YTBldGR3STVzRkFOMEFYUSIsICJDIjogIjAyZGY3ZjJmYzI5NjMxYjcxYTFkYjExYzE2M2IwYjFjYjQwNDQ0YWEyYjNkMjUzZDQzYjY4ZDc3YTcyZWQyZDYyNSJ9LCB7ImlkIjogIkRTQWw5bnZ2eWZ2YSIsICJhbW91bnQiOiAxNiwgInNlY3JldCI6ICJkX1BQYzVLcHVBQjJNNjBXWUFXNS1RIiwgIkMiOiAiMDI3MGUwYTM3ZjdhMGIyMWVhYjQzYWY3NTFkZDNjMDNmNjFmMDRjNjI2YzA0NDhmNjAzZjFkMWY1YWU1YTdkN2U2In1d
|
||||
```
|
||||
60
docs/specs/01.md
Normal file
60
docs/specs/01.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# NUT-1 - Mint public key exchange
|
||||
|
||||
This describes the basic exchange of the public mint keys that the wallet user `Alice` uses to unblind `Bob`'s signature.
|
||||
|
||||
## Description
|
||||
|
||||
Wallet user `Alice` receives public keys from mint `Bob` via `GET /keys` and stores them in a key-value store like a dictionary. The set of all public keys for each supported amount is called a *keyset*.
|
||||
|
||||
Mint `Bob` responds with his *active* [keyset][02]. The active keyset is the keyset a mint currently uses to sign promises with. The active keyset can change over time, for example due to key rotation. A mint MAY support older keysets indefinetely. Note that a mint can support multiple keysets at the same time but will only respond with the active keyset on the endpoint `GET /keys`. A wallet can ask for the keys of a specific (non-active) keyset by using the endpint `GET /keys/{keyset_id}` (see #2 [TODO: Link #2]).
|
||||
|
||||
See [TODO: Link #2] for how a wallet deals with multiple keysets.
|
||||
|
||||
Keysets are received as a JSON of the form `{<amount_1> : <mint_pubkey_1>, <amount_2> : ...}` for each `<amount_i>` of the amounts the mint `Bob` supports and the corresponding public key `<mint_pubkey_1>`, that is `K_i` (see #0 [TODO: Link #0]).
|
||||
|
||||
## Example
|
||||
|
||||
Request of `Alice`:
|
||||
|
||||
```http
|
||||
GET https://mint.host:3338/keys
|
||||
```
|
||||
|
||||
With curl:
|
||||
|
||||
```bash
|
||||
curl -X GET https://mint.host:3338/keys
|
||||
```
|
||||
|
||||
Response of `Bob`:
|
||||
|
||||
```json
|
||||
{
|
||||
"1": "03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc",
|
||||
"2": "03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de",
|
||||
"4": "02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303",
|
||||
"8": "02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
[00]: 00.md
|
||||
[01]: 02.md
|
||||
[03]: 03.md
|
||||
[04]: 04.md
|
||||
[05]: 05.md
|
||||
[06]: 06.md
|
||||
[07]: 07.md
|
||||
[08]: 08.md
|
||||
[09]: 09.md
|
||||
[10]: 10.md
|
||||
[11]: 11.md
|
||||
[12]: 12.md
|
||||
[13]: 13.md
|
||||
[14]: 14.md
|
||||
[15]: 15.md
|
||||
[16]: 16.md
|
||||
[17]: 17.md
|
||||
[18]: 18.md
|
||||
[19]: 19.md
|
||||
[20]: 20.md
|
||||
79
docs/specs/02.md
Normal file
79
docs/specs/02.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# NUT-2 - Keysets and keyset ID
|
||||
|
||||
A keyset is a set of public keys that the mint `Bob` generates and shares with its users. It refers to the set of public keys that each correspond to the amount values that the mint supports (e.g. 1, 2, 4, 8, ...) respectively.
|
||||
|
||||
## Requesting mint keyset IDs
|
||||
|
||||
A mint can have multiple keysets at the same time but **MUST** have only one *active* keyset (see #1 [TODO: Link #1]). A wallet can ask the mint for all active keyset IDs via the `GET /keysets` endpoint. A wallet **CAN** request the list of active keyset IDs from the mint upon startup and, if it does so, **MUST** choose only tokens from its database that have a keyset ID supported by the mint to interact with it.
|
||||
|
||||
This is useful in the case a wallet interacts with multiple mints. That way, a wallet always knows which tokens it can use with the mint it is currently interacting with.
|
||||
|
||||
## Example
|
||||
|
||||
Request of `Alice`:
|
||||
|
||||
```http
|
||||
GET https://mint.host:3338/keysets
|
||||
```
|
||||
|
||||
With curl:
|
||||
|
||||
```bash
|
||||
curl -X GET https://mint.host:3338/keysets
|
||||
```
|
||||
|
||||
Response of `Bob`:
|
||||
|
||||
```json
|
||||
{
|
||||
"keysets": [
|
||||
"DSAl9nvvyfva",
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 2.1 - Generating a keyset
|
||||
|
||||
A keyset is generated by the mint by a single seed `s` from which the private keys `k_i` are derived, with `i` being the index of the amount value. The derivation for the elements `k_i` goes like this:
|
||||
|
||||
```python
|
||||
for i in range(MAX_ORDER):
|
||||
k_i = HASH_SHA256(s + D + i)[:32]
|
||||
```
|
||||
|
||||
Here, `MAX_ORDER` refers to the order of the maximum token value that the mint supports, i.e., `2^MAX_ORDER`. Typically, `MAX_ORDER = 64`. `D` refers to a derivation path that is chosen by the mint. The derivation path can be used to rotate keys over time or to service multiple parallel mints with a single instance. `i` is the string representation of the index of the amount value, i.e., `0`, `1`, and so on.
|
||||
|
||||
## 2.2 - Keyset ID
|
||||
|
||||
A keyset ID is an identifier for a specific keyset. It can be derived by anyone who knows the set of public keys of a mint. The keyset ID **CAN** be stored in a Cashu token [TODO: Link to definition of token] such that the token can be used to identify which mint or keyset it was generated from.
|
||||
|
||||
### 2.2.1 - Storing the keyset ID in a token
|
||||
|
||||
A wallet can use the keyset ID in a token to recognize a mint it was issued by. For example, a wallet might store the `MINT_URL` together with the `keyset_id` in its database the first time it receives a keyset from that mint. That way, a wallet can know which mint to contact when it receives a token with a `keyset_id` of a mint that it has interacted with before.
|
||||
|
||||
[TODO: V2 tokens include the `MINT_URL` to enable the first contact when a wallet recieves a token from a mint it has never met before.]
|
||||
|
||||
### 2.2.2 - Deriving the keyset ID
|
||||
|
||||
The mint and the wallets of its users can derive a keyset ID from the keyset of the mint. To derive the keyset ID of a keyset, execute the following steps:
|
||||
|
||||
```
|
||||
1 - sort keyset by amount
|
||||
2 - concatenate all (sorted) public keys to one string
|
||||
3 - HASH_SHA256 the concatenated public keys
|
||||
4 - take the first 12 characters of the hash
|
||||
```
|
||||
|
||||
An example implementation in Python:
|
||||
|
||||
```python
|
||||
def derive_keyset_id(keys: Dict[int, PublicKey]):
|
||||
"""Deterministic derivation keyset_id from set of public keys."""
|
||||
sorted_keys = dict(sorted(keys.items()))
|
||||
pubkeys_concat = "".join([p.serialize().hex() for _, p in sorted_keys.items()])
|
||||
return base64.b64encode(
|
||||
hashlib.sha256((pubkeys_concat).encode("utf-8")).digest()
|
||||
).decode()[:12]
|
||||
```
|
||||
|
||||
30
docs/specs/03.md
Normal file
30
docs/specs/03.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# NUT-3 - Request mint
|
||||
|
||||
Minting tokens is a two-step process: requesting a mint and minting the tokens. Here, we describe the first step. A wallet requests the minting of tokens in exchange for paying a bolt11 Lightning invoice (typically generated by the mint to add funds to its reserves, and typically paid with another Lightning wallet).
|
||||
|
||||
To request the minting of tokens, a wallet `Alice` sends a `GET /mint&amount=<amount_sat>` request with the requested amount `<amount_sat>` in satoshis. The mint `Bob` then responds with a Lightning invoice.
|
||||
|
||||
## Example
|
||||
|
||||
Request of `Alice`:
|
||||
|
||||
```http
|
||||
GET https://mint.host:3338/mint&amount=1000
|
||||
```
|
||||
|
||||
With curl:
|
||||
|
||||
```bash
|
||||
curl -X GET https://mint.host:3338/mint&amount=1000
|
||||
```
|
||||
|
||||
Response of `Bob`:
|
||||
|
||||
```json
|
||||
{
|
||||
"pr": "lnbc100n1p3kdrv5sp5lpdxzghe5j67q...",
|
||||
"hash": "67d1d9ea6ada225c115418671b64a..."
|
||||
}
|
||||
```
|
||||
|
||||
with `pr` being the bolt11 payment request and `hash` the hash of the invoice. A wallet **MUST** store the `hash` and `amount_sat` in its database to later request the tokens upon paying the invoice. A wallet **SHOULD** then present the payment request (for example via QR code) to the user such that they can pay the invoice with another Lightning wallet. After the user has paid the invoice, a wallet **MUST** continue with #4 - Minting tokens [TODO: Link to #4].
|
||||
94
docs/specs/04.md
Normal file
94
docs/specs/04.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# NUT-4 - Mint tokens
|
||||
|
||||
After requesting a mint (see #3 [TODO: Link]) and paying the invoice that was returned by the mint, a wallet proceeds with requesting tokens from the mint in return for paying the invoice.
|
||||
|
||||
For that, a wallet sends a `POST /mint&payment_hash=<hash>` request with a JSON body to the mint. The body **MUST** include `BlindedMessages` that are worth a maximum of `<amount_sat>` [TODO: Refer to BlindedMessages]. If successful (i.e. the invoice has been previously paid and the `BlindedMessages` are valid), the mint responds with `Promises` [TODO: Link Promises].
|
||||
|
||||
## Example
|
||||
|
||||
Request of `Alice`:
|
||||
|
||||
```http
|
||||
POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a
|
||||
```
|
||||
|
||||
With the data being of the form `MintRequest`:
|
||||
|
||||
```json
|
||||
{
|
||||
"blinded_messages":
|
||||
[
|
||||
BlindedMessage,
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
With curl:
|
||||
|
||||
```bash
|
||||
curl -X POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a -d \
|
||||
{
|
||||
"blinded_messages":
|
||||
[
|
||||
{
|
||||
"amount": 2,
|
||||
"B_": "02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239"
|
||||
},
|
||||
{
|
||||
"amount": 8,
|
||||
"B_": "03b54ab451b15005f2c64d38fc512fca695914c8fd5094ee044e5724ad41fda247"
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Response of `Bob`:
|
||||
|
||||
If the invoice was successfully paid, `Bob` responds with a `PostMintResponse` which is essentially a list of `BlindedSignature`'s [TODO: Link PostMintResponse]
|
||||
|
||||
```json
|
||||
{
|
||||
"promises":
|
||||
[
|
||||
{
|
||||
"id": "DSAl9nvvyfva",
|
||||
"amount": 2,
|
||||
"C_": "03e61daa438fc7bcc53f6920ec6c8c357c24094fb04c1fc60e2606df4910b21ffb"
|
||||
},
|
||||
{
|
||||
"id": "DSAl9nvvyfva",
|
||||
"amount": 8,
|
||||
"C_": "03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
If the invoice was not paid yet, `Bob` responds with an error. In that case, `Alice` **CAN** repeat the same response until the Lightning invoice is settled.
|
||||
|
||||
## Unblinding signatures
|
||||
|
||||
Upon receiving the `PostMintResponse` with the list of `BlindedSignature`'s from the mint `Bob`, a wallet `Alice` **MUST** then unblind the `BlindedSignature`'s from `Bob` (see #0 Notation [TODO: Link to unblinding]) to generate a list of `Proof`'s. A `Proof` is effectively an ecash `Token` and can later be used to redeem the token. The wallet **MUST** store the `Proof` in its database.
|
||||
|
||||
A list multiple `Proof`'s is called `Proofs` and has the form:
|
||||
|
||||
```json
|
||||
{
|
||||
"proofs" :
|
||||
[
|
||||
{
|
||||
"id": "DSAl9nvvyfva",
|
||||
"amount": 2,
|
||||
"secret": "S+tDfc1Lfsrb06zaRdVTed6Izg",
|
||||
"C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3"
|
||||
},
|
||||
{
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
59
docs/specs/05.md
Normal file
59
docs/specs/05.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# NUT-5 - Melting tokens
|
||||
|
||||
Melting tokens is the opposite of minting them (see #4): the wallet `Alice` sends `Proofs` to the mint `Bob` together with a bolt11 Lightning invoice that `Alice` wants to be paid. To melt tokens, `Alice` sends a `POST /melt` request with a JSON body to the mint. The `Proofs` included in the request will be burned by the mint and the mint will pay the invoice in exchange.
|
||||
|
||||
`Alice`'s request **MUST** include a `MeltRequest` ([TODO: Link MeltRequest]) JSON body with `Proofs` that have at least the amount of the invoice to be paid.
|
||||
|
||||
## Example
|
||||
|
||||
**Request** of `Alice`:
|
||||
|
||||
```http
|
||||
POST https://mint.host:3338/melt
|
||||
```
|
||||
|
||||
With the data being of the form `MeltRequest`:
|
||||
|
||||
```json
|
||||
{
|
||||
"proofs":
|
||||
[
|
||||
Proof,
|
||||
...
|
||||
],
|
||||
"invoice": str
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
With curl:
|
||||
|
||||
```bash
|
||||
curl -X POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a -d \
|
||||
{
|
||||
"proofs" :
|
||||
[
|
||||
{
|
||||
"id": "DSAl9nvvyfva",
|
||||
"amount": 2,
|
||||
"secret": "S+tDfc1Lfsrb06zaRdVTed6Izg",
|
||||
"C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3"
|
||||
},
|
||||
{
|
||||
...
|
||||
}
|
||||
],
|
||||
"invoice": "lnbc100n1p3kdrv5sp5lpdxzghe5j67q..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response** `PostMeltResponse` from `Bob`:
|
||||
|
||||
```json
|
||||
{
|
||||
"paid": true,
|
||||
"preimage": "da225c115418671b64a67d1d9ea6a..."
|
||||
}
|
||||
```
|
||||
|
||||
Only if the `paid==true`, the wallet `Alice` **MUST** delete the `Proofs` from her database (or move them to a history). If `paid==false`, `Alice` **CAN** repeat the same multiple times until the payment is successful.
|
||||
68
docs/specs/06.md
Normal file
68
docs/specs/06.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# NUT-6 - Split tokens
|
||||
|
||||
The split operation is the most important component of the Cashu system. The wallet `Alice` can use it to redeem tokens (i.e. receive new ones in return) that she received from `Carol`, or she can split her own tokens to a target amount she needs to send to `Carol`, if she does not have the necessary amounts to compose the target amount in her wallet already.
|
||||
|
||||
The basic idea is that `Alice` sends `Bob` a set of `Proof`'s and a set of `BlindedMessage`'s with an equal amount. Additionally, she specifies the `amount` at which she would like to have the split.
|
||||
|
||||
## 6.1 - Split to send
|
||||
|
||||
To make this more clear, we make an example of a typical case of sending tokens from `Alice` to `Carol`:
|
||||
|
||||
`Alice` has 64 satoshis in her wallet, composed of two tokens, one worth 32 sats and another two worth 16 sats. She wants to send `Carol` 40 sats but does not have the necessary tokens to combine them to reach the exact target amount of 40 sats. `Alice` requests a split from the mint. For that, she sends the mint `Bob` her tokens (`Proofs`) worth `[32, 16, 16]` and asks for a split at amount 40. The mint will then return her new tokens with the amounts `[32, 8, 16, 8]`. Notice that the first two tokens can now be combined to 40 sats. The original tokens that `Alice` sent to `Bob` are now invalidated.
|
||||
|
||||
## 6.2 - Split to receive
|
||||
|
||||
Another case of how split can be useful becomes apparent if we follow up the example above where `Alice` split her tokens ready to be sent to `Carol`. `Carol` can receive these tokens, which means to invalidate the tokens she receives and redeem them for new ones, using the same mechanism. Only if `Carol` redeems them for new tokens that only she can spend, `Alice` can't double-spend them anymore and this simple transaction can be considered settled. `Carol` requests a split of the tokens (`Proofs`) worth `[32, 8]` at the amount `40` (the total amount) to receive back new tokens with the same total amount.
|
||||
|
||||
## Example
|
||||
|
||||
**Request** of `Alice`:
|
||||
|
||||
```http
|
||||
POST https://mint.host:3338/split
|
||||
```
|
||||
|
||||
With the data being of the form `SplitRequest`:
|
||||
|
||||
```json
|
||||
{
|
||||
"proofs": Proofs,
|
||||
"outputs": MintRequest,
|
||||
"amount": int
|
||||
}
|
||||
```
|
||||
|
||||
With curl:
|
||||
|
||||
```bash
|
||||
curl -X POST https://mint.host:3338/split -d \
|
||||
{
|
||||
"proofs":
|
||||
[
|
||||
{
|
||||
"id": "DSAl9nvvyfva",
|
||||
"amount": 2,
|
||||
"secret": "S+tDfc1Lfsrb06zaRdVTed6Izg",
|
||||
"C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3"
|
||||
},
|
||||
{
|
||||
...
|
||||
}
|
||||
],
|
||||
"outputs":{
|
||||
"blinded_messages":
|
||||
[
|
||||
{
|
||||
"amount": 2,
|
||||
"B_": "02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239"
|
||||
},
|
||||
{
|
||||
...
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": 40
|
||||
}
|
||||
```
|
||||
|
||||
If successful, `Bob` will respond
|
||||
36
docs/specs/README.md
Normal file
36
docs/specs/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Cashu NUTs (Notation, Usage, and Terminology)
|
||||
|
||||
|
||||
| Number | Description | Wallets |
|
||||
|----------|-------------------------------------------------------------|---------|
|
||||
| [00][00] | Notation and Models | Python-CLI, Feni, LNbits
|
||||
| [01][01] | Mint public keys | Python-CLI, Feni, LNbits
|
||||
| [02][02] | Keysets and keyset IDs | Python-CLI, Feni, LNbits
|
||||
| [03][03] | Requesting a mint | Python-CLI, Feni, LNbits
|
||||
| [04][04] | Mint tokens | Python-CLI, Feni, LNbits
|
||||
| [05][05] | Melt tokens | Python-CLI, Feni, LNbits
|
||||
| [06][06] | Split tokens | Python-CLI, Feni, LNbits
|
||||
|
||||
|
||||
|
||||
[00]: 00.md
|
||||
[01]: 01.md
|
||||
[02]: 02.md
|
||||
[03]: 03.md
|
||||
[04]: 04.md
|
||||
[05]: 05.md
|
||||
[06]: 06.md
|
||||
[07]: 07.md
|
||||
[08]: 08.md
|
||||
[09]: 09.md
|
||||
[10]: 10.md
|
||||
[11]: 11.md
|
||||
[12]: 12.md
|
||||
[13]: 13.md
|
||||
[14]: 14.md
|
||||
[15]: 15.md
|
||||
[16]: 16.md
|
||||
[17]: 17.md
|
||||
[18]: 18.md
|
||||
[19]: 19.md
|
||||
[20]: 20.md
|
||||
@@ -17,12 +17,12 @@ Mint: `Bob`
|
||||
|
||||
# Blind Diffie-Hellmann key exchange (BDH)
|
||||
- Mint `Bob` publishes `K = kG`
|
||||
- `Alice` picks secret `x` and computes `Y = hash_to_point(x)`
|
||||
- `Alice` picks secret `x` and computes `Y = hash_to_curve(x)`
|
||||
- `Alice` sends to `Bob`: `T = Y + rG` with `r` being a random nonce
|
||||
- `Bob` sends back to `Alice` blinded key: `Q = kT` (these two steps are the DH key exchange)
|
||||
- `Alice` can calculate the unblinded key as `Q - rK = kY + krG - krG = kY = Z`
|
||||
- Alice can take the pair `(x, Z)` as a token and can send it to `Carol`.
|
||||
- `Carol` can send `(x, Z)` to `Bob` who then checks that `k*hash_to_point(x) == Z`, and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets.
|
||||
- `Carol` can send `(x, Z)` to `Bob` who then checks that `k*hash_to_curve(x) == Z`, and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets.
|
||||
|
||||
# Cashu client protocol
|
||||
|
||||
@@ -38,7 +38,7 @@ Mint: `Bob`
|
||||
- `Alice` pays bolt11 invoice `payment_request` using a Bitcoin Lightning wallet.
|
||||
|
||||
### Step 2: Request tokens
|
||||
- To request tokens of value `amount : int`, `Alice` decomposes `amount` into a sum of values of `2^n`, e.g. `13` is `amounts : List[int] = [1, 4, 8]`. This can be easily done by representing `amount` as binary and using each binary digit that is `1` as part of the sum, e.g. `8` would be `1101` wich is `2^0 + 2^2 + 2^3`. In this example, `Alice` will request `N = len(amounts) = 3` tokens.
|
||||
- To request tokens of value `amount : int`, `Alice` decomposes `amount` into a sum of values of `2^n`, e.g. `13` is `amounts : List[int] = [1, 4, 8]`. This can be easily done by representing `amount` as binary and using each binary digit that is `1` as part of the sum, e.g. `13` would be `1101` wich is `2^0 + 2^2 + 2^3`. In this example, `Alice` will request `N = len(amounts) = 3` tokens.
|
||||
- `Alice` generates a random secret string `x_i` of `128` random bits with `i \in [0,..,N-1]`for each of the `N` requested tokens and encodes them in `base64`. [*TODO: remove index i*]
|
||||
- `Alice` remembers `x` for the construction of the proof in Step 5.
|
||||
|
||||
@@ -122,4 +122,4 @@ Here we describe how `Alice` can request from `Bob` to make a Lightning payment
|
||||
# Todo:
|
||||
- Call subsections 1. and 1.2 etc so they can be referenced
|
||||
- Define objets like `MintRequest` and `SplitRequests` once when they appear and reuse them.
|
||||
- Clarify whether a `TOKEN` is a single Proof or a list of Proofs
|
||||
- Clarify whether a `TOKEN` is a single Proof or a list of Proofs
|
||||
|
||||
452
poetry.lock
generated
452
poetry.lock
generated
@@ -1,6 +1,6 @@
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "3.6.1"
|
||||
version = "3.6.2"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -14,21 +14,23 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
|
||||
[package.extras]
|
||||
doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||
test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"]
|
||||
trio = ["trio (>=0.16)"]
|
||||
trio = ["trio (>=0.16,<0.22)"]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "22.1.0"
|
||||
version = "22.2.0"
|
||||
description = "Classes Without Boilerplate"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
|
||||
tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
|
||||
cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
|
||||
dev = ["attrs[docs,tests]"]
|
||||
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"]
|
||||
tests = ["attrs[tests-no-zope]", "zope.interface"]
|
||||
tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=0.971,<0.990)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
tests_no_zope = ["cloudpickle", "hypothesis", "mypy (>=0.971,<0.990)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
|
||||
[[package]]
|
||||
name = "bech32"
|
||||
@@ -48,11 +50,11 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "22.8.0"
|
||||
version = "22.12.0"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.2"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0"
|
||||
@@ -71,7 +73,7 @@ uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2022.9.24"
|
||||
version = "2022.12.7"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -113,11 +115,44 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.0.1"
|
||||
description = "Code coverage measurement for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
|
||||
|
||||
[package.extras]
|
||||
toml = ["tomli"]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "38.0.4"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
cffi = ">=1.12"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
|
||||
docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
|
||||
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
|
||||
sdist = ["setuptools-rust (>=0.11.4)"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"]
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
@@ -152,6 +187,17 @@ django = ["dj-database-url", "dj-email-url", "django-cache-url"]
|
||||
lint = ["flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)"]
|
||||
tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.1.0"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.83.0"
|
||||
@@ -167,19 +213,16 @@ starlette = "0.19.1"
|
||||
[package.extras]
|
||||
all = ["email_validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"]
|
||||
dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"]
|
||||
doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.2)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"]
|
||||
doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"]
|
||||
test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
version = "0.12.0"
|
||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
@@ -191,7 +234,7 @@ python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "5.0.0"
|
||||
version = "5.2.0"
|
||||
description = "Read metadata from Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -202,7 +245,7 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
||||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
|
||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
perf = ["ipython"]
|
||||
testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
|
||||
|
||||
@@ -216,17 +259,17 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "5.10.1"
|
||||
version = "5.11.4"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.1,<4.0"
|
||||
python-versions = ">=3.7.0"
|
||||
|
||||
[package.extras]
|
||||
colors = ["colorama (>=0.4.3,<0.5.0)"]
|
||||
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
|
||||
pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
|
||||
plugins = ["setuptools"]
|
||||
requirements_deprecated_finder = ["pip-api", "pipreqs"]
|
||||
requirements-deprecated-finder = ["pip-api", "pipreqs"]
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
@@ -245,7 +288,7 @@ dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils
|
||||
|
||||
[[package]]
|
||||
name = "marshmallow"
|
||||
version = "3.18.0"
|
||||
version = "3.19.0"
|
||||
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -255,9 +298,9 @@ python-versions = ">=3.7"
|
||||
packaging = ">=17.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["flake8 (==5.0.4)", "flake8-bugbear (==22.9.11)", "mypy (==0.971)", "pre-commit (>=2.4,<3.0)", "pytest", "pytz", "simplejson", "tox"]
|
||||
docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.9)", "sphinx (==5.1.1)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"]
|
||||
lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.9.11)", "mypy (==0.971)", "pre-commit (>=2.4,<3.0)"]
|
||||
dev = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)", "pytest", "pytz", "simplejson", "tox"]
|
||||
docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.9)", "sphinx (==5.3.0)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"]
|
||||
lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)"]
|
||||
tests = ["pytest", "pytz", "simplejson"]
|
||||
|
||||
[[package]]
|
||||
@@ -300,18 +343,15 @@ attrs = ">=19.2.0"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "21.3"
|
||||
version = "22.0"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.10.1"
|
||||
version = "0.10.3"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -319,15 +359,15 @@ python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "2.5.2"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
version = "2.6.0"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
|
||||
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
|
||||
docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"]
|
||||
test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
@@ -344,14 +384,6 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "py"
|
||||
version = "1.11.0"
|
||||
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.21"
|
||||
@@ -360,6 +392,14 @@ category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pycryptodomex"
|
||||
version = "3.16.0"
|
||||
description = "Cryptographic library for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "1.10.2"
|
||||
@@ -376,19 +416,16 @@ dotenv = ["python-dotenv (>=0.10.4)"]
|
||||
email = ["email-validator (>=1.0.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.0.9"
|
||||
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
||||
name = "PySocks"
|
||||
version = "1.7.1"
|
||||
description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.8"
|
||||
|
||||
[package.extras]
|
||||
diagrams = ["jinja2", "railroad-diagrams"]
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.1.3"
|
||||
version = "7.2.0"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -397,12 +434,12 @@ python-versions = ">=3.7"
|
||||
[package.dependencies]
|
||||
attrs = ">=19.2.0"
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
py = ">=1.8.2"
|
||||
tomli = ">=1.0.0"
|
||||
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||
@@ -422,6 +459,21 @@ typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""}
|
||||
[package.extras]
|
||||
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "4.0.0"
|
||||
description = "Pytest plugin for measuring coverage."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
coverage = {version = ">=5.2.1", extras = ["toml"]}
|
||||
pytest = ">=4.6"
|
||||
|
||||
[package.extras]
|
||||
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
|
||||
|
||||
[[package]]
|
||||
name = "python-bitcoinlib"
|
||||
version = "0.11.2"
|
||||
@@ -484,6 +536,19 @@ python-versions = "*"
|
||||
[package.dependencies]
|
||||
cffi = ">=1.3.0"
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "65.6.3"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
@@ -571,7 +636,7 @@ python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.3.0"
|
||||
version = "4.4.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -579,11 +644,11 @@ python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.12"
|
||||
version = "1.26.13"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
|
||||
@@ -606,6 +671,30 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
|
||||
[package.extras]
|
||||
standard = ["colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "websocket-client"
|
||||
version = "1.3.3"
|
||||
description = "WebSocket client for Python with low level API options"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"]
|
||||
optional = ["python-socks", "wsaccel"]
|
||||
test = ["websockets"]
|
||||
|
||||
[[package]]
|
||||
name = "wheel"
|
||||
version = "0.38.4"
|
||||
description = "A built-package format for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=3.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "win32-setctime"
|
||||
version = "1.1.0"
|
||||
@@ -619,29 +708,29 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.8.1"
|
||||
version = "3.11.0"
|
||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"]
|
||||
testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
|
||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
|
||||
testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "b4e980ee90226bab07750b1becc8c69df7752f6d168d200a79c782aa1efe61da"
|
||||
content-hash = "f7238d56229c2e957585fc22331529facc751262430698369fe5a00396344401"
|
||||
|
||||
[metadata.files]
|
||||
anyio = [
|
||||
{file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"},
|
||||
{file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"},
|
||||
{file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"},
|
||||
{file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
|
||||
{file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
|
||||
{file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"},
|
||||
{file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"},
|
||||
]
|
||||
bech32 = [
|
||||
{file = "bech32-1.2.0-py3-none-any.whl", hash = "sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"},
|
||||
@@ -653,33 +742,22 @@ bitstring = [
|
||||
{file = "bitstring-3.1.9.tar.gz", hash = "sha256:a5848a3f63111785224dca8bb4c0a75b62ecdef56a042c8d6be74b16f7e860e7"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"},
|
||||
{file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"},
|
||||
{file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"},
|
||||
{file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"},
|
||||
{file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"},
|
||||
{file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"},
|
||||
{file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"},
|
||||
{file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"},
|
||||
{file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"},
|
||||
{file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"},
|
||||
{file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"},
|
||||
{file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"},
|
||||
{file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"},
|
||||
{file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"},
|
||||
{file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"},
|
||||
{file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"},
|
||||
{file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"},
|
||||
{file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"},
|
||||
{file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"},
|
||||
{file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"},
|
||||
{file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"},
|
||||
{file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"},
|
||||
{file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"},
|
||||
{file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"},
|
||||
{file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"},
|
||||
{file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"},
|
||||
{file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"},
|
||||
{file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"},
|
||||
{file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"},
|
||||
{file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"},
|
||||
{file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"},
|
||||
{file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"},
|
||||
{file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"},
|
||||
{file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"},
|
||||
{file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
|
||||
{file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
|
||||
{file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
|
||||
{file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
|
||||
]
|
||||
cffi = [
|
||||
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
|
||||
@@ -756,8 +834,89 @@ click = [
|
||||
{file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
|
||||
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
coverage = [
|
||||
{file = "coverage-7.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b3695c4f4750bca943b3e1f74ad4be8d29e4aeab927d50772c41359107bd5d5c"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa6a5a224b7f4cfb226f4fc55a57e8537fcc096f42219128c2c74c0e7d0953e1"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74f70cd92669394eaf8d7756d1b195c8032cf7bbbdfce3bc489d4e15b3b8cf73"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b66bb21a23680dee0be66557dc6b02a3152ddb55edf9f6723fa4a93368f7158d"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d87717959d4d0ee9db08a0f1d80d21eb585aafe30f9b0a54ecf779a69cb015f6"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:854f22fa361d1ff914c7efa347398374cc7d567bdafa48ac3aa22334650dfba2"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1e414dc32ee5c3f36544ea466b6f52f28a7af788653744b8570d0bf12ff34bc0"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6c5ad996c6fa4d8ed669cfa1e8551348729d008a2caf81489ab9ea67cfbc7498"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-win32.whl", hash = "sha256:691571f31ace1837838b7e421d3a09a8c00b4aac32efacb4fc9bd0a5c647d25a"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:89caf4425fe88889e2973a8e9a3f6f5f9bbe5dd411d7d521e86428c08a873a4a"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:63d56165a7c76265468d7e0c5548215a5ba515fc2cba5232d17df97bffa10f6c"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f943a3b2bc520102dd3e0bb465e1286e12c9a54f58accd71b9e65324d9c7c01"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:830525361249dc4cd013652b0efad645a385707a5ae49350c894b67d23fbb07c"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd1b9c5adc066db699ccf7fa839189a649afcdd9e02cb5dc9d24e67e7922737d"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00c14720b8b3b6c23b487e70bd406abafc976ddc50490f645166f111c419c39"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6d55d840e1b8c0002fce66443e124e8581f30f9ead2e54fbf6709fb593181f2c"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:66b18c3cf8bbab0cce0d7b9e4262dc830e93588986865a8c78ab2ae324b3ed56"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:12a5aa77783d49e05439fbe6e6b427484f8a0f9f456b46a51d8aac022cfd024d"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-win32.whl", hash = "sha256:b77015d1cb8fe941be1222a5a8b4e3fbca88180cfa7e2d4a4e58aeabadef0ab7"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb992c47cb1e5bd6a01e97182400bcc2ba2077080a17fcd7be23aaa6e572e390"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e78e9dcbf4f3853d3ae18a8f9272111242531535ec9e1009fa8ec4a2b74557dc"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e60bef2e2416f15fdc05772bf87db06c6a6f9870d1db08fdd019fbec98ae24a9"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9823e4789ab70f3ec88724bba1a203f2856331986cd893dedbe3e23a6cfc1e4e"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9158f8fb06747ac17bd237930c4372336edc85b6e13bdc778e60f9d685c3ca37"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:486ee81fa694b4b796fc5617e376326a088f7b9729c74d9defa211813f3861e4"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1285648428a6101b5f41a18991c84f1c3959cee359e51b8375c5882fc364a13f"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2c44fcfb3781b41409d0f060a4ed748537557de9362a8a9282182fafb7a76ab4"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-win32.whl", hash = "sha256:d6814854c02cbcd9c873c0f3286a02e3ac1250625cca822ca6bc1018c5b19f1c"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f66460f17c9319ea4f91c165d46840314f0a7c004720b20be58594d162a441d8"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b373c9345c584bb4b5f5b8840df7f4ab48c4cbb7934b58d52c57020d911b856"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d3022c3007d3267a880b5adcf18c2a9bf1fc64469b394a804886b401959b8742"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92651580bd46519067e36493acb394ea0607b55b45bd81dd4e26379ed1871f55"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cfc595d2af13856505631be072835c59f1acf30028d1c860b435c5fc9c15b69"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b4b3a4d9915b2be879aff6299c0a6129f3d08a775d5a061f503cf79571f73e4"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b6f22bb64cc39bcb883e5910f99a27b200fdc14cdd79df8696fa96b0005c9444"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72d1507f152abacea81f65fee38e4ef3ac3c02ff8bc16f21d935fd3a8a4ad910"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a79137fc99815fff6a852c233628e735ec15903cfd16da0f229d9c4d45926ab"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-win32.whl", hash = "sha256:b3763e7fcade2ff6c8e62340af9277f54336920489ceb6a8cd6cc96da52fcc62"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:09f6b5a8415b6b3e136d5fec62b552972187265cb705097bf030eb9d4ffb9b60"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:978258fec36c154b5e250d356c59af7d4c3ba02bef4b99cda90b6029441d797d"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:19ec666533f0f70a0993f88b8273057b96c07b9d26457b41863ccd021a043b9a"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfded268092a84605f1cc19e5c737f9ce630a8900a3589e9289622db161967e9"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07bcfb1d8ac94af886b54e18a88b393f6a73d5959bb31e46644a02453c36e475"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b4a923cc7566bbc7ae2dfd0ba5a039b61d19c740f1373791f2ebd11caea59"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aec2d1515d9d39ff270059fd3afbb3b44e6ec5758af73caf18991807138c7118"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c20cfebcc149a4c212f6491a5f9ff56f41829cd4f607b5be71bb2d530ef243b1"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fd556ff16a57a070ce4f31c635953cc44e25244f91a0378c6e9bdfd40fdb249f"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-win32.whl", hash = "sha256:b9ea158775c7c2d3e54530a92da79496fb3fb577c876eec761c23e028f1e216c"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:d1991f1dd95eba69d2cd7708ff6c2bbd2426160ffc73c2b81f617a053ebcb1a8"},
|
||||
{file = "coverage-7.0.1-pp37.pp38.pp39-none-any.whl", hash = "sha256:3dd4ee135e08037f458425b8842d24a95a0961831a33f89685ff86b77d378f89"},
|
||||
{file = "coverage-7.0.1.tar.gz", hash = "sha256:a4a574a19eeb67575a5328a5760bbbb737faa685616586a9f9da4281f940109c"},
|
||||
]
|
||||
cryptography = [
|
||||
{file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70"},
|
||||
{file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb"},
|
||||
{file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d"},
|
||||
{file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1"},
|
||||
{file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8"},
|
||||
{file = "cryptography-38.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db"},
|
||||
{file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b"},
|
||||
{file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c"},
|
||||
{file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00"},
|
||||
{file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0"},
|
||||
{file = "cryptography-38.0.4-cp36-abi3-win32.whl", hash = "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744"},
|
||||
{file = "cryptography-38.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d"},
|
||||
{file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353"},
|
||||
{file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7"},
|
||||
{file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd"},
|
||||
{file = "cryptography-38.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2"},
|
||||
{file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b"},
|
||||
{file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6"},
|
||||
{file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876"},
|
||||
{file = "cryptography-38.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b"},
|
||||
{file = "cryptography-38.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285"},
|
||||
{file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b"},
|
||||
{file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083"},
|
||||
{file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee"},
|
||||
{file = "cryptography-38.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9"},
|
||||
{file = "cryptography-38.0.4.tar.gz", hash = "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290"},
|
||||
]
|
||||
ecdsa = [
|
||||
{file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"},
|
||||
@@ -767,37 +926,41 @@ environs = [
|
||||
{file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"},
|
||||
{file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"},
|
||||
]
|
||||
exceptiongroup = [
|
||||
{file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"},
|
||||
{file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"},
|
||||
]
|
||||
fastapi = [
|
||||
{file = "fastapi-0.83.0-py3-none-any.whl", hash = "sha256:694a2b6c2607a61029a4be1c6613f84d74019cb9f7a41c7a475dca8e715f9368"},
|
||||
{file = "fastapi-0.83.0.tar.gz", hash = "sha256:96eb692350fe13d7a9843c3c87a874f0d45102975257dd224903efd6c0fde3bd"},
|
||||
]
|
||||
h11 = [
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
|
||||
{file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
||||
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
||||
]
|
||||
importlib-metadata = [
|
||||
{file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"},
|
||||
{file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"},
|
||||
{file = "importlib_metadata-5.2.0-py3-none-any.whl", hash = "sha256:0eafa39ba42bf225fc00e67f701d71f85aead9f878569caf13c3724f704b970f"},
|
||||
{file = "importlib_metadata-5.2.0.tar.gz", hash = "sha256:404d48d62bba0b7a77ff9d405efd91501bef2e67ff4ace0bed40a0cf28c3c7cd"},
|
||||
]
|
||||
iniconfig = [
|
||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
]
|
||||
isort = [
|
||||
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
|
||||
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
|
||||
{file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"},
|
||||
{file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"},
|
||||
]
|
||||
loguru = [
|
||||
{file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"},
|
||||
{file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"},
|
||||
]
|
||||
marshmallow = [
|
||||
{file = "marshmallow-3.18.0-py3-none-any.whl", hash = "sha256:35e02a3a06899c9119b785c12a22f4cda361745d66a71ab691fd7610202ae104"},
|
||||
{file = "marshmallow-3.18.0.tar.gz", hash = "sha256:6804c16114f7fce1f5b4dadc31f4674af23317fcc7f075da21e35c1a35d781f7"},
|
||||
{file = "marshmallow-3.19.0-py3-none-any.whl", hash = "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b"},
|
||||
{file = "marshmallow-3.19.0.tar.gz", hash = "sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78"},
|
||||
]
|
||||
mypy = [
|
||||
{file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"},
|
||||
@@ -833,29 +996,53 @@ outcome = [
|
||||
{file = "outcome-1.2.0.tar.gz", hash = "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
|
||||
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
|
||||
{file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"},
|
||||
{file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"},
|
||||
]
|
||||
pathspec = [
|
||||
{file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"},
|
||||
{file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
|
||||
{file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"},
|
||||
{file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"},
|
||||
]
|
||||
platformdirs = [
|
||||
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
|
||||
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
|
||||
{file = "platformdirs-2.6.0-py3-none-any.whl", hash = "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca"},
|
||||
{file = "platformdirs-2.6.0.tar.gz", hash = "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e"},
|
||||
]
|
||||
pluggy = [
|
||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||
]
|
||||
py = [
|
||||
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||
]
|
||||
pycparser = [
|
||||
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
|
||||
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
||||
]
|
||||
pycryptodomex = [
|
||||
{file = "pycryptodomex-3.16.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b3d04c00d777c36972b539fb79958790126847d84ec0129fce1efef250bfe3ce"},
|
||||
{file = "pycryptodomex-3.16.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e5a670919076b71522c7d567a9043f66f14b202414a63c3a078b5831ae342c03"},
|
||||
{file = "pycryptodomex-3.16.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:ce338a9703f54b2305a408fc9890eb966b727ce72b69f225898bb4e9d9ed3f1f"},
|
||||
{file = "pycryptodomex-3.16.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:a1c0ae7123448ecb034c75c713189cb00ebe2d415b11682865b6c54d200d9c93"},
|
||||
{file = "pycryptodomex-3.16.0-cp27-cp27m-win32.whl", hash = "sha256:8851585ff19871e5d69e1790f4ca5f6fd1699d6b8b14413b472a4c0dbc7ea780"},
|
||||
{file = "pycryptodomex-3.16.0-cp27-cp27m-win_amd64.whl", hash = "sha256:8dd2d9e3c617d0712ed781a77efd84ea579e76c5f9b2a4bc0b684ebeddf868b2"},
|
||||
{file = "pycryptodomex-3.16.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:2ad9bb86b355b6104796567dd44c215b3dc953ef2fae5e0bdfb8516731df92cf"},
|
||||
{file = "pycryptodomex-3.16.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:e25a2f5667d91795f9417cb856f6df724ccdb0cdd5cbadb212ee9bf43946e9f8"},
|
||||
{file = "pycryptodomex-3.16.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:b0789a8490114a2936ed77c87792cfe77582c829cb43a6d86ede0f9624ba8aa3"},
|
||||
{file = "pycryptodomex-3.16.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:0da835af786fdd1c9930994c78b23e88d816dc3f99aa977284a21bbc26d19735"},
|
||||
{file = "pycryptodomex-3.16.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:22aed0868622d95179217c298e37ed7410025c7b29dac236d3230617d1e4ed56"},
|
||||
{file = "pycryptodomex-3.16.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1619087fb5b31510b0b0b058a54f001a5ffd91e6ffee220d9913064519c6a69d"},
|
||||
{file = "pycryptodomex-3.16.0-cp35-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:70288d9bfe16b2fd0d20b6c365db614428f1bcde7b20d56e74cf88ade905d9eb"},
|
||||
{file = "pycryptodomex-3.16.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7993d26dae4d83b8f4ce605bb0aecb8bee330bb3c95475ef06f3694403621e71"},
|
||||
{file = "pycryptodomex-3.16.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:1cda60207be8c1cf0b84b9138f9e3ca29335013d2b690774a5e94678ff29659a"},
|
||||
{file = "pycryptodomex-3.16.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:04610536921c1ec7adba158ef570348550c9f3a40bc24be9f8da2ef7ab387981"},
|
||||
{file = "pycryptodomex-3.16.0-cp35-abi3-win32.whl", hash = "sha256:daa67f5ebb6fbf1ee9c90decaa06ca7fc88a548864e5e484d52b0920a57fe8a5"},
|
||||
{file = "pycryptodomex-3.16.0-cp35-abi3-win_amd64.whl", hash = "sha256:231dc8008cbdd1ae0e34645d4523da2dbc7a88c325f0d4a59635a86ee25b41dd"},
|
||||
{file = "pycryptodomex-3.16.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:4dbbe18cc232b5980c7633972ae5417d0df76fe89e7db246eefd17ef4d8e6d7a"},
|
||||
{file = "pycryptodomex-3.16.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:893f8a97d533c66cc3a56e60dd3ed40a3494ddb4aafa7e026429a08772f8a849"},
|
||||
{file = "pycryptodomex-3.16.0-pp27-pypy_73-win32.whl", hash = "sha256:6a465e4f856d2a4f2a311807030c89166529ccf7ccc65bef398de045d49144b6"},
|
||||
{file = "pycryptodomex-3.16.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba57ac7861fd2c837cdb33daf822f2a052ff57dd769a2107807f52a36d0e8d38"},
|
||||
{file = "pycryptodomex-3.16.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f2b971a7b877348a27dcfd0e772a0343fb818df00b74078e91c008632284137d"},
|
||||
{file = "pycryptodomex-3.16.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e2453162f473c1eae4826eb10cd7bce19b5facac86d17fb5f29a570fde145abd"},
|
||||
{file = "pycryptodomex-3.16.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0ba28aa97cdd3ff5ed1a4f2b7f5cd04e721166bd75bd2b929e2734433882b583"},
|
||||
{file = "pycryptodomex-3.16.0.tar.gz", hash = "sha256:e9ba9d8ed638733c9e95664470b71d624a6def149e2db6cc52c1aca5a6a2df1d"},
|
||||
]
|
||||
pydantic = [
|
||||
{file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"},
|
||||
{file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"},
|
||||
@@ -894,18 +1081,23 @@ pydantic = [
|
||||
{file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"},
|
||||
{file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
|
||||
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
||||
PySocks = [
|
||||
{file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"},
|
||||
{file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"},
|
||||
{file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
|
||||
{file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
|
||||
{file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
|
||||
{file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
|
||||
]
|
||||
pytest-asyncio = [
|
||||
{file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"},
|
||||
{file = "pytest_asyncio-0.19.0-py3-none-any.whl", hash = "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa"},
|
||||
]
|
||||
pytest-cov = [
|
||||
{file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
|
||||
{file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
|
||||
]
|
||||
python-bitcoinlib = [
|
||||
{file = "python-bitcoinlib-0.11.2.tar.gz", hash = "sha256:61ba514e0d232cc84741e49862dcedaf37199b40bba252a17edc654f63d13f39"},
|
||||
{file = "python_bitcoinlib-0.11.2-py3-none-any.whl", hash = "sha256:78bd4ee717fe805cd760dfdd08765e77b7c7dbef4627f8596285e84953756508"},
|
||||
@@ -947,6 +1139,10 @@ secp256k1 = [
|
||||
{file = "secp256k1-0.14.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c9e7c024ff17e9b9d7c392bb2a917da231d6cb40ab119389ff1f51dca10339a4"},
|
||||
{file = "secp256k1-0.14.0.tar.gz", hash = "sha256:82c06712d69ef945220c8b53c1a0d424c2ff6a1f64aee609030df79ad8383397"},
|
||||
]
|
||||
setuptools = [
|
||||
{file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"},
|
||||
{file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
@@ -1030,22 +1226,30 @@ typed-ast = [
|
||||
{file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
|
||||
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
|
||||
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
|
||||
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
|
||||
{file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
|
||||
{file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"},
|
||||
{file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"},
|
||||
]
|
||||
uvicorn = [
|
||||
{file = "uvicorn-0.18.3-py3-none-any.whl", hash = "sha256:0abd429ebb41e604ed8d2be6c60530de3408f250e8d2d84967d85ba9e86fe3af"},
|
||||
{file = "uvicorn-0.18.3.tar.gz", hash = "sha256:9a66e7c42a2a95222f76ec24a4b754c158261c4696e683b9dadc72b590e0311b"},
|
||||
]
|
||||
websocket-client = [
|
||||
{file = "websocket-client-1.3.3.tar.gz", hash = "sha256:d58c5f284d6a9bf8379dab423259fe8f85b70d5fa5d2916d5791a84594b122b1"},
|
||||
{file = "websocket_client-1.3.3-py3-none-any.whl", hash = "sha256:5d55652dc1d0b3c734f044337d929aaf83f4f9138816ec680c1aefefb4dc4877"},
|
||||
]
|
||||
wheel = [
|
||||
{file = "wheel-0.38.4-py3-none-any.whl", hash = "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"},
|
||||
{file = "wheel-0.38.4.tar.gz", hash = "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac"},
|
||||
]
|
||||
win32-setctime = [
|
||||
{file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},
|
||||
{file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"},
|
||||
]
|
||||
zipp = [
|
||||
{file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"},
|
||||
{file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"},
|
||||
{file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"},
|
||||
{file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"},
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "cashu"
|
||||
version = "0.3.2"
|
||||
version = "0.7.0"
|
||||
description = "Ecash wallet and mint."
|
||||
authors = ["calle <callebtc@protonmail.com>"]
|
||||
license = "MIT"
|
||||
@@ -22,6 +22,13 @@ bitstring = "^3.1.9"
|
||||
secp256k1 = "^0.14.0"
|
||||
sqlalchemy-aio = "^0.17.0"
|
||||
python-bitcoinlib = "^0.11.2"
|
||||
h11 = "0.12.0"
|
||||
PySocks = "^1.7.1"
|
||||
cryptography = "^38.0.4"
|
||||
websocket-client = "1.3.3"
|
||||
pycryptodomex = "^3.16.0"
|
||||
setuptools = "^65.6.3"
|
||||
wheel = "^0.38.4"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = {version = "^22.8.0", allow-prereleases = true}
|
||||
@@ -31,6 +38,7 @@ isort = "^5.10.1"
|
||||
mypy = "^0.971"
|
||||
black = {version = "^22.8.0", allow-prereleases = true}
|
||||
isort = "^5.10.1"
|
||||
pytest-cov = "^4.0.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
||||
@@ -1,43 +1,48 @@
|
||||
anyio==3.6.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
attrs==22.1.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
anyio==3.6.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
attrs==22.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
bech32==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||
certifi==2022.9.24 ; python_version >= "3.7" and python_version < "4.0"
|
||||
certifi==2022.12.7 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cffi==1.15.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
charset-normalizer==2.0.12 ; python_version >= "3.7" and python_version < "4.0"
|
||||
click==8.0.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
colorama==0.4.5 ; python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" or python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
|
||||
colorama==0.4.6 ; python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" or python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
|
||||
cryptography==38.0.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
ecdsa==0.18.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
environs==9.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
exceptiongroup==1.1.0 ; python_version >= "3.7" and python_version < "3.11"
|
||||
fastapi==0.83.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
h11==0.14.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
h11==0.12.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
idna==3.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
importlib-metadata==4.12.0 ; python_version >= "3.7" and python_version < "3.8"
|
||||
importlib-metadata==5.2.0 ; python_version >= "3.7" and python_version < "3.8"
|
||||
iniconfig==1.1.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
loguru==0.6.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
marshmallow==3.18.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
marshmallow==3.19.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
outcome==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
packaging==21.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
packaging==22.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pluggy==1.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
py==1.11.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pycryptodomex==3.16.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pydantic==1.10.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyparsing==3.0.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pysocks==1.7.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pytest-asyncio==0.19.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pytest==7.1.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pytest==7.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
python-bitcoinlib==0.11.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
python-dotenv==0.21.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
represent==1.6.0.post0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
requests==2.27.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
secp256k1==0.14.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
setuptools==65.6.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
six==1.16.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sniffio==1.3.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sqlalchemy-aio==0.17.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sqlalchemy==1.3.24 ; python_version >= "3.7" and python_version < "4.0"
|
||||
starlette==0.19.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
tomli==2.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
typing-extensions==4.3.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
urllib3==1.26.12 ; python_version >= "3.7" and python_version < "4"
|
||||
tomli==2.0.1 ; python_version >= "3.7" and python_version < "3.11"
|
||||
typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
urllib3==1.26.13 ; python_version >= "3.7" and python_version < "4.0"
|
||||
uvicorn==0.18.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
websocket-client==1.3.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
wheel==0.38.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
win32-setctime==1.1.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
|
||||
zipp==3.8.1 ; python_version >= "3.7" and python_version < "3.8"
|
||||
zipp==3.11.0 ; python_version >= "3.7" and python_version < "3.8"
|
||||
|
||||
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.3.2",
|
||||
version="0.7.0",
|
||||
description="Ecash wallet and mint with Bitcoin Lightning support",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
|
||||
5
tests/test_core.py
Normal file
5
tests/test_core.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from cashu.core.split import amount_split
|
||||
|
||||
|
||||
def test_get_output_split():
|
||||
assert amount_split(13) == [1, 4, 8]
|
||||
106
tests/test_crypto.py
Normal file
106
tests/test_crypto.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import pytest
|
||||
|
||||
from cashu.core.b_dhke import hash_to_curve, step1_alice, step2_bob, step3_alice
|
||||
from cashu.core.secp import PrivateKey, PublicKey
|
||||
|
||||
|
||||
def test_hash_to_curve():
|
||||
result = hash_to_curve(
|
||||
bytes.fromhex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
)
|
||||
)
|
||||
assert (
|
||||
result.serialize().hex()
|
||||
== "0266687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925"
|
||||
)
|
||||
|
||||
result = hash_to_curve(
|
||||
bytes.fromhex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||
)
|
||||
)
|
||||
assert (
|
||||
result.serialize().hex()
|
||||
== "02ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5"
|
||||
)
|
||||
|
||||
|
||||
def test_hash_to_curve_iteration():
|
||||
"""This input causes multiple rounds of the hash_to_curve algorithm."""
|
||||
result = hash_to_curve(
|
||||
bytes.fromhex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000002"
|
||||
)
|
||||
)
|
||||
assert (
|
||||
result.serialize().hex()
|
||||
== "02076c988b353fcbb748178ecb286bc9d0b4acf474d4ba31ba62334e46c97c416a"
|
||||
)
|
||||
|
||||
|
||||
def test_step1():
|
||||
""""""
|
||||
B_, blinding_factor = step1_alice(
|
||||
"test_message",
|
||||
blinding_factor=bytes.fromhex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||
), # 32 bytes
|
||||
)
|
||||
|
||||
assert (
|
||||
B_.serialize().hex()
|
||||
== "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2"
|
||||
)
|
||||
assert blinding_factor.private_key == bytes.fromhex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||
)
|
||||
|
||||
|
||||
def test_step2():
|
||||
B_, _ = step1_alice(
|
||||
"test_message",
|
||||
blinding_factor=bytes.fromhex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||
), # 32 bytes
|
||||
)
|
||||
a = PrivateKey(
|
||||
privkey=bytes.fromhex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||
),
|
||||
raw=True,
|
||||
)
|
||||
C_ = B_.mult(a)
|
||||
assert (
|
||||
C_.serialize().hex()
|
||||
== "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2"
|
||||
)
|
||||
|
||||
|
||||
def test_step3():
|
||||
# C = C_ - A.mult(r)
|
||||
C_ = PublicKey(
|
||||
bytes.fromhex(
|
||||
"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2"
|
||||
),
|
||||
raw=True,
|
||||
)
|
||||
r = PrivateKey(
|
||||
privkey=bytes.fromhex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||
)
|
||||
)
|
||||
|
||||
A = PublicKey(
|
||||
pubkey=b"\x02"
|
||||
+ bytes.fromhex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001",
|
||||
),
|
||||
raw=True,
|
||||
)
|
||||
C = step3_alice(C_, r, A)
|
||||
|
||||
assert (
|
||||
C.serialize().hex()
|
||||
== "03c724d7e6a5443b39ac8acf11f40420adc4f99a02e7cc1b57703d9391f6d129cd"
|
||||
)
|
||||
111
tests/test_mint.py
Normal file
111
tests/test_mint.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import BlindedMessage, Proof
|
||||
from cashu.core.migrations import migrate_databases
|
||||
|
||||
SERVER_ENDPOINT = "http://localhost:3338"
|
||||
|
||||
import os
|
||||
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.settings import MAX_ORDER
|
||||
from cashu.mint import migrations
|
||||
from cashu.mint.ledger import Ledger
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
assert exc.args[0] == msg, Exception(
|
||||
f"Expected error: {msg}, got: {exc.args[0]}"
|
||||
)
|
||||
|
||||
|
||||
def assert_amt(proofs: List[Proof], expected: int):
|
||||
"""Assert amounts the proofs contain."""
|
||||
assert [p.amount for p in proofs] == expected
|
||||
|
||||
|
||||
async def start_mint_init(ledger):
|
||||
await migrate_databases(ledger.db, migrations)
|
||||
await ledger.load_used_proofs()
|
||||
await ledger.init_keysets()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def ledger():
|
||||
db_file = "data/mint/test.sqlite3"
|
||||
if os.path.exists(db_file):
|
||||
os.remove(db_file)
|
||||
ledger = Ledger(
|
||||
db=Database("test", "data/mint"),
|
||||
seed="TEST_PRIVATE_KEY",
|
||||
derivation_path="0/0/0/0",
|
||||
lightning=None,
|
||||
)
|
||||
await start_mint_init(ledger)
|
||||
yield ledger
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keysets(ledger: Ledger):
|
||||
assert len(ledger.keysets.keysets)
|
||||
assert len(ledger.keysets.get_ids())
|
||||
assert ledger.keyset.id == "XQM1wwtQbOXE"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keyset(ledger: Ledger):
|
||||
keyset = ledger.get_keyset()
|
||||
assert type(keyset) == dict
|
||||
assert len(keyset) == MAX_ORDER
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint(ledger: Ledger):
|
||||
blinded_messages_mock = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
|
||||
)
|
||||
]
|
||||
promises = await ledger.mint(blinded_messages_mock)
|
||||
assert len(promises)
|
||||
assert promises[0].amount == 8
|
||||
assert (
|
||||
promises[0].C_
|
||||
== "032dfadd74bb3abba8170ecbae5401507e384eafd312defda94148fa37314c0ef0"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_invalid_blinded_message(ledger: Ledger):
|
||||
blinded_messages_mock_invalid_key = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bff02d7fa2365379b0840afe249a7a9d71237",
|
||||
)
|
||||
]
|
||||
await assert_err(
|
||||
ledger.mint(blinded_messages_mock_invalid_key), "invalid public key"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_promises(ledger: Ledger):
|
||||
blinded_messages_mock = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
|
||||
)
|
||||
]
|
||||
promises = await ledger._generate_promises(blinded_messages_mock)
|
||||
assert (
|
||||
promises[0].C_
|
||||
== "032dfadd74bb3abba8170ecbae5401507e384eafd312defda94148fa37314c0ef0"
|
||||
)
|
||||
22
tests/test_tor.py
Normal file
22
tests/test_tor.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from cashu.tor.tor import TorProxy
|
||||
|
||||
|
||||
# @pytest.mark.skip
|
||||
def test_tor_setup():
|
||||
s = requests.Session()
|
||||
|
||||
tor = TorProxy(timeout=False)
|
||||
tor.run_daemon()
|
||||
socks_host, socks_port = "localhost", 9050
|
||||
|
||||
proxies = {
|
||||
"http": f"socks5://{socks_host}:{socks_port}",
|
||||
"https": f"socks5://{socks_host}:{socks_port}",
|
||||
}
|
||||
s.proxies.update(proxies)
|
||||
|
||||
resp = s.get("https://google.com")
|
||||
resp.raise_for_status()
|
||||
@@ -1,11 +1,15 @@
|
||||
import time
|
||||
from re import S
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.helpers import async_unwrap
|
||||
from cashu.core.base import Proof
|
||||
from cashu.core.helpers import async_unwrap, sum_proofs
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.settings import MAX_ORDER
|
||||
from cashu.wallet import migrations
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
from cashu.wallet.wallet import Wallet as Wallet2
|
||||
|
||||
@@ -22,139 +26,237 @@ async def assert_err(f, msg):
|
||||
)
|
||||
|
||||
|
||||
def assert_amt(proofs, expected):
|
||||
def assert_amt(proofs: List[Proof], expected: int):
|
||||
"""Assert amounts the proofs contain."""
|
||||
assert [p["amount"] for p in proofs] == expected
|
||||
assert [p.amount for p in proofs] == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def run_test():
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1():
|
||||
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1")
|
||||
await migrate_databases(wallet1.db, migrations)
|
||||
await wallet1.load_mint()
|
||||
wallet1.status()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2():
|
||||
wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2")
|
||||
await migrate_databases(wallet2.db, migrations)
|
||||
await wallet2.load_mint()
|
||||
wallet2.status()
|
||||
yield wallet2
|
||||
|
||||
proofs = []
|
||||
|
||||
# Mint a proof of promise. We obtain a proof for 64 coins
|
||||
proofs += await wallet1.mint(64)
|
||||
print(proofs)
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keys(wallet1: Wallet):
|
||||
assert len(wallet1.keys) == MAX_ORDER
|
||||
keyset = await wallet1._get_keys(wallet1.url)
|
||||
assert type(keyset.id) == str
|
||||
assert len(keyset.id) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keyset(wallet1: Wallet):
|
||||
assert len(wallet1.keys) == MAX_ORDER
|
||||
# ket's get the keys first so we can get a keyset ID that we use later
|
||||
keys1 = await wallet1._get_keys(wallet1.url)
|
||||
# gets the keys of a specific keyset
|
||||
keys2 = await wallet1._get_keyset(wallet1.url, keys1.id)
|
||||
assert len(keys1.public_keys) == len(keys2.public_keys)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keysets(wallet1: Wallet):
|
||||
keyset = await wallet1._get_keysets(wallet1.url)
|
||||
assert type(keyset) == dict
|
||||
assert type(keyset["keysets"]) == list
|
||||
assert len(keyset["keysets"]) > 0
|
||||
assert keyset["keysets"][-1] == wallet1.keyset_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint(wallet1: Wallet):
|
||||
await wallet1.mint(64)
|
||||
assert wallet1.balance == 64
|
||||
wallet1.status()
|
||||
|
||||
# Mint an odd amount (not in 2^n)
|
||||
proofs += await wallet1.mint(63)
|
||||
assert wallet1.balance == 64 + 63
|
||||
|
||||
w1_frst_proofs, w1_scnd_proofs = await wallet1.split(wallet1.proofs, 65)
|
||||
assert wallet1.balance == 63 + 64
|
||||
wallet1.status()
|
||||
@pytest.mark.asyncio
|
||||
async def test_split(wallet1: Wallet):
|
||||
await wallet1.mint(64)
|
||||
p1, p2 = await wallet1.split(wallet1.proofs, 20)
|
||||
assert wallet1.balance == 64
|
||||
assert sum_proofs(p1) == 44
|
||||
assert [p.amount for p in p1] == [4, 8, 32]
|
||||
assert sum_proofs(p2) == 20
|
||||
assert [p.amount for p in p2] == [4, 16]
|
||||
assert all([p.id == wallet1.keyset_id for p in p1])
|
||||
assert all([p.id == wallet1.keyset_id for p in p2])
|
||||
|
||||
# Error: We try to double-spend by providing a valid proof twice
|
||||
await assert_err(
|
||||
wallet1.split(wallet1.proofs + proofs, 20),
|
||||
f"Mint Error: tokens already spent. Secret: {proofs[0]['secret']}",
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_to_send(wallet1: Wallet):
|
||||
await wallet1.mint(64)
|
||||
keep_proofs, spendable_proofs = await wallet1.split_to_send(
|
||||
wallet1.proofs, 32, set_reserved=True
|
||||
)
|
||||
assert wallet1.balance == 63 + 64
|
||||
wallet1.status()
|
||||
get_spendable = await wallet1._get_spendable_proofs(wallet1.proofs)
|
||||
assert keep_proofs == get_spendable
|
||||
|
||||
w1_frst_proofs, w1_scnd_proofs = await wallet1.split(wallet1.proofs, 20)
|
||||
# we expect 44 and 20 -> [4, 8, 32], [4, 16]
|
||||
print(w1_frst_proofs)
|
||||
print(w1_scnd_proofs)
|
||||
# assert [p["amount"] for p in w1_frst_proofs] == [4, 8, 32]
|
||||
assert [p["amount"] for p in w1_scnd_proofs] == [4, 16]
|
||||
assert wallet1.balance == 63 + 64
|
||||
wallet1.status()
|
||||
assert sum_proofs(spendable_proofs) == 32
|
||||
assert wallet1.balance == 64
|
||||
assert wallet1.available_balance == 32
|
||||
|
||||
# Error: We try to double-spend and it fails
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_more_than_balance(wallet1: Wallet):
|
||||
await wallet1.mint(64)
|
||||
await assert_err(
|
||||
wallet1.split([proofs[0]], 10),
|
||||
f"Mint Error: tokens already spent. Secret: {proofs[0]['secret']}",
|
||||
wallet1.split(wallet1.proofs, 128),
|
||||
"Mint Error: split amount is higher than the total sum.",
|
||||
)
|
||||
assert wallet1.balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_to_send_more_than_balance(wallet1: Wallet):
|
||||
await wallet1.mint(64)
|
||||
await assert_err(
|
||||
wallet1.split_to_send(wallet1.proofs, 128, set_reserved=True),
|
||||
"balance too low.",
|
||||
)
|
||||
assert wallet1.balance == 64
|
||||
assert wallet1.available_balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_double_spend(wallet1: Wallet):
|
||||
doublespend = await wallet1.mint(64)
|
||||
await wallet1.split(wallet1.proofs, 20)
|
||||
await assert_err(
|
||||
wallet1.split(doublespend, 20),
|
||||
f"Mint Error: tokens already spent. Secret: {doublespend[0]['secret']}",
|
||||
)
|
||||
assert wallet1.balance == 64
|
||||
assert wallet1.available_balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_duplicate_proofs_double_spent(wallet1: Wallet):
|
||||
doublespend = await wallet1.mint(64)
|
||||
await assert_err(
|
||||
wallet1.split(wallet1.proofs + doublespend, 20),
|
||||
"Mint Error: proofs already pending.",
|
||||
)
|
||||
assert wallet1.balance == 64
|
||||
assert wallet1.available_balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet):
|
||||
await wallet1.mint(64)
|
||||
_, spendable_proofs = await wallet1.split_to_send(
|
||||
wallet1.proofs, 32, set_reserved=True
|
||||
)
|
||||
await wallet2.redeem(spendable_proofs)
|
||||
assert wallet2.balance == 32
|
||||
|
||||
assert wallet1.balance == 64
|
||||
assert wallet1.available_balance == 32
|
||||
await wallet1.invalidate(spendable_proofs)
|
||||
assert wallet1.balance == 32
|
||||
assert wallet1.available_balance == 32
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_invalid_amount(wallet1: Wallet):
|
||||
await wallet1.mint(64)
|
||||
await assert_err(
|
||||
wallet1.split(wallet1.proofs, -1),
|
||||
"Mint Error: invalid split amount: -1",
|
||||
)
|
||||
|
||||
assert wallet1.balance == 63 + 64
|
||||
wallet1.status()
|
||||
|
||||
# Redeem the tokens in wallet2
|
||||
w2_frst_proofs, w2_scnd_proofs = await wallet2.redeem(w1_scnd_proofs)
|
||||
print(w2_frst_proofs)
|
||||
print(w2_scnd_proofs)
|
||||
assert wallet1.balance == 63 + 64
|
||||
assert wallet2.balance == 20
|
||||
wallet2.status()
|
||||
|
||||
# wallet1 invalidates his proofs
|
||||
await wallet1.invalidate(w1_scnd_proofs)
|
||||
assert wallet1.balance == 63 + 64 - 20
|
||||
wallet1.status()
|
||||
|
||||
w1_frst_proofs2, w1_scnd_proofs2 = await wallet1.split(w1_frst_proofs, 5)
|
||||
# we expect 15 and 5 -> [1, 2, 4, 8], [1, 4]
|
||||
print(w1_frst_proofs2)
|
||||
print(w1_scnd_proofs2)
|
||||
assert wallet1.balance == 63 + 64 - 20
|
||||
wallet1.status()
|
||||
|
||||
# Error: We try to double-spend and it fails
|
||||
await assert_err(
|
||||
wallet1.split(w1_scnd_proofs, 5),
|
||||
f"Mint Error: tokens already spent. Secret: {w1_scnd_proofs[0]['secret']}",
|
||||
)
|
||||
|
||||
assert wallet1.balance == 63 + 64 - 20
|
||||
wallet1.status()
|
||||
|
||||
assert wallet1.proof_amounts() == [1, 2, 4, 4, 32, 64]
|
||||
assert wallet2.proof_amounts() == [4, 16]
|
||||
|
||||
# manipulate the proof amount
|
||||
# w1_frst_proofs2_manipulated = w1_frst_proofs2.copy()
|
||||
# w1_frst_proofs2_manipulated[0]["amount"] = 123
|
||||
# await assert_err(
|
||||
# wallet1.split(w1_frst_proofs2_manipulated, 20),
|
||||
# "Error: 123",
|
||||
# )
|
||||
|
||||
# try to split an invalid amount
|
||||
await assert_err(
|
||||
wallet1.split(w1_scnd_proofs, -500),
|
||||
"Mint Error: invalid split amount: -500",
|
||||
)
|
||||
|
||||
# mint with secrets
|
||||
@pytest.mark.asyncio
|
||||
async def test_split_with_secret(wallet1: Wallet):
|
||||
await wallet1.mint(64)
|
||||
secret = f"asdasd_{time.time()}"
|
||||
w1_frst_proofs, w1_scnd_proofs = await wallet1.split(
|
||||
wallet1.proofs, 65, scnd_secret=secret
|
||||
wallet1.proofs, 32, scnd_secret=secret
|
||||
)
|
||||
# check if index prefix is in secret
|
||||
assert w1_scnd_proofs[0].secret == "0:" + secret
|
||||
|
||||
# p2sh test
|
||||
p2shscript = await wallet2.create_p2sh_lock()
|
||||
txin_p2sh_address = p2shscript.address
|
||||
lock = f"P2SH:{txin_p2sh_address}"
|
||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock)
|
||||
_, _ = await wallet2.redeem(
|
||||
send_proofs, scnd_script=p2shscript.script, scnd_siganture=p2shscript.signature
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redeem_without_secret(wallet1: Wallet):
|
||||
await wallet1.mint(64)
|
||||
# strip away the secrets
|
||||
w1_scnd_proofs_manipulated = w1_scnd_proofs.copy()
|
||||
w1_scnd_proofs_manipulated = wallet1.proofs.copy()
|
||||
for p in w1_scnd_proofs_manipulated:
|
||||
p.secret = ""
|
||||
await assert_err(
|
||||
wallet2.redeem(w1_scnd_proofs_manipulated),
|
||||
wallet1.redeem(w1_scnd_proofs_manipulated),
|
||||
"Mint Error: no secret in proof.",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
async_unwrap(run_test())
|
||||
@pytest.mark.asyncio
|
||||
async def no_test_p2sh(wallet1: Wallet, wallet2: Wallet):
|
||||
await wallet1.mint(64)
|
||||
# p2sh test
|
||||
p2shscript = await wallet1.create_p2sh_lock()
|
||||
txin_p2sh_address = p2shscript.address
|
||||
lock = f"P2SH:{txin_p2sh_address}"
|
||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock)
|
||||
|
||||
assert send_proofs[0].secret.startswith("P2SH:")
|
||||
|
||||
frst_proofs, scnd_proofs = await wallet2.redeem(
|
||||
send_proofs, scnd_script=p2shscript.script, scnd_siganture=p2shscript.signature
|
||||
)
|
||||
assert len(frst_proofs) == 0
|
||||
assert len(scnd_proofs) == 1
|
||||
assert sum_proofs(scnd_proofs) == 8
|
||||
assert wallet2.balance == 8
|
||||
|
||||
|
||||
def test():
|
||||
async_unwrap(run_test())
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2sh_receive_wrong_script(wallet1: Wallet, wallet2: Wallet):
|
||||
await wallet1.mint(64)
|
||||
# p2sh test
|
||||
p2shscript = await wallet1.create_p2sh_lock()
|
||||
txin_p2sh_address = p2shscript.address
|
||||
lock = f"P2SH:{txin_p2sh_address}"
|
||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock)
|
||||
|
||||
wrong_script = "asad" + p2shscript.script
|
||||
|
||||
await assert_err(
|
||||
wallet2.redeem(
|
||||
send_proofs, scnd_script=wrong_script, scnd_siganture=p2shscript.signature
|
||||
),
|
||||
"Mint Error: ('Script verification failed:', VerifyScriptError('scriptPubKey returned false'))",
|
||||
)
|
||||
assert wallet2.balance == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2sh_receive_wrong_signature(wallet1: Wallet, wallet2: Wallet):
|
||||
await wallet1.mint(64)
|
||||
# p2sh test
|
||||
p2shscript = await wallet1.create_p2sh_lock()
|
||||
txin_p2sh_address = p2shscript.address
|
||||
lock = f"P2SH:{txin_p2sh_address}"
|
||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock)
|
||||
|
||||
wrong_signature = "asda" + p2shscript.signature
|
||||
|
||||
await assert_err(
|
||||
wallet2.redeem(
|
||||
send_proofs, scnd_script=p2shscript.script, scnd_siganture=wrong_signature
|
||||
),
|
||||
"Mint Error: ('Script evaluation failed:', EvalScriptError('EvalScript: OP_RETURN called'))",
|
||||
)
|
||||
assert wallet2.balance == 0
|
||||
|
||||
Reference in New Issue
Block a user