This commit is contained in:
callebtc
2022-09-15 17:50:19 +03:00
parent 0e0d094044
commit c409619aea
9 changed files with 218 additions and 105 deletions

View File

@@ -12,5 +12,6 @@ MINT_PRIVATE_KEY=supersecretprivatekey
MINT_SERVER_HOST=127.0.0.1
MINT_SERVER_PORT=3338
LIGHTNING=TRUE
LNBITS_ENDPOINT=https://legend.lnbits.com
LNBITS_KEY=yourkeyasdasdasd

170
README.md
View File

@@ -8,6 +8,7 @@ Big thanks to [phyro](https://github.com/phyro) for their work and further discu
## Install
### Prerequisites
These steps help you install Python via pyenv and Poetry. If you already have Poetry running on your computer, you can skip this step and jump right to [Install Cashu](#install-cashu).
```bash
sudo apt install -y build-essential pkg-config libffi-dev libpq-dev zlib1g-dev libssl-dev python3-dev
# on mac: brew install postgres
@@ -46,80 +47,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.
## Run a mint yourself
This runs the mint on your local computer. Skip this step if you want to use the [public test mint](#test-instance) instead.
```bash
poetry run mint
```
## Use wallet
#### 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
poetry run cashu mint 420
```
Returns:
```bash
Balance: 0
{
'pr': 'lnbc4200n1p3jp5clsp5vcfkyqtnkcx9287auhesqwj40che77pd4ymaltc3ruazh3vcgs3qpp5qzwkavpd4pmfkdmq9trdnrk2lswkt0fypqg55h2sucx6yq9ushzsdq4vdshx6r4ypjx2ur0wd5hgxqyjw5qcqpjrzjq0qly7quwdwq2wr52et5gl65dagdgqdwgn9an58mhejnsvmmu996xzetgvqqwzcqqqqqqqqqqqqqqqqq9q9qyysgqfjwnl4za4naf7l2wwcck2gk6y9mvjt5dz9gptfkpl0j50ygkdkuxyjcy3zgd2tk4995yw8gx39cx2qwm9dgwc0t9t6hrgvjzauykqrqpgw0xx3',
'hash': '009d6eb02da8769b37602ac6d98ecafc1d65bd2408114a5d50e60da200bc85c5'
}
```
#### Mint tokens
After paying the invoice, copy the `hash` value from above and add it to the command
```bash
poetry run cashu mint 420 --hash=009d6eb02da8769b37602ac6d98ecafc1d65bd2408114a5d50e60da200bc85c5
```
You should see your balance update accordingly:
```bash
Balance: 0
Balance: 420
```
#### Check balance
```bash
poetry run cashu balance
```
#### Send tokens
To send tokens to another user, enter
```bash
poetry run 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...
```
#### Receive tokens
To receive tokens, another user enters:
```bash
poetry run cashu receive W3siYW1vdW50IjogMSwgIkMiOi...
```
You should see the balance increase:
```bash
wallet balance: 0
wallet balance: 69
```
#### 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
poetry run cashu burn W3siYW1vdW50IjogMSwgIkMiOi...
```
Returns:
```bash
wallet balance: 420
wallet balance: 351
```
## Test instance
#### 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.*
@@ -129,5 +57,95 @@ MINT_HOST=8333.space
MINT_PORT=3338
```
## Screenshot
![screenshot](https://user-images.githubusercontent.com/93376500/189533335-68a863e2-bacd-47c1-aecc-e4fb09883d11.jpg)
# Using Cashu
Cashu should be now installed. To execute the following commands, activate your virtual Poetry environment via
```bash
poetry shell
```
If you don't activate your environment, just prepent `poetry run` to all following commands.
## Using the wallet
#### 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
```
Returns:
```bash
Balance: 0 sat (Available: 0 sat in 0 tokens)
{
'pr': 'lnbc4200n1p3jp5clsp5vcfkyqtnkcx9287auhesqwj40che77pd4ymaltc3ruazh3vcgs3qpp5qzwkavpd4pmfkdmq9trdnrk2lswkt0fypqg55h2sucx6yq9ushzsdq4vdshx6r4ypjx2ur0wd5hgxqyjw5qcqpjrzjq0qly7quwdwq2wr52et5gl65dagdgqdwgn9an58mhejnsvmmu996xzetgvqqwzcqqqqqqqqqqqqqqqqq9q9qyysgqfjwnl4za4naf7l2wwcck2gk6y9mvjt5dz9gptfkpl0j50ygkdkuxyjcy3zgd2tk4995yw8gx39cx2qwm9dgwc0t9t6hrgvjzauykqrqpgw0xx3',
'hash': '009d6eb02da8769b37602ac6d98ecafc1d65bd2408114a5d50e60da200bc85c5'
}
```
#### Mint tokens
After paying the invoice, copy the `hash` value from above and add it to the command
```bash
cashu mint 420 --hash=009d6eb02da8769b37602ac6d98ecafc1d65bd2408114a5d50e60da200bc85c5
```
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
```
#### Send tokens
To send tokens to another user, enter
```bash
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...
```
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)
```
#### Receive tokens
To receive tokens, another user enters:
```bash
poetry run 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)
```
#### 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
poetry run cashu burn W3siYW1vdW50IjogMSwgIkMiOi...
```
Returns:
```bash
Balance: 420 sat (Available: 351 sat in 7 tokens)
Balance: 351 sat (Available: 351 sat in 7 tokens)
```
## Run a mint yourself
This command runs the mint on your local computer. Skip this step if you want to use the [public test mint](#test-instance) instead.
```bash
mint
```
You can turn off Lightning support and mint as many tokens as you like by setting `LIGHTNING=FALSE` in the `.env` file.

View File

@@ -15,18 +15,44 @@ class Proof(BaseModel):
amount: int
C: BasePoint
secret: str
reserved: bool = False # whether this proof is reserved for sending
@classmethod
def from_row(cls, row: Row):
return dict(
return cls(
amount=row[0],
C=dict(
x=int(row[1]),
y=int(row[2]),
),
secret=row[3],
reserved=row[4] or False,
)
@classmethod
def from_dict(cls, d: dict):
return cls(
amount=d["amount"],
C=dict(
x=int(d["C"]["x"]),
y=int(d["C"]["y"]),
),
secret=d["secret"],
reserved=d["reserved"] or False,
)
def __getitem__(self, key):
return self.__getattribute__(key)
def __setitem__(self, key, val):
self.__setattr__(key, val)
class Proofs(BaseModel):
"""TODO: Use this model"""
proofs: List[Proof]
class Invoice(BaseModel):
amount: int

View File

@@ -4,6 +4,7 @@ env = Env()
env.read_env()
DEBUG = env.bool("DEBUG", default=False)
LIGHTNING = env.bool("LIGHTNING", default=False)
MINT_PRIVATE_KEY = env.str("MINT_PRIVATE_KEY")

View File

@@ -1,6 +1,15 @@
from core.db import Database
# from wallet import db
async def m000_create_migrations_table(db):
await db.execute(
"""
CREATE TABLE IF NOT EXISTS dbversions (
db TEXT PRIMARY KEY,
version INT NOT NULL
)
"""
)
async def m001_initial(db: Database):

View File

@@ -8,14 +8,16 @@ from functools import wraps
import click
from bech32 import bech32_decode, bech32_encode, convertbits
from core.settings import MINT_URL
from wallet.migrations import m001_initial
from core.settings import MINT_URL, LIGHTNING
from core.migrations import migrate_databases
from core.base import Proof
from wallet.wallet import Wallet as Wallet
from wallet import migrations
async def init_wallet(wallet: Wallet):
"""Performs migrations and loads proofs from db."""
await m001_initial(db=wallet.db)
await migrate_databases(wallet.db, migrations)
await wallet.load_proofs()
@@ -58,17 +60,25 @@ def coro(f):
@coro
async def mint(ctx, amount: int, hash: str):
wallet: Wallet = ctx.obj["WALLET"]
await m001_initial(db=wallet.db)
await wallet.load_proofs()
await init_wallet(wallet)
if not LIGHTNING:
wallet.status()
r = await wallet.mint(amount)
wallet.status()
return
if amount and not hash:
print(f"Balance: {wallet.balance}")
r = await wallet.request_mint(amount)
print(r)
return
if amount and hash:
print(f"Balance: {wallet.balance}")
await wallet.mint(amount, hash)
print(f"Balance: {wallet.balance}")
return
@cli.command("balance", help="See balance.")
@@ -88,8 +98,11 @@ async def send(ctx, amount: int):
wallet: Wallet = ctx.obj["WALLET"]
await init_wallet(wallet)
wallet.status()
_, send_proofs = await wallet.split(wallet.proofs, amount)
print(base64.urlsafe_b64encode(json.dumps(send_proofs).encode()).decode())
_, send_proofs = await wallet.split_to_send(wallet.proofs, amount)
await wallet.set_reserved(send_proofs, reserved=True)
proofs_serialized = [p.dict() for p in send_proofs]
print(base64.urlsafe_b64encode(json.dumps(proofs_serialized).encode()).decode())
wallet.status()
@cli.command("receive", help="Receive tokens.")
@@ -100,7 +113,7 @@ async def receive(ctx, token: str):
wallet: Wallet = ctx.obj["WALLET"]
await init_wallet(wallet)
wallet.status()
proofs = json.loads(base64.urlsafe_b64decode(token))
proofs = [Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(token))]
_, _ = await wallet.redeem(proofs)
wallet.status()
@@ -113,6 +126,6 @@ async def receive(ctx, token: str):
wallet: Wallet = ctx.obj["WALLET"]
await init_wallet(wallet)
wallet.status()
proofs = json.loads(base64.urlsafe_b64decode(token))
proofs = [Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(token))]
await wallet.invalidate(proofs)
wallet.status()

View File

@@ -1,7 +1,5 @@
import secrets
from typing import Optional
# from wallet import db
from core.base import Proof
from core.db import Connection, Database
@@ -19,10 +17,10 @@ async def store_proof(
VALUES (?, ?, ?, ?)
""",
(
proof["amount"],
str(proof["C"]["x"]),
str(proof["C"]["y"]),
str(proof["secret"]),
proof.amount,
str(proof.C.x),
str(proof.C.y),
str(proof.secret),
),
)
@@ -41,7 +39,7 @@ async def get_proofs(
async def invalidate_proof(
proof: dict,
proof: Proof,
db: Database,
conn: Optional[Connection] = None,
):
@@ -61,9 +59,21 @@ async def invalidate_proof(
VALUES (?, ?, ?, ?)
""",
(
proof["amount"],
str(proof["C"]["x"]),
str(proof["C"]["y"]),
str(proof["secret"]),
proof.amount,
str(proof.C.x),
str(proof.C.y),
str(proof.secret),
),
)
async def update_proof_reserved(
proof: Proof,
reserved: bool,
db: Database,
conn: Optional[Connection] = None,
):
await (conn or db).execute(
"UPDATE proofs SET reserved = ? WHERE secret = ?",
(reserved, str(proof.secret)),
)

View File

@@ -1,6 +1,15 @@
from core.db import Database
# from wallet import db
async def m000_create_migrations_table(db):
await db.execute(
"""
CREATE TABLE IF NOT EXISTS dbversions (
db TEXT PRIMARY KEY,
version INT NOT NULL
)
"""
)
async def m001_initial(db: Database):
@@ -53,3 +62,11 @@ async def m001_initial(db: Database):
);
"""
)
async def m002_add_proofs_reserved(db):
"""
Column for marking proofs as reserved when they are being sent.
"""
await db.execute("ALTER TABLE proofs ADD COLUMN reserved BOOL")

View File

@@ -8,7 +8,7 @@ import core.b_dhke as b_dhke
from core.base import BasePoint, MintPayload, MintPayloads, Proof, SplitPayload
from core.db import Database
from core.split import amount_split
from wallet.crud import get_proofs, invalidate_proof, store_proof
from wallet.crud import get_proofs, invalidate_proof, store_proof, update_proof_reserved
class LedgerAPI:
@@ -41,7 +41,7 @@ class LedgerAPI:
C = b_dhke.step3_bob(C_, r, self.keys[promise["amount"]])
c_point = BasePoint(x=C.x, y=C.y)
proof = Proof(amount=promise["amount"], C=c_point, secret=secret)
proofs.append(proof.dict())
proofs.append(proof)
return proofs
def request_mint(self, amount):
@@ -135,10 +135,10 @@ class Wallet(LedgerAPI):
self.proofs += proofs
return proofs
async def redeem(self, proofs):
async def redeem(self, proofs: List[Proof]):
return await self.split(proofs, sum(p["amount"] for p in proofs))
async def split(self, proofs, amount):
async def split(self, proofs: List[Proof], amount: int):
assert len(proofs) > 0, ValueError("no proofs provided.")
fst_proofs, snd_proofs = super().split(proofs, amount)
if len(fst_proofs) == 0 and len(snd_proofs) == 0:
@@ -148,12 +148,24 @@ class Wallet(LedgerAPI):
filter(lambda p: p["secret"] not in used_secrets, self.proofs)
)
self.proofs += fst_proofs + snd_proofs
# store in db
for proof in proofs:
await invalidate_proof(proof, db=self.db)
await self._store_proofs(fst_proofs + snd_proofs)
return fst_proofs, snd_proofs
async def split_to_send(self, proofs: List[Proof], amount):
"""Like self.split but only considers non-reserved tokens."""
if len([p for p in proofs if not p.reserved]) <= 0:
raise Exception("balance too low")
return await self.split([p for p in proofs if not p.reserved], amount)
async def set_reserved(self, proofs: List[Proof], reserved: bool):
"""Mark a proof as reserved to avoid reuse or delete marking."""
for proof in proofs:
proof.reserved = True
await update_proof_reserved(proof, reserved=reserved, db=self.db)
async def invalidate(self, proofs):
# first we make sure that the server has invalidated these proofs
try:
@@ -175,8 +187,14 @@ class Wallet(LedgerAPI):
def balance(self):
return sum(p["amount"] for p in self.proofs)
@property
def available_balance(self):
return sum(p["amount"] for p in self.proofs if not p.reserved)
def status(self):
print(f"{self.name} balance: {self.balance}")
print(
f"Balance: {self.balance} sat (Available: {self.available_balance} sat in {len([p for p in self.proofs if not p.reserved])} tokens)"
)
def proof_amounts(self):
return [p["amount"] for p in sorted(self.proofs, key=lambda p: p["amount"])]