diff --git a/.env.example b/.env.example index 2b58494..281171b 100644 --- a/.env.example +++ b/.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 \ No newline at end of file +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" diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..3f6fd13 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: https://legend.lnbits.com/tipjar/794 diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 0000000..b966124 --- /dev/null +++ b/.github/codecov.yml @@ -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% diff --git a/.github/workflows/formatting.yml b/.github/workflows/checks.yml similarity index 50% rename from .github/workflows/formatting.yml rename to .github/workflows/checks.yml index b913e54..ce9e3bb 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/checks.yml @@ -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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f32ad3d..bf68747 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..eaa87cc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "cashu/nostr"] + path = cashu/nostr + url = https://github.com/callebtc/python-nostr/ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7f6abad --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include requirements.txt +recursive-include cashu/tor * \ No newline at end of file diff --git a/README.md b/README.md index add9593..2f40cc3 100644 --- a/README.md +++ b/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.** + +Release Downloads Coverage -Release *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 Run a mint

+### 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 --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. diff --git a/cashu/core/b_dhke.py b/cashu/core/b_dhke.py index be9a141..80735ef 100644 --- a/cashu/core/b_dhke.py +++ b/cashu/core/b_dhke.py @@ -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) diff --git a/cashu/core/base.py b/cashu/core/base.py index b76b6c4..78dfc86 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -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 + ) diff --git a/cashu/core/crypto.py b/cashu/core/crypto.py index 6cad72b..a3e0500 100644 --- a/cashu/core/crypto.py +++ b/cashu/core/crypto.py @@ -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] diff --git a/cashu/core/legacy.py b/cashu/core/legacy.py new file mode 100644 index 0000000..7434bdf --- /dev/null +++ b/cashu/core/legacy.py @@ -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) diff --git a/cashu/core/migrations.py b/cashu/core/migrations.py index cc5041f..2388187 100644 --- a/cashu/core/migrations.py +++ b/cashu/core/migrations.py @@ -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) diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 3df4eac..e22b118 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -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" diff --git a/cashu/lightning/__init__.py b/cashu/lightning/__init__.py index bc14007..54af20c 100644 --- a/cashu/lightning/__init__.py +++ b/cashu/lightning/__init__.py @@ -1,3 +1,3 @@ -from cashu.lightning.lnbits import LNbitsWallet +# from cashu.lightning.lnbits import LNbitsWallet -WALLET = LNbitsWallet() +# WALLET = LNbitsWallet() diff --git a/cashu/lightning/base.py b/cashu/lightning/base.py index e38b6d8..adde18b 100644 --- a/cashu/lightning/base.py +++ b/cashu/lightning/base.py @@ -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): diff --git a/cashu/lightning/lnbits.py b/cashu/lightning/lnbits.py index 97f8907..c422d10 100644 --- a/cashu/lightning/lnbits.py +++ b/cashu/lightning/lnbits.py @@ -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) diff --git a/cashu/mint/__init__.py b/cashu/mint/__init__.py index cfe9d4c..8b13789 100644 --- a/cashu/mint/__init__.py +++ b/cashu/mint/__init__.py @@ -1,4 +1 @@ -from cashu.core.settings import MINT_PRIVATE_KEY -from cashu.mint.ledger import Ledger -ledger = Ledger(MINT_PRIVATE_KEY, "data/mint") diff --git a/cashu/mint/app.py b/cashu/mint/app.py index f0a6b73..a0c572e 100644 --- a/cashu/mint/app.py +++ b/cashu/mint/app.py @@ -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() diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index 19d0f0c..aa53bd5 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -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] diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 73126c5..af4129b 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -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 diff --git a/cashu/mint/main.py b/cashu/mint/main.py index 50c4a6a..5cce3a1 100644 --- a/cashu/mint/main.py +++ b/cashu/mint/main.py @@ -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: diff --git a/cashu/mint/migrations.py b/cashu/mint/migrations.py index 15c8233..4b6b5e3 100644 --- a/cashu/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -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) + + ); + """ + ) diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 260d8bd..c7fe9f9 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -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 diff --git a/cashu/mint/startup.py b/cashu/mint/startup.py index f148e2c..a006e07 100644 --- a/cashu/mint/startup.py +++ b/cashu/mint/startup.py @@ -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") diff --git a/cashu/nostr b/cashu/nostr new file mode 160000 index 0000000..d7fb45f --- /dev/null +++ b/cashu/nostr @@ -0,0 +1 @@ +Subproject commit d7fb45f6a1c685be0037afd4e7aa172e0c7e3517 diff --git a/cashu/tor/LICENCE_tor b/cashu/tor/LICENCE_tor new file mode 100755 index 0000000..31dd84f --- /dev/null +++ b/cashu/tor/LICENCE_tor @@ -0,0 +1,1609 @@ +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +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. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +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. +""" + +The Node.js license applies to all parts of Node.js that are not externally +maintained libraries. + +The externally maintained libraries used by Node.js are: + +- Acorn, located at deps/acorn, is licensed as follows: + """ + MIT License + + Copyright (C) 2012-2018 by various contributors (see AUTHORS) + + 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. + """ + +- Acorn plugins, located at deps/acorn-plugins, is licensed as follows: + """ + Copyright (C) 2017-2018 by Adrian Heine + + 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. + """ + +- c-ares, located at deps/cares, is licensed as follows: + """ + Copyright (c) 2007 - 2018, Daniel Stenberg with many contributors, see AUTHORS + file. + + Copyright 1998 by the Massachusetts Institute of Technology. + + Permission to use, copy, modify, and distribute this software and its + documentation for any purpose and without fee is hereby granted, provided that + the above copyright notice appear in all copies and that both that copyright + notice and this permission notice appear in supporting documentation, and that + the name of M.I.T. not be used in advertising or publicity pertaining to + distribution of the software without specific, written prior permission. + M.I.T. makes no representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied warranty. + """ + +- cjs-module-lexer, located at deps/cjs-module-lexer, is licensed as follows: + """ + MIT License + ----------- + + Copyright (C) 2018-2020 Guy Bedford + + 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. + """ + +- ICU, located at deps/icu-small, is licensed as follows: + """ + COPYRIGHT AND PERMISSION NOTICE (ICU 58 and later) + + Copyright © 1991-2020 Unicode, Inc. All rights reserved. + Distributed under the Terms of Use in https://www.unicode.org/copyright.html. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of the Unicode data files and any associated documentation + (the "Data Files") or Unicode software and any associated documentation + (the "Software") to deal in the Data Files or Software + without restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, and/or sell copies of + the Data Files or Software, and to permit persons to whom the Data Files + or Software are furnished to do so, provided that either + (a) this copyright and permission notice appear with all copies + of the Data Files or Software, or + (b) this copyright and permission notice appear in associated + Documentation. + + THE DATA FILES AND SOFTWARE ARE 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 OF THIRD PARTY RIGHTS. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS + NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL 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 THE DATA FILES OR SOFTWARE. + + Except as contained in this notice, the name of a copyright holder + shall not be used in advertising or otherwise to promote the sale, + use or other dealings in these Data Files or Software without prior + written authorization of the copyright holder. + + --------------------- + + Third-Party Software Licenses + + This section contains third-party software notices and/or additional + terms for licensed third-party software components included within ICU + libraries. + + 1. ICU License - ICU 1.8.1 to ICU 57.1 + + COPYRIGHT AND PERMISSION NOTICE + + Copyright (c) 1995-2016 International Business Machines Corporation and others + All rights reserved. + + 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, and/or sell copies of the Software, and to permit persons + to whom the Software is furnished to do so, provided that the above + copyright notice(s) and this permission notice appear in all copies of + the Software and that both the above copyright notice(s) and this + permission notice appear in supporting documentation. + + 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 + OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY + SPECIAL 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. + + Except as contained in this notice, the name of a copyright holder + shall not be used in advertising or otherwise to promote the sale, use + or other dealings in this Software without prior written authorization + of the copyright holder. + + All trademarks and registered trademarks mentioned herein are the + property of their respective owners. + + 2. Chinese/Japanese Word Break Dictionary Data (cjdict.txt) + + # The Google Chrome software developed by Google is licensed under + # the BSD license. Other software included in this distribution is + # provided under other licenses, as set forth below. + # + # The BSD License + # http://opensource.org/licenses/bsd-license.php + # Copyright (C) 2006-2008, 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. + # + # + # The word list in cjdict.txt are generated by combining three word lists + # listed below with further processing for compound word breaking. The + # frequency is generated with an iterative training against Google web + # corpora. + # + # * Libtabe (Chinese) + # - https://sourceforge.net/project/?group_id=1519 + # - Its license terms and conditions are shown below. + # + # * IPADIC (Japanese) + # - http://chasen.aist-nara.ac.jp/chasen/distribution.html + # - Its license terms and conditions are shown below. + # + # ---------COPYING.libtabe ---- BEGIN-------------------- + # + # /* + # * Copyright (c) 1999 TaBE Project. + # * Copyright (c) 1999 Pai-Hsiang Hsiao. + # * 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 the TaBE Project 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 + # * 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. + # */ + # + # /* + # * Copyright (c) 1999 Computer Systems and Communication Lab, + # * Institute of Information Science, Academia + # * Sinica. 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 the Computer Systems and Communication Lab + # * 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 + # * 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. + # */ + # + # Copyright 1996 Chih-Hao Tsai @ Beckman Institute, + # University of Illinois + # c-tsai4@uiuc.edu http://casper.beckman.uiuc.edu/~c-tsai4 + # + # ---------------COPYING.libtabe-----END-------------------------------- + # + # + # ---------------COPYING.ipadic-----BEGIN------------------------------- + # + # Copyright 2000, 2001, 2002, 2003 Nara Institute of Science + # and Technology. All Rights Reserved. + # + # Use, reproduction, and distribution of this software is permitted. + # Any copy of this software, whether in its original form or modified, + # must include both the above copyright notice and the following + # paragraphs. + # + # Nara Institute of Science and Technology (NAIST), + # the copyright holders, disclaims all warranties with regard to this + # software, including all implied warranties of merchantability and + # fitness, in no event shall NAIST be liable for + # any special, 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 tortuous action, arising out + # of or in connection with the use or performance of this software. + # + # A large portion of the dictionary entries + # originate from ICOT Free Software. The following conditions for ICOT + # Free Software applies to the current dictionary as well. + # + # Each User may also freely distribute the Program, whether in its + # original form or modified, to any third party or parties, PROVIDED + # that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear + # on, or be attached to, the Program, which is distributed substantially + # in the same form as set out herein and that such intended + # distribution, if actually made, will neither violate or otherwise + # contravene any of the laws and regulations of the countries having + # jurisdiction over the User or the intended distribution itself. + # + # NO WARRANTY + # + # The program was produced on an experimental basis in the course of the + # research and development conducted during the project and is provided + # to users as so produced on an experimental basis. Accordingly, the + # program is provided without any warranty whatsoever, whether express, + # implied, statutory or otherwise. The term "warranty" used herein + # includes, but is not limited to, any warranty of the quality, + # performance, merchantability and fitness for a particular purpose of + # the program and the nonexistence of any infringement or violation of + # any right of any third party. + # + # Each user of the program will agree and understand, and be deemed to + # have agreed and understood, that there is no warranty whatsoever for + # the program and, accordingly, the entire risk arising from or + # otherwise connected with the program is assumed by the user. + # + # Therefore, neither ICOT, the copyright holder, or any other + # organization that participated in or was otherwise related to the + # development of the program and their respective officials, directors, + # officers and other employees shall be held liable for any and all + # damages, including, without limitation, general, special, incidental + # and consequential damages, arising out of or otherwise in connection + # with the use or inability to use the program or any product, material + # or result produced or otherwise obtained by using the program, + # regardless of whether they have been advised of, or otherwise had + # knowledge of, the possibility of such damages at any time during the + # project or thereafter. Each user will be deemed to have agreed to the + # foregoing by his or her commencement of use of the program. The term + # "use" as used herein includes, but is not limited to, the use, + # modification, copying and distribution of the program and the + # production of secondary products from the program. + # + # In the case where the program, whether in its original form or + # modified, was distributed or delivered to or received by a user from + # any person, organization or entity other than ICOT, unless it makes or + # grants independently of ICOT any specific warranty to the user in + # writing, such person, organization or entity, will also be exempted + # from and not be held liable to the user for any such damages as noted + # above as far as the program is concerned. + # + # ---------------COPYING.ipadic-----END---------------------------------- + + 3. Lao Word Break Dictionary Data (laodict.txt) + + # Copyright (c) 2013 International Business Machines Corporation + # and others. All Rights Reserved. + # + # Project: https://github.com/veer66/lao-dictionary + # Dictionary: https://github.com/veer66/lao-dictionary/blob/master/Lao-Dictionary.txt + # License: https://github.com/veer66/lao-dictionary/blob/master/Lao-Dictionary-LICENSE.txt + # (copied below) + # + # This file is derived from the above dictionary, with slight + # modifications. + # ---------------------------------------------------------------------- + # Copyright (C) 2013 Brian Eugene Wilson, Robert Martin Campbell. + # 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. + # + # + # 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 HOLDER 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. + # -------------------------------------------------------------------------- + + 4. Burmese Word Break Dictionary Data (burmesedict.txt) + + # Copyright (c) 2014 International Business Machines Corporation + # and others. All Rights Reserved. + # + # This list is part of a project hosted at: + # github.com/kanyawtech/myanmar-karen-word-lists + # + # -------------------------------------------------------------------------- + # Copyright (c) 2013, LeRoy Benjamin Sharon + # 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 Myanmar Karen Word Lists, 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 HOLDER 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. + # -------------------------------------------------------------------------- + + 5. Time Zone Database + + ICU uses the public domain data and code derived from Time Zone + Database for its time zone support. The ownership of the TZ database + is explained in BCP 175: Procedure for Maintaining the Time Zone + Database section 7. + + # 7. Database Ownership + # + # The TZ database itself is not an IETF Contribution or an IETF + # document. Rather it is a pre-existing and regularly updated work + # that is in the public domain, and is intended to remain in the + # public domain. Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do + # not apply to the TZ Database or contributions that individuals make + # to it. Should any claims be made and substantiated against the TZ + # Database, the organization that is providing the IANA + # Considerations defined in this RFC, under the memorandum of + # understanding with the IETF, currently ICANN, may act in accordance + # with all competent court orders. No ownership claims will be made + # by ICANN or the IETF Trust on the database or the code. Any person + # making a contribution to the database or code waives all rights to + # future claims in that contribution or in the TZ Database. + + 6. Google double-conversion + + Copyright 2006-2011, the V8 project authors. 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. + """ + +- libuv, located at deps/uv, is licensed as follows: + """ + libuv is licensed for use as follows: + + ==== + Copyright (c) 2015-present libuv project contributors. + + 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. + ==== + + This license applies to parts of libuv originating from the + https://github.com/joyent/libuv repository: + + ==== + + Copyright Joyent, Inc. and other Node contributors. All rights reserved. + 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. + + ==== + + This license applies to all parts of libuv that are not externally + maintained libraries. + + The externally maintained libraries used by libuv are: + + - tree.h (from FreeBSD), copyright Niels Provos. Two clause BSD license. + + - inet_pton and inet_ntop implementations, contained in src/inet.c, are + copyright the Internet Systems Consortium, Inc., and licensed under the ISC + license. + + - stdint-msvc2008.h (from msinttypes), copyright Alexander Chemeris. Three + clause BSD license. + + - pthread-fixes.c, copyright Google Inc. and Sony Mobile Communications AB. + Three clause BSD license. + + - android-ifaddrs.h, android-ifaddrs.c, copyright Berkeley Software Design + Inc, Kenneth MacKay and Emergya (Cloud4all, FP7/2007-2013, grant agreement + n° 289016). Three clause BSD license. + """ + +- llhttp, located at deps/llhttp, is licensed as follows: + """ + This software is licensed under the MIT License. + + Copyright Fedor Indutny, 2018. + + 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. + """ + +- OpenSSL, located at deps/openssl, is licensed as follows: + """ + Copyright (c) 1998-2019 The OpenSSL Project. 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. All advertising materials mentioning features or use of this + software must display the following acknowledgment: + "This product includes software developed by the OpenSSL Project + for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + + 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + endorse or promote products derived from this software without + prior written permission. For written permission, please contact + openssl-core@openssl.org. + + 5. Products derived from this software may not be called "OpenSSL" + nor may "OpenSSL" appear in their names without prior written + permission of the OpenSSL Project. + + 6. Redistributions of any form whatsoever must retain the following + acknowledgment: + "This product includes software developed by the OpenSSL Project + for use in the OpenSSL Toolkit (http://www.openssl.org/)" + + THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + EXPRESSED 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 OpenSSL PROJECT OR + ITS 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. + ==================================================================== + + This product includes cryptographic software written by Eric Young + (eay@cryptsoft.com). This product includes software written by Tim + Hudson (tjh@cryptsoft.com). + """ + +- Punycode.js, located at lib/punycode.js, is licensed as follows: + """ + Copyright Mathias Bynens + + 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. + """ + +- V8, located at deps/v8, is licensed as follows: + """ + This license applies to all parts of V8 that are not externally + maintained libraries. The externally maintained libraries used by V8 + are: + + - PCRE test suite, located in + test/mjsunit/third_party/regexp-pcre/regexp-pcre.js. This is based on the + test suite from PCRE-7.3, which is copyrighted by the University + of Cambridge and Google, Inc. The copyright notice and license + are embedded in regexp-pcre.js. + + - Layout tests, located in test/mjsunit/third_party/object-keys. These are + based on layout tests from webkit.org which are copyrighted by + Apple Computer, Inc. and released under a 3-clause BSD license. + + - Strongtalk assembler, the basis of the files assembler-arm-inl.h, + assembler-arm.cc, assembler-arm.h, assembler-ia32-inl.h, + assembler-ia32.cc, assembler-ia32.h, assembler-x64-inl.h, + assembler-x64.cc, assembler-x64.h, assembler-mips-inl.h, + assembler-mips.cc, assembler-mips.h, assembler.cc and assembler.h. + This code is copyrighted by Sun Microsystems Inc. and released + under a 3-clause BSD license. + + - Valgrind client API header, located at src/third_party/valgrind/valgrind.h + This is released under the BSD license. + + - The Wasm C/C++ API headers, located at third_party/wasm-api/wasm.{h,hh} + This is released under the Apache license. The API's upstream prototype + implementation also formed the basis of V8's implementation in + src/wasm/c-api.cc. + + These libraries have their own licenses; we recommend you read them, + as their terms may differ from the terms below. + + Further license information can be found in LICENSE files located in + sub-directories. + + Copyright 2014, the V8 project authors. 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. + """ + +- SipHash, located at deps/v8/src/third_party/siphash, is licensed as follows: + """ + SipHash reference C implementation + + Copyright (c) 2016 Jean-Philippe Aumasson + + To the extent possible under law, the author(s) have dedicated all + copyright and related and neighboring rights to this software to the public + domain worldwide. This software is distributed without any warranty. + """ + +- zlib, located at deps/zlib, is licensed as follows: + """ + zlib.h -- interface of the 'zlib' general purpose compression library + version 1.2.11, January 15th, 2017 + + Copyright (C) 1995-2017 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + """ + +- npm, located at deps/npm, is licensed as follows: + """ + The npm application + Copyright (c) npm, Inc. and Contributors + Licensed on the terms of The Artistic License 2.0 + + Node package dependencies of the npm application + Copyright (c) their respective copyright owners + Licensed on their respective license terms + + The npm public registry at https://registry.npmjs.org + and the npm website at https://www.npmjs.com + Operated by npm, Inc. + Use governed by terms published on https://www.npmjs.com + + "Node.js" + Trademark Joyent, Inc., https://joyent.com + Neither npm nor npm, Inc. are affiliated with Joyent, Inc. + + The Node.js application + Project of Node Foundation, https://nodejs.org + + The npm Logo + Copyright (c) Mathias Pettersson and Brian Hammond + + "Gubblebum Blocky" typeface + Copyright (c) Tjarda Koster, https://jelloween.deviantart.com + Used with permission + + -------- + + The Artistic License 2.0 + + Copyright (c) 2000-2006, The Perl Foundation. + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + This license establishes the terms under which a given free software + Package may be copied, modified, distributed, and/or redistributed. + The intent is that the Copyright Holder maintains some artistic + control over the development of that Package while still keeping the + Package available as open source and free software. + + You are always permitted to make arrangements wholly outside of this + license directly with the Copyright Holder of a given Package. If the + terms of this license do not permit the full use that you propose to + make of the Package, you should contact the Copyright Holder and seek + a different licensing arrangement. + + Definitions + + "Copyright Holder" means the individual(s) or organization(s) + named in the copyright notice for the entire Package. + + "Contributor" means any party that has contributed code or other + material to the Package, in accordance with the Copyright Holder's + procedures. + + "You" and "your" means any person who would like to copy, + distribute, or modify the Package. + + "Package" means the collection of files distributed by the + Copyright Holder, and derivatives of that collection and/or of + those files. A given Package may consist of either the Standard + Version, or a Modified Version. + + "Distribute" means providing a copy of the Package or making it + accessible to anyone else, or in the case of a company or + organization, to others outside of your company or organization. + + "Distributor Fee" means any fee that you charge for Distributing + this Package or providing support for this Package to another + party. It does not mean licensing fees. + + "Standard Version" refers to the Package if it has not been + modified, or has been modified only in ways explicitly requested + by the Copyright Holder. + + "Modified Version" means the Package, if it has been changed, and + such changes were not explicitly requested by the Copyright + Holder. + + "Original License" means this Artistic License as Distributed with + the Standard Version of the Package, in its current version or as + it may be modified by The Perl Foundation in the future. + + "Source" form means the source code, documentation source, and + configuration files for the Package. + + "Compiled" form means the compiled bytecode, object code, binary, + or any other form resulting from mechanical transformation or + translation of the Source form. + + Permission for Use and Modification Without Distribution + + (1) You are permitted to use the Standard Version and create and use + Modified Versions for any purpose without restriction, provided that + you do not Distribute the Modified Version. + + Permissions for Redistribution of the Standard Version + + (2) You may Distribute verbatim copies of the Source form of the + Standard Version of this Package in any medium without restriction, + either gratis or for a Distributor Fee, provided that you duplicate + all of the original copyright notices and associated disclaimers. At + your discretion, such verbatim copies may or may not include a + Compiled form of the Package. + + (3) You may apply any bug fixes, portability changes, and other + modifications made available from the Copyright Holder. The resulting + Package will still be considered the Standard Version, and as such + will be subject to the Original License. + + Distribution of Modified Versions of the Package as Source + + (4) You may Distribute your Modified Version as Source (either gratis + or for a Distributor Fee, and with or without a Compiled form of the + Modified Version) provided that you clearly document how it differs + from the Standard Version, including, but not limited to, documenting + any non-standard features, executables, or modules, and provided that + you do at least ONE of the following: + + (a) make the Modified Version available to the Copyright Holder + of the Standard Version, under the Original License, so that the + Copyright Holder may include your modifications in the Standard + Version. + + (b) ensure that installation of your Modified Version does not + prevent the user installing or running the Standard Version. In + addition, the Modified Version must bear a name that is different + from the name of the Standard Version. + + (c) allow anyone who receives a copy of the Modified Version to + make the Source form of the Modified Version available to others + under + + (i) the Original License or + + (ii) a license that permits the licensee to freely copy, + modify and redistribute the Modified Version using the same + licensing terms that apply to the copy that the licensee + received, and requires that the Source form of the Modified + Version, and of any works derived from it, be made freely + available in that license fees are prohibited but Distributor + Fees are allowed. + + Distribution of Compiled Forms of the Standard Version + or Modified Versions without the Source + + (5) You may Distribute Compiled forms of the Standard Version without + the Source, provided that you include complete instructions on how to + get the Source of the Standard Version. Such instructions must be + valid at the time of your distribution. If these instructions, at any + time while you are carrying out such distribution, become invalid, you + must provide new instructions on demand or cease further distribution. + If you provide valid instructions or cease distribution within thirty + days after you become aware that the instructions are invalid, then + you do not forfeit any of your rights under this license. + + (6) You may Distribute a Modified Version in Compiled form without + the Source, provided that you comply with Section 4 with respect to + the Source of the Modified Version. + + Aggregating or Linking the Package + + (7) You may aggregate the Package (either the Standard Version or + Modified Version) with other packages and Distribute the resulting + aggregation provided that you do not charge a licensing fee for the + Package. Distributor Fees are permitted, and licensing fees for other + components in the aggregation are permitted. The terms of this license + apply to the use and Distribution of the Standard or Modified Versions + as included in the aggregation. + + (8) You are permitted to link Modified and Standard Versions with + other works, to embed the Package in a larger work of your own, or to + build stand-alone binary or bytecode versions of applications that + include the Package, and Distribute the result without restriction, + provided the result does not expose a direct interface to the Package. + + Items That are Not Considered Part of a Modified Version + + (9) Works (including, but not limited to, modules and scripts) that + merely extend or make use of the Package, do not, by themselves, cause + the Package to be a Modified Version. In addition, such works are not + considered parts of the Package itself, and are not subject to the + terms of this license. + + General Provisions + + (10) Any use, modification, and distribution of the Standard or + Modified Versions is governed by this Artistic License. By using, + modifying or distributing the Package, you accept this license. Do not + use, modify, or distribute the Package, if you do not accept this + license. + + (11) If your Modified Version has been derived from a Modified + Version made by someone other than you, you are nevertheless required + to ensure that your Modified Version complies with the requirements of + this license. + + (12) This license does not grant you the right to use any trademark, + service mark, tradename, or logo of the Copyright Holder. + + (13) This license includes the non-exclusive, worldwide, + free-of-charge patent license to make, have made, use, offer to sell, + sell, import and otherwise transfer the Package with respect to any + patent claims licensable by the Copyright Holder that are necessarily + infringed by the Package. If you institute patent litigation + (including a cross-claim or counterclaim) against any party alleging + that the Package constitutes direct or contributory patent + infringement, then this Artistic License to you shall terminate on the + date that such litigation is filed. + + (14) Disclaimer of Warranty: + THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS + IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED + WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR + NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL + LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL + BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL + DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + -------- + """ + +- GYP, located at tools/gyp, is licensed as follows: + """ + Copyright (c) 2020 Node.js contributors. All rights reserved. + Copyright (c) 2009 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. + """ + +- inspector_protocol, located at tools/inspector_protocol, is licensed as follows: + """ + // Copyright 2016 The Chromium Authors. 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. + """ + +- jinja2, located at tools/inspector_protocol/jinja2, is licensed as follows: + """ + Copyright (c) 2009 by the Jinja Team, see AUTHORS for more details. + + Some 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. + + * The names of the contributors may not 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. + """ + +- markupsafe, located at tools/inspector_protocol/markupsafe, is licensed as follows: + """ + Copyright (c) 2010 by Armin Ronacher and contributors. See AUTHORS + for more details. + + Some rights reserved. + + Redistribution and use in source and binary forms of the software as well + as documentation, 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. + + * The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE AND DOCUMENTATION 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 AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH + DAMAGE. + """ + +- cpplint.py, located at tools/cpplint.py, is licensed as follows: + """ + Copyright (c) 2009 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. + """ + +- ESLint, located at tools/node_modules/eslint, is licensed as follows: + """ + Copyright JS Foundation and other contributors, https://js.foundation + + 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. + """ + +- Babel, located at tools/node_modules/@babel, is licensed as follows: + """ + MIT License + + Copyright (c) 2014-present Sebastian McKenzie and other contributors + + 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. + """ + +- gtest, located at test/cctest/gtest, is licensed as follows: + """ + Copyright 2008, 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. + """ + +- nghttp2, located at deps/nghttp2, is licensed as follows: + """ + The MIT License + + Copyright (c) 2012, 2014, 2015, 2016 Tatsuhiro Tsujikawa + Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors + + 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. + """ + +- large_pages, located at src/large_pages, is licensed as follows: + """ + Copyright (C) 2018 Intel Corporation + + 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. + """ + +- caja, located at lib/internal/freeze_intrinsics.js, is licensed as follows: + """ + Adapted from SES/Caja - Copyright (C) 2011 Google Inc. + Copyright (C) 2018 Agoric + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + """ + +- brotli, located at deps/brotli, is licensed as follows: + """ + Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. + + 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. + """ + +- HdrHistogram, located at deps/histogram, is licensed as follows: + """ + The code in this repository code was Written by Gil Tene, Michael Barker, + and Matt Warren, and released to the public domain, as explained at + http://creativecommons.org/publicdomain/zero/1.0/ + + For users of this code who wish to consume it under the "BSD" license + rather than under the public domain or CC0 contribution text mentioned + above, the code found under this directory is *also* provided under the + following license (commonly referred to as the BSD 2-Clause License). This + license does not detract from the above stated release of the code into + the public domain, and simply represents an additional license granted by + the Author. + + ----------------------------------------------------------------------------- + ** Beginning of "BSD 2-Clause License" text. ** + + Copyright (c) 2012, 2013, 2014 Gil Tene + Copyright (c) 2014 Michael Barker + Copyright (c) 2014 Matt Warren + 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. + + 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 HOLDER 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. + """ + +- highlight.js, located at doc/api_assets/highlight.pack.js, is licensed as follows: + """ + BSD 3-Clause License + + Copyright (c) 2006, Ivan Sagalaev. + 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 the copyright holder 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 HOLDER 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. + """ + +- node-heapdump, located at src/heap_utils.cc, is licensed as follows: + """ + ISC License + + Copyright (c) 2012, Ben Noordhuis + + Permission to use, copy, modify, and/or 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. + + === src/compat.h src/compat-inl.h === + + ISC License + + Copyright (c) 2014, StrongLoop Inc. + + Permission to use, copy, modify, and/or 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. + """ + +- rimraf, located at lib/internal/fs/rimraf.js, is licensed as follows: + """ + The ISC License + + Copyright (c) Isaac Z. Schlueter and Contributors + + Permission to use, copy, modify, and/or 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. + """ + +- uvwasi, located at deps/uvwasi, is licensed as follows: + """ + MIT License + + Copyright (c) 2019 Colin Ihrig and Contributors + + 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. + """ + +- ngtcp2, located at deps/ngtcp2/ngtcp2/, is licensed as follows: + """ + The MIT License + + Copyright (c) 2016 ngtcp2 contributors + + 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. + """ + +- nghttp3, located at deps/ngtcp2/nghttp3/, is licensed as follows: + """ + The MIT License + + Copyright (c) 2019 nghttp3 contributors + + 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. + """ \ No newline at end of file diff --git a/cashu/wallet/wallet_live/.placeholder b/cashu/tor/__init__.py similarity index 100% rename from cashu/wallet/wallet_live/.placeholder rename to cashu/tor/__init__.py diff --git a/cashu/wallet/wallet_live/.DS_Store b/cashu/tor/bundle/.DS_Store old mode 100644 new mode 100755 similarity index 97% rename from cashu/wallet/wallet_live/.DS_Store rename to cashu/tor/bundle/.DS_Store index 5008ddf..6c7df35 Binary files a/cashu/wallet/wallet_live/.DS_Store and b/cashu/tor/bundle/.DS_Store differ diff --git a/cashu/tor/bundle/linux/LICENSE b/cashu/tor/bundle/linux/LICENSE new file mode 100755 index 0000000..abab8e3 --- /dev/null +++ b/cashu/tor/bundle/linux/LICENSE @@ -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 + * 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 + + 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 . + +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 + + 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/)" +=============================================================================== diff --git a/cashu/tor/bundle/linux/libcrypto.so.1.1 b/cashu/tor/bundle/linux/libcrypto.so.1.1 new file mode 100755 index 0000000..26cfb21 Binary files /dev/null and b/cashu/tor/bundle/linux/libcrypto.so.1.1 differ diff --git a/cashu/tor/bundle/linux/libevent-2.1.so.7 b/cashu/tor/bundle/linux/libevent-2.1.so.7 new file mode 100755 index 0000000..08de765 Binary files /dev/null and b/cashu/tor/bundle/linux/libevent-2.1.so.7 differ diff --git a/cashu/tor/bundle/linux/libssl.so.1.1 b/cashu/tor/bundle/linux/libssl.so.1.1 new file mode 100755 index 0000000..90f9756 Binary files /dev/null and b/cashu/tor/bundle/linux/libssl.so.1.1 differ diff --git a/cashu/tor/bundle/linux/libstdc++/libstdc++.so.6 b/cashu/tor/bundle/linux/libstdc++/libstdc++.so.6 new file mode 100755 index 0000000..020519b Binary files /dev/null and b/cashu/tor/bundle/linux/libstdc++/libstdc++.so.6 differ diff --git a/cashu/tor/bundle/linux/tor b/cashu/tor/bundle/linux/tor new file mode 100755 index 0000000..80634bb Binary files /dev/null and b/cashu/tor/bundle/linux/tor differ diff --git a/cashu/tor/bundle/mac/libevent-2.1.7.dylib b/cashu/tor/bundle/mac/libevent-2.1.7.dylib new file mode 100755 index 0000000..9ec800a Binary files /dev/null and b/cashu/tor/bundle/mac/libevent-2.1.7.dylib differ diff --git a/cashu/tor/bundle/mac/tor b/cashu/tor/bundle/mac/tor new file mode 100755 index 0000000..9687091 Binary files /dev/null and b/cashu/tor/bundle/mac/tor differ diff --git a/cashu/tor/timeout.py b/cashu/tor/timeout.py new file mode 100755 index 0000000..c4e6e88 --- /dev/null +++ b/cashu/tor/timeout.py @@ -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() diff --git a/cashu/tor/tor.py b/cashu/tor/tor.py new file mode 100755 index 0000000..50f49ce --- /dev/null +++ b/cashu/tor/tor.py @@ -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() diff --git a/cashu/tor/torrc b/cashu/tor/torrc new file mode 100755 index 0000000..47f788f --- /dev/null +++ b/cashu/tor/torrc @@ -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 +## You might also include your PGP or GPG fingerprint if you have one: +#ContactInfo 0xFFFFFFFF Random Person + +## 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 diff --git a/cashu/wallet/__init__.py b/cashu/wallet/__init__.py index bf11b51..b4cdb6a 100644 --- a/cashu/wallet/__init__.py +++ b/cashu/wallet/__init__.py @@ -1,3 +1,3 @@ import sys -sys.tracebacklimit = None +sys.tracebacklimit = None # type: ignore diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py old mode 100755 new mode 100644 index 8c623c1..3b7138e --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -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_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 --lock P2SH:{txin_p2sh_address}" + f"Anyone can send tokens to this lock:\n\ncashu send --lock P2SH:{txin_p2sh_address}" ) print("") print( - f"Only you can receive coins from this lock:\n\ncashu receive --lock P2SH:{txin_p2sh_address}\n" + f"Only you can receive tokens from this lock:\n\ncashu receive --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 --lock P2SH:{l.address}") + print(f"Receive: cashu receive --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 diff --git a/cashu/wallet/cli_helpers.py b/cashu/wallet/cli_helpers.py new file mode 100644 index 0000000..132b6fe --- /dev/null +++ b/cashu/wallet/cli_helpers.py @@ -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 diff --git a/cashu/wallet/crud.py b/cashu/wallet/crud.py index 12d8401..3c1c883 100644 --- a/cashu/wallet/crud.py +++ b/cashu/wallet/crud.py @@ -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, + ), + ) diff --git a/cashu/wallet/migrations.py b/cashu/wallet/migrations.py index 0a0a73e..bdb3c36 100644 --- a/cashu/wallet/migrations.py +++ b/cashu/wallet/migrations.py @@ -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) + + ); + """ + ) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index b7ec8bb..b265d27 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -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"])] diff --git a/cashu/wallet/wallet_live/wallet.sqlite3 b/cashu/wallet/wallet_live/wallet.sqlite3 deleted file mode 100644 index d3e29bb..0000000 Binary files a/cashu/wallet/wallet_live/wallet.sqlite3 and /dev/null differ diff --git a/docs/README.md b/docs/README.md index 6e3f5b3..904634f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,7 @@ # Notation -Sending user: `Alice` -Receivung user: `Carol` +Sending user: `Alice`
+Receiving user: `Carol`
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. \ No newline at end of file +- `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. diff --git a/docs/specs/00.md b/docs/specs/00.md new file mode 100644 index 0000000..7957895 --- /dev/null +++ b/docs/specs/00.md @@ -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 +``` \ No newline at end of file diff --git a/docs/specs/01.md b/docs/specs/01.md new file mode 100644 index 0000000..d495da7 --- /dev/null +++ b/docs/specs/01.md @@ -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 `{ : , : ...}` for each `` of the amounts the mint `Bob` supports and the corresponding public key ``, 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 \ No newline at end of file diff --git a/docs/specs/02.md b/docs/specs/02.md new file mode 100644 index 0000000..57dbbf2 --- /dev/null +++ b/docs/specs/02.md @@ -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] +``` + diff --git a/docs/specs/03.md b/docs/specs/03.md new file mode 100644 index 0000000..f39b4c3 --- /dev/null +++ b/docs/specs/03.md @@ -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=` request with the requested amount `` 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]. \ No newline at end of file diff --git a/docs/specs/04.md b/docs/specs/04.md new file mode 100644 index 0000000..6f088f3 --- /dev/null +++ b/docs/specs/04.md @@ -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=` request with a JSON body to the mint. The body **MUST** include `BlindedMessages` that are worth a maximum of `` [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" + }, + { + ... + } + ] +} +``` \ No newline at end of file diff --git a/docs/specs/05.md b/docs/specs/05.md new file mode 100644 index 0000000..1cfb716 --- /dev/null +++ b/docs/specs/05.md @@ -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. \ No newline at end of file diff --git a/docs/specs/06.md b/docs/specs/06.md new file mode 100644 index 0000000..6106e98 --- /dev/null +++ b/docs/specs/06.md @@ -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 \ No newline at end of file diff --git a/docs/specs/README.md b/docs/specs/README.md new file mode 100644 index 0000000..e7b364a --- /dev/null +++ b/docs/specs/README.md @@ -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 \ No newline at end of file diff --git a/docs/specs/cashu_client_spec.md b/docs/specs/cashu_client_spec.md index d449804..73bb92d 100644 --- a/docs/specs/cashu_client_spec.md +++ b/docs/specs/cashu_client_spec.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 \ No newline at end of file +- Clarify whether a `TOKEN` is a single Proof or a list of Proofs diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..39efb2f --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +exclude = cashu/nostr \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 7d8c34d..f4362d5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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"}, ] diff --git a/pyproject.toml b/pyproject.toml index 8eef93e..8745a1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cashu" -version = "0.3.2" +version = "0.7.0" description = "Ecash wallet and mint." authors = ["calle "] 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"] diff --git a/requirements.txt b/requirements.txt index 8fc17f6..aeacdf9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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" diff --git a/setup.py b/setup.py index e944e11..20093d0 100644 --- a/setup.py +++ b/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", diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..b50b07c --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,5 @@ +from cashu.core.split import amount_split + + +def test_get_output_split(): + assert amount_split(13) == [1, 4, 8] diff --git a/tests/test_crypto.py b/tests/test_crypto.py new file mode 100644 index 0000000..e5e2ef5 --- /dev/null +++ b/tests/test_crypto.py @@ -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" + ) diff --git a/tests/test_mint.py b/tests/test_mint.py new file mode 100644 index 0000000..f175ce6 --- /dev/null +++ b/tests/test_mint.py @@ -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" + ) diff --git a/tests/test_tor.py b/tests/test_tor.py new file mode 100644 index 0000000..4d792bb --- /dev/null +++ b/tests/test_tor.py @@ -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() diff --git a/tests/test_wallet.py b/tests/test_wallet.py index c14cc7c..493503f 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -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