From 10da63f6d4f4d9815455a4c372d8669df0add3e5 Mon Sep 17 00:00:00 2001 From: ben Date: Thu, 16 Feb 2023 15:48:22 +0000 Subject: [PATCH] remove cashu --- lnbits/extensions/cashu/README.md | 11 - lnbits/extensions/cashu/__init__.py | 47 - lnbits/extensions/cashu/config.json | 7 - lnbits/extensions/cashu/crud.py | 51 - lnbits/extensions/cashu/migrations.py | 33 - lnbits/extensions/cashu/models.py | 148 - .../extensions/cashu/static/image/cashu.png | Bin 23602 -> 0 bytes lnbits/extensions/cashu/static/js/base64.js | 37 - lnbits/extensions/cashu/static/js/dhke.js | 31 - .../cashu/static/js/noble-secp256k1.js | 1178 ------- lnbits/extensions/cashu/static/js/utils.js | 23 - lnbits/extensions/cashu/tasks.py | 31 - .../cashu/templates/cashu/_api_docs.html | 80 - .../cashu/templates/cashu/_cashu.html | 28 - .../cashu/templates/cashu/index.html | 367 -- .../cashu/templates/cashu/mint.html | 92 - .../cashu/templates/cashu/wallet.html | 2992 ----------------- lnbits/extensions/cashu/views.py | 253 -- lnbits/extensions/cashu/views_api.py | 440 --- 19 files changed, 5849 deletions(-) delete mode 100644 lnbits/extensions/cashu/README.md delete mode 100644 lnbits/extensions/cashu/__init__.py delete mode 100644 lnbits/extensions/cashu/config.json delete mode 100644 lnbits/extensions/cashu/crud.py delete mode 100644 lnbits/extensions/cashu/migrations.py delete mode 100644 lnbits/extensions/cashu/models.py delete mode 100644 lnbits/extensions/cashu/static/image/cashu.png delete mode 100644 lnbits/extensions/cashu/static/js/base64.js delete mode 100644 lnbits/extensions/cashu/static/js/dhke.js delete mode 100644 lnbits/extensions/cashu/static/js/noble-secp256k1.js delete mode 100644 lnbits/extensions/cashu/static/js/utils.js delete mode 100644 lnbits/extensions/cashu/tasks.py delete mode 100644 lnbits/extensions/cashu/templates/cashu/_api_docs.html delete mode 100644 lnbits/extensions/cashu/templates/cashu/_cashu.html delete mode 100644 lnbits/extensions/cashu/templates/cashu/index.html delete mode 100644 lnbits/extensions/cashu/templates/cashu/mint.html delete mode 100644 lnbits/extensions/cashu/templates/cashu/wallet.html delete mode 100644 lnbits/extensions/cashu/views.py delete mode 100644 lnbits/extensions/cashu/views_api.py diff --git a/lnbits/extensions/cashu/README.md b/lnbits/extensions/cashu/README.md deleted file mode 100644 index 8f53b474..00000000 --- a/lnbits/extensions/cashu/README.md +++ /dev/null @@ -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 diff --git a/lnbits/extensions/cashu/__init__.py b/lnbits/extensions/cashu/__init__.py deleted file mode 100644 index ca831ce0..00000000 --- a/lnbits/extensions/cashu/__init__.py +++ /dev/null @@ -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)) diff --git a/lnbits/extensions/cashu/config.json b/lnbits/extensions/cashu/config.json deleted file mode 100644 index 14ff1743..00000000 --- a/lnbits/extensions/cashu/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "Cashu", - "short_description": "Ecash mint and wallet", - "tile": "/cashu/static/image/cashu.png", - "contributors": ["calle", "vlad", "arcbtc"], - "hidden": false -} diff --git a/lnbits/extensions/cashu/crud.py b/lnbits/extensions/cashu/crud.py deleted file mode 100644 index e27cc98c..00000000 --- a/lnbits/extensions/cashu/crud.py +++ /dev/null @@ -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,)) diff --git a/lnbits/extensions/cashu/migrations.py b/lnbits/extensions/cashu/migrations.py deleted file mode 100644 index b54c4108..00000000 --- a/lnbits/extensions/cashu/migrations.py +++ /dev/null @@ -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 - ); - """ - ) diff --git a/lnbits/extensions/cashu/models.py b/lnbits/extensions/cashu/models.py deleted file mode 100644 index aaff195f..00000000 --- a/lnbits/extensions/cashu/models.py +++ /dev/null @@ -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 diff --git a/lnbits/extensions/cashu/static/image/cashu.png b/lnbits/extensions/cashu/static/image/cashu.png deleted file mode 100644 index e90611fc061c20d0c1c14bd2317c745181c4a96c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23602 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 diff --git a/lnbits/extensions/cashu/static/js/base64.js b/lnbits/extensions/cashu/static/js/base64.js deleted file mode 100644 index b150882f..00000000 --- a/lnbits/extensions/cashu/static/js/base64.js +++ /dev/null @@ -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 -})({}) diff --git a/lnbits/extensions/cashu/static/js/dhke.js b/lnbits/extensions/cashu/static/js/dhke.js deleted file mode 100644 index cebef240..00000000 --- a/lnbits/extensions/cashu/static/js/dhke.js +++ /dev/null @@ -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 -} diff --git a/lnbits/extensions/cashu/static/js/noble-secp256k1.js b/lnbits/extensions/cashu/static/js/noble-secp256k1.js deleted file mode 100644 index 6a6bd441..00000000 --- a/lnbits/extensions/cashu/static/js/noble-secp256k1.js +++ /dev/null @@ -1,1178 +0,0 @@ -;(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' - ? factory(exports) - : typeof define === 'function' && define.amd - ? define(['exports'], factory) - : ((global = - typeof globalThis !== 'undefined' ? globalThis : global || self), - factory((global.nobleSecp256k1 = {}))) -})(this, function (exports) { - 'use strict' - - const _nodeResolve_empty = {} - - const nodeCrypto = /*#__PURE__*/ Object.freeze({ - __proto__: null, - default: _nodeResolve_empty - }) - - /*! noble-secp256k1 - MIT License (c) 2019 Paul Miller (paulmillr.com) */ - const _0n = BigInt(0) - const _1n = BigInt(1) - const _2n = BigInt(2) - const _3n = BigInt(3) - const _8n = BigInt(8) - const POW_2_256 = _2n ** BigInt(256) - const CURVE = { - a: _0n, - b: BigInt(7), - P: POW_2_256 - _2n ** BigInt(32) - BigInt(977), - n: POW_2_256 - BigInt('432420386565659656852420866394968145599'), - h: _1n, - Gx: BigInt( - '55066263022277343669578718895168534326250603453777594175500187360389116729240' - ), - Gy: BigInt( - '32670510020758816978083085130507043184471273380659243275938904335757337482424' - ), - beta: BigInt( - '0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee' - ) - } - function weistrass(x) { - const {a, b} = CURVE - const x2 = mod(x * x) - const x3 = mod(x2 * x) - return mod(x3 + a * x + b) - } - const USE_ENDOMORPHISM = CURVE.a === _0n - class JacobianPoint { - constructor(x, y, z) { - this.x = x - this.y = y - this.z = z - } - static fromAffine(p) { - if (!(p instanceof Point)) { - throw new TypeError('JacobianPoint#fromAffine: expected Point') - } - return new JacobianPoint(p.x, p.y, _1n) - } - static toAffineBatch(points) { - const toInv = invertBatch(points.map(p => p.z)) - return points.map((p, i) => p.toAffine(toInv[i])) - } - static normalizeZ(points) { - return JacobianPoint.toAffineBatch(points).map(JacobianPoint.fromAffine) - } - equals(other) { - if (!(other instanceof JacobianPoint)) - throw new TypeError('JacobianPoint expected') - const {x: X1, y: Y1, z: Z1} = this - const {x: X2, y: Y2, z: Z2} = other - const Z1Z1 = mod(Z1 ** _2n) - const Z2Z2 = mod(Z2 ** _2n) - const U1 = mod(X1 * Z2Z2) - const U2 = mod(X2 * Z1Z1) - const S1 = mod(mod(Y1 * Z2) * Z2Z2) - const S2 = mod(mod(Y2 * Z1) * Z1Z1) - return U1 === U2 && S1 === S2 - } - negate() { - return new JacobianPoint(this.x, mod(-this.y), this.z) - } - double() { - const {x: X1, y: Y1, z: Z1} = this - const A = mod(X1 ** _2n) - const B = mod(Y1 ** _2n) - const C = mod(B ** _2n) - const D = mod(_2n * (mod((X1 + B) ** _2n) - A - C)) - const E = mod(_3n * A) - const F = mod(E ** _2n) - const X3 = mod(F - _2n * D) - const Y3 = mod(E * (D - X3) - _8n * C) - const Z3 = mod(_2n * Y1 * Z1) - return new JacobianPoint(X3, Y3, Z3) - } - add(other) { - if (!(other instanceof JacobianPoint)) - throw new TypeError('JacobianPoint expected') - const {x: X1, y: Y1, z: Z1} = this - const {x: X2, y: Y2, z: Z2} = other - if (X2 === _0n || Y2 === _0n) return this - if (X1 === _0n || Y1 === _0n) return other - const Z1Z1 = mod(Z1 ** _2n) - const Z2Z2 = mod(Z2 ** _2n) - const U1 = mod(X1 * Z2Z2) - const U2 = mod(X2 * Z1Z1) - const S1 = mod(mod(Y1 * Z2) * Z2Z2) - const S2 = mod(mod(Y2 * Z1) * Z1Z1) - const H = mod(U2 - U1) - const r = mod(S2 - S1) - if (H === _0n) { - if (r === _0n) { - return this.double() - } else { - return JacobianPoint.ZERO - } - } - const HH = mod(H ** _2n) - const HHH = mod(H * HH) - const V = mod(U1 * HH) - const X3 = mod(r ** _2n - HHH - _2n * V) - const Y3 = mod(r * (V - X3) - S1 * HHH) - const Z3 = mod(Z1 * Z2 * H) - return new JacobianPoint(X3, Y3, Z3) - } - subtract(other) { - return this.add(other.negate()) - } - multiplyUnsafe(scalar) { - const P0 = JacobianPoint.ZERO - if (typeof scalar === 'bigint' && scalar === _0n) return P0 - let n = normalizeScalar(scalar) - if (n === _1n) return this - if (!USE_ENDOMORPHISM) { - let p = P0 - let d = this - while (n > _0n) { - if (n & _1n) p = p.add(d) - d = d.double() - n >>= _1n - } - return p - } - let {k1neg, k1, k2neg, k2} = splitScalarEndo(n) - let k1p = P0 - let k2p = P0 - let d = this - while (k1 > _0n || k2 > _0n) { - if (k1 & _1n) k1p = k1p.add(d) - if (k2 & _1n) k2p = k2p.add(d) - d = d.double() - k1 >>= _1n - k2 >>= _1n - } - if (k1neg) k1p = k1p.negate() - if (k2neg) k2p = k2p.negate() - k2p = new JacobianPoint(mod(k2p.x * CURVE.beta), k2p.y, k2p.z) - return k1p.add(k2p) - } - precomputeWindow(W) { - const windows = USE_ENDOMORPHISM ? 128 / W + 1 : 256 / W + 1 - const points = [] - let p = this - let base = p - for (let window = 0; window < windows; window++) { - base = p - points.push(base) - for (let i = 1; i < 2 ** (W - 1); i++) { - base = base.add(p) - points.push(base) - } - p = base.double() - } - return points - } - wNAF(n, affinePoint) { - if (!affinePoint && this.equals(JacobianPoint.BASE)) - affinePoint = Point.BASE - const W = (affinePoint && affinePoint._WINDOW_SIZE) || 1 - if (256 % W) { - throw new Error( - 'Point#wNAF: Invalid precomputation window, must be power of 2' - ) - } - let precomputes = affinePoint && pointPrecomputes.get(affinePoint) - if (!precomputes) { - precomputes = this.precomputeWindow(W) - if (affinePoint && W !== 1) { - precomputes = JacobianPoint.normalizeZ(precomputes) - pointPrecomputes.set(affinePoint, precomputes) - } - } - let p = JacobianPoint.ZERO - let f = JacobianPoint.ZERO - const windows = 1 + (USE_ENDOMORPHISM ? 128 / W : 256 / W) - const windowSize = 2 ** (W - 1) - const mask = BigInt(2 ** W - 1) - const maxNumber = 2 ** W - const shiftBy = BigInt(W) - for (let window = 0; window < windows; window++) { - const offset = window * windowSize - let wbits = Number(n & mask) - n >>= shiftBy - if (wbits > windowSize) { - wbits -= maxNumber - n += _1n - } - if (wbits === 0) { - let pr = precomputes[offset] - if (window % 2) pr = pr.negate() - f = f.add(pr) - } else { - let cached = precomputes[offset + Math.abs(wbits) - 1] - if (wbits < 0) cached = cached.negate() - p = p.add(cached) - } - } - return {p, f} - } - multiply(scalar, affinePoint) { - let n = normalizeScalar(scalar) - let point - let fake - if (USE_ENDOMORPHISM) { - const {k1neg, k1, k2neg, k2} = splitScalarEndo(n) - let {p: k1p, f: f1p} = this.wNAF(k1, affinePoint) - let {p: k2p, f: f2p} = this.wNAF(k2, affinePoint) - if (k1neg) k1p = k1p.negate() - if (k2neg) k2p = k2p.negate() - k2p = new JacobianPoint(mod(k2p.x * CURVE.beta), k2p.y, k2p.z) - point = k1p.add(k2p) - fake = f1p.add(f2p) - } else { - const {p, f} = this.wNAF(n, affinePoint) - point = p - fake = f - } - return JacobianPoint.normalizeZ([point, fake])[0] - } - toAffine(invZ = invert(this.z)) { - const {x, y, z} = this - const iz1 = invZ - const iz2 = mod(iz1 * iz1) - const iz3 = mod(iz2 * iz1) - const ax = mod(x * iz2) - const ay = mod(y * iz3) - const zz = mod(z * iz1) - if (zz !== _1n) throw new Error('invZ was invalid') - return new Point(ax, ay) - } - } - JacobianPoint.BASE = new JacobianPoint(CURVE.Gx, CURVE.Gy, _1n) - JacobianPoint.ZERO = new JacobianPoint(_0n, _1n, _0n) - const pointPrecomputes = new WeakMap() - class Point { - constructor(x, y) { - this.x = x - this.y = y - } - _setWindowSize(windowSize) { - this._WINDOW_SIZE = windowSize - pointPrecomputes.delete(this) - } - static fromCompressedHex(bytes) { - const isShort = bytes.length === 32 - const x = bytesToNumber(isShort ? bytes : bytes.subarray(1)) - if (!isValidFieldElement(x)) throw new Error('Point is not on curve') - const y2 = weistrass(x) - let y = sqrtMod(y2) - const isYOdd = (y & _1n) === _1n - if (isShort) { - if (isYOdd) y = mod(-y) - } else { - const isFirstByteOdd = (bytes[0] & 1) === 1 - if (isFirstByteOdd !== isYOdd) y = mod(-y) - } - const point = new Point(x, y) - point.assertValidity() - return point - } - static fromUncompressedHex(bytes) { - const x = bytesToNumber(bytes.subarray(1, 33)) - const y = bytesToNumber(bytes.subarray(33, 65)) - const point = new Point(x, y) - point.assertValidity() - return point - } - static fromHex(hex) { - const bytes = ensureBytes(hex) - const len = bytes.length - const header = bytes[0] - if (len === 32 || (len === 33 && (header === 0x02 || header === 0x03))) { - return this.fromCompressedHex(bytes) - } - if (len === 65 && header === 0x04) return this.fromUncompressedHex(bytes) - throw new Error( - `Point.fromHex: received invalid point. Expected 32-33 compressed bytes or 65 uncompressed bytes, not ${len}` - ) - } - static fromPrivateKey(privateKey) { - return Point.BASE.multiply(normalizePrivateKey(privateKey)) - } - static fromSignature(msgHash, signature, recovery) { - msgHash = ensureBytes(msgHash) - const h = truncateHash(msgHash) - const {r, s} = normalizeSignature(signature) - if (recovery !== 0 && recovery !== 1) { - throw new Error('Cannot recover signature: invalid recovery bit') - } - const prefix = recovery & 1 ? '03' : '02' - const R = Point.fromHex(prefix + numTo32bStr(r)) - const {n} = CURVE - const rinv = invert(r, n) - const u1 = mod(-h * rinv, n) - const u2 = mod(s * rinv, n) - const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2) - if (!Q) throw new Error('Cannot recover signature: point at infinify') - Q.assertValidity() - return Q - } - toRawBytes(isCompressed = false) { - return hexToBytes(this.toHex(isCompressed)) - } - toHex(isCompressed = false) { - const x = numTo32bStr(this.x) - if (isCompressed) { - const prefix = this.y & _1n ? '03' : '02' - return `${prefix}${x}` - } else { - return `04${x}${numTo32bStr(this.y)}` - } - } - toHexX() { - return this.toHex(true).slice(2) - } - toRawX() { - return this.toRawBytes(true).slice(1) - } - assertValidity() { - const msg = 'Point is not on elliptic curve' - const {x, y} = this - if (!isValidFieldElement(x) || !isValidFieldElement(y)) - throw new Error(msg) - const left = mod(y * y) - const right = weistrass(x) - if (mod(left - right) !== _0n) throw new Error(msg) - } - equals(other) { - return this.x === other.x && this.y === other.y - } - negate() { - return new Point(this.x, mod(-this.y)) - } - double() { - return JacobianPoint.fromAffine(this).double().toAffine() - } - add(other) { - return JacobianPoint.fromAffine(this) - .add(JacobianPoint.fromAffine(other)) - .toAffine() - } - subtract(other) { - return this.add(other.negate()) - } - multiply(scalar) { - return JacobianPoint.fromAffine(this).multiply(scalar, this).toAffine() - } - multiplyAndAddUnsafe(Q, a, b) { - const P = JacobianPoint.fromAffine(this) - const aP = - a === _0n || a === _1n || this !== Point.BASE - ? P.multiplyUnsafe(a) - : P.multiply(a) - const bQ = JacobianPoint.fromAffine(Q).multiplyUnsafe(b) - const sum = aP.add(bQ) - return sum.equals(JacobianPoint.ZERO) ? undefined : sum.toAffine() - } - } - Point.BASE = new Point(CURVE.Gx, CURVE.Gy) - Point.ZERO = new Point(_0n, _0n) - function sliceDER(s) { - return Number.parseInt(s[0], 16) >= 8 ? '00' + s : s - } - function parseDERInt(data) { - if (data.length < 2 || data[0] !== 0x02) { - throw new Error(`Invalid signature integer tag: ${bytesToHex(data)}`) - } - const len = data[1] - const res = data.subarray(2, len + 2) - if (!len || res.length !== len) { - throw new Error(`Invalid signature integer: wrong length`) - } - if (res[0] === 0x00 && res[1] <= 0x7f) { - throw new Error('Invalid signature integer: trailing length') - } - return {data: bytesToNumber(res), left: data.subarray(len + 2)} - } - function parseDERSignature(data) { - if (data.length < 2 || data[0] != 0x30) { - throw new Error(`Invalid signature tag: ${bytesToHex(data)}`) - } - if (data[1] !== data.length - 2) { - throw new Error('Invalid signature: incorrect length') - } - const {data: r, left: sBytes} = parseDERInt(data.subarray(2)) - const {data: s, left: rBytesLeft} = parseDERInt(sBytes) - if (rBytesLeft.length) { - throw new Error( - `Invalid signature: left bytes after parsing: ${bytesToHex(rBytesLeft)}` - ) - } - return {r, s} - } - class Signature { - constructor(r, s) { - this.r = r - this.s = s - this.assertValidity() - } - static fromCompact(hex) { - const arr = isUint8a(hex) - const name = 'Signature.fromCompact' - if (typeof hex !== 'string' && !arr) - throw new TypeError(`${name}: Expected string or Uint8Array`) - const str = arr ? bytesToHex(hex) : hex - if (str.length !== 128) throw new Error(`${name}: Expected 64-byte hex`) - return new Signature( - hexToNumber(str.slice(0, 64)), - hexToNumber(str.slice(64, 128)) - ) - } - static fromDER(hex) { - const arr = isUint8a(hex) - if (typeof hex !== 'string' && !arr) - throw new TypeError(`Signature.fromDER: Expected string or Uint8Array`) - const {r, s} = parseDERSignature(arr ? hex : hexToBytes(hex)) - return new Signature(r, s) - } - static fromHex(hex) { - return this.fromDER(hex) - } - assertValidity() { - const {r, s} = this - if (!isWithinCurveOrder(r)) - throw new Error('Invalid Signature: r must be 0 < r < n') - if (!isWithinCurveOrder(s)) - throw new Error('Invalid Signature: s must be 0 < s < n') - } - hasHighS() { - const HALF = CURVE.n >> _1n - return this.s > HALF - } - normalizeS() { - return this.hasHighS() ? new Signature(this.r, CURVE.n - this.s) : this - } - toDERRawBytes(isCompressed = false) { - return hexToBytes(this.toDERHex(isCompressed)) - } - toDERHex(isCompressed = false) { - const sHex = sliceDER(numberToHexUnpadded(this.s)) - if (isCompressed) return sHex - const rHex = sliceDER(numberToHexUnpadded(this.r)) - const rLen = numberToHexUnpadded(rHex.length / 2) - const sLen = numberToHexUnpadded(sHex.length / 2) - const length = numberToHexUnpadded(rHex.length / 2 + sHex.length / 2 + 4) - return `30${length}02${rLen}${rHex}02${sLen}${sHex}` - } - toRawBytes() { - return this.toDERRawBytes() - } - toHex() { - return this.toDERHex() - } - toCompactRawBytes() { - return hexToBytes(this.toCompactHex()) - } - toCompactHex() { - return numTo32bStr(this.r) + numTo32bStr(this.s) - } - } - function concatBytes(...arrays) { - if (!arrays.every(isUint8a)) throw new Error('Uint8Array list expected') - if (arrays.length === 1) return arrays[0] - const length = arrays.reduce((a, arr) => a + arr.length, 0) - const result = new Uint8Array(length) - for (let i = 0, pad = 0; i < arrays.length; i++) { - const arr = arrays[i] - result.set(arr, pad) - pad += arr.length - } - return result - } - function isUint8a(bytes) { - return bytes instanceof Uint8Array - } - const hexes = Array.from({length: 256}, (v, i) => - i.toString(16).padStart(2, '0') - ) - function bytesToHex(uint8a) { - if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array') - let hex = '' - for (let i = 0; i < uint8a.length; i++) { - hex += hexes[uint8a[i]] - } - return hex - } - function numTo32bStr(num) { - if (num > POW_2_256) throw new Error('Expected number < 2^256') - return num.toString(16).padStart(64, '0') - } - function numTo32b(num) { - return hexToBytes(numTo32bStr(num)) - } - function numberToHexUnpadded(num) { - const hex = num.toString(16) - return hex.length & 1 ? `0${hex}` : hex - } - function hexToNumber(hex) { - if (typeof hex !== 'string') { - throw new TypeError('hexToNumber: expected string, got ' + typeof hex) - } - return BigInt(`0x${hex}`) - } - function hexToBytes(hex) { - if (typeof hex !== 'string') { - throw new TypeError('hexToBytes: expected string, got ' + typeof hex) - } - if (hex.length % 2) - throw new Error('hexToBytes: received invalid unpadded hex' + hex.length) - const array = new Uint8Array(hex.length / 2) - for (let i = 0; i < array.length; i++) { - const j = i * 2 - const hexByte = hex.slice(j, j + 2) - const byte = Number.parseInt(hexByte, 16) - if (Number.isNaN(byte) || byte < 0) - throw new Error('Invalid byte sequence') - array[i] = byte - } - return array - } - function bytesToNumber(bytes) { - return hexToNumber(bytesToHex(bytes)) - } - function ensureBytes(hex) { - return hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes(hex) - } - function normalizeScalar(num) { - if (typeof num === 'number' && Number.isSafeInteger(num) && num > 0) - return BigInt(num) - if (typeof num === 'bigint' && isWithinCurveOrder(num)) return num - throw new TypeError('Expected valid private scalar: 0 < scalar < curve.n') - } - function mod(a, b = CURVE.P) { - const result = a % b - return result >= _0n ? result : b + result - } - function pow2(x, power) { - const {P} = CURVE - let res = x - while (power-- > _0n) { - res *= res - res %= P - } - return res - } - function sqrtMod(x) { - const {P} = CURVE - const _6n = BigInt(6) - const _11n = BigInt(11) - const _22n = BigInt(22) - const _23n = BigInt(23) - const _44n = BigInt(44) - const _88n = BigInt(88) - const b2 = (x * x * x) % P - const b3 = (b2 * b2 * x) % P - const b6 = (pow2(b3, _3n) * b3) % P - const b9 = (pow2(b6, _3n) * b3) % P - const b11 = (pow2(b9, _2n) * b2) % P - const b22 = (pow2(b11, _11n) * b11) % P - const b44 = (pow2(b22, _22n) * b22) % P - const b88 = (pow2(b44, _44n) * b44) % P - const b176 = (pow2(b88, _88n) * b88) % P - const b220 = (pow2(b176, _44n) * b44) % P - const b223 = (pow2(b220, _3n) * b3) % P - const t1 = (pow2(b223, _23n) * b22) % P - const t2 = (pow2(t1, _6n) * b2) % P - return pow2(t2, _2n) - } - function invert(number, modulo = CURVE.P) { - if (number === _0n || modulo <= _0n) { - throw new Error( - `invert: expected positive integers, got n=${number} mod=${modulo}` - ) - } - let a = mod(number, modulo) - let b = modulo - let x = _0n, - u = _1n - while (a !== _0n) { - const q = b / a - const r = b % a - const m = x - u * q - ;(b = a), (a = r), (x = u), (u = m) - } - const gcd = b - if (gcd !== _1n) throw new Error('invert: does not exist') - return mod(x, modulo) - } - function invertBatch(nums, p = CURVE.P) { - const scratch = new Array(nums.length) - const lastMultiplied = nums.reduce((acc, num, i) => { - if (num === _0n) return acc - scratch[i] = acc - return mod(acc * num, p) - }, _1n) - const inverted = invert(lastMultiplied, p) - nums.reduceRight((acc, num, i) => { - if (num === _0n) return acc - scratch[i] = mod(acc * scratch[i], p) - return mod(acc * num, p) - }, inverted) - return scratch - } - const divNearest = (a, b) => (a + b / _2n) / b - const POW_2_128 = _2n ** BigInt(128) - function splitScalarEndo(k) { - const {n} = CURVE - const a1 = BigInt('0x3086d221a7d46bcde86c90e49284eb15') - const b1 = -_1n * BigInt('0xe4437ed6010e88286f547fa90abfe4c3') - const a2 = BigInt('0x114ca50f7a8e2f3f657c1108d9d44cfd8') - const b2 = a1 - const c1 = divNearest(b2 * k, n) - const c2 = divNearest(-b1 * k, n) - let k1 = mod(k - c1 * a1 - c2 * a2, n) - let k2 = mod(-c1 * b1 - c2 * b2, n) - const k1neg = k1 > POW_2_128 - const k2neg = k2 > POW_2_128 - if (k1neg) k1 = n - k1 - if (k2neg) k2 = n - k2 - if (k1 > POW_2_128 || k2 > POW_2_128) { - throw new Error('splitScalarEndo: Endomorphism failed, k=' + k) - } - return {k1neg, k1, k2neg, k2} - } - function truncateHash(hash) { - const {n} = CURVE - const byteLength = hash.length - const delta = byteLength * 8 - 256 - let h = bytesToNumber(hash) - if (delta > 0) h = h >> BigInt(delta) - if (h >= n) h -= n - return h - } - class HmacDrbg { - constructor() { - this.v = new Uint8Array(32).fill(1) - this.k = new Uint8Array(32).fill(0) - this.counter = 0 - } - hmac(...values) { - return utils.hmacSha256(this.k, ...values) - } - hmacSync(...values) { - if (typeof utils.hmacSha256Sync !== 'function') - throw new Error('utils.hmacSha256Sync is undefined, you need to set it') - const res = utils.hmacSha256Sync(this.k, ...values) - if (res instanceof Promise) - throw new Error('To use sync sign(), ensure utils.hmacSha256 is sync') - return res - } - incr() { - if (this.counter >= 1000) { - throw new Error('Tried 1,000 k values for sign(), all were invalid') - } - this.counter += 1 - } - async reseed(seed = new Uint8Array()) { - this.k = await this.hmac(this.v, Uint8Array.from([0x00]), seed) - this.v = await this.hmac(this.v) - if (seed.length === 0) return - this.k = await this.hmac(this.v, Uint8Array.from([0x01]), seed) - this.v = await this.hmac(this.v) - } - reseedSync(seed = new Uint8Array()) { - this.k = this.hmacSync(this.v, Uint8Array.from([0x00]), seed) - this.v = this.hmacSync(this.v) - if (seed.length === 0) return - this.k = this.hmacSync(this.v, Uint8Array.from([0x01]), seed) - this.v = this.hmacSync(this.v) - } - async generate() { - this.incr() - this.v = await this.hmac(this.v) - return this.v - } - generateSync() { - this.incr() - this.v = this.hmacSync(this.v) - return this.v - } - } - function isWithinCurveOrder(num) { - return _0n < num && num < CURVE.n - } - function isValidFieldElement(num) { - return _0n < num && num < CURVE.P - } - function kmdToSig(kBytes, m, d) { - const k = bytesToNumber(kBytes) - if (!isWithinCurveOrder(k)) return - const {n} = CURVE - const q = Point.BASE.multiply(k) - const r = mod(q.x, n) - if (r === _0n) return - const s = mod(invert(k, n) * mod(m + d * r, n), n) - if (s === _0n) return - const sig = new Signature(r, s) - const recovery = (q.x === sig.r ? 0 : 2) | Number(q.y & _1n) - return {sig, recovery} - } - function normalizePrivateKey(key) { - let num - if (typeof key === 'bigint') { - num = key - } else if ( - typeof key === 'number' && - Number.isSafeInteger(key) && - key > 0 - ) { - num = BigInt(key) - } else if (typeof key === 'string') { - if (key.length !== 64) throw new Error('Expected 32 bytes of private key') - num = hexToNumber(key) - } else if (isUint8a(key)) { - if (key.length !== 32) throw new Error('Expected 32 bytes of private key') - num = bytesToNumber(key) - } else { - throw new TypeError('Expected valid private key') - } - if (!isWithinCurveOrder(num)) - throw new Error('Expected private key: 0 < key < n') - return num - } - function normalizePublicKey(publicKey) { - if (publicKey instanceof Point) { - publicKey.assertValidity() - return publicKey - } else { - return Point.fromHex(publicKey) - } - } - function normalizeSignature(signature) { - if (signature instanceof Signature) { - signature.assertValidity() - return signature - } - try { - return Signature.fromDER(signature) - } catch (error) { - return Signature.fromCompact(signature) - } - } - function getPublicKey(privateKey, isCompressed = false) { - return Point.fromPrivateKey(privateKey).toRawBytes(isCompressed) - } - function recoverPublicKey( - msgHash, - signature, - recovery, - isCompressed = false - ) { - return Point.fromSignature(msgHash, signature, recovery).toRawBytes( - isCompressed - ) - } - function isPub(item) { - const arr = isUint8a(item) - const str = typeof item === 'string' - const len = (arr || str) && item.length - if (arr) return len === 33 || len === 65 - if (str) return len === 66 || len === 130 - if (item instanceof Point) return true - return false - } - function getSharedSecret(privateA, publicB, isCompressed = false) { - if (isPub(privateA)) - throw new TypeError('getSharedSecret: first arg must be private key') - if (!isPub(publicB)) - throw new TypeError('getSharedSecret: second arg must be public key') - const b = normalizePublicKey(publicB) - b.assertValidity() - return b.multiply(normalizePrivateKey(privateA)).toRawBytes(isCompressed) - } - function bits2int(bytes) { - const slice = bytes.length > 32 ? bytes.slice(0, 32) : bytes - return bytesToNumber(slice) - } - function bits2octets(bytes) { - const z1 = bits2int(bytes) - const z2 = mod(z1, CURVE.n) - return int2octets(z2 < _0n ? z1 : z2) - } - function int2octets(num) { - if (typeof num !== 'bigint') throw new Error('Expected bigint') - const hex = numTo32bStr(num) - return hexToBytes(hex) - } - function initSigArgs(msgHash, privateKey, extraEntropy) { - if (msgHash == null) - throw new Error(`sign: expected valid message hash, not "${msgHash}"`) - const h1 = ensureBytes(msgHash) - const d = normalizePrivateKey(privateKey) - const seedArgs = [int2octets(d), bits2octets(h1)] - if (extraEntropy != null) { - if (extraEntropy === true) extraEntropy = utils.randomBytes(32) - const e = ensureBytes(extraEntropy) - if (e.length !== 32) - throw new Error('sign: Expected 32 bytes of extra data') - seedArgs.push(e) - } - const seed = concatBytes(...seedArgs) - const m = bits2int(h1) - return {seed, m, d} - } - function finalizeSig(recSig, opts) { - let {sig, recovery} = recSig - const {canonical, der, recovered} = Object.assign( - {canonical: true, der: true}, - opts - ) - if (canonical && sig.hasHighS()) { - sig = sig.normalizeS() - recovery ^= 1 - } - const hashed = der ? sig.toDERRawBytes() : sig.toCompactRawBytes() - return recovered ? [hashed, recovery] : hashed - } - async function sign(msgHash, privKey, opts = {}) { - const {seed, m, d} = initSigArgs(msgHash, privKey, opts.extraEntropy) - let sig - const drbg = new HmacDrbg() - await drbg.reseed(seed) - while (!(sig = kmdToSig(await drbg.generate(), m, d))) await drbg.reseed() - return finalizeSig(sig, opts) - } - function signSync(msgHash, privKey, opts = {}) { - const {seed, m, d} = initSigArgs(msgHash, privKey, opts.extraEntropy) - let sig - const drbg = new HmacDrbg() - drbg.reseedSync(seed) - while (!(sig = kmdToSig(drbg.generateSync(), m, d))) drbg.reseedSync() - return finalizeSig(sig, opts) - } - const vopts = {strict: true} - function verify(signature, msgHash, publicKey, opts = vopts) { - let sig - try { - sig = normalizeSignature(signature) - msgHash = ensureBytes(msgHash) - } catch (error) { - return false - } - const {r, s} = sig - if (opts.strict && sig.hasHighS()) return false - const h = truncateHash(msgHash) - let P - try { - P = normalizePublicKey(publicKey) - } catch (error) { - return false - } - const {n} = CURVE - const sinv = invert(s, n) - const u1 = mod(h * sinv, n) - const u2 = mod(r * sinv, n) - const R = Point.BASE.multiplyAndAddUnsafe(P, u1, u2) - if (!R) return false - const v = mod(R.x, n) - return v === r - } - function finalizeSchnorrChallenge(ch) { - return mod(bytesToNumber(ch), CURVE.n) - } - function hasEvenY(point) { - return (point.y & _1n) === _0n - } - class SchnorrSignature { - constructor(r, s) { - this.r = r - this.s = s - this.assertValidity() - } - static fromHex(hex) { - const bytes = ensureBytes(hex) - if (bytes.length !== 64) - throw new TypeError( - `SchnorrSignature.fromHex: expected 64 bytes, not ${bytes.length}` - ) - const r = bytesToNumber(bytes.subarray(0, 32)) - const s = bytesToNumber(bytes.subarray(32, 64)) - return new SchnorrSignature(r, s) - } - assertValidity() { - const {r, s} = this - if (!isValidFieldElement(r) || !isWithinCurveOrder(s)) - throw new Error('Invalid signature') - } - toHex() { - return numTo32bStr(this.r) + numTo32bStr(this.s) - } - toRawBytes() { - return hexToBytes(this.toHex()) - } - } - function schnorrGetPublicKey(privateKey) { - return Point.fromPrivateKey(privateKey).toRawX() - } - function initSchnorrSigArgs(message, privateKey, auxRand) { - if (message == null) - throw new TypeError(`sign: Expected valid message, not "${message}"`) - const m = ensureBytes(message) - const d0 = normalizePrivateKey(privateKey) - const rand = ensureBytes(auxRand) - if (rand.length !== 32) - throw new TypeError('sign: Expected 32 bytes of aux randomness') - const P = Point.fromPrivateKey(d0) - const px = P.toRawX() - const d = hasEvenY(P) ? d0 : CURVE.n - d0 - return {m, P, px, d, rand} - } - function initSchnorrNonce(d, t0h) { - return numTo32b(d ^ bytesToNumber(t0h)) - } - function finalizeSchnorrNonce(k0h) { - const k0 = mod(bytesToNumber(k0h), CURVE.n) - if (k0 === _0n) - throw new Error('sign: Creation of signature failed. k is zero') - const R = Point.fromPrivateKey(k0) - const rx = R.toRawX() - const k = hasEvenY(R) ? k0 : CURVE.n - k0 - return {R, rx, k} - } - function finalizeSchnorrSig(R, k, e, d) { - return new SchnorrSignature(R.x, mod(k + e * d, CURVE.n)).toRawBytes() - } - async function schnorrSign( - message, - privateKey, - auxRand = utils.randomBytes() - ) { - const {m, px, d, rand} = initSchnorrSigArgs(message, privateKey, auxRand) - const t = initSchnorrNonce(d, await utils.taggedHash(TAGS.aux, rand)) - const {R, rx, k} = finalizeSchnorrNonce( - await utils.taggedHash(TAGS.nonce, t, px, m) - ) - const e = finalizeSchnorrChallenge( - await utils.taggedHash(TAGS.challenge, rx, px, m) - ) - const sig = finalizeSchnorrSig(R, k, e, d) - const isValid = await schnorrVerify(sig, m, px) - if (!isValid) throw new Error('sign: Invalid signature produced') - return sig - } - function schnorrSignSync(message, privateKey, auxRand = utils.randomBytes()) { - const {m, px, d, rand} = initSchnorrSigArgs(message, privateKey, auxRand) - const t = initSchnorrNonce(d, utils.taggedHashSync(TAGS.aux, rand)) - const {R, rx, k} = finalizeSchnorrNonce( - utils.taggedHashSync(TAGS.nonce, t, px, m) - ) - const e = finalizeSchnorrChallenge( - utils.taggedHashSync(TAGS.challenge, rx, px, m) - ) - const sig = finalizeSchnorrSig(R, k, e, d) - const isValid = schnorrVerifySync(sig, m, px) - if (!isValid) throw new Error('sign: Invalid signature produced') - return sig - } - function initSchnorrVerify(signature, message, publicKey) { - const raw = signature instanceof SchnorrSignature - const sig = raw ? signature : SchnorrSignature.fromHex(signature) - if (raw) sig.assertValidity() - return { - ...sig, - m: ensureBytes(message), - P: normalizePublicKey(publicKey) - } - } - function finalizeSchnorrVerify(r, P, s, e) { - const R = Point.BASE.multiplyAndAddUnsafe( - P, - normalizePrivateKey(s), - mod(-e, CURVE.n) - ) - if (!R || !hasEvenY(R) || R.x !== r) return false - return true - } - async function schnorrVerify(signature, message, publicKey) { - try { - const {r, s, m, P} = initSchnorrVerify(signature, message, publicKey) - const e = finalizeSchnorrChallenge( - await utils.taggedHash(TAGS.challenge, numTo32b(r), P.toRawX(), m) - ) - return finalizeSchnorrVerify(r, P, s, e) - } catch (error) { - return false - } - } - function schnorrVerifySync(signature, message, publicKey) { - try { - const {r, s, m, P} = initSchnorrVerify(signature, message, publicKey) - const e = finalizeSchnorrChallenge( - utils.taggedHashSync(TAGS.challenge, numTo32b(r), P.toRawX(), m) - ) - return finalizeSchnorrVerify(r, P, s, e) - } catch (error) { - return false - } - } - const schnorr = { - Signature: SchnorrSignature, - getPublicKey: schnorrGetPublicKey, - sign: schnorrSign, - verify: schnorrVerify, - signSync: schnorrSignSync, - verifySync: schnorrVerifySync - } - Point.BASE._setWindowSize(8) - const crypto = { - node: nodeCrypto, - web: typeof self === 'object' && 'crypto' in self ? self.crypto : undefined - } - const TAGS = { - challenge: 'BIP0340/challenge', - aux: 'BIP0340/aux', - nonce: 'BIP0340/nonce' - } - const TAGGED_HASH_PREFIXES = {} - const utils = { - isValidPrivateKey(privateKey) { - try { - normalizePrivateKey(privateKey) - return true - } catch (error) { - return false - } - }, - privateAdd: (privateKey, tweak) => { - const p = normalizePrivateKey(privateKey) - const t = normalizePrivateKey(tweak) - return numTo32b(mod(p + t, CURVE.n)) - }, - privateNegate: privateKey => { - const p = normalizePrivateKey(privateKey) - return numTo32b(CURVE.n - p) - }, - pointAddScalar: (p, tweak, isCompressed) => { - const P = Point.fromHex(p) - const t = normalizePrivateKey(tweak) - const Q = Point.BASE.multiplyAndAddUnsafe(P, t, _1n) - if (!Q) throw new Error('Tweaked point at infinity') - return Q.toRawBytes(isCompressed) - }, - pointMultiply: (p, tweak, isCompressed) => { - const P = Point.fromHex(p) - const t = bytesToNumber(ensureBytes(tweak)) - return P.multiply(t).toRawBytes(isCompressed) - }, - hashToPrivateKey: hash => { - hash = ensureBytes(hash) - if (hash.length < 40 || hash.length > 1024) - throw new Error('Expected 40-1024 bytes of private key as per FIPS 186') - const num = mod(bytesToNumber(hash), CURVE.n - _1n) + _1n - return numTo32b(num) - }, - randomBytes: (bytesLength = 32) => { - if (crypto.web) { - return crypto.web.getRandomValues(new Uint8Array(bytesLength)) - } else if (crypto.node) { - const {randomBytes} = crypto.node - return Uint8Array.from(randomBytes(bytesLength)) - } else { - throw new Error("The environment doesn't have randomBytes function") - } - }, - randomPrivateKey: () => { - return utils.hashToPrivateKey(utils.randomBytes(40)) - }, - bytesToHex, - hexToBytes, - concatBytes, - mod, - invert, - sha256: async (...messages) => { - if (crypto.web) { - const buffer = await crypto.web.subtle.digest( - 'SHA-256', - concatBytes(...messages) - ) - return new Uint8Array(buffer) - } else if (crypto.node) { - const {createHash} = crypto.node - const hash = createHash('sha256') - messages.forEach(m => hash.update(m)) - return Uint8Array.from(hash.digest()) - } else { - throw new Error("The environment doesn't have sha256 function") - } - }, - hmacSha256: async (key, ...messages) => { - if (crypto.web) { - const ckey = await crypto.web.subtle.importKey( - 'raw', - key, - {name: 'HMAC', hash: {name: 'SHA-256'}}, - false, - ['sign'] - ) - const message = concatBytes(...messages) - const buffer = await crypto.web.subtle.sign('HMAC', ckey, message) - return new Uint8Array(buffer) - } else if (crypto.node) { - const {createHmac} = crypto.node - const hash = createHmac('sha256', key) - messages.forEach(m => hash.update(m)) - return Uint8Array.from(hash.digest()) - } else { - throw new Error("The environment doesn't have hmac-sha256 function") - } - }, - sha256Sync: undefined, - hmacSha256Sync: undefined, - taggedHash: async (tag, ...messages) => { - let tagP = TAGGED_HASH_PREFIXES[tag] - if (tagP === undefined) { - const tagH = await utils.sha256( - Uint8Array.from(tag, c => c.charCodeAt(0)) - ) - tagP = concatBytes(tagH, tagH) - TAGGED_HASH_PREFIXES[tag] = tagP - } - return utils.sha256(tagP, ...messages) - }, - taggedHashSync: (tag, ...messages) => { - if (typeof utils.sha256Sync !== 'function') - throw new Error('utils.sha256Sync is undefined, you need to set it') - let tagP = TAGGED_HASH_PREFIXES[tag] - if (tagP === undefined) { - const tagH = utils.sha256Sync( - Uint8Array.from(tag, c => c.charCodeAt(0)) - ) - tagP = concatBytes(tagH, tagH) - TAGGED_HASH_PREFIXES[tag] = tagP - } - return utils.sha256Sync(tagP, ...messages) - }, - precompute(windowSize = 8, point = Point.BASE) { - const cached = point === Point.BASE ? point : new Point(point.x, point.y) - cached._setWindowSize(windowSize) - cached.multiply(_3n) - return cached - } - } - - exports.CURVE = CURVE - exports.Point = Point - exports.Signature = Signature - exports.getPublicKey = getPublicKey - exports.getSharedSecret = getSharedSecret - exports.recoverPublicKey = recoverPublicKey - exports.schnorr = schnorr - exports.sign = sign - exports.signSync = signSync - exports.utils = utils - exports.verify = verify - - Object.defineProperty(exports, '__esModule', {value: true}) -}) diff --git a/lnbits/extensions/cashu/static/js/utils.js b/lnbits/extensions/cashu/static/js/utils.js deleted file mode 100644 index cf852b58..00000000 --- a/lnbits/extensions/cashu/static/js/utils.js +++ /dev/null @@ -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}`) -} diff --git a/lnbits/extensions/cashu/tasks.py b/lnbits/extensions/cashu/tasks.py deleted file mode 100644 index 982d3ac1..00000000 --- a/lnbits/extensions/cashu/tasks.py +++ /dev/null @@ -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 diff --git a/lnbits/extensions/cashu/templates/cashu/_api_docs.html b/lnbits/extensions/cashu/templates/cashu/_api_docs.html deleted file mode 100644 index f7bb19f6..00000000 --- a/lnbits/extensions/cashu/templates/cashu/_api_docs.html +++ /dev/null @@ -1,80 +0,0 @@ - - - - diff --git a/lnbits/extensions/cashu/templates/cashu/_cashu.html b/lnbits/extensions/cashu/templates/cashu/_cashu.html deleted file mode 100644 index 370e9eb4..00000000 --- a/lnbits/extensions/cashu/templates/cashu/_cashu.html +++ /dev/null @@ -1,28 +0,0 @@ - - - -

Create Cashu ecash mints and wallets.

- Created by - arcbtc, - vlad, - calle. -
-
-
diff --git a/lnbits/extensions/cashu/templates/cashu/index.html b/lnbits/extensions/cashu/templates/cashu/index.html deleted file mode 100644 index 2599669c..00000000 --- a/lnbits/extensions/cashu/templates/cashu/index.html +++ /dev/null @@ -1,367 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - Cashu mint and wallet -

-

- 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. -

- Important -

-

- 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. -

-
-
- - - -
-
-
Mints
-
-
- Export to CSV -
-
- - {% raw %} - - - - {% endraw %} - - New Mint -
-
-
- -
- - -
{{SITE_TITLE}} Cashu extension
-
- - - - {% include "cashu/_api_docs.html" %} - - {% include "cashu/_cashu.html" %} - - -
-
- - - - - - - -
- Create Mint - - Cancel -
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/cashu/templates/cashu/mint.html b/lnbits/extensions/cashu/templates/cashu/mint.html deleted file mode 100644 index a6959ec2..00000000 --- a/lnbits/extensions/cashu/templates/cashu/mint.html +++ /dev/null @@ -1,92 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -
- -

{{ mint_name }}

- - click to open wallet -
-
-
- - -
Read the following carefully!
-

- This is a - Cashu - mint. Cashu is an ecash system for Bitcoin. -

-

- Open this page in your native browser
- 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. -

-

- Add wallet to home screen
- 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. -

-

- Backup your wallet
- 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. -

-

- This service is in BETA
- 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! -

-
-
-
-
- -{% endblock %} {% block scripts %} - - - -{% endblock %} diff --git a/lnbits/extensions/cashu/templates/cashu/wallet.html b/lnbits/extensions/cashu/templates/cashu/wallet.html deleted file mode 100644 index 9638f347..00000000 --- a/lnbits/extensions/cashu/templates/cashu/wallet.html +++ /dev/null @@ -1,2992 +0,0 @@ -{% extends "public.html" %} {% block toolbar_title %} {% raw %} Cashu wallet {% -endraw %} {% endblock %} {% block footer %}{% endblock %} {% block -page_container %} - - -
-
- - -
-
-
-

-
- {% raw %} {{ getBalance() }} - {{tickerShort}} {% endraw %} -
-

-
-
- - -
-
-
- - - -
-
- Get Ecash -
-
-
- - Send Ecash -
-
- - - - - - - - - - - - -
- - - - Mints - You can connect your wallet to multiple Cashu mints. - Enter a mint URL and select the mint your want to - use. - - - - -
- {% raw %} - - - - - - - - - {{mint.url}} - - - - - - - - - - - {% endraw %} - -
-
-
-
-
- - - - - - - -
-
-
- - - - Multimint Swaps - Swap funds from one mint to another via Lightning. - Warning: this feature is still experimental and could - behave in unexpected ways! - - - - - - - - - - - Swap - - -
-
- - - - - {% raw %} - - {% endraw %} - - - - - - - - {% raw %} - - {% endraw %} - - - - - - - - {% raw %} - - {% endraw %} - - -
-
-
- -
-
-
- Create invoice - -
- -
-
- Warning - Download wallet backup - InstallInstall Cashu -
-
-
- Pay invoice - -
-
-
-
- - - -
- - - - - - - - -
- - - -
- {% raw %} -
- Pay {{ payInvoiceData.invoice.fsat }}{% endraw %} - {{LNBITS_DENOMINATION}} {% raw %} -
- -

- Description: {{ - payInvoiceData.invoice.description }}
- Expire date: {{ payInvoiceData.invoice.expireDate - }}
- Hash: {{ payInvoiceData.invoice.hash }} -

- {% endraw %} -
- - Cancel -
-
- Not enough funds! - Cancel -
-
- -
- {% raw %} - -

- {{ payInvoiceData.domain }} is requesting {{ - payInvoiceData.lnurlpay.maxSendable | msatoshiFormat }} {% - endraw %} {{LNBITS_DENOMINATION}} {% raw %} - -

-

- {{ payInvoiceData.lnurlpay.targetUser || - payInvoiceData.domain }} - is requesting
- between - {{ payInvoiceData.lnurlpay.minSendable | msatoshiFormat }} - and - {{ payInvoiceData.lnurlpay.maxSendable | msatoshiFormat }} - {% endraw %} {{LNBITS_DENOMINATION}} {% raw %} - -

- -
-

- {{ payInvoiceData.lnurlpay.description }} -

-

- -

-
-
-
- {% endraw %} - -
-
- -
-
-
- Send - Cancel -
-
-
-
- - - -
- Enter - - - Close -
-
-
- - - -
- - Cancel - -
-
-
-
-
- - - -
- -
-
- Cancel -
-
-
- - - - - - - - Cashu - wallet - - -

Please take a moment to read the following information.

- -

- Open this wallet on your device's native browser - Cashu stores your ecash on your device locally. For the best - experience, use this wallet with your device's native web browser - (for example Safari for iOS, Chrome for Android). -

-

- Add to home screen. - Add Cashu to your home screen as a progressive web app (PWA). On - Android Chrome, click the hamburger menu at the upper right. On - iOS Safari, click the share button. Now press the Add to Home - screen button. -

-

- This software is in BETA! We hold no - responsibility for people losing access to funds. Use at your own - risk! Ecash is a bearer asset, meaning losing access to this - wallet will mean you will lose the funds. This wallet stores ecash - tokens in its database. If you lose the link or delete your your - data without backing up, you will lose your tokens. Press the - Backup button to download a copy of your tokens. -

-
- Install Cashu - Copy URL - Continue -
-
-
-
- - - -
Warning
-

- Bookmark this page and backup your tokens! - Ecash is a bearer asset, meaning losing access to this wallet will - mean you will lose the funds. This wallet stores ecash tokens in its - database. If you lose the link or delete your your data without - backing up, you will lose your tokens. Press the Backup button to - download a copy of your tokens. -

-

- Add to home screen. - You can add Cashu to your home screen as a progressive web app - (PWA). On Android Chrome, click the hamburger menu at the upper - right. On iOS Safari, click the share button. Now press the Add to - Home screen button. -

-

- This software is in BETA! We hold no responsibility - for people losing access to funds. Use at your own risk! -

-
- Copy wallet URL - I understand -
-
-
- - - -
-
-
- Create a Lightning invoice -
-
- - -
- -
- Copy invoice - Create Invoice - Close -
-
-
- - - -
-
-
- How much would you like to send? -
-
- - -
-
-
- - - - - - -
- -
-
- Send Tokens - -
- Copy token - Copy link -
- - Close -
-
-
- - - -
-
-
- Receive Cashu tokens -
-
- -
- -
- Receive - - Close -
-
-
- - -
Do you trust this mint?
-

- A Cashu mint does not know about your financial activity but it - controls your funds. Make sure that you trust the operator of this - mint. -

- -
- Add mint - Cancel -
-
-
-
-
-
-{% endblock %} {% block styles %} - -{% endblock %} {% block scripts %} - - - - - -{% endblock %} diff --git a/lnbits/extensions/cashu/views.py b/lnbits/extensions/cashu/views.py deleted file mode 100644 index 09d314c3..00000000 --- a/lnbits/extensions/cashu/views.py +++ /dev/null @@ -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", - }, - ], - } - ], - } diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py deleted file mode 100644 index 682412a4..00000000 --- a/lnbits/extensions/cashu/views_api.py +++ /dev/null @@ -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