Files
snort/packages/wallet/src/AlbyWallet.ts

203 lines
4.8 KiB
TypeScript

import { base64 } from "@scure/base";
import { unixNow, unwrap } from "@snort/shared";
import {
InvoiceRequest,
LNWallet,
prToWalletInvoice,
WalletError,
WalletErrorCode,
WalletEvents,
WalletInfo,
WalletInvoice,
WalletInvoiceState,
} from ".";
import EventEmitter from "eventemitter3";
export interface OAuthToken {
access_token: string;
created_at: number;
expires_in: number;
refresh_token: string;
scope: string;
token_type: string;
clientId: string;
clientSecret: string;
}
export default class AlbyWallet extends EventEmitter<WalletEvents> implements LNWallet {
#token: OAuthToken;
constructor(token: OAuthToken) {
super();
this.#token = token;
}
isReady() {
return true;
}
canAutoLogin() {
return true;
}
canGetInvoices() {
return this.#token.scope.includes("invoices:read");
}
canGetBalance() {
return this.#token.scope.includes("balance:read");
}
canCreateInvoice() {
return true;
}
canPayInvoice() {
return true;
}
async getInfo() {
const me = await this.#fetch<GetUserResponse>("/user/me");
return { alias: me.lightning_address } as WalletInfo;
}
async login() {
return true;
}
close() {
return Promise.resolve(true);
}
async getBalance() {
await this.#refreshToken();
const bal = await this.#fetch<GetBalanceResponse>("/balance");
return bal.balance;
}
async createInvoice(req: InvoiceRequest) {
const inv = await this.#fetch<CreateInvoiceResponse>("/invoices", "POST", {
amount: req.amount,
memo: req.memo,
});
return unwrap(prToWalletInvoice(inv.payment_request));
}
async payInvoice(pr: string) {
const pay = await this.#fetch<PayInvoiceResponse>("/payments/bolt11", "POST", {
invoice: pr,
});
return {
...prToWalletInvoice(pay.payment_request),
fees: pay.fee,
preimage: pay.payment_preimage,
state: WalletInvoiceState.Paid,
direction: "out",
} as WalletInvoice;
}
async getInvoices() {
const invoices = await this.#fetch<Array<GetInvoiceResponse>>("/invoices?page=1&items=20");
return invoices.map(a => {
return {
...prToWalletInvoice(a.payment_request),
memo: a.comment,
preimage: a.preimage,
state: a.settled ? WalletInvoiceState.Paid : WalletInvoiceState.Pending,
direction: a.type === "incoming" ? "in" : "out",
} as WalletInvoice;
});
}
async #fetch<T>(path: string, method: "GET" | "POST" = "GET", body?: object) {
const req = await fetch(`https://api.getalby.com${path}`, {
method: method,
body: body ? JSON.stringify(body) : undefined,
headers: {
accept: "application/json",
authorization: `Bearer ${this.#token.access_token}`,
...(body ? { "content-type": "application/json" } : {}),
},
});
const json = await req.text();
if (req.ok) {
return JSON.parse(json) as T;
} else {
if (json.length > 0) {
throw new WalletError(WalletErrorCode.GeneralError, JSON.parse(json).message as string);
} else {
throw new WalletError(WalletErrorCode.GeneralError, `Error: ${json} (${req.status})`);
}
}
}
async #refreshToken() {
if (this.#token.created_at + this.#token.expires_in < unixNow()) {
const params = new URLSearchParams();
params.set("refresh_token", this.#token.refresh_token);
params.set("grant_type", "refresh_token");
const req = await fetch("https://api.getalby.com/oauth/token", {
method: "POST",
body: params,
headers: {
accept: "application/json",
"content-type": "application/x-www-form-urlencoded",
authorization: `Basic ${base64.encode(
new TextEncoder().encode(`${this.#token.clientId}:${this.#token.clientSecret}`),
)}`,
},
});
const json = await req.json();
if (req.ok) {
this.#token = {
...(json as OAuthToken),
created_at: unixNow(),
};
this.emit("change", JSON.stringify(this.#token));
}
}
}
}
interface GetBalanceResponse {
balance: number;
currency: string;
unit: string;
}
interface CreateInvoiceResponse {
expires_at: string;
payment_hash: string;
payment_request: string;
}
interface PayInvoiceResponse {
amount: number;
description?: string;
destination: string;
fee: number;
payment_hash: string;
payment_preimage: string;
payment_request: string;
}
interface GetInvoiceResponse {
amount: number;
comment?: string;
created_at: string;
creation_date: number;
currency: string;
expires_at: string;
preimage: string;
payment_request: string;
settled: boolean;
settled_at: string;
type: "incoming" | "outgoing";
}
interface GetUserResponse {
lightning_address: string;
}