mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-28 16:58:07 +01:00
Merge branch 'lnbits:main' into main
This commit is contained in:
commit
ca2aaa1221
30 changed files with 5624 additions and 362 deletions
|
@ -103,3 +103,8 @@ ECLAIR_PASS=eclairpw
|
||||||
# Enter /api in LightningTipBot to get your key
|
# Enter /api in LightningTipBot to get your key
|
||||||
LNTIPS_API_KEY=LNTIPS_ADMIN_KEY
|
LNTIPS_API_KEY=LNTIPS_ADMIN_KEY
|
||||||
LNTIPS_API_ENDPOINT=https://ln.tips
|
LNTIPS_API_ENDPOINT=https://ln.tips
|
||||||
|
|
||||||
|
# Cashu Mint
|
||||||
|
# Use a long-enough random (!) private key.
|
||||||
|
# Once set, you cannot change this key as for now.
|
||||||
|
CASHU_PRIVATE_KEY="SuperSecretPrivateKey"
|
||||||
|
|
|
@ -65,8 +65,7 @@ async def migrate_databases():
|
||||||
(db_name, version, version),
|
(db_name, version, version),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def run_migration(db, migrations_module):
|
async def run_migration(db, migrations_module, db_name):
|
||||||
db_name = migrations_module.__name__.split(".")[-2]
|
|
||||||
for key, migrate in migrations_module.__dict__.items():
|
for key, migrate in migrations_module.__dict__.items():
|
||||||
match = match = matcher.match(key)
|
match = match = matcher.match(key)
|
||||||
if match:
|
if match:
|
||||||
|
@ -97,20 +96,24 @@ async def migrate_databases():
|
||||||
rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall()
|
rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall()
|
||||||
current_versions = {row["db"]: row["version"] for row in rows}
|
current_versions = {row["db"]: row["version"] for row in rows}
|
||||||
matcher = re.compile(r"^m(\d\d\d)_")
|
matcher = re.compile(r"^m(\d\d\d)_")
|
||||||
await run_migration(conn, core_migrations)
|
db_name = core_migrations.__name__.split(".")[-2]
|
||||||
|
await run_migration(conn, core_migrations, db_name)
|
||||||
|
|
||||||
for ext in get_valid_extensions():
|
for ext in get_valid_extensions():
|
||||||
try:
|
try:
|
||||||
ext_migrations = importlib.import_module(
|
|
||||||
f"lnbits.extensions.{ext.code}.migrations"
|
module_str = (
|
||||||
|
ext.migration_module or f"lnbits.extensions.{ext.code}.migrations"
|
||||||
)
|
)
|
||||||
|
ext_migrations = importlib.import_module(module_str)
|
||||||
ext_db = importlib.import_module(f"lnbits.extensions.{ext.code}").db
|
ext_db = importlib.import_module(f"lnbits.extensions.{ext.code}").db
|
||||||
|
db_name = ext.db_name or module_str.split(".")[-2]
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
f"Please make sure that the extension `{ext.code}` has a migrations file."
|
f"Please make sure that the extension `{ext.code}` has a migrations file."
|
||||||
)
|
)
|
||||||
|
|
||||||
async with ext_db.connect() as ext_conn:
|
async with ext_db.connect() as ext_conn:
|
||||||
await run_migration(ext_conn, ext_migrations)
|
await run_migration(ext_conn, ext_migrations, db_name)
|
||||||
|
|
||||||
logger.info("✔️ All migrations done.")
|
logger.info("✔️ All migrations done.")
|
||||||
|
|
|
@ -86,7 +86,8 @@ class Connection(Compat):
|
||||||
return raw_html
|
return raw_html
|
||||||
|
|
||||||
# tuple to list and back to tuple
|
# tuple to list and back to tuple
|
||||||
values = tuple([cleanhtml(l) for l in list(values)])
|
value_list = [values] if isinstance(values, str) else list(values)
|
||||||
|
values = tuple([cleanhtml(l) for l in value_list])
|
||||||
return values
|
return values
|
||||||
|
|
||||||
async def fetchall(self, query: str, values: tuple = ()) -> list:
|
async def fetchall(self, query: str, values: tuple = ()) -> list:
|
||||||
|
|
11
lnbits/extensions/cashu/README.md
Normal file
11
lnbits/extensions/cashu/README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# Cashu
|
||||||
|
|
||||||
|
## Create ecash mint for pegging in/out of ecash
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
1. Enable extension
|
||||||
|
2. Create a Mint
|
||||||
|
3. Share wallet
|
48
lnbits/extensions/cashu/__init__.py
Normal file
48
lnbits/extensions/cashu/__init__.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from environs import Env # type: ignore
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
|
from lnbits.db import Database
|
||||||
|
from lnbits.helpers import template_renderer
|
||||||
|
from lnbits.tasks import catch_everything_and_restart
|
||||||
|
|
||||||
|
db = Database("ext_cashu")
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
cashu_static_files = [
|
||||||
|
{
|
||||||
|
"path": "/cashu/static",
|
||||||
|
"app": StaticFiles(directory="lnbits/extensions/cashu/static"),
|
||||||
|
"name": "cashu_static",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
from cashu.mint.ledger import Ledger
|
||||||
|
|
||||||
|
env = Env()
|
||||||
|
env.read_env()
|
||||||
|
|
||||||
|
ledger = Ledger(
|
||||||
|
db=db,
|
||||||
|
seed=env.str("CASHU_PRIVATE_KEY", default="SuperSecretPrivateKey"),
|
||||||
|
derivation_path="0/0/0/1",
|
||||||
|
)
|
||||||
|
|
||||||
|
cashu_ext: APIRouter = APIRouter(prefix="/cashu", tags=["cashu"])
|
||||||
|
|
||||||
|
|
||||||
|
def cashu_renderer():
|
||||||
|
return template_renderer(["lnbits/extensions/cashu/templates"])
|
||||||
|
|
||||||
|
|
||||||
|
from .tasks import startup_cashu_mint, wait_for_paid_invoices
|
||||||
|
from .views import * # noqa
|
||||||
|
from .views_api import * # noqa
|
||||||
|
|
||||||
|
|
||||||
|
def cashu_start():
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.create_task(catch_everything_and_restart(startup_cashu_mint))
|
||||||
|
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
7
lnbits/extensions/cashu/config.json
Normal file
7
lnbits/extensions/cashu/config.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "Cashu",
|
||||||
|
"short_description": "Ecash mint and wallet",
|
||||||
|
"icon": "account_balance",
|
||||||
|
"contributors": ["calle", "vlad", "arcbtc"],
|
||||||
|
"hidden": false
|
||||||
|
}
|
63
lnbits/extensions/cashu/crud.py
Normal file
63
lnbits/extensions/cashu/crud.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from binascii import hexlify, unhexlify
|
||||||
|
from typing import Any, List, Optional, Union
|
||||||
|
|
||||||
|
from cashu.core.base import MintKeyset
|
||||||
|
from embit import bip32, bip39, ec, script
|
||||||
|
from embit.networks import NETWORKS
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.db import Connection, Database
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
|
from . import db
|
||||||
|
from .models import Cashu, Pegs, Promises, Proof
|
||||||
|
|
||||||
|
|
||||||
|
async def create_cashu(
|
||||||
|
cashu_id: str, keyset_id: str, wallet_id: str, data: Cashu
|
||||||
|
) -> Cashu:
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO cashu.cashu (id, wallet, name, tickershort, fraction, maxsats, coins, keyset_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
cashu_id,
|
||||||
|
wallet_id,
|
||||||
|
data.name,
|
||||||
|
data.tickershort,
|
||||||
|
data.fraction,
|
||||||
|
data.maxsats,
|
||||||
|
data.coins,
|
||||||
|
keyset_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
cashu = await get_cashu(cashu_id)
|
||||||
|
assert cashu, "Newly created cashu couldn't be retrieved"
|
||||||
|
return cashu
|
||||||
|
|
||||||
|
|
||||||
|
async def get_cashu(cashu_id) -> Optional[Cashu]:
|
||||||
|
row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,))
|
||||||
|
return Cashu(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_cashus(wallet_ids: Union[str, List[str]]) -> List[Cashu]:
|
||||||
|
if isinstance(wallet_ids, str):
|
||||||
|
wallet_ids = [wallet_ids]
|
||||||
|
|
||||||
|
q = ",".join(["?"] * len(wallet_ids))
|
||||||
|
rows = await db.fetchall(
|
||||||
|
f"SELECT * FROM cashu.cashu WHERE wallet IN ({q})", (*wallet_ids,)
|
||||||
|
)
|
||||||
|
|
||||||
|
return [Cashu(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_cashu(cashu_id) -> None:
|
||||||
|
await db.execute("DELETE FROM cashu.cashu WHERE id = ?", (cashu_id,))
|
33
lnbits/extensions/cashu/migrations.py
Normal file
33
lnbits/extensions/cashu/migrations.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
async def m001_initial(db):
|
||||||
|
"""
|
||||||
|
Initial cashu table.
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE cashu.cashu (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
wallet TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
tickershort TEXT DEFAULT 'sats',
|
||||||
|
fraction BOOL,
|
||||||
|
maxsats INT,
|
||||||
|
coins INT,
|
||||||
|
keyset_id TEXT NOT NULL,
|
||||||
|
issued_sat INT
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Initial cashus table.
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE cashu.pegs (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
wallet TEXT NOT NULL,
|
||||||
|
inout BOOL NOT NULL,
|
||||||
|
amount INT
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
147
lnbits/extensions/cashu/models.py
Normal file
147
lnbits/extensions/cashu/models.py
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
from sqlite3 import Row
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
from fastapi import Query
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Cashu(BaseModel):
|
||||||
|
id: str = Query(None)
|
||||||
|
name: str = Query(None)
|
||||||
|
wallet: str = Query(None)
|
||||||
|
tickershort: str = Query(None)
|
||||||
|
fraction: bool = Query(None)
|
||||||
|
maxsats: int = Query(0)
|
||||||
|
coins: int = Query(0)
|
||||||
|
keyset_id: str = Query(None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_row(cls, row: Row):
|
||||||
|
return cls(**dict(row))
|
||||||
|
|
||||||
|
|
||||||
|
class Pegs(BaseModel):
|
||||||
|
id: str
|
||||||
|
wallet: str
|
||||||
|
inout: str
|
||||||
|
amount: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_row(cls, row: Row):
|
||||||
|
return cls(**dict(row))
|
||||||
|
|
||||||
|
|
||||||
|
class PayLnurlWData(BaseModel):
|
||||||
|
lnurl: str
|
||||||
|
|
||||||
|
|
||||||
|
class Promises(BaseModel):
|
||||||
|
id: str
|
||||||
|
amount: int
|
||||||
|
B_b: str
|
||||||
|
C_b: str
|
||||||
|
cashu_id: str
|
||||||
|
|
||||||
|
|
||||||
|
class Proof(BaseModel):
|
||||||
|
amount: int
|
||||||
|
secret: str
|
||||||
|
C: str
|
||||||
|
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 "",
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d: dict):
|
||||||
|
assert "secret" in d, "no secret in proof"
|
||||||
|
assert "amount" in d, "no amount in proof"
|
||||||
|
return cls(
|
||||||
|
amount=d.get("amount"),
|
||||||
|
C=d.get("C"),
|
||||||
|
secret=d.get("secret"),
|
||||||
|
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 "",
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return dict(amount=self.amount, secret=self.secret, C=self.C)
|
||||||
|
|
||||||
|
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
|
||||||
|
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]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BlindedMessage(BaseModel):
|
||||||
|
amount: int
|
||||||
|
B_: str
|
||||||
|
|
||||||
|
|
||||||
|
class BlindedSignature(BaseModel):
|
||||||
|
amount: int
|
||||||
|
C_: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d: dict):
|
||||||
|
return cls(
|
||||||
|
amount=d["amount"],
|
||||||
|
C_=d["C_"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MintPayloads(BaseModel):
|
||||||
|
blinded_messages: List[BlindedMessage] = []
|
||||||
|
|
||||||
|
|
||||||
|
class SplitPayload(BaseModel):
|
||||||
|
proofs: List[Proof]
|
||||||
|
amount: int
|
||||||
|
output_data: MintPayloads
|
||||||
|
|
||||||
|
|
||||||
|
class CheckPayload(BaseModel):
|
||||||
|
proofs: List[Proof]
|
||||||
|
|
||||||
|
|
||||||
|
class MeltPayload(BaseModel):
|
||||||
|
proofs: List[Proof]
|
||||||
|
amount: int
|
||||||
|
invoice: str
|
37
lnbits/extensions/cashu/static/js/base64.js
Normal file
37
lnbits/extensions/cashu/static/js/base64.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
function unescapeBase64Url(str) {
|
||||||
|
return (str + '==='.slice((str.length + 3) % 4))
|
||||||
|
.replace(/-/g, '+')
|
||||||
|
.replace(/_/g, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeBase64Url(str) {
|
||||||
|
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8ToBase64 = (function (exports) {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var fromCharCode = String.fromCharCode
|
||||||
|
var encode = function encode(uint8array) {
|
||||||
|
var output = []
|
||||||
|
|
||||||
|
for (var i = 0, length = uint8array.length; i < length; i++) {
|
||||||
|
output.push(fromCharCode(uint8array[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return btoa(output.join(''))
|
||||||
|
}
|
||||||
|
|
||||||
|
var asCharCode = function asCharCode(c) {
|
||||||
|
return c.charCodeAt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decode = function decode(chars) {
|
||||||
|
return Uint8Array.from(atob(chars), asCharCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.decode = decode
|
||||||
|
exports.encode = encode
|
||||||
|
|
||||||
|
return exports
|
||||||
|
})({})
|
39
lnbits/extensions/cashu/static/js/dhke.js
Normal file
39
lnbits/extensions/cashu/static/js/dhke.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
async function hashToCurve(secretMessage) {
|
||||||
|
console.log(
|
||||||
|
'### secretMessage',
|
||||||
|
nobleSecp256k1.utils.bytesToHex(secretMessage)
|
||||||
|
)
|
||||||
|
let point
|
||||||
|
while (!point) {
|
||||||
|
const hash = await nobleSecp256k1.utils.sha256(secretMessage)
|
||||||
|
const hashHex = nobleSecp256k1.utils.bytesToHex(hash)
|
||||||
|
const pointX = '02' + hashHex
|
||||||
|
console.log('### pointX', pointX)
|
||||||
|
try {
|
||||||
|
point = nobleSecp256k1.Point.fromHex(pointX)
|
||||||
|
console.log('### point', point.toHex())
|
||||||
|
} catch (error) {
|
||||||
|
secretMessage = await nobleSecp256k1.utils.sha256(secretMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return point
|
||||||
|
}
|
||||||
|
|
||||||
|
async function step1Alice(secretMessage) {
|
||||||
|
// todo: document & validate `secretMessage` format
|
||||||
|
secretMessage = uint8ToBase64.encode(secretMessage)
|
||||||
|
secretMessage = new TextEncoder().encode(secretMessage)
|
||||||
|
const Y = await hashToCurve(secretMessage)
|
||||||
|
const rpk = nobleSecp256k1.utils.randomPrivateKey()
|
||||||
|
const r = bytesToNumber(rpk)
|
||||||
|
const P = nobleSecp256k1.Point.fromPrivateKey(r)
|
||||||
|
const B_ = Y.add(P)
|
||||||
|
return {B_: B_.toHex(true), r: nobleSecp256k1.utils.bytesToHex(rpk)}
|
||||||
|
}
|
||||||
|
|
||||||
|
function step3Alice(C_, r, A) {
|
||||||
|
// const rInt = BigInt(r)
|
||||||
|
const rInt = bytesToNumber(r)
|
||||||
|
const C = C_.subtract(A.multiply(rInt))
|
||||||
|
return C
|
||||||
|
}
|
1178
lnbits/extensions/cashu/static/js/noble-secp256k1.js
Normal file
1178
lnbits/extensions/cashu/static/js/noble-secp256k1.js
Normal file
File diff suppressed because it is too large
Load diff
23
lnbits/extensions/cashu/static/js/utils.js
Normal file
23
lnbits/extensions/cashu/static/js/utils.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
function splitAmount(value) {
|
||||||
|
const chunks = []
|
||||||
|
for (let i = 0; i < 32; i++) {
|
||||||
|
const mask = 1 << i
|
||||||
|
if ((value & mask) !== 0) chunks.push(Math.pow(2, i))
|
||||||
|
}
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
function bytesToNumber(bytes) {
|
||||||
|
return hexToNumber(nobleSecp256k1.utils.bytesToHex(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bigIntStringify(key, value) {
|
||||||
|
return typeof value === 'bigint' ? value.toString() : value
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToNumber(hex) {
|
||||||
|
if (typeof hex !== 'string') {
|
||||||
|
throw new TypeError('hexToNumber: expected string, got ' + typeof hex)
|
||||||
|
}
|
||||||
|
return BigInt(`0x${hex}`)
|
||||||
|
}
|
33
lnbits/extensions/cashu/tasks.py
Normal file
33
lnbits/extensions/cashu/tasks.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
|
from cashu.core.migrations import migrate_databases
|
||||||
|
from cashu.mint import migrations
|
||||||
|
|
||||||
|
from lnbits.core.models import Payment
|
||||||
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
||||||
|
from . import db, ledger
|
||||||
|
from .crud import get_cashu
|
||||||
|
|
||||||
|
|
||||||
|
async def startup_cashu_mint():
|
||||||
|
await migrate_databases(db, migrations)
|
||||||
|
await ledger.load_used_proofs()
|
||||||
|
await ledger.init_keysets(autosave=False)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def wait_for_paid_invoices():
|
||||||
|
invoice_queue = asyncio.Queue()
|
||||||
|
register_invoice_listener(invoice_queue)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
payment = await invoice_queue.get()
|
||||||
|
await on_invoice_paid(payment)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
|
if payment.extra and not payment.extra.get("tag") == "cashu":
|
||||||
|
return
|
||||||
|
return
|
80
lnbits/extensions/cashu/templates/cashu/_api_docs.html
Normal file
80
lnbits/extensions/cashu/templates/cashu/_api_docs.html
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<q-expansion-item
|
||||||
|
group="extras"
|
||||||
|
icon="swap_vertical_circle"
|
||||||
|
label="API info"
|
||||||
|
:content-inset-level="0.5"
|
||||||
|
>
|
||||||
|
<q-btn flat label="Swagger API" type="a" href="../docs#/cashu"></q-btn>
|
||||||
|
<!-- <q-expansion-item group="api" dense expand-separator label="List TPoS">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code><span class="text-blue">GET</span> /cashu/api/v1/mints</code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Returns 200 OK (application/json)
|
||||||
|
</h5>
|
||||||
|
<code>[<cashu_object>, ...]</code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>curl -X GET {{ request.base_url }}cashu/api/v1/mints -H "X-Api-Key:
|
||||||
|
<invoice_key>"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-expansion-item group="api" dense expand-separator label="Create a TPoS">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code><span class="text-green">POST</span> /cashu/api/v1/mints</code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||||
|
<code
|
||||||
|
>{"name": <string>, "currency": <string*ie USD*>}</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Returns 201 CREATED (application/json)
|
||||||
|
</h5>
|
||||||
|
<code
|
||||||
|
>{"currency": <string>, "id": <string>, "name":
|
||||||
|
<string>, "wallet": <string>}</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>curl -X POST {{ request.base_url }}cashu/api/v1/mints -d '{"name":
|
||||||
|
<string>, "currency": <string>}' -H "Content-type:
|
||||||
|
application/json" -H "X-Api-Key: <admin_key>"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
group="api"
|
||||||
|
dense
|
||||||
|
expand-separator
|
||||||
|
label="Delete a TPoS"
|
||||||
|
class="q-pb-md"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-pink">DELETE</span>
|
||||||
|
/cashu/api/v1/mints/<cashu_id></code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
|
||||||
|
<code></code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>curl -X DELETE {{ request.base_url
|
||||||
|
}}cashu/api/v1/mints/<cashu_id> -H "X-Api-Key:
|
||||||
|
<admin_key>"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item> -->
|
||||||
|
</q-expansion-item>
|
13
lnbits/extensions/cashu/templates/cashu/_cashu.html
Normal file
13
lnbits/extensions/cashu/templates/cashu/_cashu.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<q-expansion-item group="extras" icon="info" label="About">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<p>Create Cashu ecash mints and wallets.</p>
|
||||||
|
<small
|
||||||
|
>Created by
|
||||||
|
<a href="https://github.com/arcbtc" target="_blank">arcbtc</a>,
|
||||||
|
<a href="https://github.com/motorina0" target="_blank">vlad</a>,
|
||||||
|
<a href="https://github.com/calle" target="_blank">calle</a>.</small
|
||||||
|
>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
367
lnbits/extensions/cashu/templates/cashu/index.html
Normal file
367
lnbits/extensions/cashu/templates/cashu/index.html
Normal file
|
@ -0,0 +1,367 @@
|
||||||
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
|
%} {% block page %}
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<b>Cashu mint and wallet</b>
|
||||||
|
<p></p>
|
||||||
|
<p>
|
||||||
|
Here you can create multiple cashu mints that you can share. Each mint
|
||||||
|
can service many users but all ecash tokens of a mint are only valid
|
||||||
|
inside that mint and not across different mints. To exchange funds
|
||||||
|
between mints, use Lightning payments.
|
||||||
|
</p>
|
||||||
|
<b>Important</b>
|
||||||
|
<p></p>
|
||||||
|
<p>
|
||||||
|
If you are the operator of this LNbits instance, make sure to set
|
||||||
|
CASHU_PRIVATE_KEY="randomkey" in your configuration file. Do not
|
||||||
|
create mints before setting the key and do not change the key once
|
||||||
|
set.
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col">
|
||||||
|
<h5 class="text-subtitle1 q-my-none">Mints</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
:data="cashus"
|
||||||
|
row-key="id"
|
||||||
|
:columns="cashusTable.columns"
|
||||||
|
:pagination.sync="cashusTable.pagination"
|
||||||
|
>
|
||||||
|
{% raw %}
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th auto-width></q-th>
|
||||||
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
<q-th auto-width></q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
icon="account_balance_wallet"
|
||||||
|
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||||
|
type="a"
|
||||||
|
:href="'wallet/?' + 'mint_id=' + props.row.id"
|
||||||
|
target="_blank"
|
||||||
|
><q-tooltip>Shareable wallet</q-tooltip></q-btn
|
||||||
|
>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
icon="account_balance"
|
||||||
|
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||||
|
type="a"
|
||||||
|
:href="'mint/' + props.row.id"
|
||||||
|
target="_blank"
|
||||||
|
><q-tooltip>Shareable mint page</q-tooltip></q-btn
|
||||||
|
>
|
||||||
|
</q-td>
|
||||||
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
{{ (col.name == 'tip_options' && col.value ?
|
||||||
|
JSON.parse(col.value).join(", ") : col.value) }}
|
||||||
|
</q-td>
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="deleteMint(props.row.id)"
|
||||||
|
icon="cancel"
|
||||||
|
color="pink"
|
||||||
|
></q-btn>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
{% endraw %}
|
||||||
|
</q-table>
|
||||||
|
<q-btn
|
||||||
|
class="q-pt-l"
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
@click="formDialog.show = true"
|
||||||
|
>New Mint</q-btn
|
||||||
|
>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} Cashu extension</h6>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-list>
|
||||||
|
{% include "cashu/_api_docs.html" %}
|
||||||
|
<q-separator></q-separator>
|
||||||
|
{% include "cashu/_cashu.html" %}
|
||||||
|
</q-list>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
|
<q-form @submit="createMint" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.name"
|
||||||
|
label="Mint Name"
|
||||||
|
placeholder="Cashu Mint"
|
||||||
|
></q-input>
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="formDialog.data.wallet"
|
||||||
|
:options="g.user.walletOptions"
|
||||||
|
label="Cashu wallet *"
|
||||||
|
></q-select>
|
||||||
|
<!-- <q-toggle
|
||||||
|
v-model="toggleAdvanced"
|
||||||
|
label="Show advanced options"
|
||||||
|
></q-toggle>
|
||||||
|
<div v-show="toggleAdvanced">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-5">
|
||||||
|
<q-checkbox
|
||||||
|
v-model="formDialog.data.fraction"
|
||||||
|
color="primary"
|
||||||
|
label="sats/coins?"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
>Use with hedging extension to create a stablecoin!</q-tooltip
|
||||||
|
>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="col-7">
|
||||||
|
<q-input
|
||||||
|
v-if="!formDialog.data.fraction"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
type="number"
|
||||||
|
v-model.trim="formDialog.data.cost"
|
||||||
|
label="Sat coin cost (optional)"
|
||||||
|
value="1"
|
||||||
|
type="number"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
v-if="!formDialog.data.fraction"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.tickershort"
|
||||||
|
label="Ticker shorthand"
|
||||||
|
placeholder="sats"
|
||||||
|
#
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-input
|
||||||
|
class="q-mt-md"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
type="number"
|
||||||
|
v-model.trim="formDialog.data.maxsats"
|
||||||
|
label="Maximum mint liquidity (optional)"
|
||||||
|
placeholder="∞"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
class="q-mt-md"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
type="number"
|
||||||
|
v-model.trim="formDialog.data.coins"
|
||||||
|
label="Coins that 'exist' in mint (optional)"
|
||||||
|
placeholder="∞"
|
||||||
|
></q-input>
|
||||||
|
</div> -->
|
||||||
|
<div class="row q-mt-md">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="formDialog.data.wallet == null || formDialog.data.name == null"
|
||||||
|
type="submit"
|
||||||
|
>Create Mint
|
||||||
|
</q-btn>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
||||||
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
|
<script>
|
||||||
|
var mapMint = function (obj) {
|
||||||
|
obj.date = Quasar.utils.date.formatDate(
|
||||||
|
new Date(obj.time * 1000),
|
||||||
|
'YYYY-MM-DD HH:mm'
|
||||||
|
)
|
||||||
|
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||||
|
obj.cashu = ['/cashu/', obj.id].join('')
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
cashus: [],
|
||||||
|
hostname: location.protocol + '//' + location.host + '/cashu/mint/',
|
||||||
|
toggleAdvanced: false,
|
||||||
|
cashusTable: {
|
||||||
|
columns: [
|
||||||
|
{name: 'id', align: 'left', label: 'Mint ID', field: 'id'},
|
||||||
|
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||||
|
// {
|
||||||
|
// name: 'tickershort',
|
||||||
|
// align: 'left',
|
||||||
|
// label: 'Ticker',
|
||||||
|
// field: 'tickershort'
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
name: 'wallet',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Mint wallet',
|
||||||
|
field: 'wallet'
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
// name: 'fraction',
|
||||||
|
// align: 'left',
|
||||||
|
// label: 'Using fraction',
|
||||||
|
// field: 'fraction'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'maxsats',
|
||||||
|
// align: 'left',
|
||||||
|
// label: 'Max Sats',
|
||||||
|
// field: 'maxsats'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'coins',
|
||||||
|
// align: 'left',
|
||||||
|
// label: 'No. of coins',
|
||||||
|
// field: 'coins'
|
||||||
|
// }
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {fraction: false}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeFormDialog: function () {
|
||||||
|
this.formDialog.data = {}
|
||||||
|
},
|
||||||
|
getMints: function () {
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/cashu/api/v1/mints?all_wallets=true',
|
||||||
|
this.g.user.wallets[0].inkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.cashus = response.data.map(function (obj) {
|
||||||
|
return mapMint(obj)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
createMint: function () {
|
||||||
|
if (this.formDialog.data.maxliquid == null) {
|
||||||
|
this.formDialog.data.maxliquid = 0
|
||||||
|
}
|
||||||
|
var data = {
|
||||||
|
name: this.formDialog.data.name,
|
||||||
|
tickershort: this.formDialog.data.tickershort,
|
||||||
|
maxliquid: this.formDialog.data.maxliquid
|
||||||
|
}
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'POST',
|
||||||
|
'/cashu/api/v1/mints',
|
||||||
|
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
|
||||||
|
.inkey,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.cashus.push(mapMint(response.data))
|
||||||
|
self.formDialog.show = false
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteMint: function (cashuId) {
|
||||||
|
var self = this
|
||||||
|
var cashu = _.findWhere(this.cashus, {id: cashuId})
|
||||||
|
console.log(cashu)
|
||||||
|
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog(
|
||||||
|
"Are you sure you want to delete this Mint? This mint's users will not be able to redeem their tokens!"
|
||||||
|
)
|
||||||
|
.onOk(function () {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'DELETE',
|
||||||
|
'/cashu/api/v1/mints/' + cashuId,
|
||||||
|
_.findWhere(self.g.user.wallets, {id: cashu.wallet}).adminkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.cashus = _.reject(self.cashus, function (obj) {
|
||||||
|
return obj.id == cashuId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
exportCSV: function () {
|
||||||
|
LNbits.utils.exportCSV(this.cashusTable.columns, this.cashus)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
if (this.g.user.wallets.length) {
|
||||||
|
this.getMints()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
76
lnbits/extensions/cashu/templates/cashu/mint.html
Normal file
76
lnbits/extensions/cashu/templates/cashu/mint.html
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
{% extends "public.html" %} {% block page %}
|
||||||
|
<div class="row q-col-gutter-md justify-center">
|
||||||
|
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||||
|
<q-card class="q-pa-lg q-mb-xl">
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<center>
|
||||||
|
<q-icon
|
||||||
|
name="account_balance"
|
||||||
|
class="text-grey"
|
||||||
|
style="font-size: 10rem"
|
||||||
|
></q-icon>
|
||||||
|
<h4 class="q-mt-none q-mb-md">{{ mint_name }}</h4>
|
||||||
|
<a
|
||||||
|
class="q-my-xl text-white"
|
||||||
|
style="font-size: 1.5rem"
|
||||||
|
href="../wallet?mint_id={{ mint_id }}"
|
||||||
|
>Open wallet</a
|
||||||
|
>
|
||||||
|
</center>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
<q-card class="q-pa-lg q-mb-xl">
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<h5 class="q-my-md">Read the following carefully!</h5>
|
||||||
|
<p>
|
||||||
|
This is a
|
||||||
|
<a href="https://cashu.space/" style="color: white" target="”_blank”"
|
||||||
|
>Cashu</a
|
||||||
|
>
|
||||||
|
mint. Cashu is an ecash system for Bitcoin.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Open this page in your native browser</strong><br />
|
||||||
|
Before you continue to the wallet, make sure to open this page in your
|
||||||
|
device's native browser application (Safari for iOS, Chrome for
|
||||||
|
Android). Do not use Cashu in an embedded browser that opens when you
|
||||||
|
click a link in a messenger.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Add wallet to home screen</strong><br />
|
||||||
|
You can add Cashu to your home screen as a progressive web app (PWA).
|
||||||
|
After opening the wallet in your browser (click the link above), on
|
||||||
|
Android (Chrome), click the menu at the upper right. On iOS (Safari),
|
||||||
|
click the share button. Now press the Add to Home screen button.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Backup your wallet</strong><br />
|
||||||
|
Ecash is a bearer asset. That means losing access to your wallet will
|
||||||
|
make you lose your funds. The wallet stores ecash tokens on your
|
||||||
|
device's database. If you lose the link or delete your your data
|
||||||
|
without backing up, you will lose your tokens. Press the Backup button
|
||||||
|
in the wallet to download a copy of your tokens.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>This service is in BETA</strong> <br />
|
||||||
|
We hold no responsibility for people losing access to funds. Use at
|
||||||
|
your own risk!
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %} {% block scripts %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
2337
lnbits/extensions/cashu/templates/cashu/wallet.html
Normal file
2337
lnbits/extensions/cashu/templates/cashu/wallet.html
Normal file
File diff suppressed because it is too large
Load diff
224
lnbits/extensions/cashu/views.py
Normal file
224
lnbits/extensions/cashu/views.py
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.params import Depends
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
|
from lnbits.core.models import User
|
||||||
|
from lnbits.decorators import check_user_exists
|
||||||
|
|
||||||
|
from . import cashu_ext, cashu_renderer
|
||||||
|
from .crud import get_cashu
|
||||||
|
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.get("/", response_class=HTMLResponse)
|
||||||
|
async def index(
|
||||||
|
request: Request,
|
||||||
|
user: User = Depends(check_user_exists), # type: ignore
|
||||||
|
):
|
||||||
|
return cashu_renderer().TemplateResponse(
|
||||||
|
"cashu/index.html", {"request": request, "user": user.dict()}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.get("/wallet")
|
||||||
|
async def wallet(request: Request, mint_id: str):
|
||||||
|
return cashu_renderer().TemplateResponse(
|
||||||
|
"cashu/wallet.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"web_manifest": f"/cashu/manifest/{mint_id}.webmanifest",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.get("/mint/{mintID}")
|
||||||
|
async def cashu(request: Request, mintID):
|
||||||
|
cashu = await get_cashu(mintID)
|
||||||
|
if not cashu:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist."
|
||||||
|
)
|
||||||
|
return cashu_renderer().TemplateResponse(
|
||||||
|
"cashu/mint.html",
|
||||||
|
{"request": request, "mint_name": cashu.name, "mint_id": mintID},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.get("/manifest/{cashu_id}.webmanifest")
|
||||||
|
async def manifest(cashu_id: str):
|
||||||
|
cashu = await get_cashu(cashu_id)
|
||||||
|
if not cashu:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"short_name": "Cashu",
|
||||||
|
"name": "Cashu" + " - " + cashu.name,
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-512-512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-96-96.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"id": "/cashu/wallet?mint_id=" + cashu_id,
|
||||||
|
"start_url": "/cashu/wallet?mint_id=" + cashu_id,
|
||||||
|
"background_color": "#1F2234",
|
||||||
|
"description": "Cashu ecash wallet",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": "/cashu/",
|
||||||
|
"theme_color": "#1F2234",
|
||||||
|
"protocol_handlers": [
|
||||||
|
{"protocol": "cashu", "url": "&recv_token=%s"},
|
||||||
|
{"protocol": "lightning", "url": "&lightning=%s"},
|
||||||
|
],
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "Cashu" + " - " + cashu.name,
|
||||||
|
"short_name": "Cashu",
|
||||||
|
"description": "Cashu" + " - " + cashu.name,
|
||||||
|
"url": "/cashu/wallet?mint_id=" + cashu_id,
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-512-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-192-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-144-144.png",
|
||||||
|
"sizes": "144x144",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-96-96.png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-72-72.png",
|
||||||
|
"sizes": "72x72",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-48-48.png",
|
||||||
|
"sizes": "48x48",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/16.png",
|
||||||
|
"sizes": "16x16",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/20.png",
|
||||||
|
"sizes": "20x20",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/29.png",
|
||||||
|
"sizes": "29x29",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/32.png",
|
||||||
|
"sizes": "32x32",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/40.png",
|
||||||
|
"sizes": "40x40",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/50.png",
|
||||||
|
"sizes": "50x50",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/57.png",
|
||||||
|
"sizes": "57x57",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/58.png",
|
||||||
|
"sizes": "58x58",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/60.png",
|
||||||
|
"sizes": "60x60",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/64.png",
|
||||||
|
"sizes": "64x64",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/72.png",
|
||||||
|
"sizes": "72x72",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/76.png",
|
||||||
|
"sizes": "76x76",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/80.png",
|
||||||
|
"sizes": "80x80",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/87.png",
|
||||||
|
"sizes": "87x87",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/100.png",
|
||||||
|
"sizes": "100x100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/114.png",
|
||||||
|
"sizes": "114x114",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/120.png",
|
||||||
|
"sizes": "120x120",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/128.png",
|
||||||
|
"sizes": "128x128",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/144.png",
|
||||||
|
"sizes": "144x144",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/152.png",
|
||||||
|
"sizes": "152x152",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/167.png",
|
||||||
|
"sizes": "167x167",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/180.png",
|
||||||
|
"sizes": "180x180",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/256.png",
|
||||||
|
"sizes": "256x256",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/1024.png",
|
||||||
|
"sizes": "1024x1024",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
382
lnbits/extensions/cashu/views_api.py
Normal file
382
lnbits/extensions/cashu/views_api.py
Normal file
|
@ -0,0 +1,382 @@
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Dict, List, Union
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
# -------- cashu imports
|
||||||
|
from cashu.core.base import (
|
||||||
|
BlindedSignature,
|
||||||
|
CheckFeesRequest,
|
||||||
|
CheckFeesResponse,
|
||||||
|
CheckRequest,
|
||||||
|
GetMeltResponse,
|
||||||
|
GetMintResponse,
|
||||||
|
Invoice,
|
||||||
|
MeltRequest,
|
||||||
|
MintRequest,
|
||||||
|
PostSplitResponse,
|
||||||
|
Proof,
|
||||||
|
SplitRequest,
|
||||||
|
)
|
||||||
|
from fastapi import Query
|
||||||
|
from fastapi.params import Depends
|
||||||
|
from lnurl import decode as decode_lnurl
|
||||||
|
from loguru import logger
|
||||||
|
from secp256k1 import PublicKey
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
|
from lnbits import bolt11
|
||||||
|
from lnbits.core.crud import check_internal, get_user
|
||||||
|
from lnbits.core.services import (
|
||||||
|
check_transaction_status,
|
||||||
|
create_invoice,
|
||||||
|
fee_reserve,
|
||||||
|
pay_invoice,
|
||||||
|
)
|
||||||
|
from lnbits.core.views.api import api_payment
|
||||||
|
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
from lnbits.wallets.base import PaymentStatus
|
||||||
|
|
||||||
|
from . import cashu_ext, ledger
|
||||||
|
from .crud import create_cashu, delete_cashu, get_cashu, get_cashus
|
||||||
|
from .models import Cashu
|
||||||
|
|
||||||
|
# --------- extension imports
|
||||||
|
|
||||||
|
|
||||||
|
LIGHTNING = True
|
||||||
|
|
||||||
|
########################################
|
||||||
|
############### LNBITS MINTS ###########
|
||||||
|
########################################
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.get("/api/v1/mints", status_code=HTTPStatus.OK)
|
||||||
|
async def api_cashus(
|
||||||
|
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get all mints of this wallet.
|
||||||
|
"""
|
||||||
|
wallet_ids = [wallet.wallet.id]
|
||||||
|
if all_wallets:
|
||||||
|
user = await get_user(wallet.wallet.user)
|
||||||
|
if user:
|
||||||
|
wallet_ids = user.wallet_ids
|
||||||
|
|
||||||
|
return [cashu.dict() for cashu in await get_cashus(wallet_ids)]
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.post("/api/v1/mints", status_code=HTTPStatus.CREATED)
|
||||||
|
async def api_cashu_create(
|
||||||
|
data: Cashu,
|
||||||
|
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a new mint for this wallet.
|
||||||
|
"""
|
||||||
|
cashu_id = urlsafe_short_hash()
|
||||||
|
# generate a new keyset in cashu
|
||||||
|
keyset = await ledger.load_keyset(cashu_id)
|
||||||
|
|
||||||
|
cashu = await create_cashu(
|
||||||
|
cashu_id=cashu_id, keyset_id=keyset.id, wallet_id=wallet.wallet.id, data=data
|
||||||
|
)
|
||||||
|
logger.debug(cashu)
|
||||||
|
return cashu.dict()
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.delete("/api/v1/mints/{cashu_id}")
|
||||||
|
async def api_cashu_delete(
|
||||||
|
cashu_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) # type: ignore
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Delete an existing cashu mint.
|
||||||
|
"""
|
||||||
|
cashu = await get_cashu(cashu_id)
|
||||||
|
|
||||||
|
if not cashu:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Cashu mint does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
if cashu.wallet != wallet.wallet.id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN, detail="Not your Cashu mint."
|
||||||
|
)
|
||||||
|
|
||||||
|
await delete_cashu(cashu_id)
|
||||||
|
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
########### CASHU ENDPOINTS ###########
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.get("/api/v1/{cashu_id}/keys", status_code=HTTPStatus.OK)
|
||||||
|
async def keys(cashu_id: str = Query(None)) -> dict[int, str]:
|
||||||
|
"""Get the public keys of the mint"""
|
||||||
|
cashu: Union[Cashu, None] = await get_cashu(cashu_id)
|
||||||
|
|
||||||
|
if not cashu:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
return ledger.get_keyset(keyset_id=cashu.keyset_id)
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.get("/api/v1/{cashu_id}/keysets", status_code=HTTPStatus.OK)
|
||||||
|
async def keysets(cashu_id: str = Query(None)) -> dict[str, list[str]]:
|
||||||
|
"""Get the public keys of the mint"""
|
||||||
|
cashu: Union[Cashu, None] = await get_cashu(cashu_id)
|
||||||
|
|
||||||
|
if not cashu:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"keysets": [cashu.keyset_id]}
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.get("/api/v1/{cashu_id}/mint")
|
||||||
|
async def request_mint(cashu_id: str = Query(None), 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.
|
||||||
|
|
||||||
|
Call `POST /mint` after paying the invoice.
|
||||||
|
"""
|
||||||
|
cashu: Union[Cashu, None] = await get_cashu(cashu_id)
|
||||||
|
|
||||||
|
if not cashu:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
# create an invoice that the wallet needs to pay
|
||||||
|
try:
|
||||||
|
payment_hash, payment_request = await create_invoice(
|
||||||
|
wallet_id=cashu.wallet,
|
||||||
|
amount=amount,
|
||||||
|
memo=f"{cashu.name}",
|
||||||
|
extra={"tag": "cashu"},
|
||||||
|
)
|
||||||
|
invoice = Invoice(
|
||||||
|
amount=amount, pr=payment_request, hash=payment_hash, issued=False
|
||||||
|
)
|
||||||
|
# await store_lightning_invoice(cashu_id, invoice)
|
||||||
|
await ledger.crud.store_lightning_invoice(invoice=invoice, db=ledger.db)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
||||||
|
|
||||||
|
print(f"Lightning invoice: {payment_request}")
|
||||||
|
resp = GetMintResponse(pr=payment_request, hash=payment_hash)
|
||||||
|
# return {"pr": payment_request, "hash": payment_hash}
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.post("/api/v1/{cashu_id}/mint")
|
||||||
|
async def mint_coins(
|
||||||
|
data: MintRequest,
|
||||||
|
cashu_id: str = Query(None),
|
||||||
|
payment_hash: str = Query(None),
|
||||||
|
) -> List[BlindedSignature]:
|
||||||
|
"""
|
||||||
|
Requests the minting of tokens belonging to a paid payment request.
|
||||||
|
Call this endpoint after `GET /mint`.
|
||||||
|
"""
|
||||||
|
cashu: Union[Cashu, None] = await get_cashu(cashu_id)
|
||||||
|
if cashu is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
if LIGHTNING:
|
||||||
|
invoice: Invoice = await ledger.crud.get_lightning_invoice(
|
||||||
|
db=ledger.db, hash=payment_hash
|
||||||
|
)
|
||||||
|
if invoice is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail="Mint does not know this invoice.",
|
||||||
|
)
|
||||||
|
if invoice.issued == True:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||||
|
detail="Tokens already issued for this invoice.",
|
||||||
|
)
|
||||||
|
|
||||||
|
total_requested = sum([bm.amount for bm in data.blinded_messages])
|
||||||
|
if total_requested > invoice.amount:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||||
|
detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}",
|
||||||
|
)
|
||||||
|
|
||||||
|
status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash)
|
||||||
|
|
||||||
|
if status.paid != True:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
keyset = ledger.keysets.keysets[cashu.keyset_id]
|
||||||
|
|
||||||
|
promises = await ledger._generate_promises(
|
||||||
|
B_s=data.blinded_messages, keyset=keyset
|
||||||
|
)
|
||||||
|
assert len(promises), HTTPException(
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="No promises returned."
|
||||||
|
)
|
||||||
|
await ledger.crud.update_lightning_invoice(
|
||||||
|
db=ledger.db, hash=payment_hash, issued=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return promises
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.post("/api/v1/{cashu_id}/melt")
|
||||||
|
async def melt_coins(
|
||||||
|
payload: MeltRequest, cashu_id: str = Query(None)
|
||||||
|
) -> GetMeltResponse:
|
||||||
|
"""Invalidates proofs and pays a Lightning invoice."""
|
||||||
|
cashu: Union[None, Cashu] = await get_cashu(cashu_id)
|
||||||
|
if cashu is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
|
)
|
||||||
|
proofs = payload.proofs
|
||||||
|
invoice = payload.invoice
|
||||||
|
|
||||||
|
# !!!!!!! MAKE SURE THAT PROOFS ARE ONLY FROM THIS CASHU KEYSET ID
|
||||||
|
# THIS IS NECESSARY BECAUSE THE CASHU BACKEND WILL ACCEPT ANY VALID
|
||||||
|
# TOKENS
|
||||||
|
assert all([p.id == cashu.keyset_id for p in proofs]), HTTPException(
|
||||||
|
status_code=HTTPStatus.METHOD_NOT_ALLOWED,
|
||||||
|
detail="Error: Tokens are from another mint.",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert all([ledger._verify_proof(p) for p in proofs]), HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="Could not verify proofs.",
|
||||||
|
)
|
||||||
|
|
||||||
|
total_provided = sum([p["amount"] for p in proofs])
|
||||||
|
invoice_obj = bolt11.decode(invoice)
|
||||||
|
amount = math.ceil(invoice_obj.amount_msat / 1000)
|
||||||
|
|
||||||
|
internal_checking_id = await check_internal(invoice_obj.payment_hash)
|
||||||
|
|
||||||
|
if not internal_checking_id:
|
||||||
|
fees_msat = fee_reserve(invoice_obj.amount_msat)
|
||||||
|
else:
|
||||||
|
fees_msat = 0
|
||||||
|
assert total_provided >= amount + fees_msat / 1000, Exception(
|
||||||
|
f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)."
|
||||||
|
)
|
||||||
|
|
||||||
|
await pay_invoice(
|
||||||
|
wallet_id=cashu.wallet,
|
||||||
|
payment_request=invoice,
|
||||||
|
description=f"pay cashu invoice",
|
||||||
|
extra={"tag": "cashu", "cahsu_name": cashu.name},
|
||||||
|
)
|
||||||
|
|
||||||
|
status: PaymentStatus = await check_transaction_status(
|
||||||
|
cashu.wallet, invoice_obj.payment_hash
|
||||||
|
)
|
||||||
|
if status.paid == True:
|
||||||
|
await ledger._invalidate_proofs(proofs)
|
||||||
|
return GetMeltResponse(paid=status.paid, preimage=status.preimage)
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.post("/api/v1/{cashu_id}/check")
|
||||||
|
async def check_spendable(
|
||||||
|
payload: CheckRequest, cashu_id: str = Query(None)
|
||||||
|
) -> Dict[int, bool]:
|
||||||
|
"""Check whether a secret has been spent already or not."""
|
||||||
|
cashu: Union[None, Cashu] = await get_cashu(cashu_id)
|
||||||
|
if cashu is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
|
)
|
||||||
|
return await ledger.check_spendable(payload.proofs)
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.post("/api/v1/{cashu_id}/checkfees")
|
||||||
|
async def check_fees(
|
||||||
|
payload: CheckFeesRequest, cashu_id: str = Query(None)
|
||||||
|
) -> CheckFeesResponse:
|
||||||
|
"""
|
||||||
|
Responds with the fees necessary to pay a Lightning invoice.
|
||||||
|
Used by wallets for figuring out the fees they need to supply.
|
||||||
|
This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu).
|
||||||
|
"""
|
||||||
|
cashu: Union[None, Cashu] = await get_cashu(cashu_id)
|
||||||
|
if cashu is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
|
)
|
||||||
|
invoice_obj = bolt11.decode(payload.pr)
|
||||||
|
internal_checking_id = await check_internal(invoice_obj.payment_hash)
|
||||||
|
|
||||||
|
if not internal_checking_id:
|
||||||
|
fees_msat = fee_reserve(invoice_obj.amount_msat)
|
||||||
|
else:
|
||||||
|
fees_msat = 0
|
||||||
|
return CheckFeesResponse(fee=fees_msat / 1000)
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.post("/api/v1/{cashu_id}/split")
|
||||||
|
async def split(
|
||||||
|
payload: SplitRequest, cashu_id: str = Query(None)
|
||||||
|
) -> PostSplitResponse:
|
||||||
|
"""
|
||||||
|
Requetst a set of tokens with amount "total" to be split into two
|
||||||
|
newly minted sets with amount "split" and "total-split".
|
||||||
|
"""
|
||||||
|
cashu: Union[None, Cashu] = await get_cashu(cashu_id)
|
||||||
|
if cashu is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
|
)
|
||||||
|
proofs = payload.proofs
|
||||||
|
|
||||||
|
# !!!!!!! MAKE SURE THAT PROOFS ARE ONLY FROM THIS CASHU KEYSET ID
|
||||||
|
# THIS IS NECESSARY BECAUSE THE CASHU BACKEND WILL ACCEPT ANY VALID
|
||||||
|
# TOKENS
|
||||||
|
if not all([p.id == cashu.keyset_id for p in proofs]):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.METHOD_NOT_ALLOWED,
|
||||||
|
detail="Error: Tokens are from another mint.",
|
||||||
|
)
|
||||||
|
|
||||||
|
amount = payload.amount
|
||||||
|
outputs = payload.outputs.blinded_messages
|
||||||
|
assert outputs, Exception("no outputs provided.")
|
||||||
|
split_return = None
|
||||||
|
try:
|
||||||
|
keyset = ledger.keysets.keysets[cashu.keyset_id]
|
||||||
|
split_return = await ledger.split(proofs, amount, outputs, keyset)
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=str(exc),
|
||||||
|
)
|
||||||
|
if not split_return:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="there was an error with the split",
|
||||||
|
)
|
||||||
|
frst_promises, scnd_promises = split_return
|
||||||
|
resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises)
|
||||||
|
return resp
|
|
@ -16,6 +16,7 @@ from .models import Charges, CreateCharge, SatsPayThemes
|
||||||
|
|
||||||
|
|
||||||
async def create_charge(user: str, data: CreateCharge) -> Charges:
|
async def create_charge(user: str, data: CreateCharge) -> Charges:
|
||||||
|
data = CreateCharge(**data.dict())
|
||||||
charge_id = urlsafe_short_hash()
|
charge_id = urlsafe_short_hash()
|
||||||
if data.onchainwallet:
|
if data.onchainwallet:
|
||||||
config = await get_config(user)
|
config = await get_config(user)
|
||||||
|
|
|
@ -6,6 +6,10 @@ from typing import Optional
|
||||||
from fastapi.param_functions import Query
|
from fastapi.param_functions import Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
DEFAULT_MEMPOOL_CONFIG = (
|
||||||
|
'{"mempool_endpoint": "https://mempool.space", "network": "Mainnet"}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CreateCharge(BaseModel):
|
class CreateCharge(BaseModel):
|
||||||
onchainwallet: str = Query(None)
|
onchainwallet: str = Query(None)
|
||||||
|
@ -17,7 +21,7 @@ class CreateCharge(BaseModel):
|
||||||
custom_css: Optional[str]
|
custom_css: Optional[str]
|
||||||
time: int = Query(..., ge=1)
|
time: int = Query(..., ge=1)
|
||||||
amount: int = Query(..., ge=1)
|
amount: int = Query(..., ge=1)
|
||||||
extra: str = "{}"
|
extra: str = DEFAULT_MEMPOOL_CONFIG
|
||||||
|
|
||||||
|
|
||||||
class ChargeConfig(BaseModel):
|
class ChargeConfig(BaseModel):
|
||||||
|
@ -38,8 +42,8 @@ class Charges(BaseModel):
|
||||||
webhook: Optional[str]
|
webhook: Optional[str]
|
||||||
completelink: Optional[str]
|
completelink: Optional[str]
|
||||||
completelinktext: Optional[str] = "Back to Merchant"
|
completelinktext: Optional[str] = "Back to Merchant"
|
||||||
extra: str = "{}"
|
|
||||||
custom_css: Optional[str]
|
custom_css: Optional[str]
|
||||||
|
extra: str = DEFAULT_MEMPOOL_CONFIG
|
||||||
time: int
|
time: int
|
||||||
amount: int
|
amount: int
|
||||||
balance: int
|
balance: int
|
||||||
|
|
|
@ -203,7 +203,7 @@ async def update_address(id: str, **kwargs) -> Optional[Address]:
|
||||||
f"""UPDATE watchonly.addresses SET {q} WHERE id = ? """,
|
f"""UPDATE watchonly.addresses SET {q} WHERE id = ? """,
|
||||||
(*kwargs.values(), id),
|
(*kwargs.values(), id),
|
||||||
)
|
)
|
||||||
row = await db.fetchone("SELECT * FROM watchonly.addresses WHERE id = ?", (id))
|
row = await db.fetchone("SELECT * FROM watchonly.addresses WHERE id = ?", (id,))
|
||||||
return Address.from_row(row) if row else None
|
return Address.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,8 @@ class Extension(NamedTuple):
|
||||||
icon: Optional[str] = None
|
icon: Optional[str] = None
|
||||||
contributors: Optional[List[str]] = None
|
contributors: Optional[List[str]] = None
|
||||||
hidden: bool = False
|
hidden: bool = False
|
||||||
|
migration_module: Optional[str] = None
|
||||||
|
db_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ExtensionManager:
|
class ExtensionManager:
|
||||||
|
@ -66,6 +68,8 @@ class ExtensionManager:
|
||||||
config.get("icon"),
|
config.get("icon"),
|
||||||
config.get("contributors"),
|
config.get("contributors"),
|
||||||
config.get("hidden") or False,
|
config.get("hidden") or False,
|
||||||
|
config.get("migration_module"),
|
||||||
|
config.get("db_name"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
744
poetry.lock
generated
744
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -12,58 +12,60 @@ script = "build.py"
|
||||||
python = "^3.10 | ^3.9 | ^3.8 | ^3.7"
|
python = "^3.10 | ^3.9 | ^3.8 | ^3.7"
|
||||||
aiofiles = "0.8.0"
|
aiofiles = "0.8.0"
|
||||||
asgiref = "3.4.1"
|
asgiref = "3.4.1"
|
||||||
attrs = "21.2.0"
|
attrs = "22.1.0"
|
||||||
bech32 = "1.2.0"
|
bech32 = "1.2.0"
|
||||||
bitstring = "3.1.9"
|
bitstring = "3.1.9"
|
||||||
certifi = "2021.5.30"
|
certifi = "2022.9.24"
|
||||||
charset-normalizer = "2.0.6"
|
charset-normalizer = "2.0.12"
|
||||||
click = "8.0.1"
|
click = "8.0.4"
|
||||||
ecdsa = "0.17.0"
|
ecdsa = "0.18.0"
|
||||||
embit = "0.4.9"
|
embit = "0.4.9"
|
||||||
environs = "9.3.3"
|
environs = "9.5.0"
|
||||||
fastapi = "0.78.0"
|
fastapi = "0.83.0"
|
||||||
h11 = "0.12.0"
|
h11 = "0.12.0"
|
||||||
httpcore = "0.15.0"
|
httpcore = "0.15.0"
|
||||||
httptools = "0.4.0"
|
httptools = "0.4.0"
|
||||||
httpx = "0.23.0"
|
httpx = "0.23.0"
|
||||||
idna = "3.2"
|
idna = "3.4"
|
||||||
importlib-metadata = "4.8.1"
|
importlib-metadata = "5.0.0"
|
||||||
jinja2 = "3.0.1"
|
jinja2 = "3.0.1"
|
||||||
lnurl = "0.3.6"
|
lnurl = "0.3.6"
|
||||||
markupsafe = "2.0.1"
|
markupsafe = "2.0.1"
|
||||||
marshmallow = "3.17.0"
|
marshmallow = "3.18.0"
|
||||||
outcome = "1.1.0"
|
outcome = "1.2.0"
|
||||||
psycopg2-binary = "2.9.1"
|
psycopg2-binary = "2.9.1"
|
||||||
pycryptodomex = "3.14.1"
|
pycryptodomex = "3.14.1"
|
||||||
pydantic = "1.8.2"
|
pydantic = "1.10.2"
|
||||||
pypng = "0.0.21"
|
pypng = "0.0.21"
|
||||||
pyqrcode = "1.2.1"
|
pyqrcode = "1.2.1"
|
||||||
pyScss = "1.4.0"
|
pyScss = "1.4.0"
|
||||||
python-dotenv = "0.19.0"
|
python-dotenv = "0.21.0"
|
||||||
pyyaml = "5.4.1"
|
pyyaml = "5.4.1"
|
||||||
represent = "1.6.0.post0"
|
represent = "1.6.0.post0"
|
||||||
rfc3986 = "1.5.0"
|
rfc3986 = "1.5.0"
|
||||||
secp256k1 = "0.14.0"
|
secp256k1 = "0.14.0"
|
||||||
shortuuid = "1.0.1"
|
shortuuid = "1.0.1"
|
||||||
six = "1.16.0"
|
six = "1.16.0"
|
||||||
sniffio = "1.2.0"
|
sniffio = "1.3.0"
|
||||||
sqlalchemy = "1.3.23"
|
sqlalchemy = "1.3.24"
|
||||||
sqlalchemy-aio = "0.17.0"
|
sqlalchemy-aio = "0.17.0"
|
||||||
sse-starlette = "0.6.2"
|
sse-starlette = "0.6.2"
|
||||||
typing-extensions = "3.10.0.2"
|
typing-extensions = "^4.4.0"
|
||||||
uvicorn = "0.18.1"
|
uvicorn = "0.18.3"
|
||||||
uvloop = "0.16.0"
|
uvloop = "0.16.0"
|
||||||
watchgod = "0.7"
|
watchgod = "0.7"
|
||||||
websockets = "10.0"
|
websockets = "10.0"
|
||||||
zipp = "3.5.0"
|
zipp = "3.9.0"
|
||||||
loguru = "0.5.3"
|
loguru = "0.6.0"
|
||||||
cffi = "1.15.0"
|
cffi = "1.15.1"
|
||||||
websocket-client = "1.3.3"
|
websocket-client = "1.3.3"
|
||||||
grpcio = "^1.49.1"
|
grpcio = "^1.49.1"
|
||||||
protobuf = "^4.21.6"
|
protobuf = "^4.21.6"
|
||||||
Cerberus = "^1.3.4"
|
Cerberus = "^1.3.4"
|
||||||
async-timeout = "^4.0.2"
|
async-timeout = "^4.0.2"
|
||||||
pyln-client = "0.11.1"
|
pyln-client = "0.11.1"
|
||||||
|
cashu = "0.5.5"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
isort = "^5.10.1"
|
isort = "^5.10.1"
|
||||||
|
|
|
@ -1,45 +1,49 @@
|
||||||
aiofiles==0.8.0 ; python_version >= "3.7" and python_version < "4.0"
|
aiofiles==0.8.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
anyio==3.6.1 ; python_version >= "3.7" and python_version < "4.0"
|
anyio==3.6.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
asgiref==3.4.1 ; python_version >= "3.7" and python_version < "4.0"
|
asgiref==3.4.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
asn1crypto==1.5.1 ; python_version >= "3.7" and python_version < "4.0"
|
asn1crypto==1.5.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
async-timeout==4.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
async-timeout==4.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
attrs==21.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
attrs==22.1.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
base58==2.1.1 ; python_version >= "3.7" and python_version < "4.0"
|
base58==2.1.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
bech32==1.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"
|
bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
|
cashu==0.5.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
cerberus==1.3.4 ; python_version >= "3.7" and python_version < "4.0"
|
cerberus==1.3.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
certifi==2021.5.30 ; python_version >= "3.7" and python_version < "4.0"
|
certifi==2022.9.24 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
cffi==1.15.0 ; 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.6 ; 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.1 ; python_version >= "3.7" and python_version < "4.0"
|
click==8.0.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
coincurve==17.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
coincurve==17.0.0 ; 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.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"
|
||||||
cryptography==36.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
cryptography==36.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
ecdsa==0.17.0 ; python_version >= "3.7" and python_version < "4.0"
|
ecdsa==0.18.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
embit==0.4.9 ; python_version >= "3.7" and python_version < "4.0"
|
embit==0.4.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
enum34==1.1.10 ; python_version >= "3.7" and python_version < "4.0"
|
enum34==1.1.10 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
environs==9.3.3 ; python_version >= "3.7" and python_version < "4.0"
|
environs==9.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
fastapi==0.78.0 ; python_version >= "3.7" and python_version < "4.0"
|
fastapi==0.83.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
grpcio==1.49.1 ; python_version >= "3.7" and python_version < "4.0"
|
grpcio==1.50.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
h11==0.12.0 ; python_version >= "3.7" and python_version < "4.0"
|
h11==0.12.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
httpcore==0.15.0 ; python_version >= "3.7" and python_version < "4.0"
|
httpcore==0.15.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
httptools==0.4.0 ; python_version >= "3.7" and python_version < "4.0"
|
httptools==0.4.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
httpx==0.23.0 ; python_version >= "3.7" and python_version < "4.0"
|
httpx==0.23.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
idna==3.2 ; python_version >= "3.7" and python_version < "4.0"
|
idna==3.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
importlib-metadata==4.8.1 ; python_version >= "3.7" and python_version < "4.0"
|
importlib-metadata==5.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
|
iniconfig==1.1.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
jinja2==3.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
jinja2==3.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
lnurl==0.3.6 ; python_version >= "3.7" and python_version < "4.0"
|
lnurl==0.3.6 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
loguru==0.5.3 ; python_version >= "3.7" and python_version < "4.0"
|
loguru==0.6.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
markupsafe==2.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
markupsafe==2.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
marshmallow==3.17.0 ; python_version >= "3.7" and python_version < "4.0"
|
marshmallow==3.18.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
outcome==1.1.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==21.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
pathlib2==2.3.7.post1 ; python_version >= "3.7" and python_version < "4.0"
|
pathlib2==2.3.7.post1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
protobuf==4.21.7 ; python_version >= "3.7" and python_version < "4.0"
|
pluggy==1.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
|
protobuf==4.21.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
psycopg2-binary==2.9.1 ; python_version >= "3.7" and python_version < "4.0"
|
psycopg2-binary==2.9.1 ; 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"
|
pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
pycryptodomex==3.14.1 ; python_version >= "3.7" and python_version < "4.0"
|
pycryptodomex==3.14.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
pydantic==1.8.2 ; python_version >= "3.7" and python_version < "4.0"
|
pydantic==1.10.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
pyln-bolt7==1.0.246 ; python_version >= "3.7" and python_version < "4.0"
|
pyln-bolt7==1.0.246 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
pyln-client==0.11.1 ; python_version >= "3.7" and python_version < "4.0"
|
pyln-client==0.11.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
pyln-proto==0.11.1 ; python_version >= "3.7" and python_version < "4.0"
|
pyln-proto==0.11.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
|
@ -48,25 +52,31 @@ pypng==0.0.21 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
pyqrcode==1.2.1 ; python_version >= "3.7" and python_version < "4.0"
|
pyqrcode==1.2.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
pyscss==1.4.0 ; python_version >= "3.7" and python_version < "4.0"
|
pyscss==1.4.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
pysocks==1.7.1 ; python_version >= "3.7" and python_version < "4.0"
|
pysocks==1.7.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
python-dotenv==0.19.0 ; 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"
|
||||||
|
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"
|
||||||
pyyaml==5.4.1 ; python_version >= "3.7" and python_version < "4.0"
|
pyyaml==5.4.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
represent==1.6.0.post0 ; 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"
|
||||||
rfc3986==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
rfc3986==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
rfc3986[idna2008]==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
rfc3986[idna2008]==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
secp256k1==0.14.0 ; python_version >= "3.7" and python_version < "4.0"
|
secp256k1==0.14.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
setuptools==65.4.1 ; python_version >= "3.7" and python_version < "4.0"
|
setuptools==65.6.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
shortuuid==1.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
shortuuid==1.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
six==1.16.0 ; python_version >= "3.7" and python_version < "4.0"
|
six==1.16.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
sniffio==1.2.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-aio==0.17.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
sqlalchemy==1.3.23 ; python_version >= "3.7" and python_version < "4.0"
|
sqlalchemy==1.3.24 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
sse-starlette==0.6.2 ; python_version >= "3.7" and python_version < "4.0"
|
sse-starlette==0.6.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
starlette==0.19.1 ; python_version >= "3.7" and python_version < "4.0"
|
starlette==0.19.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
typing-extensions==3.10.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
tomli==2.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
uvicorn==0.18.1 ; python_version >= "3.7" and python_version < "4.0"
|
typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
|
urllib3==1.26.12 ; python_version >= "3.7" and python_version < "4"
|
||||||
|
uvicorn==0.18.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
uvloop==0.16.0 ; python_version >= "3.7" and python_version < "4.0"
|
uvloop==0.16.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
watchgod==0.7 ; python_version >= "3.7" and python_version < "4.0"
|
watchgod==0.7 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
websocket-client==1.3.3 ; python_version >= "3.7" and python_version < "4.0"
|
websocket-client==1.3.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
websockets==10.0 ; python_version >= "3.7" and python_version < "4.0"
|
websockets==10.0 ; 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"
|
win32-setctime==1.1.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
|
||||||
zipp==3.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
zipp==3.9.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
|
|
Binary file not shown.
|
@ -133,6 +133,10 @@ def migrate_db(file: str, schema: str, exclude_tables: List[str] = []):
|
||||||
|
|
||||||
for table in tables:
|
for table in tables:
|
||||||
tableName = table[0]
|
tableName = table[0]
|
||||||
|
print(f"Migrating table {tableName}")
|
||||||
|
# hard coded skip for dbversions (already produced during startup)
|
||||||
|
if tableName == "dbversions":
|
||||||
|
continue
|
||||||
if tableName in exclude_tables:
|
if tableName in exclude_tables:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -156,7 +160,7 @@ def build_insert_query(schema, tableName, columns):
|
||||||
def to_column_type(columnType):
|
def to_column_type(columnType):
|
||||||
if columnType == "TIMESTAMP":
|
if columnType == "TIMESTAMP":
|
||||||
return "to_timestamp(%s)"
|
return "to_timestamp(%s)"
|
||||||
if columnType == "BOOLEAN":
|
if columnType in ["BOOLEAN", "BOOL"]:
|
||||||
return "%s::boolean"
|
return "%s::boolean"
|
||||||
return "%s"
|
return "%s"
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue