remove cashu
This commit is contained in:
parent
8637e7fd22
commit
10da63f6d4
|
@ -1,11 +0,0 @@
|
||||||
# Cashu
|
|
||||||
|
|
||||||
## Create ecash mint for pegging in/out of ecash
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
|
|
||||||
1. Enable extension
|
|
||||||
2. Create a Mint
|
|
||||||
3. Share wallet
|
|
|
@ -1,47 +0,0 @@
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from environs import Env
|
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
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: F401,F403
|
|
||||||
from .views_api import * # noqa: F401,F403
|
|
||||||
|
|
||||||
|
|
||||||
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))
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Cashu",
|
|
||||||
"short_description": "Ecash mint and wallet",
|
|
||||||
"tile": "/cashu/static/image/cashu.png",
|
|
||||||
"contributors": ["calle", "vlad", "arcbtc"],
|
|
||||||
"hidden": false
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
from typing import List, Optional, Union
|
|
||||||
|
|
||||||
from . import db
|
|
||||||
from .models import Cashu
|
|
||||||
|
|
||||||
|
|
||||||
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,))
|
|
|
@ -1,33 +0,0 @@
|
||||||
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
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
|
@ -1,148 +0,0 @@
|
||||||
from sqlite3 import Row
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
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"
|
|
||||||
assert "C" in d, "no C in proof"
|
|
||||||
return cls(
|
|
||||||
amount=d["amount"],
|
|
||||||
C=d["C"],
|
|
||||||
secret=d["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
|
|
Binary file not shown.
Before Width: | Height: | Size: 23 KiB |
|
@ -1,37 +0,0 @@
|
||||||
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
|
|
||||||
})({})
|
|
|
@ -1,31 +0,0 @@
|
||||||
async function hashToCurve(secretMessage) {
|
|
||||||
let point
|
|
||||||
while (!point) {
|
|
||||||
const hash = await nobleSecp256k1.utils.sha256(secretMessage)
|
|
||||||
const hashHex = nobleSecp256k1.utils.bytesToHex(hash)
|
|
||||||
const pointX = '02' + hashHex
|
|
||||||
try {
|
|
||||||
point = nobleSecp256k1.Point.fromHex(pointX)
|
|
||||||
} catch (error) {
|
|
||||||
secretMessage = await nobleSecp256k1.utils.sha256(secretMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return point
|
|
||||||
}
|
|
||||||
|
|
||||||
async function step1Alice(secretMessage) {
|
|
||||||
secretMessage = uint8ToBase64.encode(secretMessage)
|
|
||||||
secretMessage = new TextEncoder().encode(secretMessage)
|
|
||||||
const Y = await hashToCurve(secretMessage)
|
|
||||||
const r_bytes = nobleSecp256k1.utils.randomPrivateKey()
|
|
||||||
const r = bytesToNumber(r_bytes)
|
|
||||||
const P = nobleSecp256k1.Point.fromPrivateKey(r)
|
|
||||||
const B_ = Y.add(P)
|
|
||||||
return {B_: B_.toHex(true), r: nobleSecp256k1.utils.bytesToHex(r_bytes)}
|
|
||||||
}
|
|
||||||
|
|
||||||
function step3Alice(C_, r, A) {
|
|
||||||
const rInt = bytesToNumber(r)
|
|
||||||
const C = C_.subtract(A.multiply(rInt))
|
|
||||||
return C
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,23 +0,0 @@
|
||||||
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}`)
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
import asyncio
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
async def startup_cashu_mint():
|
|
||||||
await migrate_databases(db, migrations)
|
|
||||||
await ledger.load_used_proofs()
|
|
||||||
await ledger.init_keysets(autosave=False)
|
|
||||||
|
|
||||||
|
|
||||||
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.get("tag") != "cashu":
|
|
||||||
return
|
|
||||||
|
|
||||||
return
|
|
|
@ -1,80 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,28 +0,0 @@
|
||||||
<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
|
|
||||||
class="text-secondary"
|
|
||||||
href="https://github.com/arcbtc"
|
|
||||||
target="_blank"
|
|
||||||
>arcbtc</a
|
|
||||||
>,
|
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
href="https://github.com/motorina0"
|
|
||||||
target="_blank"
|
|
||||||
>vlad</a
|
|
||||||
>,
|
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
href="https://github.com/calle"
|
|
||||||
target="_blank"
|
|
||||||
>calle</a
|
|
||||||
>.</small
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
|
@ -1,367 +0,0 @@
|
||||||
{% 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 %}
|
|
|
@ -1,92 +0,0 @@
|
||||||
{% 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-img
|
|
||||||
src="/cashu/static/image/cashu.png"
|
|
||||||
spinner-color="white"
|
|
||||||
style="max-width: 20%"
|
|
||||||
></q-img>
|
|
||||||
<h4 class="q-mt-sm q-mb-md">{{ mint_name }}</h4>
|
|
||||||
<!-- <a class="text-secondary">Mint URL: {{testfield}} </a> <br /> -->
|
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
class="q-my-xl text-white"
|
|
||||||
style="font-size: 1.5rem"
|
|
||||||
href="../wallet?mint_id={{ mint_id }}"
|
|
||||||
>click to 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
|
|
||||||
class="text-secondary"
|
|
||||||
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 />
|
|
||||||
Cashu is still experimental and in active development. There are
|
|
||||||
likely bugs in this implementation so please use this with caution. We
|
|
||||||
hold no responsibility for people losing access to funds. Use at your
|
|
||||||
own risk!
|
|
||||||
</p>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %} {% block scripts %}
|
|
||||||
|
|
||||||
<script>
|
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [windowMixin],
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
testfield: 'asd',
|
|
||||||
mintURL: {
|
|
||||||
location: window.location,
|
|
||||||
base_url: location.protocol + '//' + location.host + location.pathname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,253 +0,0 @@
|
||||||
from http import HTTPStatus
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from fastapi import Depends, Request
|
|
||||||
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),
|
|
||||||
):
|
|
||||||
return cashu_renderer().TemplateResponse(
|
|
||||||
"cashu/index.html", {"request": request, "user": user.dict()}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.get("/wallet")
|
|
||||||
async def wallet(request: Request, mint_id: Optional[str] = None):
|
|
||||||
if mint_id is not None:
|
|
||||||
cashu = await get_cashu(mint_id)
|
|
||||||
if not cashu:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
|
||||||
)
|
|
||||||
manifest_url = f"/cashu/manifest/{mint_id}.webmanifest"
|
|
||||||
mint_name = cashu.name
|
|
||||||
else:
|
|
||||||
manifest_url = "/cashu/cashu.webmanifest"
|
|
||||||
mint_name = "Cashu mint"
|
|
||||||
|
|
||||||
return cashu_renderer().TemplateResponse(
|
|
||||||
"cashu/wallet.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"web_manifest": manifest_url,
|
|
||||||
"mint_name": mint_name,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@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="Mint does not exist."
|
|
||||||
)
|
|
||||||
return cashu_renderer().TemplateResponse(
|
|
||||||
"cashu/mint.html",
|
|
||||||
{"request": request, "mint_id": mintID},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.get("/manifest/{cashu_id}.webmanifest")
|
|
||||||
async def manifest_lnbits(cashu_id: str):
|
|
||||||
cashu = await get_cashu(cashu_id)
|
|
||||||
if not cashu:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
|
||||||
)
|
|
||||||
|
|
||||||
return get_manifest(cashu_id, cashu.name)
|
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.get("/cashu.webmanifest")
|
|
||||||
async def manifest():
|
|
||||||
return get_manifest()
|
|
||||||
|
|
||||||
|
|
||||||
def get_manifest(mint_id: Optional[str] = None, mint_name: Optional[str] = None):
|
|
||||||
manifest_name = "Cashu"
|
|
||||||
if mint_name:
|
|
||||||
manifest_name += " - " + mint_name
|
|
||||||
manifest_url = "/cashu/wallet"
|
|
||||||
if mint_id:
|
|
||||||
manifest_url += "?mint_id=" + mint_id
|
|
||||||
|
|
||||||
return {
|
|
||||||
"short_name": "Cashu",
|
|
||||||
"name": manifest_name,
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/512x512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/96x96.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"id": manifest_url,
|
|
||||||
"start_url": manifest_url,
|
|
||||||
"background_color": "#1F2234",
|
|
||||||
"description": "Cashu ecash wallet",
|
|
||||||
"display": "standalone",
|
|
||||||
"scope": "/cashu/",
|
|
||||||
"theme_color": "#1F2234",
|
|
||||||
"protocol_handlers": [
|
|
||||||
{"protocol": "web+cashu", "url": "&recv_token=%s"},
|
|
||||||
{"protocol": "web+lightning", "url": "&lightning=%s"},
|
|
||||||
],
|
|
||||||
"shortcuts": [
|
|
||||||
{
|
|
||||||
"name": manifest_name,
|
|
||||||
"short_name": "Cashu",
|
|
||||||
"description": manifest_name,
|
|
||||||
"url": manifest_url,
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/144x144.png",
|
|
||||||
"sizes": "144x144",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/96x96.png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/72x72.png",
|
|
||||||
"sizes": "72x72",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/48x48.png",
|
|
||||||
"sizes": "48x48",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/16x16.png",
|
|
||||||
"sizes": "16x16",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/20x20.png",
|
|
||||||
"sizes": "20x20",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/29x29.png",
|
|
||||||
"sizes": "29x29",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/32x32.png",
|
|
||||||
"sizes": "32x32",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/40x40.png",
|
|
||||||
"sizes": "40x40",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/50x50.png",
|
|
||||||
"sizes": "50x50",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/57x57.png",
|
|
||||||
"sizes": "57x57",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/58x58.png",
|
|
||||||
"sizes": "58x58",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/60x60.png",
|
|
||||||
"sizes": "60x60",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/64x64.png",
|
|
||||||
"sizes": "64x64",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/72x72.png",
|
|
||||||
"sizes": "72x72",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/76x76.png",
|
|
||||||
"sizes": "76x76",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/80x80.png",
|
|
||||||
"sizes": "80x80",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/87x87.png",
|
|
||||||
"sizes": "87x87",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/100x100.png",
|
|
||||||
"sizes": "100x100",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/114x114.png",
|
|
||||||
"sizes": "114x114",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/120x120.png",
|
|
||||||
"sizes": "120x120",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/128x128.png",
|
|
||||||
"sizes": "128x128",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/144x144.png",
|
|
||||||
"sizes": "144x144",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/152x152.png",
|
|
||||||
"sizes": "152x152",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/167x167.png",
|
|
||||||
"sizes": "167x167",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/180x180.png",
|
|
||||||
"sizes": "180x180",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/256x256.png",
|
|
||||||
"sizes": "256x256",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/1024x1024.png",
|
|
||||||
"sizes": "1024x1024",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
|
@ -1,440 +0,0 @@
|
||||||
import math
|
|
||||||
from http import HTTPStatus
|
|
||||||
from typing import Dict, Union
|
|
||||||
|
|
||||||
# -------- cashu imports
|
|
||||||
from cashu.core.base import (
|
|
||||||
CheckFeesRequest,
|
|
||||||
CheckFeesResponse,
|
|
||||||
CheckSpendableRequest,
|
|
||||||
CheckSpendableResponse,
|
|
||||||
GetMeltResponse,
|
|
||||||
GetMintResponse,
|
|
||||||
Invoice,
|
|
||||||
PostMeltRequest,
|
|
||||||
PostMintRequest,
|
|
||||||
PostMintResponse,
|
|
||||||
PostSplitRequest,
|
|
||||||
PostSplitResponse,
|
|
||||||
)
|
|
||||||
from fastapi import Depends, Query
|
|
||||||
from loguru import logger
|
|
||||||
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.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
|
|
||||||
|
|
||||||
# WARNING: Do not set this to False in production! This will create
|
|
||||||
# tokens for free otherwise. This is for testing purposes only!
|
|
||||||
|
|
||||||
LIGHTNING = True
|
|
||||||
|
|
||||||
if not LIGHTNING:
|
|
||||||
logger.warning(
|
|
||||||
"Cashu: LIGHTNING is set False! That means that I will create ecash for free!"
|
|
||||||
)
|
|
||||||
|
|
||||||
########################################
|
|
||||||
############### 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)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
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),
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
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}/keys/{idBase64Urlsafe}")
|
|
||||||
async def keyset_keys(
|
|
||||||
cashu_id: str = Query(None), idBase64Urlsafe: str = Query(None)
|
|
||||||
) -> dict[int, str]:
|
|
||||||
"""
|
|
||||||
Get the public keys of the mint of a specificy keyset id.
|
|
||||||
The id is encoded in base64_urlsafe and needs to be converted back to
|
|
||||||
normal base64 before it can be processed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
cashu: Union[Cashu, None] = await get_cashu(cashu_id)
|
|
||||||
|
|
||||||
if not cashu:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
|
||||||
)
|
|
||||||
|
|
||||||
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
|
|
||||||
keyset = ledger.get_keyset(keyset_id=id)
|
|
||||||
return keyset
|
|
||||||
|
|
||||||
|
|
||||||
@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(
|
|
||||||
data: PostMintRequest,
|
|
||||||
cashu_id: str = Query(None),
|
|
||||||
payment_hash: str = Query(None),
|
|
||||||
) -> PostMintResponse:
|
|
||||||
"""
|
|
||||||
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."
|
|
||||||
)
|
|
||||||
|
|
||||||
keyset = ledger.keysets.keysets[cashu.keyset_id]
|
|
||||||
|
|
||||||
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:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
|
||||||
detail="Tokens already issued for this invoice.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# set this invoice as issued
|
|
||||||
await ledger.crud.update_lightning_invoice(
|
|
||||||
db=ledger.db, hash=payment_hash, issued=True
|
|
||||||
)
|
|
||||||
|
|
||||||
status: PaymentStatus = await check_transaction_status(
|
|
||||||
cashu.wallet, payment_hash
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
total_requested = sum([bm.amount for bm in data.outputs])
|
|
||||||
if total_requested > invoice.amount:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
|
||||||
detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}",
|
|
||||||
)
|
|
||||||
|
|
||||||
if not status.paid:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
|
|
||||||
)
|
|
||||||
|
|
||||||
promises = await ledger._generate_promises(B_s=data.outputs, keyset=keyset)
|
|
||||||
return PostMintResponse(promises=promises)
|
|
||||||
except (Exception, HTTPException) as e:
|
|
||||||
logger.debug(f"Cashu: /melt {str(e) or getattr(e, 'detail')}")
|
|
||||||
# unset issued flag because something went wrong
|
|
||||||
await ledger.crud.update_lightning_invoice(
|
|
||||||
db=ledger.db, hash=payment_hash, issued=False
|
|
||||||
)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=getattr(e, "status_code")
|
|
||||||
or HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
detail=str(e) or getattr(e, "detail"),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# only used for testing when LIGHTNING=false
|
|
||||||
promises = await ledger._generate_promises(B_s=data.outputs, keyset=keyset)
|
|
||||||
return PostMintResponse(promises=promises)
|
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.post("/api/v1/{cashu_id}/melt")
|
|
||||||
async def melt_coins(
|
|
||||||
payload: PostMeltRequest, 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.pr
|
|
||||||
|
|
||||||
# !!!!!!! 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.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# set proofs as pending
|
|
||||||
await ledger._set_proofs_pending(proofs)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await ledger._verify_proofs(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 + math.ceil(fees_msat / 1000), Exception(
|
|
||||||
f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)."
|
|
||||||
)
|
|
||||||
logger.debug(f"Cashu: Initiating payment of {total_provided} sats")
|
|
||||||
try:
|
|
||||||
await pay_invoice(
|
|
||||||
wallet_id=cashu.wallet,
|
|
||||||
payment_request=invoice,
|
|
||||||
description="Pay cashu invoice",
|
|
||||||
extra={"tag": "cashu", "cashu_name": cashu.name},
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Cashu error paying invoice {invoice_obj.payment_hash}: {e}")
|
|
||||||
raise e
|
|
||||||
finally:
|
|
||||||
logger.debug(
|
|
||||||
f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}"
|
|
||||||
)
|
|
||||||
status: PaymentStatus = await check_transaction_status(
|
|
||||||
cashu.wallet, invoice_obj.payment_hash
|
|
||||||
)
|
|
||||||
if status.paid is True:
|
|
||||||
logger.debug(
|
|
||||||
f"Cashu: Payment successful, invalidating proofs for {invoice_obj.payment_hash}"
|
|
||||||
)
|
|
||||||
await ledger._invalidate_proofs(proofs)
|
|
||||||
else:
|
|
||||||
logger.debug(f"Cashu: Payment failed for {invoice_obj.payment_hash}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Cashu: Exception: {str(e)}")
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail=f"Cashu: {str(e)}",
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
logger.debug("Cashu: Unset pending")
|
|
||||||
# delete proofs from pending list
|
|
||||||
await ledger._unset_proofs_pending(proofs)
|
|
||||||
|
|
||||||
return GetMeltResponse(paid=status.paid, preimage=status.preimage)
|
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.post("/api/v1/{cashu_id}/check")
|
|
||||||
async def check_spendable(
|
|
||||||
payload: CheckSpendableRequest, 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."
|
|
||||||
)
|
|
||||||
spendableList = await ledger.check_spendable(payload.proofs)
|
|
||||||
return CheckSpendableResponse(spendable=spendableList)
|
|
||||||
|
|
||||||
|
|
||||||
@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=math.ceil(fees_msat / 1000))
|
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.post("/api/v1/{cashu_id}/split")
|
|
||||||
async def split(
|
|
||||||
payload: PostSplitRequest, 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
|
|
||||||
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
|
|
Loading…
Reference in New Issue
Block a user