refactor: extract wallet system
This commit is contained in:
202
packages/wallet/src/AlbyWallet.ts
Normal file
202
packages/wallet/src/AlbyWallet.ts
Normal file
@ -0,0 +1,202 @@
|
||||
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;
|
||||
}
|
Reference in New Issue
Block a user