Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for user-selected mints #22

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/controller/claimController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export async function balanceController(req: Request, res: Response) {
const payload = {
proofs: proofs.map((p) => ({ secret: p.secret })),
};
const { spendable } = await new CashuMint(process.env.MINTURL!).check(
const { spendable } = await new CashuMint(allClaims[0].mint_url).check(
payload,
);
const spendableProofs: Proof[] = [];
Expand Down Expand Up @@ -50,7 +50,7 @@ export async function claimGetController(req: Request, res: Response) {
const spendableProofs = proofs.filter((_, i) => spendable[i]);
const token = getEncodedToken({
memo: "",
token: [{ mint: process.env.MINTURL!, proofs: spendableProofs }],
token: [{ mint: allClaims[0].mint_url, proofs: spendableProofs }],
});
if (spendableProofs.length === 0) {
return res.json({ error: true, message: "No proofs to claim" });
Expand Down
29 changes: 21 additions & 8 deletions src/controller/infoController.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { NextFunction, Request, Response } from "express";
import { User } from "../models";
import { Claim, User } from "../models";
import { sign, verify } from "jsonwebtoken";
import { lnProvider } from "..";
import { PaymentJWTPayload } from "../types";
import { usernameRegex } from "../constants/regex";
import { isValidMint } from "../utils/mint";

export async function getInfoController(req: Request, res: Response) {
const username = await User.getUserByPubkey(req.authData?.data.pubkey!);
Expand All @@ -19,16 +20,28 @@ export async function putMintInfoController(
res: Response,
next: NextFunction,
) {
const authData = req.authData!;
const { mintUrl } = req.body;
try {
new URL(mintUrl);
} catch {
if (!mintUrl) {
res.status(400);
return next(new Error("Invalid URL"));
return res.json({ error: true, message: "missing parameters" });
}
if (!mintUrl) {
const userObj = await User.getUserByPubkey(authData.data.pubkey);
const activeClaims = await Claim.getAllUserReadyClaims(
authData.data.pubkey,
userObj?.name,
);
if (activeClaims.length > 0) {
res.status(400);
return res.json({
error: true,
message: "can not change mint with balance. Please claim balance first",
});
}
const isValid = await isValidMint(mintUrl);
if (!isValid) {
res.status(400);
return next(new Error("Missing parameters"));
return res.json({ error: true, message: "invalid mint url" });
}
try {
await User.upsertMintByPubkey(req.authData?.data.pubkey!, mintUrl);
Expand All @@ -37,7 +50,7 @@ export async function putMintInfoController(
res.status(500);
return next(new Error("Failed to update DB"));
}
res.status(204).send();
res.json({ error: false, data: { mintUrl } });
}

export async function putUsernameInfoController(
Expand Down
19 changes: 17 additions & 2 deletions src/controller/lnurlController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { NextFunction, Request, Response } from "express";
import { Event, nip19 } from "nostr-tools";

import { parseInvoice } from ".././utils/lightning";
import { lnProvider, wallet } from "..";
import { lnProvider } from "..";
import { Transaction, User } from "../models";
import { createLnurlResponse } from "../utils/lnurl";
import { decodeAndValidateZapRequest } from "../utils/nostr";
import { createHash } from "crypto";
import { getWalletFromCache } from "../utils/mint";

export async function lnurlController(
req: Request<
Expand All @@ -21,11 +22,22 @@ export async function lnurlController(
const { amount, nostr } = req.query;
const userParam = req.params.user;
let username: string | User | undefined;
let mintUrl: string = process.env.MINTURL!;
let zapRequest: Event | undefined;
if (userParam.startsWith("npub")) {
try {
nip19.decode(userParam as `npub1${string}`);
const pk = nip19.decode(userParam as `npub1${string}`).data;
username = userParam;
try {
const userObj = await User.getUserByPubkey(pk);
if (userObj) {
mintUrl = userObj.mint_url;
}
} catch (e) {
console.log(e);
res.status(500);
return res.json({ error: true, message: "internal server error" });
}
} catch {
res.status(401);
return next(new Error("Invalid npub / public key"));
Expand All @@ -37,6 +49,7 @@ export async function lnurlController(
return next(new Error("User not found"));
}
username = userObj.name;
mintUrl = userObj.mint_url;
}
if (!amount) {
const lnurlResponse = createLnurlResponse(username);
Expand All @@ -60,6 +73,7 @@ export async function lnurlController(
}
}
try {
const wallet = getWalletFromCache(mintUrl);
const { pr: mintPr, hash: mintHash } = await wallet.requestMint(
Math.floor(parsedAmount / 1000),
);
Expand All @@ -79,6 +93,7 @@ export async function lnurlController(
username,
zapRequest,
parsedAmount / 1000,
mintUrl,
);
res.json({
pr: invoiceRes.paymentRequest,
Expand Down
6 changes: 4 additions & 2 deletions src/controller/paidController.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Request, Response } from "express";
import { lnProvider, nostrPool, wallet } from "..";
import { lnProvider, nostrPool } from "..";
import { Claim, Transaction } from "../models";
import { createZapReceipt, extractZapRequestData } from "../utils/nostr";
import { getWalletFromCache } from "../utils/mint";

const relays = [
"wss://relay.current.fyi",
Expand Down Expand Up @@ -59,13 +60,14 @@ export async function paidController(
console.error(e);
console.error("Could not pay mint invoice!");
}
const wallet = getWalletFromCache(internalTx.mint_url);
const { proofs } = await wallet.requestTokens(
transaction.settlementAmount,
internalTx.mint_hash,
);
await Claim.createClaims(
internalTx.user,
process.env.MINTURL!,
internalTx.mint_url,
proofs,
internalTx.id,
);
Expand Down
2 changes: 0 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import express, { Response } from "express";
import bodyparser from "body-parser";
import cors from "cors";
import { CashuMint, CashuWallet } from "@cashu/cashu-ts";

import routes from "./routes";
import { LightningHandler } from "./utils/lightning";
Expand All @@ -20,7 +19,6 @@ useWebSocketImplementation(require("ws"));

checkEnvVars(["LNURL_MAX_AMOUNT", "LNURL_MIN_AMOUNT", "MINTURL"]);

export const wallet = new CashuWallet(new CashuMint(process.env.MINTURL!));
export const lnProvider = new LightningHandler(new BlinkProvider());
export const nostrPool = new SimplePool();

Expand Down
11 changes: 9 additions & 2 deletions src/models/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class Transaction {
zap_request: Event | undefined;
fulfilled: boolean;
amount: number;
mint_url: string;

constructor(
id: number,
Expand All @@ -25,6 +26,7 @@ export class Transaction {
zapRequest: Event | undefined,
fulfilled: boolean,
amount: number,
mint_url: string,
) {
this.id = id;
this.mint_pr = mintPr;
Expand All @@ -36,6 +38,7 @@ export class Transaction {
this.zap_request = zapRequest;
this.fulfilled = fulfilled;
this.amount = amount;
this.mint_url = mint_url;
}

async recordFailedPayment() {
Expand Down Expand Up @@ -74,11 +77,12 @@ export class Transaction {
user: string,
zapRequest: Event | undefined,
amount: number,
mint_url: string,
) {
const res = await queryWrapper<Transaction>(
`INSERT INTO l_transactions
(mint_pr, mint_hash, server_pr, server_hash, "user", zap_request, fulfilled, amount)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at`,
(mint_pr, mint_hash, server_pr, server_hash, "user", zap_request, fulfilled, amount, mint_url)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, created_at`,
[
mint_pr,
mint_hash,
Expand All @@ -88,6 +92,7 @@ VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at`,
zapRequest,
false,
amount,
mint_url,
],
);
if (res.rowCount === 0) {
Expand All @@ -104,6 +109,7 @@ VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at`,
zapRequest,
false,
amount,
mint_url,
);
}

Expand All @@ -126,6 +132,7 @@ VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at`,
res.rows[0].zap_request,
res.rows[0].fulfilled,
res.rows[0].amount,
res.rows[0].mint_url,
);
}
}
52 changes: 52 additions & 0 deletions src/utils/__tests__/mint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { afterEach, describe, expect, test } from "vitest";
import {
cashuWalletMap,
clearWalletCache,
getWalletFromCache,
isValidMint,
} from "../mint";
import { CashuMint, CashuWallet } from "@cashu/cashu-ts";

describe("Verify Mint Url", () => {
test("Valid Mint URL", async () => {
const isValid = await isValidMint("https://mint.minibits.cash/Bitcoin");
expect(isValid).toBe(true);
});
test("Invalid URL", async () => {
const isValid = await isValidMint("Not a url");
expect(isValid).toBe(false);
});
test("Valid URL, but not a mint", async () => {
const isValid = await isValidMint("https://bitcoin.org");
expect(isValid).toBe(false);
});
});

describe("Mint cache", () => {
afterEach(() => {
clearWalletCache();
});
test("Get a stored mint from cache", () => {
const url = "https://mint.minibits.cash/Bitcoin";
const newMint = new CashuWallet(new CashuMint(url));
cashuWalletMap[url] = newMint;
const cachedWallet = getWalletFromCache(url);
expect(cachedWallet).toBe(newMint);
});
test("clearing wallet cache", () => {
const url = "https://mint.minibits.cash/Bitcoin";
const newMint = new CashuWallet(new CashuMint(url));
cashuWalletMap[url] = newMint;
expect(Object.keys(cashuWalletMap).length).toBe(1);
clearWalletCache();
expect(Object.keys(cashuWalletMap).length).toBe(0);
});
test("Adding 2 mints to cache", () => {
getWalletFromCache("https://mint.minibits.cash/Bitcoin");
expect(Object.keys(cashuWalletMap).length).toBe(1);
getWalletFromCache("https://mint.macadamia.cash");
expect(Object.keys(cashuWalletMap).length).toBe(2);
getWalletFromCache("https://mint.macadamia.cash");
expect(Object.keys(cashuWalletMap).length).toBe(2);
});
});
33 changes: 33 additions & 0 deletions src/utils/mint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { CashuMint, CashuWallet } from "@cashu/cashu-ts";

export const cashuWalletMap: { [mintUrl: string]: CashuWallet } = {};

export function getWalletFromCache(mintUrl: string) {
if (cashuWalletMap[mintUrl]) {
return cashuWalletMap[mintUrl];
}
const newWallet = new CashuWallet(new CashuMint(mintUrl));
cashuWalletMap[mintUrl] = newWallet;
return newWallet;
}

export function clearWalletCache() {
const keys = Object.keys(cashuWalletMap);
keys.forEach((k) => {
delete cashuWalletMap[k];
});
}

export async function isValidMint(url: string) {
try {
new URL(url);
const res = await fetch(`${url}/v1/info`);
const data = await res.json();
if (!data.pubkey) {
return false;
}
return true;
} catch (e) {
return false;
}
}