Skip to content

Commit

Permalink
Merge pull request #2270 from getAlby/feat/webln-getbalance
Browse files Browse the repository at this point in the history
feat: webln.getBalance()
  • Loading branch information
bumi authored Aug 6, 2023
2 parents d9041ff + 9f61886 commit b08e130
Show file tree
Hide file tree
Showing 19 changed files with 343 additions and 15 deletions.
2 changes: 1 addition & 1 deletion src/app/screens/ConfirmRequestPermission/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const ConfirmRequestPermission: FC = () => {
<div className="mb-6 center dark:text-white">
<p className="font-semibold">{requestMethod}</p>
{description && (
<p className="text-sm text-gray-700">
<p className="text-sm text-gray-700 dark:text-neutral-500">
{tPermissions(
description as unknown as TemplateStringsArray
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import utils from "~/common/lib/utils";
import { getBalanceOrPrompt } from "~/extension/background-script/actions/webln";
import type Connector from "~/extension/background-script/connectors/connector.interface";
import db from "~/extension/background-script/db";
import type { MessageBalanceGet, OriginData } from "~/types";

// suppress console logs when running tests
console.error = jest.fn();

jest.mock("~/common/lib/utils", () => ({
openPrompt: jest.fn(() => Promise.resolve({ data: {} })),
}));

// overwrite "connector" in tests later
let connector: Connector;
const ConnectorClass = jest.fn().mockImplementation(() => {
return connector;
});

jest.mock("~/extension/background-script/state", () => ({
getState: () => ({
getConnector: jest.fn(() => Promise.resolve(new ConnectorClass())),
currentAccountId: "8b7f1dc6-ab87-4c6c-bca5-19fa8632731e",
}),
}));

const allowanceInDB = {
enabled: true,
host: "pro.kollider.xyz",
id: 1,
imageURL: "https://pro.kollider.xyz/favicon.ico",
lastPaymentAt: 0,
lnurlAuth: true,
name: "pro kollider",
remainingBudget: 500,
totalBudget: 500,
createdAt: "123456",
tag: "",
};

const permissionInDB = {
id: 1,
accountId: "8b7f1dc6-ab87-4c6c-bca5-19fa8632731e",
allowanceId: allowanceInDB.id,
createdAt: "1487076708000",
host: allowanceInDB.host,
method: "webln.getbalance",
blocked: false,
enabled: true,
};

const message: MessageBalanceGet = {
action: "getBalance",
origin: { host: allowanceInDB.host } as OriginData,
};

const requestResponse = { data: { balance: 123 } };
const fullConnector = {
// hacky fix because Jest doesn't return constructor name
constructor: {
name: "lnd",
},
getBalance: jest.fn(() => Promise.resolve(requestResponse)),
} as unknown as Connector;

// prepare DB with allowance
db.allowances.bulkAdd([allowanceInDB]);

// resets after every test
afterEach(async () => {
jest.clearAllMocks();
// ensure a clear permission table in DB
await db.permissions.clear();
// set a default connector if overwritten in a previous test
connector = fullConnector;
});

describe("throws error", () => {
test("if the host's allowance does not exist", async () => {
const messageWithUndefinedAllowanceHost = {
...message,
origin: {
...message.origin,
host: "some-host",
},
};

const result = await getBalanceOrPrompt(messageWithUndefinedAllowanceHost);

expect(console.error).toHaveBeenCalledTimes(1);
expect(result).toStrictEqual({
error: "Could not find an allowance for this host",
});
});

test("if the getBalance call itself throws an exception", async () => {
connector = {
...fullConnector,
getBalance: jest.fn(() => Promise.reject(new Error("Some API error"))),
};

const result = await getBalanceOrPrompt(message);

expect(console.error).toHaveBeenCalledTimes(1);
expect(result).toStrictEqual({
error: "Some API error",
});
});
});

describe("prompts the user first and then calls getBalance", () => {
test("if the permission for getBalance does not exist", async () => {
// prepare DB with other permission
const otherPermission = {
...permissionInDB,
method: "webln/sendpayment",
};
await db.permissions.bulkAdd([otherPermission]);

const result = await getBalanceOrPrompt(message);

expect(utils.openPrompt).toHaveBeenCalledWith({
args: {
requestPermission: {
method: "getBalance",
description: "webln.getbalance",
},
},
origin: message.origin,
action: "public/confirmRequestPermission",
});
expect(connector.getBalance).toHaveBeenCalled();
expect(result).toStrictEqual(requestResponse);
});

test("if the permission for the getBalance exists but is not enabled", async () => {
// prepare DB with disabled permission
const disabledPermission = {
...permissionInDB,
enabled: false,
};
await db.permissions.bulkAdd([disabledPermission]);

const result = await getBalanceOrPrompt(message);

expect(utils.openPrompt).toHaveBeenCalledWith({
args: {
requestPermission: {
method: "getBalance",
description: "webln.getbalance",
},
},
origin: message.origin,
action: "public/confirmRequestPermission",
});
expect(connector.getBalance).toHaveBeenCalled();
expect(result).toStrictEqual(requestResponse);
});
});

describe("directly calls getBalance of Connector", () => {
test("if permission for this website exists and is enabled", async () => {
// prepare DB with matching permission
await db.permissions.bulkAdd([permissionInDB]);

// console.log(db.permissions);
const result = await getBalanceOrPrompt(message);

expect(connector.getBalance).toHaveBeenCalled();
expect(utils.openPrompt).not.toHaveBeenCalled();
expect(result).toStrictEqual(requestResponse);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import utils from "~/common/lib/utils";
import db from "~/extension/background-script/db";
import state from "~/extension/background-script/state";
import { MessageDefault } from "~/types";

const getBalanceOrPrompt = async (message: MessageDefault) => {
if (!("host" in message.origin)) return;

const connector = await state.getState().getConnector();
const accountId = state.getState().currentAccountId;

try {
const allowance = await db.allowances
.where("host")
.equalsIgnoreCase(message.origin.host)
.first();

if (!allowance?.id) {
throw new Error("Could not find an allowance for this host");
}

if (!accountId) {
// type guard
throw new Error("Could not find a selected account");
}

const permission = await db.permissions
.where("host")
.equalsIgnoreCase(message.origin.host)
.and((p) => p.accountId === accountId && p.method === "webln.getbalance")
.first();

// request method is allowed to be called
if (permission && permission.enabled) {
const response = await connector.getBalance();
return response;
} else {
// throws an error if the user rejects
const promptResponse = await utils.openPrompt<{
enabled: boolean;
blocked: boolean;
}>({
args: {
requestPermission: {
method: "getBalance",
description: `webln.getbalance`,
},
},
origin: message.origin,
action: "public/confirmRequestPermission",
});

const response = await connector.getBalance();

// add permission to db only if user decided to always allow this request
if (promptResponse.data.enabled) {
const permissionIsAdded = await db.permissions.add({
createdAt: Date.now().toString(),
accountId: accountId,
allowanceId: allowance.id,
host: message.origin.host,
method: "webln.getbalance", // ensure to store the prefixed method string
enabled: promptResponse.data.enabled,
blocked: promptResponse.data.blocked,
});

!!permissionIsAdded && (await db.saveToStorage());
}

return response;
}
} catch (e) {
console.error(e);
return {
error:
e instanceof Error
? e.message
: `Something went wrong with during webln.getBalance()`,
};
}
};

export default getBalanceOrPrompt;
2 changes: 2 additions & 0 deletions src/extension/background-script/actions/webln/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import getBalanceOrPrompt from "./getBalanceOrPrompt";
import keysendOrPrompt from "./keysendOrPrompt";
import lnurl from "./lnurl";
import makeInvoiceOrPrompt from "./makeInvoiceOrPrompt";
Expand All @@ -10,4 +11,5 @@ export {
signMessageOrPrompt,
makeInvoiceOrPrompt,
lnurl,
getBalanceOrPrompt,
};
2 changes: 1 addition & 1 deletion src/extension/background-script/connectors/alby.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default class Alby implements Connector {
}

get supportedMethods() {
return ["getInfo", "keysend", "makeInvoice", "sendPayment"];
return ["getInfo", "keysend", "makeInvoice", "sendPayment", "getBalance"];
}

// not yet implemented
Expand Down
8 changes: 7 additions & 1 deletion src/extension/background-script/connectors/citadel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ class CitadelConnector implements Connector {
}

get supportedMethods() {
return ["makeInvoice", "sendPayment", "signMessage", "getInfo"];
return [
"makeInvoice",
"sendPayment",
"signMessage",
"getInfo",
"getBalance",
];
}

async getInfo(): Promise<GetInfoResponse> {
Expand Down
11 changes: 10 additions & 1 deletion src/extension/background-script/connectors/commando.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Connector, {
ConnectorInvoice,
ConnectPeerArgs,
ConnectPeerResponse,
flattenRequestMethods,
GetBalanceResponse,
GetInfoResponse,
GetInvoicesResponse,
Expand Down Expand Up @@ -142,7 +143,15 @@ export default class Commando implements Connector {
}

get supportedMethods() {
return supportedMethods;
return [
"getInfo",
"keysend",
"makeInvoice",
"sendPayment",
"signMessage",
"getBalance",
...flattenRequestMethods(supportedMethods),
];
}

async requestMethod(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,7 @@ export default interface Connector {
): Promise<{ data: unknown }>;
getOAuthToken?(): OAuthToken | undefined;
}

export function flattenRequestMethods(methods: string[]) {
return methods.map((method) => `request.${method}`);
}
2 changes: 1 addition & 1 deletion src/extension/background-script/connectors/eclair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class Eclair implements Connector {
"makeInvoice",
"sendPayment",
"signMessage",
"listInvoices",
"getBalance",
];
}

Expand Down
8 changes: 7 additions & 1 deletion src/extension/background-script/connectors/galoy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ class Galoy implements Connector {
}

get supportedMethods() {
return ["getInfo", "makeInvoice", "sendPayment", "signMessage"];
return [
"getInfo",
"makeInvoice",
"sendPayment",
"signMessage",
"getBalance",
];
}

async getInfo(): Promise<GetInfoResponse> {
Expand Down
8 changes: 7 additions & 1 deletion src/extension/background-script/connectors/lnbits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,13 @@ class LnBits implements Connector {
}

get supportedMethods() {
return ["getInfo", "makeInvoice", "sendPayment", "signMessage"];
return [
"getInfo",
"makeInvoice",
"sendPayment",
"signMessage",
"getBalance",
];
}

getInfo(): Promise<GetInfoResponse> {
Expand Down
11 changes: 10 additions & 1 deletion src/extension/background-script/connectors/lnc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Connector, {
CheckPaymentResponse,
ConnectorInvoice,
ConnectPeerResponse,
flattenRequestMethods,
GetBalanceResponse,
GetInfoResponse,
GetInvoicesResponse,
Expand Down Expand Up @@ -188,7 +189,15 @@ class Lnc implements Connector {
}

get supportedMethods() {
return Object.keys(methods);
return [
"getInfo",
"keysend",
"makeInvoice",
"sendPayment",
"signMessage",
"getBalance",
...flattenRequestMethods(Object.keys(methods)),
];
}

async requestMethod(
Expand Down
Loading

0 comments on commit b08e130

Please sign in to comment.