mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 18:44:20 +01:00
works
This commit is contained in:
@@ -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
170
README.md
@@ -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
|
||||

|
||||
# 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.
|
||||
|
||||
|
||||
28
core/base.py
28
core/base.py
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"])]
|
||||
|
||||
Reference in New Issue
Block a user