diff --git a/.env.example b/.env.example
index 32a67f75b..0a3f8f140 100644
--- a/.env.example
+++ b/.env.example
@@ -6,15 +6,16 @@ PORT=5000
DEBUG=false
-# User IDs seperated by comma
+# Allow users and admins by user IDs (comma separated list)
+LNBITS_ALLOWED_USERS=""
LNBITS_ADMIN_USERS=""
# Extensions only admin can access
-LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
+LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available
-LNBITS_ADMIN_UI=false
+LNBITS_ADMIN_UI=false
# Restricts access, User IDs seperated by comma
-LNBITS_ALLOWED_USERS=""
+LNBITS_ALLOWED_USERS=""
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
@@ -108,3 +109,8 @@ ECLAIR_PASS=eclairpw
# Enter /api in LightningTipBot to get your key
LNTIPS_API_KEY=LNTIPS_ADMIN_KEY
LNTIPS_API_ENDPOINT=https://ln.tips
+
+# Cashu Mint
+# Use a long-enough random (!) private key.
+# Once set, you cannot change this key as for now.
+CASHU_PRIVATE_KEY="SuperSecretPrivateKey"
diff --git a/lnbits/commands.py b/lnbits/commands.py
index 830033144..95b767ecd 100644
--- a/lnbits/commands.py
+++ b/lnbits/commands.py
@@ -69,8 +69,7 @@ async def migrate_databases():
(db_name, version, version),
)
- async def run_migration(db, migrations_module):
- db_name = migrations_module.__name__.split(".")[-2]
+ async def run_migration(db, migrations_module, db_name):
for key, migrate in migrations_module.__dict__.items():
match = match = matcher.match(key)
if match:
@@ -101,20 +100,24 @@ async def migrate_databases():
rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall()
current_versions = {row["db"]: row["version"] for row in rows}
matcher = re.compile(r"^m(\d\d\d)_")
- await run_migration(conn, core_migrations)
+ db_name = core_migrations.__name__.split(".")[-2]
+ await run_migration(conn, core_migrations, db_name)
for ext in get_valid_extensions():
try:
- ext_migrations = importlib.import_module(
- f"lnbits.extensions.{ext.code}.migrations"
+
+ module_str = (
+ ext.migration_module or f"lnbits.extensions.{ext.code}.migrations"
)
+ ext_migrations = importlib.import_module(module_str)
ext_db = importlib.import_module(f"lnbits.extensions.{ext.code}").db
+ db_name = ext.db_name or module_str.split(".")[-2]
except ImportError:
raise ImportError(
f"Please make sure that the extension `{ext.code}` has a migrations file."
)
async with ext_db.connect() as ext_conn:
- await run_migration(ext_conn, ext_migrations)
+ await run_migration(ext_conn, ext_migrations, db_name)
logger.info("✔️ All migrations done.")
diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py
index aab55b036..dc5fb89c5 100644
--- a/lnbits/core/views/api.py
+++ b/lnbits/core/views/api.py
@@ -12,7 +12,7 @@ from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
import async_timeout
import httpx
import pyqrcode
-from fastapi import Depends, Header, Query, Request
+from fastapi import Depends, Header, Query, Request, Response
from fastapi.exceptions import HTTPException
from fastapi.params import Body
from loguru import logger
@@ -584,8 +584,8 @@ class DecodePayment(BaseModel):
data: str
-@core_app.post("/api/v1/payments/decode")
-async def api_payments_decode(data: DecodePayment):
+@core_app.post("/api/v1/payments/decode", status_code=HTTPStatus.OK)
+async def api_payments_decode(data: DecodePayment, response: Response):
payment_str = data.data
try:
if payment_str[:5] == "LNURL":
@@ -606,6 +606,7 @@ async def api_payments_decode(data: DecodePayment):
"min_final_cltv_expiry": invoice.min_final_cltv_expiry,
}
except:
+ response.status_code = HTTPStatus.BAD_REQUEST
return {"message": "Failed to decode"}
diff --git a/lnbits/db.py b/lnbits/db.py
index 8ae10f720..00bf849bb 100644
--- a/lnbits/db.py
+++ b/lnbits/db.py
@@ -1,6 +1,7 @@
import asyncio
import datetime
import os
+import re
import time
from contextlib import asynccontextmanager
from typing import Optional
@@ -73,18 +74,40 @@ class Connection(Compat):
query = query.replace("?", "%s")
return query
+ def rewrite_values(self, values):
+ # strip html
+ CLEANR = re.compile("<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});")
+
+ def cleanhtml(raw_html):
+ if isinstance(raw_html, str):
+ cleantext = re.sub(CLEANR, "", raw_html)
+ return cleantext
+ else:
+ return raw_html
+
+ # tuple to list and back to tuple
+ value_list = [values] if isinstance(values, str) else list(values)
+ values = tuple([cleanhtml(l) for l in value_list])
+ return values
+
async def fetchall(self, query: str, values: tuple = ()) -> list:
- result = await self.conn.execute(self.rewrite_query(query), values)
+ result = await self.conn.execute(
+ self.rewrite_query(query), self.rewrite_values(values)
+ )
return await result.fetchall()
async def fetchone(self, query: str, values: tuple = ()):
- result = await self.conn.execute(self.rewrite_query(query), values)
+ result = await self.conn.execute(
+ self.rewrite_query(query), self.rewrite_values(values)
+ )
row = await result.fetchone()
await result.close()
return row
async def execute(self, query: str, values: tuple = ()):
- return await self.conn.execute(self.rewrite_query(query), values)
+ return await self.conn.execute(
+ self.rewrite_query(query), self.rewrite_values(values)
+ )
class Database(Compat):
diff --git a/lnbits/extensions/cashu/README.md b/lnbits/extensions/cashu/README.md
new file mode 100644
index 000000000..8f53b474b
--- /dev/null
+++ b/lnbits/extensions/cashu/README.md
@@ -0,0 +1,11 @@
+# Cashu
+
+## Create ecash mint for pegging in/out of ecash
+
+
+
+### Usage
+
+1. Enable extension
+2. Create a Mint
+3. Share wallet
diff --git a/lnbits/extensions/cashu/__init__.py b/lnbits/extensions/cashu/__init__.py
new file mode 100644
index 000000000..e6507bba9
--- /dev/null
+++ b/lnbits/extensions/cashu/__init__.py
@@ -0,0 +1,48 @@
+import asyncio
+
+from environs import Env # type: ignore
+from fastapi import APIRouter
+from fastapi.staticfiles import StaticFiles
+
+from lnbits.db import Database
+from lnbits.helpers import template_renderer
+from lnbits.tasks import catch_everything_and_restart
+
+db = Database("ext_cashu")
+
+import sys
+
+cashu_static_files = [
+ {
+ "path": "/cashu/static",
+ "app": StaticFiles(directory="lnbits/extensions/cashu/static"),
+ "name": "cashu_static",
+ }
+]
+from cashu.mint.ledger import Ledger
+
+env = Env()
+env.read_env()
+
+ledger = Ledger(
+ db=db,
+ seed=env.str("CASHU_PRIVATE_KEY", default="SuperSecretPrivateKey"),
+ derivation_path="0/0/0/1",
+)
+
+cashu_ext: APIRouter = APIRouter(prefix="/cashu", tags=["cashu"])
+
+
+def cashu_renderer():
+ return template_renderer(["lnbits/extensions/cashu/templates"])
+
+
+from .tasks import startup_cashu_mint, wait_for_paid_invoices
+from .views import * # noqa
+from .views_api import * # noqa
+
+
+def cashu_start():
+ loop = asyncio.get_event_loop()
+ loop.create_task(catch_everything_and_restart(startup_cashu_mint))
+ loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
diff --git a/lnbits/extensions/cashu/config.json b/lnbits/extensions/cashu/config.json
new file mode 100644
index 000000000..af202d43c
--- /dev/null
+++ b/lnbits/extensions/cashu/config.json
@@ -0,0 +1,7 @@
+{
+ "name": "Cashu",
+ "short_description": "Ecash mint and wallet",
+ "icon": "account_balance",
+ "contributors": ["calle", "vlad", "arcbtc"],
+ "hidden": false
+}
diff --git a/lnbits/extensions/cashu/crud.py b/lnbits/extensions/cashu/crud.py
new file mode 100644
index 000000000..773a11fde
--- /dev/null
+++ b/lnbits/extensions/cashu/crud.py
@@ -0,0 +1,63 @@
+import os
+import random
+import time
+from binascii import hexlify, unhexlify
+from typing import Any, List, Optional, Union
+
+from cashu.core.base import MintKeyset
+from embit import bip32, bip39, ec, script
+from embit.networks import NETWORKS
+from loguru import logger
+
+from lnbits.db import Connection, Database
+from lnbits.helpers import urlsafe_short_hash
+
+from . import db
+from .models import Cashu, Pegs, Promises, Proof
+
+
+async def create_cashu(
+ cashu_id: str, keyset_id: str, wallet_id: str, data: Cashu
+) -> Cashu:
+
+ await db.execute(
+ """
+ INSERT INTO cashu.cashu (id, wallet, name, tickershort, fraction, maxsats, coins, keyset_id)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ cashu_id,
+ wallet_id,
+ data.name,
+ data.tickershort,
+ data.fraction,
+ data.maxsats,
+ data.coins,
+ keyset_id,
+ ),
+ )
+
+ cashu = await get_cashu(cashu_id)
+ assert cashu, "Newly created cashu couldn't be retrieved"
+ return cashu
+
+
+async def get_cashu(cashu_id) -> Optional[Cashu]:
+ row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,))
+ return Cashu(**row) if row else None
+
+
+async def get_cashus(wallet_ids: Union[str, List[str]]) -> List[Cashu]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = await db.fetchall(
+ f"SELECT * FROM cashu.cashu WHERE wallet IN ({q})", (*wallet_ids,)
+ )
+
+ return [Cashu(**row) for row in rows]
+
+
+async def delete_cashu(cashu_id) -> None:
+ await db.execute("DELETE FROM cashu.cashu WHERE id = ?", (cashu_id,))
diff --git a/lnbits/extensions/cashu/migrations.py b/lnbits/extensions/cashu/migrations.py
new file mode 100644
index 000000000..b54c41087
--- /dev/null
+++ b/lnbits/extensions/cashu/migrations.py
@@ -0,0 +1,33 @@
+async def m001_initial(db):
+ """
+ Initial cashu table.
+ """
+ await db.execute(
+ """
+ CREATE TABLE cashu.cashu (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+ name TEXT NOT NULL,
+ tickershort TEXT DEFAULT 'sats',
+ fraction BOOL,
+ maxsats INT,
+ coins INT,
+ keyset_id TEXT NOT NULL,
+ issued_sat INT
+ );
+ """
+ )
+
+ """
+ Initial cashus table.
+ """
+ await db.execute(
+ """
+ CREATE TABLE cashu.pegs (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+ inout BOOL NOT NULL,
+ amount INT
+ );
+ """
+ )
diff --git a/lnbits/extensions/cashu/models.py b/lnbits/extensions/cashu/models.py
new file mode 100644
index 000000000..c820d12ec
--- /dev/null
+++ b/lnbits/extensions/cashu/models.py
@@ -0,0 +1,147 @@
+from sqlite3 import Row
+from typing import List, Union
+
+from fastapi import Query
+from pydantic import BaseModel
+
+
+class Cashu(BaseModel):
+ id: str = Query(None)
+ name: str = Query(None)
+ wallet: str = Query(None)
+ tickershort: str = Query(None)
+ fraction: bool = Query(None)
+ maxsats: int = Query(0)
+ coins: int = Query(0)
+ keyset_id: str = Query(None)
+
+ @classmethod
+ def from_row(cls, row: Row):
+ return cls(**dict(row))
+
+
+class Pegs(BaseModel):
+ id: str
+ wallet: str
+ inout: str
+ amount: str
+
+ @classmethod
+ def from_row(cls, row: Row):
+ return cls(**dict(row))
+
+
+class PayLnurlWData(BaseModel):
+ lnurl: str
+
+
+class Promises(BaseModel):
+ id: str
+ amount: int
+ B_b: str
+ C_b: str
+ cashu_id: str
+
+
+class Proof(BaseModel):
+ amount: int
+ secret: str
+ C: str
+ reserved: bool = False # whether this proof is reserved for sending
+ send_id: str = "" # unique ID of send attempt
+ time_created: str = ""
+ time_reserved: str = ""
+
+ @classmethod
+ def from_row(cls, row: Row):
+ return cls(
+ amount=row[0],
+ C=row[1],
+ secret=row[2],
+ reserved=row[3] or False,
+ send_id=row[4] or "",
+ time_created=row[5] or "",
+ time_reserved=row[6] or "",
+ )
+
+ @classmethod
+ def from_dict(cls, d: dict):
+ assert "secret" in d, "no secret in proof"
+ assert "amount" in d, "no amount in proof"
+ return cls(
+ amount=d.get("amount"),
+ C=d.get("C"),
+ secret=d.get("secret"),
+ reserved=d.get("reserved") or False,
+ send_id=d.get("send_id") or "",
+ time_created=d.get("time_created") or "",
+ time_reserved=d.get("time_reserved") or "",
+ )
+
+ def to_dict(self):
+ return dict(amount=self.amount, secret=self.secret, C=self.C)
+
+ def __getitem__(self, key):
+ return self.__getattribute__(key)
+
+ def __setitem__(self, key, val):
+ self.__setattr__(key, val)
+
+
+class Proofs(BaseModel):
+ """TODO: Use this model"""
+
+ proofs: List[Proof]
+
+
+class Invoice(BaseModel):
+ amount: int
+ pr: str
+ hash: str
+ issued: bool = False
+
+ @classmethod
+ def from_row(cls, row: Row):
+ return cls(
+ amount=int(row[0]),
+ pr=str(row[1]),
+ hash=str(row[2]),
+ issued=bool(row[3]),
+ )
+
+
+class BlindedMessage(BaseModel):
+ amount: int
+ B_: str
+
+
+class BlindedSignature(BaseModel):
+ amount: int
+ C_: str
+
+ @classmethod
+ def from_dict(cls, d: dict):
+ return cls(
+ amount=d["amount"],
+ C_=d["C_"],
+ )
+
+
+class MintPayloads(BaseModel):
+ blinded_messages: List[BlindedMessage] = []
+
+
+class SplitPayload(BaseModel):
+ proofs: List[Proof]
+ amount: int
+ output_data: MintPayloads
+
+
+class CheckPayload(BaseModel):
+ proofs: List[Proof]
+
+
+class MeltPayload(BaseModel):
+ proofs: List[Proof]
+ amount: int
+ invoice: str
diff --git a/lnbits/extensions/cashu/static/js/base64.js b/lnbits/extensions/cashu/static/js/base64.js
new file mode 100644
index 000000000..b150882f7
--- /dev/null
+++ b/lnbits/extensions/cashu/static/js/base64.js
@@ -0,0 +1,37 @@
+function unescapeBase64Url(str) {
+ return (str + '==='.slice((str.length + 3) % 4))
+ .replace(/-/g, '+')
+ .replace(/_/g, '/')
+}
+
+function escapeBase64Url(str) {
+ return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
+}
+
+const uint8ToBase64 = (function (exports) {
+ 'use strict'
+
+ var fromCharCode = String.fromCharCode
+ var encode = function encode(uint8array) {
+ var output = []
+
+ for (var i = 0, length = uint8array.length; i < length; i++) {
+ output.push(fromCharCode(uint8array[i]))
+ }
+
+ return btoa(output.join(''))
+ }
+
+ var asCharCode = function asCharCode(c) {
+ return c.charCodeAt(0)
+ }
+
+ var decode = function decode(chars) {
+ return Uint8Array.from(atob(chars), asCharCode)
+ }
+
+ exports.decode = decode
+ exports.encode = encode
+
+ return exports
+})({})
diff --git a/lnbits/extensions/cashu/static/js/dhke.js b/lnbits/extensions/cashu/static/js/dhke.js
new file mode 100644
index 000000000..41c2fb46c
--- /dev/null
+++ b/lnbits/extensions/cashu/static/js/dhke.js
@@ -0,0 +1,39 @@
+async function hashToCurve(secretMessage) {
+ console.log(
+ '### secretMessage',
+ nobleSecp256k1.utils.bytesToHex(secretMessage)
+ )
+ let point
+ while (!point) {
+ const hash = await nobleSecp256k1.utils.sha256(secretMessage)
+ const hashHex = nobleSecp256k1.utils.bytesToHex(hash)
+ const pointX = '02' + hashHex
+ console.log('### pointX', pointX)
+ try {
+ point = nobleSecp256k1.Point.fromHex(pointX)
+ console.log('### point', point.toHex())
+ } catch (error) {
+ secretMessage = await nobleSecp256k1.utils.sha256(secretMessage)
+ }
+ }
+ return point
+}
+
+async function step1Alice(secretMessage) {
+ // todo: document & validate `secretMessage` format
+ secretMessage = uint8ToBase64.encode(secretMessage)
+ secretMessage = new TextEncoder().encode(secretMessage)
+ const Y = await hashToCurve(secretMessage)
+ const rpk = nobleSecp256k1.utils.randomPrivateKey()
+ const r = bytesToNumber(rpk)
+ const P = nobleSecp256k1.Point.fromPrivateKey(r)
+ const B_ = Y.add(P)
+ return {B_: B_.toHex(true), r: nobleSecp256k1.utils.bytesToHex(rpk)}
+}
+
+function step3Alice(C_, r, A) {
+ // const rInt = BigInt(r)
+ const rInt = bytesToNumber(r)
+ const C = C_.subtract(A.multiply(rInt))
+ return C
+}
diff --git a/lnbits/extensions/cashu/static/js/noble-secp256k1.js b/lnbits/extensions/cashu/static/js/noble-secp256k1.js
new file mode 100644
index 000000000..6a6bd4417
--- /dev/null
+++ b/lnbits/extensions/cashu/static/js/noble-secp256k1.js
@@ -0,0 +1,1178 @@
+;(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
new file mode 100644
index 000000000..cf852b58b
--- /dev/null
+++ b/lnbits/extensions/cashu/static/js/utils.js
@@ -0,0 +1,23 @@
+function splitAmount(value) {
+ const chunks = []
+ for (let i = 0; i < 32; i++) {
+ const mask = 1 << i
+ if ((value & mask) !== 0) chunks.push(Math.pow(2, i))
+ }
+ return chunks
+}
+
+function bytesToNumber(bytes) {
+ return hexToNumber(nobleSecp256k1.utils.bytesToHex(bytes))
+}
+
+function bigIntStringify(key, value) {
+ return typeof value === 'bigint' ? value.toString() : value
+}
+
+function hexToNumber(hex) {
+ if (typeof hex !== 'string') {
+ throw new TypeError('hexToNumber: expected string, got ' + typeof hex)
+ }
+ return BigInt(`0x${hex}`)
+}
diff --git a/lnbits/extensions/cashu/tasks.py b/lnbits/extensions/cashu/tasks.py
new file mode 100644
index 000000000..9de17a1cf
--- /dev/null
+++ b/lnbits/extensions/cashu/tasks.py
@@ -0,0 +1,33 @@
+import asyncio
+import json
+
+from cashu.core.migrations import migrate_databases
+from cashu.mint import migrations
+
+from lnbits.core.models import Payment
+from lnbits.tasks import register_invoice_listener
+
+from . import db, ledger
+from .crud import get_cashu
+
+
+async def startup_cashu_mint():
+ await migrate_databases(db, migrations)
+ await ledger.load_used_proofs()
+ await ledger.init_keysets(autosave=False)
+ pass
+
+
+async def wait_for_paid_invoices():
+ invoice_queue = asyncio.Queue()
+ register_invoice_listener(invoice_queue)
+
+ while True:
+ payment = await invoice_queue.get()
+ await on_invoice_paid(payment)
+
+
+async def on_invoice_paid(payment: Payment) -> None:
+ if payment.extra and not payment.extra.get("tag") == "cashu":
+ return
+ return
diff --git a/lnbits/extensions/cashu/templates/cashu/_api_docs.html b/lnbits/extensions/cashu/templates/cashu/_api_docs.html
new file mode 100644
index 000000000..f7bb19f60
--- /dev/null
+++ b/lnbits/extensions/cashu/templates/cashu/_api_docs.html
@@ -0,0 +1,80 @@
+ Create Cashu ecash mints and wallets.
+ 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. +
++ 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
+ We hold no responsibility for people losing access to funds. Use at
+ your own risk!
+
+ Description: {{
+ payInvoiceData.invoice.description }}
+ Expire date: {{ payInvoiceData.invoice.expireDate
+ }}
+ Hash: {{ payInvoiceData.invoice.hash }}
+
+ Authenticate with {{ payInvoiceData.lnurlauth.domain }}? +
++ For every website and for every LNbits wallet, a new keypair + will be deterministically generated so your identity can't be + tied to your LNbits wallet or linked across websites. No other + data will be shared with {{ payInvoiceData.lnurlauth.domain }}. +
++ Your public key for + {{ payInvoiceData.lnurlauth.domain }} is: +
+
+
+ {{ payInvoiceData.lnurlauth.pubkey }}
+
+
+ {{ payInvoiceData.lnurlpay.domain }} is requesting {{
+ payInvoiceData.lnurlpay.maxSendable | msatoshiFormat }}
+ {{LNBITS_DENOMINATION}}
+
+
+ and a {{payInvoiceData.lnurlpay.commentAllowed}}-char comment
+
+
+ {{ payInvoiceData.lnurlpay.targetUser ||
+ payInvoiceData.lnurlpay.domain }}
+ is requesting
+ between
+ {{ payInvoiceData.lnurlpay.minSendable | msatoshiFormat }}
+ and
+ {{ payInvoiceData.lnurlpay.maxSendable | msatoshiFormat }}
+ {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
+
+
+ and a {{payInvoiceData.lnurlpay.commentAllowed}}-char comment
+
+
+ {{ payInvoiceData.lnurlpay.description }} +
+
+
+ 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 service is in BETA! We hold no responsibility + for people losing access to funds. Use at your own risk! +
+