snort/packages/app/src/Wallet/LNDHub.ts

179 lines
4.4 KiB
TypeScript
Raw Normal View History

2023-10-31 15:40:12 +00:00
import { throwIfOffline } from "@snort/shared";
2023-01-31 18:56:31 +00:00
import {
InvoiceRequest,
LNWallet,
2023-03-02 15:23:53 +00:00
prToWalletInvoice,
2023-01-31 18:56:31 +00:00
Sats,
WalletError,
2023-03-02 15:23:53 +00:00
WalletErrorCode,
2023-01-31 18:56:31 +00:00
WalletInfo,
WalletInvoice,
WalletInvoiceState,
2023-11-17 11:52:10 +00:00
} from "@/Wallet";
2023-01-31 18:56:31 +00:00
const defaultHeaders = {
Accept: "application/json",
"Content-Type": "application/json",
};
export default class LNDHubWallet implements LNWallet {
2023-03-28 14:34:01 +00:00
type: "lndhub";
2023-03-02 21:56:25 +00:00
url: URL;
2023-01-31 18:56:31 +00:00
user: string;
password: string;
auth?: AuthResponse;
2023-03-28 14:34:01 +00:00
constructor(url: string) {
2023-02-13 15:29:25 +00:00
if (url.startsWith("lndhub://")) {
const regex = /^lndhub:\/\/([\S-]+):([\S-]+)@(.*)$/i;
const parsedUrl = url.match(regex);
if (!parsedUrl || parsedUrl.length !== 4) {
throw new Error("Invalid LNDHUB config");
}
2023-03-02 21:56:25 +00:00
this.url = new URL(parsedUrl[3]);
2023-02-13 15:29:25 +00:00
this.user = parsedUrl[1];
this.password = parsedUrl[2];
this.type = "lndhub";
} else {
throw new Error("Invalid config");
2023-01-31 18:56:31 +00:00
}
}
2023-03-02 15:23:53 +00:00
isReady(): boolean {
return this.auth !== undefined;
2023-02-24 10:25:14 +00:00
}
canAutoLogin(): boolean {
return true;
}
2023-03-02 15:23:53 +00:00
close(): Promise<boolean> {
return Promise.resolve(true);
2023-01-31 18:56:31 +00:00
}
async getInfo() {
return await this.getJson<WalletInfo>("GET", "/getinfo");
}
async login() {
const rsp = await this.getJson<AuthResponse>("POST", "/auth?type=auth", {
login: this.user,
password: this.password,
});
this.auth = rsp as AuthResponse;
return true;
}
2023-03-02 15:23:53 +00:00
async getBalance(): Promise<Sats> {
2023-02-13 15:29:25 +00:00
const rsp = await this.getJson<GetBalanceResponse>("GET", "/balance");
const bal = Math.floor((rsp as GetBalanceResponse).BTC.AvailableBalance);
2023-01-31 18:56:31 +00:00
return bal as Sats;
}
async createInvoice(req: InvoiceRequest) {
2023-02-13 15:29:25 +00:00
const rsp = await this.getJson<UserInvoicesResponse>("POST", "/addinvoice", {
amt: req.amount,
memo: req.memo,
});
const pRsp = rsp as UserInvoicesResponse;
return {
pr: pRsp.payment_request,
memo: req.memo,
amount: req.amount,
paymentHash: pRsp.payment_hash,
timestamp: pRsp.timestamp,
} as WalletInvoice;
2023-01-31 18:56:31 +00:00
}
async payInvoice(pr: string) {
2023-02-13 15:29:25 +00:00
const rsp = await this.getJson<PayInvoiceResponse>("POST", "/payinvoice", {
invoice: pr,
});
const pRsp = rsp as PayInvoiceResponse;
return {
pr: pr,
paymentHash: pRsp.payment_hash,
preimage: pRsp.payment_preimage,
state: pRsp.payment_error
? WalletInvoiceState.Failed
: pRsp.payment_preimage
2023-11-20 19:16:47 +00:00
? WalletInvoiceState.Paid
: WalletInvoiceState.Pending,
2023-02-13 15:29:25 +00:00
} as WalletInvoice;
2023-01-31 18:56:31 +00:00
}
2023-03-02 15:23:53 +00:00
async getInvoices(): Promise<WalletInvoice[]> {
2023-02-13 15:29:25 +00:00
const rsp = await this.getJson<UserInvoicesResponse[]>("GET", "/getuserinvoices");
return (rsp as UserInvoicesResponse[]).map(a => {
2023-03-02 15:23:53 +00:00
const decodedInvoice = prToWalletInvoice(a.payment_request);
if (!decodedInvoice) {
throw new WalletError(WalletErrorCode.InvalidInvoice, "Failed to parse invoice");
}
2023-01-31 18:56:31 +00:00
return {
2023-03-02 15:23:53 +00:00
...decodedInvoice,
state: a.ispaid ? WalletInvoiceState.Paid : decodedInvoice.state,
2023-01-31 18:56:31 +00:00
paymentHash: a.payment_hash,
2023-03-02 15:23:53 +00:00
memo: a.description,
2023-01-31 18:56:31 +00:00
} as WalletInvoice;
});
}
2023-03-02 15:23:53 +00:00
private async getJson<T>(method: "GET" | "POST", path: string, body?: unknown): Promise<T> {
2023-10-31 15:40:12 +00:00
throwIfOffline();
2023-03-28 14:34:01 +00:00
const auth = `Bearer ${this.auth?.access_token}`;
2023-03-02 21:56:25 +00:00
const url = `${this.url.pathname === "/" ? this.url.toString().slice(0, -1) : this.url.toString()}${path}`;
const rsp = await fetch(url, {
2023-01-31 18:56:31 +00:00
method: method,
body: body ? JSON.stringify(body) : undefined,
headers: {
...defaultHeaders,
2023-02-13 15:29:25 +00:00
Authorization: auth,
2023-01-31 18:56:31 +00:00
},
});
const json = await rsp.json();
if ("code" in json && !rsp.ok) {
2023-03-02 15:23:53 +00:00
const err = json as ErrorResponse;
throw new WalletError(err.code, err.message);
2023-01-31 18:56:31 +00:00
}
return json as T;
}
}
interface AuthResponse {
refresh_token?: string;
access_token?: string;
token_type?: string;
}
interface GetBalanceResponse {
BTC: {
AvailableBalance: number;
};
}
2023-02-13 15:29:25 +00:00
interface UserInvoicesResponse {
2023-01-31 18:56:31 +00:00
amt: number;
description: string;
ispaid: boolean;
type: string;
timestamp: number;
pay_req: string;
payment_hash: string;
payment_request: string;
2023-02-13 15:29:25 +00:00
r_hash: string;
}
interface PayInvoiceResponse {
payment_error?: string;
payment_hash: string;
payment_preimage: string;
payment_route?: { total_amt: number; total_fees: number };
2023-01-31 18:56:31 +00:00
}
2023-03-02 15:23:53 +00:00
interface ErrorResponse {
code: number;
message: string;
}