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 e90611fc..00000000 Binary files a/lnbits/extensions/cashu/static/image/cashu.png and /dev/null differ 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