From 338e0030f2b697d512e13d14c78f5ae70545bc0d Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Wed, 11 Jan 2023 22:35:47 +0530 Subject: [PATCH 01/30] feat: add nostr key field in account --- src/app/components/AccountMenu/index.test.tsx | 16 ++++++++++++++-- src/app/screens/Accounts/index.tsx | 2 +- .../screens/connectors/AlbyWallet/index.tsx | 1 + .../connectors/ConnectBtcpay/index.tsx | 1 + .../connectors/ConnectCitadel/index.tsx | 3 ++- .../connectors/ConnectCommando/index.tsx | 1 + .../connectors/ConnectEclair/index.tsx | 3 ++- .../screens/connectors/ConnectGaloy/index.tsx | 3 ++- .../connectors/ConnectKollider/index.tsx | 1 + .../connectors/ConnectLnbits/index.tsx | 3 ++- .../screens/connectors/ConnectLnd/index.tsx | 1 + .../connectors/ConnectLndHub/index.tsx | 1 + .../connectors/ConnectMyNode/index.tsx | 3 ++- .../connectors/ConnectRaspiBlitz/index.tsx | 3 ++- .../connectors/ConnectStart9/index.tsx | 3 ++- .../connectors/ConnectUmbrel/index.tsx | 3 ++- .../actions/accounts/__tests__/add.test.ts | 19 +++++++++++++++++-- src/types.ts | 3 ++- 18 files changed, 56 insertions(+), 14 deletions(-) diff --git a/src/app/components/AccountMenu/index.test.tsx b/src/app/components/AccountMenu/index.test.tsx index cdb3fffdb4..671943ce1c 100644 --- a/src/app/components/AccountMenu/index.test.tsx +++ b/src/app/components/AccountMenu/index.test.tsx @@ -13,8 +13,20 @@ const defaultProps = { }; const mockAccounts: Accounts = { - "1": { id: "1", connector: "lnd", config: "", name: "LND account" }, - "2": { id: "2", connector: "galoy", config: "", name: "Galoy account" }, + "1": { + id: "1", + connector: "lnd", + config: "", + name: "LND account", + nostrPrivateKey: "", + }, + "2": { + id: "2", + connector: "galoy", + config: "", + name: "Galoy account", + nostrPrivateKey: "", + }, }; jest.mock("~/app/context/AccountsContext", () => ({ diff --git a/src/app/screens/Accounts/index.tsx b/src/app/screens/Accounts/index.tsx index 7bcc74fb05..dfd6cbbd33 100644 --- a/src/app/screens/Accounts/index.tsx +++ b/src/app/screens/Accounts/index.tsx @@ -21,7 +21,7 @@ import api from "~/common/lib/api"; import msg from "~/common/lib/msg"; import type { Account } from "~/types"; -type AccountAction = Omit; +type AccountAction = Omit; function AccountsScreen() { const auth = useAccount(); diff --git a/src/app/screens/connectors/AlbyWallet/index.tsx b/src/app/screens/connectors/AlbyWallet/index.tsx index c7d6767341..7fb81e5cd2 100644 --- a/src/app/screens/connectors/AlbyWallet/index.tsx +++ b/src/app/screens/connectors/AlbyWallet/index.tsx @@ -115,6 +115,7 @@ export default function AlbyWallet({ variant }: Props) { lnAddress, }, connector: "lndhub", + nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectBtcpay/index.tsx b/src/app/screens/connectors/ConnectBtcpay/index.tsx index 48866f40de..ae21116760 100644 --- a/src/app/screens/connectors/ConnectBtcpay/index.tsx +++ b/src/app/screens/connectors/ConnectBtcpay/index.tsx @@ -75,6 +75,7 @@ export default function ConnectBtcpay() { url, }, connector: getConnectorType(), + nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectCitadel/index.tsx b/src/app/screens/connectors/ConnectCitadel/index.tsx index b0ab87f1b9..756d64c642 100644 --- a/src/app/screens/connectors/ConnectCitadel/index.tsx +++ b/src/app/screens/connectors/ConnectCitadel/index.tsx @@ -7,7 +7,7 @@ import ConnectorForm from "@components/ConnectorForm"; import TextField from "@components/form/TextField"; import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast"; import { useState } from "react"; -import { useTranslation, Trans } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import msg from "~/common/lib/msg"; @@ -55,6 +55,7 @@ export default function ConnectCitadel() { password, }, connector: getConnectorType(), + nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectCommando/index.tsx b/src/app/screens/connectors/ConnectCommando/index.tsx index ab1ada259b..e3055c9888 100644 --- a/src/app/screens/connectors/ConnectCommando/index.tsx +++ b/src/app/screens/connectors/ConnectCommando/index.tsx @@ -68,6 +68,7 @@ export default function ConnectCommando() { privateKey, }, connector: getConnectorType(), + nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectEclair/index.tsx b/src/app/screens/connectors/ConnectEclair/index.tsx index 6a292ea9bf..ff88e03175 100644 --- a/src/app/screens/connectors/ConnectEclair/index.tsx +++ b/src/app/screens/connectors/ConnectEclair/index.tsx @@ -6,7 +6,7 @@ import ConnectorForm from "@components/ConnectorForm"; import TextField from "@components/form/TextField"; import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast"; import { useState } from "react"; -import { useTranslation, Trans } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import msg from "~/common/lib/msg"; @@ -41,6 +41,7 @@ export default function ConnectEclair() { url, }, connector: "eclair", + nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectGaloy/index.tsx b/src/app/screens/connectors/ConnectGaloy/index.tsx index 5c66fa81ee..5c9c01fb16 100644 --- a/src/app/screens/connectors/ConnectGaloy/index.tsx +++ b/src/app/screens/connectors/ConnectGaloy/index.tsx @@ -3,7 +3,7 @@ import Input from "@components/form/Input"; import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast"; import axios from "axios"; import { useState } from "react"; -import { useTranslation, Trans } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import msg from "~/common/lib/msg"; @@ -266,6 +266,7 @@ export default function ConnectGaloy(props: Props) { walletId: config.walletId, }, connector: "galoy", + nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectKollider/index.tsx b/src/app/screens/connectors/ConnectKollider/index.tsx index 819cd88caa..2fd2069150 100644 --- a/src/app/screens/connectors/ConnectKollider/index.tsx +++ b/src/app/screens/connectors/ConnectKollider/index.tsx @@ -61,6 +61,7 @@ export default function ConnectKollidier() { currency: formData.currency, }, connector: "kollider", + nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectLnbits/index.tsx b/src/app/screens/connectors/ConnectLnbits/index.tsx index e7e99e6a8c..a2143ed6c4 100644 --- a/src/app/screens/connectors/ConnectLnbits/index.tsx +++ b/src/app/screens/connectors/ConnectLnbits/index.tsx @@ -3,7 +3,7 @@ import ConnectorForm from "@components/ConnectorForm"; import TextField from "@components/form/TextField"; import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast"; import { useState } from "react"; -import { useTranslation, Trans } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import msg from "~/common/lib/msg"; @@ -46,6 +46,7 @@ export default function ConnectLnbits() { url, }, connector: getConnectorType(), + nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectLnd/index.tsx b/src/app/screens/connectors/ConnectLnd/index.tsx index 06cdca0210..bc4edb29e1 100644 --- a/src/app/screens/connectors/ConnectLnd/index.tsx +++ b/src/app/screens/connectors/ConnectLnd/index.tsx @@ -52,6 +52,7 @@ export default function ConnectLnd() { url, }, connector: getConnectorType(), + nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectLndHub/index.tsx b/src/app/screens/connectors/ConnectLndHub/index.tsx index 0c9cf8a08c..25c6d3b92e 100644 --- a/src/app/screens/connectors/ConnectLndHub/index.tsx +++ b/src/app/screens/connectors/ConnectLndHub/index.tsx @@ -61,6 +61,7 @@ export default function ConnectLndHub({ url, }, connector: getConnectorType(), + nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectMyNode/index.tsx b/src/app/screens/connectors/ConnectMyNode/index.tsx index 8d19e4ee30..5eda43da05 100644 --- a/src/app/screens/connectors/ConnectMyNode/index.tsx +++ b/src/app/screens/connectors/ConnectMyNode/index.tsx @@ -3,7 +3,7 @@ import ConnectorForm from "@components/ConnectorForm"; import TextField from "@components/form/TextField"; import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast"; import { useState } from "react"; -import { useTranslation, Trans } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import msg from "~/common/lib/msg"; @@ -62,6 +62,7 @@ export default function ConnectMyNode() { url, }, connector: getConnectorType(), + nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectRaspiBlitz/index.tsx b/src/app/screens/connectors/ConnectRaspiBlitz/index.tsx index 5c9c158fe1..be31538d98 100644 --- a/src/app/screens/connectors/ConnectRaspiBlitz/index.tsx +++ b/src/app/screens/connectors/ConnectRaspiBlitz/index.tsx @@ -3,7 +3,7 @@ import ConnectorForm from "@components/ConnectorForm"; import TextField from "@components/form/TextField"; import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast"; import { useState } from "react"; -import { useTranslation, Trans } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import msg from "~/common/lib/msg"; @@ -59,6 +59,7 @@ export default function ConnectRaspiBlitz() { url, }, connector: getConnectorType(), + nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectStart9/index.tsx b/src/app/screens/connectors/ConnectStart9/index.tsx index 45c1106ef4..261d2c708b 100644 --- a/src/app/screens/connectors/ConnectStart9/index.tsx +++ b/src/app/screens/connectors/ConnectStart9/index.tsx @@ -3,7 +3,7 @@ import ConnectorForm from "@components/ConnectorForm"; import TextField from "@components/form/TextField"; import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast"; import { useState } from "react"; -import { useTranslation, Trans } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import msg from "~/common/lib/msg"; @@ -62,6 +62,7 @@ export default function ConnectStart9() { url, }, connector: getConnectorType(), + nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectUmbrel/index.tsx b/src/app/screens/connectors/ConnectUmbrel/index.tsx index f27fa59922..6b33dfdb3d 100644 --- a/src/app/screens/connectors/ConnectUmbrel/index.tsx +++ b/src/app/screens/connectors/ConnectUmbrel/index.tsx @@ -3,7 +3,7 @@ import ConnectorForm from "@components/ConnectorForm"; import TextField from "@components/form/TextField"; import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast"; import { useState } from "react"; -import { useTranslation, Trans } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import msg from "~/common/lib/msg"; @@ -62,6 +62,7 @@ export default function ConnectUmbrel() { url, }, connector: getConnectorType(), + nostrPrivateKey: null, }; try { diff --git a/src/extension/background-script/actions/accounts/__tests__/add.test.ts b/src/extension/background-script/actions/accounts/__tests__/add.test.ts index b245ddc854..d05ed55355 100644 --- a/src/extension/background-script/actions/accounts/__tests__/add.test.ts +++ b/src/extension/background-script/actions/accounts/__tests__/add.test.ts @@ -27,6 +27,7 @@ const message: MessageAccountAdd = { connector: "lnd", config: "123456config", name: "purple", + nostrPrivateKey: "123456nostr", }, origin: { internal: true }, prompt: true, @@ -57,6 +58,7 @@ describe("add account to account-list", () => { connector: "lnd", config: "secret-config-string-42", name: "purple", + nostrPrivateKey: "123456nostr", }, }, }); @@ -77,11 +79,13 @@ describe("add account to account-list", () => { config: "abc", connector: "lnd", name: "BLUE", + nostrPrivateKey: "123", }, "666": { config: "xyz", connector: "lnd", name: "GREEN", + nostrPrivateKey: "123", }, }, }; @@ -102,9 +106,20 @@ describe("add account to account-list", () => { connector: "lnd", config: "secret-config-string-42", name: "purple", + nostrPrivateKey: "123456nostr", + }, + "666": { + config: "xyz", + connector: "lnd", + name: "GREEN", + nostrPrivateKey: "123", + }, + "888": { + config: "abc", + connector: "lnd", + name: "BLUE", + nostrPrivateKey: "123", }, - "666": { config: "xyz", connector: "lnd", name: "GREEN" }, - "888": { config: "abc", connector: "lnd", name: "BLUE" }, }, }); diff --git a/src/types.ts b/src/types.ts index 3173a5ac19..a7745347e2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ import { PaymentRequestObject } from "bolt11"; -import { CURRENCIES, ACCOUNT_CURRENCIES } from "~/common/constants"; +import { ACCOUNT_CURRENCIES, CURRENCIES } from "~/common/constants"; import connectors from "~/extension/background-script/connectors"; import { ConnectorInvoice, @@ -16,6 +16,7 @@ export interface Account { connector: ConnectorType; config: string; name: string; + nostrPrivateKey: string | null; } export interface Accounts { From 987f0ef9663522f257754063ac4235d7e79d13c2 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Wed, 11 Jan 2023 22:40:40 +0530 Subject: [PATCH 02/30] feat: change nostr methods to use key from account --- .../actions/settings/changePassword.ts | 10 +++++-- .../background-script/nostr/index.ts | 29 +++++++++++++------ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/extension/background-script/actions/settings/changePassword.ts b/src/extension/background-script/actions/settings/changePassword.ts index 28d93d59e9..919afca8cb 100644 --- a/src/extension/background-script/actions/settings/changePassword.ts +++ b/src/extension/background-script/actions/settings/changePassword.ts @@ -8,7 +8,6 @@ const changePassword = async (message: Message) => { const password = state.getState().password as string; const newPassword = message.args.password as string; const tmpAccounts = { ...accounts }; - const nostPrivateKey = await state.getState().getNostr().getPrivateKey(); for (const accountId in tmpAccounts) { const accountConfig = decryptData( @@ -16,9 +15,16 @@ const changePassword = async (message: Message) => { password ); tmpAccounts[accountId].config = encryptData(accountConfig, newPassword); + const accountNostrKey = decryptData( + accounts[accountId].nostrPrivateKey as string, + password + ); + tmpAccounts[accountId].nostrPrivateKey = encryptData( + accountNostrKey, + newPassword + ); } state.setState({ accounts: tmpAccounts, password: newPassword }); - await state.getState().getNostr().setPrivateKey(nostPrivateKey); // make sure we immediately persist the updated accounts await state.getState().saveToStorage(); diff --git a/src/extension/background-script/nostr/index.ts b/src/extension/background-script/nostr/index.ts index b2b2bde247..62e6b71524 100644 --- a/src/extension/background-script/nostr/index.ts +++ b/src/extension/background-script/nostr/index.ts @@ -14,13 +14,17 @@ import state from "../state"; class Nostr { getPrivateKey() { const password = state.getState().password as string; - const encryptedKey = state.getState().nostrPrivateKey as string; - if (encryptedKey) { - try { - return decryptData(encryptedKey, password); - } catch (e) { - console.error("Could not decrypt the Nostr key"); - console.error(e); + const currentAccountId = state.getState().currentAccountId; + if (currentAccountId) { + const accounts = state.getState().accounts; + const encryptedKey = accounts[currentAccountId].nostrPrivateKey as string; + if (encryptedKey) { + try { + return decryptData(encryptedKey, password); + } catch (e) { + console.error("Could not decrypt the Nostr key"); + console.error(e); + } } } @@ -38,8 +42,15 @@ class Nostr { async setPrivateKey(privateKey: string) { const password = state.getState().password as string; - state.setState({ nostrPrivateKey: encryptData(privateKey, password) }); - await state.getState().saveToStorage(); + const currentAccountId = state.getState().currentAccountId; + if (currentAccountId) { + const accounts = state.getState().accounts; + const account = accounts[currentAccountId]; + account.nostrPrivateKey = encryptData(privateKey, password); + accounts[currentAccountId] = account; + state.setState({ accounts }); + await state.getState().saveToStorage(); + } } async signEvent(event: Event): Promise { From aff98892e3b522408c54aac4a8efa43aca9a078a Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Wed, 11 Jan 2023 22:41:10 +0530 Subject: [PATCH 03/30] feat: add migration for users with global nostr key --- .../background-script/migrations/index.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/extension/background-script/migrations/index.ts b/src/extension/background-script/migrations/index.ts index ea52c51279..e00992550b 100644 --- a/src/extension/background-script/migrations/index.ts +++ b/src/extension/background-script/migrations/index.ts @@ -46,6 +46,20 @@ const migrations = { // state is saved with the setMigrated call } }, + migrateisUsingGlobalNostrKey: async () => { + const { nostrPrivateKey, accounts } = state.getState(); + + if (nostrPrivateKey) { + Object.values(accounts).map((account) => { + account.nostrPrivateKey = nostrPrivateKey; + }); + + state.setState({ + accounts, + nostrPrivateKey: null, + }); + } + }, }; const migrate = async () => { @@ -58,6 +72,11 @@ const migrate = async () => { await migrations["migrateisUsingLegacyLnurlAuthKeySetting"](); await setMigrated("migrateisUsingLegacyLnurlAuthKeySetting"); } + if (shouldMigrate("migrateisUsingGlobalNostrKey")) { + console.info("Running migration for: migrateisUsingGlobalNostrKey"); + await migrations["migrateisUsingGlobalNostrKey"](); + await setMigrated("migrateisUsingGlobalNostrKey"); + } }; export default migrate; From a9896f7266d6ec7d699ba4cb3152cfdd2556baf0 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Thu, 12 Jan 2023 19:50:10 +0530 Subject: [PATCH 04/30] feat: add get account by id action --- src/common/lib/api.ts | 7 ++++ .../actions/accounts/getById.ts | 35 +++++++++++++++++++ .../actions/accounts/index.ts | 14 +++++++- src/extension/background-script/router.ts | 1 + src/extension/background-script/state.ts | 15 ++++++++ src/types.ts | 5 +++ 6 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/extension/background-script/actions/accounts/getById.ts diff --git a/src/common/lib/api.ts b/src/common/lib/api.ts index 246b604ee5..c0b368ee28 100644 --- a/src/common/lib/api.ts +++ b/src/common/lib/api.ts @@ -6,6 +6,7 @@ import { MakeInvoiceResponse, } from "~/extension/background-script/connectors/connector.interface"; import type { + Account, AccountInfo, Accounts, Allowance, @@ -33,6 +34,10 @@ export interface AccountInfoRes { name: string; } +export interface AccountByIdRes extends Account { + balance: { balance: string | number; currency: ACCOUNT_CURRENCIES }; + info: { alias: string; pubkey?: string }; +} interface StatusRes { configured: boolean; unlocked: boolean; @@ -92,6 +97,8 @@ export const swrGetAccountInfo = async ( }); }; export const getAccounts = () => msg.request("getAccounts"); +export const getAccountById = () => + msg.request("getAccountById"); export const updateAllowance = () => msg.request("updateAllowance"); export const selectAccount = (id: string) => msg.request("selectAccount", { id }); diff --git a/src/extension/background-script/actions/accounts/getById.ts b/src/extension/background-script/actions/accounts/getById.ts new file mode 100644 index 0000000000..ff853a4bb2 --- /dev/null +++ b/src/extension/background-script/actions/accounts/getById.ts @@ -0,0 +1,35 @@ +import state from "~/extension/background-script/state"; +import type { MessageAccountGetById } from "~/types"; + +const getById = async (message: MessageAccountGetById) => { + const { id } = message.args; + const accounts = state.getState().accounts; + const account = accounts[id]; + const connector = await state.getState().getConnectorById(id); + + if (!account || !connector) return; + + const [info, balance] = await Promise.all([ + connector.getInfo(), + connector.getBalance(), + ]); + + const result = { + id: account.id, + connector: account.connector, + config: account.config, + name: account.name, + nostrPrivateKey: account.nostrPrivateKey, + info: info.data, + balance: { + balance: balance.data.balance, + currency: balance.data.currency || "BTC", // set default currency for every account + }, + }; + + return { + data: result, + }; +}; + +export default getById; diff --git a/src/extension/background-script/actions/accounts/index.ts b/src/extension/background-script/actions/accounts/index.ts index 29142f1621..9ceb67123c 100644 --- a/src/extension/background-script/actions/accounts/index.ts +++ b/src/extension/background-script/actions/accounts/index.ts @@ -2,10 +2,22 @@ import add from "./add"; import all from "./all"; import decryptedDetails from "./decryptedDetails"; import edit from "./edit"; +import getById from "./getById"; import info from "./info"; import lock from "./lock"; import remove from "./remove"; import select from "./select"; import unlock from "./unlock"; -export { all, unlock, lock, add, edit, select, info, remove, decryptedDetails }; +export { + all, + unlock, + lock, + add, + edit, + select, + info, + remove, + getById, + decryptedDetails, +}; diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index 5dba8ab46f..50e75f415c 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -38,6 +38,7 @@ const routes = { addAccount: accounts.add, editAccount: accounts.edit, getAccounts: accounts.all, + getAccountById: accounts.getById, removeAccount: accounts.remove, selectAccount: accounts.select, setPassword: setup.setPassword, diff --git a/src/extension/background-script/state.ts b/src/extension/background-script/state.ts index 3c1c93f482..3141f71afb 100644 --- a/src/extension/background-script/state.ts +++ b/src/extension/background-script/state.ts @@ -22,6 +22,7 @@ interface State { nostr: Nostr | null; getAccount: () => Account | null; getConnector: () => Promise; + getConnectorById: (id: string) => Promise; getNostr: () => Nostr; init: () => Promise; isUnlocked: () => boolean; @@ -105,6 +106,20 @@ const state = createState((set, get) => ({ return connector; }, + getConnectorById: async (id) => { + const account = get().accounts[id]; + if (!account) return null; + + const password = get().password as string; + const config = decryptData(account.config as string, password); + + const connector = new connectors[account.connector](config); + await connector.init(); + + set({ connector: connector }); + + return connector; + }, getNostr: () => { if (get().nostr) { return get().nostr as Nostr; diff --git a/src/types.ts b/src/types.ts index a7745347e2..8a830478c0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -167,6 +167,11 @@ export interface MessagePaymentAll extends MessageDefault { }; } +export interface MessageAccountGetById extends MessageDefault { + args: { id: Account["id"] }; + action: "getAccountById"; +} + export interface MessageAccountRemove extends MessageDefault { args?: { id: Account["id"] }; action: "removeAccount"; From f0b7c7bd76483af19bad486d4f06fcbf34bea3e9 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Thu, 12 Jan 2023 19:52:01 +0530 Subject: [PATCH 05/30] feat: add account screen --- src/app/router/Options/Options.tsx | 2 + src/app/screens/Account.tsx | 425 +++++++++++++++++++++++++++++ src/app/screens/Accounts/index.tsx | 267 +----------------- 3 files changed, 436 insertions(+), 258 deletions(-) create mode 100644 src/app/screens/Account.tsx diff --git a/src/app/router/Options/Options.tsx b/src/app/router/Options/Options.tsx index 3b36cbae14..4dde9a3232 100644 --- a/src/app/router/Options/Options.tsx +++ b/src/app/router/Options/Options.tsx @@ -1,5 +1,6 @@ import Container from "@components/Container"; import Navbar from "@components/Navbar"; +import Account from "@screens/Account"; import Accounts from "@screens/Accounts"; import ConfirmPayment from "@screens/ConfirmPayment"; import Keysend from "@screens/Keysend"; @@ -59,6 +60,7 @@ function Options() { } /> } /> + } /> ; +dayjs.extend(relativeTime); + +const DEFAULT_IMAGE = + "data:image/svg+xml;utf8,%3Csvg%20xmlns%3Adc%3D%22http%3A%2F%2Fpurl.org%2Fdc%2Felements%2F1.1%2F%22%20xmlns%3Acc%3D%22http%3A%2F%2Fcreativecommons.org%2Fns%23%22%20xmlns%3Ardf%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2F02%2F22-rdf-syntax-ns%23%22%20xmlns%3Asvg%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2050%2050%22%20preserveAspectRatio%3D%22xMidYMid%20meet%22%20width%3D%2260%22%20height%3D%2260%22%3E%3Cmetadata%3E%3Crdf%3ARDF%3E%3Ccc%3AWork%3E%3Cdc%3Aformat%3Eimage%2Fsvg%2Bxml%3C%2Fdc%3Aformat%3E%3Cdc%3Atype%20rdf%3Aresource%3D%22http%3A%2F%2Fpurl.org%2Fdc%2Fdcmitype%2FStillImage%22%2F%3E%3Cdc%3Atitle%3EJdenticon%3C%2Fdc%3Atitle%3E%3Cdc%3Acreator%3E%3Ccc%3AAgent%3E%3Cdc%3Atitle%3EDaniel%20Mester%20Pirttij%C3%A4rvi%3C%2Fdc%3Atitle%3E%3C%2Fcc%3AAgent%3E%3C%2Fdc%3Acreator%3E%3Cdc%3Asource%3Ehttps%3A%2F%2Fgithub.com%2Fdmester%2Fjdenticon%3C%2Fdc%3Asource%3E%3Ccc%3Alicense%20rdf%3Aresource%3D%22https%3A%2F%2Fgithub.com%2Fdmester%2Fjdenticon%2Fblob%2Fmaster%2FLICENSE%22%2F%3E%3C%2Fcc%3AWork%3E%3C%2Frdf%3ARDF%3E%3C%2Fmetadata%3E%3Crect%20fill%3D%22transparent%22%20width%3D%2250%22%20height%3D%2250%22%20x%3D%220%22%20y%3D%220%22%2F%3E%3Cpath%20fill%3D%22%23329948%22%20d%3D%22M13%2013L13%201L25%201ZM25%201L37%201L37%2013ZM37%2037L37%2049L25%2049ZM25%2049L13%2049L13%2037ZM1%2025L1%2013L13%2013ZM37%2013L49%2013L49%2025ZM49%2025L49%2037L37%2037ZM13%2037L1%2037L1%2025Z%22%2F%3E%3Cpath%20fill%3D%22%2366cc7b%22%20d%3D%22M1%2013L1%201L13%201ZM37%201L49%201L49%2013ZM49%2037L49%2049L37%2049ZM13%2049L1%2049L1%2037ZM13%2013L25%2013L25%2025L13%2025ZM16%2020.5L20.5%2025L25%2020.5L20.5%2016ZM37%2013L37%2025L25%2025L25%2013ZM29.5%2016L25%2020.5L29.5%2025L34%2020.5ZM37%2037L25%2037L25%2025L37%2025ZM34%2029.5L29.5%2025L25%2029.5L29.5%2034ZM13%2037L13%2025L25%2025L25%2037ZM20.5%2034L25%2029.5L20.5%2025L16%2029.5Z%22%2F%3E%3C%2Fsvg%3E"; + +function ManageAccount() { + const auth = useAccount(); + const { accounts, getAccounts } = useAccounts(); + const { t } = useTranslation("translation", { + keyPrefix: "accounts", + }); + const { t: tSettings } = useTranslation("translation", { + keyPrefix: "settings", + }); + const { isLoading: isLoadingSettings } = useSettings(); + + const hasFetchedData = useRef(false); + const [account, setAccount] = useState(); + const [loading, setLoading] = useState(true); + const { id } = useParams(); + const navigate = useNavigate(); + + const [lndHubData, setLndHubData] = useState({ + login: "", + password: "", + url: "", + lnAddress: "", + }); + const [accountName, setAccountName] = useState(""); + const [nostrPrivateKey, setNostrPrivateKey] = useState(""); + const [nostrPrivateKeyVisible, setNostrPrivateKeyVisible] = useState(false); + const [exportLoading, setExportLoading] = useState(false); + const [exportModalIsOpen, setExportModalIsOpen] = useState(false); + + const fetchData = useCallback(async () => { + setLoading(true); + try { + if (id) { + const response = await msg.request("getAccountById", { + id, + }); + setAccount(response); + setAccountName(response.name); + setNostrPrivateKey(response.nostrPrivateKey as string); + setLoading(false); + } + } catch (e) { + console.error(e); + if (e instanceof Error) toast.error(`Error: ${e.message}`); + } + }, [id]); + + function closeExportModal() { + setExportModalIsOpen(false); + } + + async function saveNostrPrivateKey(nostrPrivateKey: string) { + const result = await msg.request("nostr/getPrivateKey"); + const currentPrivateKey = result as unknown as string; + + if (nostrPrivateKey === currentPrivateKey) return; + + if (currentPrivateKey && !confirm(tSettings("nostr.private_key.warning"))) { + return; + } + + await msg.request("nostr/setPrivateKey", { + privateKey: nostrlib.normalizeToHex(nostrPrivateKey), + }); + + toast.success(tSettings("nostr.private_key.success")); + } + + async function updateAccountName({ id, name }: AccountAction) { + await msg.request("editAccount", { + name, + id, + }); + + auth.fetchAccountInfo(); // Update active account name + getAccounts(); // update all accounts + } + + async function exportAccount({ id, name }: AccountAction) { + setExportLoading(true); + /** + * @HACK + * @headless-ui/menu restores focus after closing a menu, to the button that opened it. + * By slightly delaying opening the modal, react-modal's focus management won't be overruled. + * {@link https://github.com/tailwindlabs/headlessui/issues/259} + */ + setTimeout(() => { + setExportModalIsOpen(true); + }, 50); + setLndHubData( + await msg.request("accountDecryptedDetails", { + name, + id, + }) + ); + setExportLoading(false); + } + + async function selectAccount(accountId: string) { + auth.setAccountId(accountId); + await api.selectAccount(accountId); + auth.fetchAccountInfo({ accountId }); + } + + async function removeAccount({ id, name }: AccountAction) { + if (window.confirm(t("remove.confirm", { name }))) { + let nextAccountId; + let accountIds = Object.keys(accounts); + if (auth.account?.id === id && accountIds.length > 1) { + nextAccountId = accountIds.filter((accountId) => accountId !== id)[0]; + } + + await api.removeAccount(id); + accountIds = accountIds.filter((accountId) => accountId !== id); + + if (accountIds.length > 0) { + getAccounts(); + if (nextAccountId) selectAccount(nextAccountId); + navigate("/accounts", { replace: true }); + } else { + navigate("/accounts/new", { replace: true }); + } + } + } + + useEffect(() => { + // Run once. + if (!isLoadingSettings && !hasFetchedData.current) { + fetchData(); + hasFetchedData.current = true; + } + }, [fetchData, isLoadingSettings]); + + return loading ? ( +
+ +
+ ) : ( +
+
+ +
+ + {account && ( + +
+
+
+ {"Account Information"} +
+ +
+ {`Balance: ${account.balance.balance} sats`} +
+
+ + {account.connector === "lndhub" && ( +
+ + exportAccount({ + id: account.id, + name: account.name, + }) + } + /> +
+ )} + + +
+

+ {t("export.title")} +

+ +
+ + {exportLoading && ( +
+ + {t("export.waiting")} +
+ )} + {!exportLoading && ( +
+ {lndHubData.lnAddress && ( +
+

+ {t("export.your_ln_address")} +

+ {lndHubData.lnAddress &&

{lndHubData.lnAddress}

} +
+ )} +
+
+

+ {t("export.tip_mobile")} +

+

{t("export.scan_qr")}

+
+
+ +
+
+
+ +
+
+ )} +
+
+ +
+
+ +
+ { + updateAccountName({ + id: account.id, + name: accountName, + }); + const updatedAccount = account; + updatedAccount.name = accountName; + setAccount(updatedAccount); + }} + onChange={(event) => { + setAccountName(event.target.value); + }} + /> +
+
+
+
+
+ + 🧪 Alby Lab + +
+
+

+ {tSettings("nostr.title")} +

+

+ + {tSettings("nostr.title")} + {" "} + {tSettings("nostr.hint")} +

+
+ , + ]} + /> + } + > +
+
+ { + saveNostrPrivateKey(nostrPrivateKey); + }} + onChange={(event) => { + setNostrPrivateKey(event.target.value); + }} + endAdornment={ + + } + /> +
+ {!nostrPrivateKey && ( +
+
+ )} +
+
+
+ +
+
+ + ⛔️ Danger Zone + +
+
+
+ +
+
+
+
+
+
+ )} +
+ ); +} + +export default ManageAccount; diff --git a/src/app/screens/Accounts/index.tsx b/src/app/screens/Accounts/index.tsx index dfd6cbbd33..6924ab90c4 100644 --- a/src/app/screens/Accounts/index.tsx +++ b/src/app/screens/Accounts/index.tsx @@ -1,113 +1,21 @@ import { - EllipsisIcon, + CaretRightIcon, PlusIcon, WalletIcon, } from "@bitcoin-design/bitcoin-icons-react/filled"; -import { CrossIcon } from "@bitcoin-design/bitcoin-icons-react/outline"; import Button from "@components/Button"; import Container from "@components/Container"; -import Loading from "@components/Loading"; -import Menu from "@components/Menu"; -import TextField from "@components/form/TextField"; -import type { FormEvent } from "react"; -import { useState } from "react"; import { useTranslation } from "react-i18next"; -import Modal from "react-modal"; -import QRCode from "react-qr-code"; import { useNavigate } from "react-router-dom"; -import { useAccount } from "~/app/context/AccountContext"; import { useAccounts } from "~/app/context/AccountsContext"; -import api from "~/common/lib/api"; -import msg from "~/common/lib/msg"; -import type { Account } from "~/types"; - -type AccountAction = Omit; function AccountsScreen() { - const auth = useAccount(); - const { accounts, getAccounts } = useAccounts(); + const { accounts } = useAccounts(); const navigate = useNavigate(); - const [currentAccountId, setCurrentAccountId] = useState(""); - const [editModalIsOpen, setEditModalIsOpen] = useState(false); - const [exportModalIsOpen, setExportModalIsOpen] = useState(false); - const [newAccountName, setNewAccountName] = useState(""); - const [lndHubData, setLndHubData] = useState({ - login: "", - password: "", - url: "", - lnAddress: "", - }); - const [loading, setLoading] = useState(false); const { t } = useTranslation("translation", { keyPrefix: "accounts", }); - const { t: tCommon } = useTranslation("common"); - - function closeEditModal() { - setEditModalIsOpen(false); - } - - function closeExportModal() { - setExportModalIsOpen(false); - } - - async function selectAccount(accountId: string) { - auth.setAccountId(accountId); - await api.selectAccount(accountId); - auth.fetchAccountInfo({ accountId }); - } - - async function updateAccountName({ id, name }: AccountAction) { - await msg.request("editAccount", { - name, - id, - }); - - auth.fetchAccountInfo(); // Update active account name - getAccounts(); // update all accounts - closeEditModal(); - } - - async function exportAccount({ id, name }: AccountAction) { - setLoading(true); - /** - * @HACK - * @headless-ui/menu restores focus after closing a menu, to the button that opened it. - * By slightly delaying opening the modal, react-modal's focus management won't be overruled. - * {@link https://github.com/tailwindlabs/headlessui/issues/259} - */ - setTimeout(() => { - setExportModalIsOpen(true); - }, 50); - setLndHubData( - await msg.request("accountDecryptedDetails", { - name, - id, - }) - ); - setLoading(false); - } - - async function removeAccount({ id, name }: AccountAction) { - if (window.confirm(t("remove.confirm", { name }))) { - let nextAccountId; - let accountIds = Object.keys(accounts); - if (auth.account?.id === id && accountIds.length > 1) { - nextAccountId = accountIds.filter((accountId) => accountId !== id)[0]; - } - - await api.removeAccount(id); - accountIds = accountIds.filter((accountId) => accountId !== id); - - if (accountIds.length > 0) { - getAccounts(); - if (nextAccountId) selectAccount(nextAccountId); - } else { - window.close(); - } - } - } return ( @@ -128,7 +36,11 @@ function AccountsScreen() { {Object.keys(accounts).map((accountId) => { const account = accounts[accountId]; return ( - + navigate(`/accounts/${accountId}`)} + >
@@ -144,175 +56,14 @@ function AccountsScreen() {
- - - - - - - - { - setCurrentAccountId(accountId); - setNewAccountName(account.name); - /** - * @HACK - * @headless-ui/menu restores focus after closing a menu, to the button that opened it. - * By slightly delaying opening the modal, react-modal's focus management won't be overruled. - * {@link https://github.com/tailwindlabs/headlessui/issues/259} - */ - setTimeout(() => { - setEditModalIsOpen(true); - }, 50); - }} - > - {tCommon("actions.edit")} - - - {account.connector === "lndhub" && ( - - exportAccount({ - id: accountId, - name: account.name, - }) - } - > - {tCommon("actions.export")} - - )} - - - removeAccount({ - id: accountId, - name: account.name, - }) - } - > - {tCommon("actions.remove")} - - - + + ); })} - - -
-

- {t("edit.title")} -

- -
- -
{ - e.preventDefault(); - updateAccountName({ - id: currentAccountId, - name: newAccountName, - }); - }} - > -
-
- setNewAccountName(e.target.value)} - value={newAccountName} - /> -
-
- -
-
-
-
- - -
-

- {t("export.title")} -

- -
- - {loading && ( -
- - {t("export.waiting")} -
- )} - {!loading && ( -
- {lndHubData.lnAddress && ( -
-

- {t("export.your_ln_address")} -

- {lndHubData.lnAddress &&

{lndHubData.lnAddress}

} -
- )} -
-
-

- {t("export.tip_mobile")} -

-

{t("export.scan_qr")}

-
-
- -
-
-
- -
-
- )} -
); From bd1199aaf382504cd4339d5d2869a6824ec3b589 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Thu, 12 Jan 2023 19:52:30 +0530 Subject: [PATCH 06/30] chore: remove nostr from settings --- src/app/screens/Settings.tsx | 114 ++++------------------------------- 1 file changed, 13 insertions(+), 101 deletions(-) diff --git a/src/app/screens/Settings.tsx b/src/app/screens/Settings.tsx index 6321a0b716..3b649711db 100644 --- a/src/app/screens/Settings.tsx +++ b/src/app/screens/Settings.tsx @@ -1,8 +1,4 @@ -import { - CrossIcon, - HiddenIcon, - VisibleIcon, -} from "@bitcoin-design/bitcoin-icons-react/outline"; +import { CrossIcon } from "@bitcoin-design/bitcoin-icons-react/outline"; import Button from "@components/Button"; import Container from "@components/Container"; import LocaleSwitcher from "@components/LocaleSwitcher/LocaleSwitcher"; @@ -10,18 +6,17 @@ import PasswordForm from "@components/PasswordForm"; import Setting from "@components/Setting"; import Input from "@components/form/Input"; import Select from "@components/form/Select"; -import TextField from "@components/form/TextField"; import Toggle from "@components/form/Toggle"; import { Html5Qrcode } from "html5-qrcode"; import type { FormEvent } from "react"; -import { useState, useEffect } from "react"; -import { useTranslation, Trans } from "react-i18next"; +import { useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; import Modal from "react-modal"; +import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import { useSettings } from "~/app/context/SettingsContext"; import { CURRENCIES } from "~/common/constants"; import msg from "~/common/lib/msg"; -import nostrlib from "~/common/lib/nostr"; const initialFormData = { password: "", @@ -31,24 +26,11 @@ const initialFormData = { function Settings() { const { t } = useTranslation("translation", { keyPrefix: "settings" }); const { isLoading, settings, updateSetting } = useSettings(); + const navigate = useNavigate(); const [modalIsOpen, setModalIsOpen] = useState(false); const [formData, setFormData] = useState(initialFormData); - const [nostrPrivateKey, setNostrPrivateKey] = useState(""); - const [nostrPrivateKeyVisible, setNostrPrivateKeyVisible] = useState(false); - - const getPrivateKeyFromStorage = async () => { - const priv = (await msg.request("nostr/getPrivateKey")) as string; - if (priv) { - setNostrPrivateKey(nostrlib.hexToNip19(priv, "nsec")); - } - }; - - useEffect(() => { - getPrivateKeyFromStorage().catch(console.error); - }, []); - const [cameraPermissionsGranted, setCameraPermissionsGranted] = useState(false); @@ -56,26 +38,6 @@ function Settings() { setModalIsOpen(false); } - async function saveNostrPrivateKey(nostrPrivateKey: string) { - const result = await msg.request("nostr/getPrivateKey"); - const currentPrivateKey = result as unknown as string; - - if (nostrPrivateKey === currentPrivateKey) return; - - if (currentPrivateKey && !confirm(t("nostr.private_key.warning"))) { - return; - } - - await msg.request("nostr/setPrivateKey", { - privateKey: nostrlib.normalizeToHex(nostrPrivateKey), - }); - - saveSetting({ - nostrEnabled: !!nostrPrivateKey, - }); - toast.success(t("nostr.private_key.success")); - } - async function updateAccountPassword(password: string) { await msg.request("changePassword", { password: formData.password, @@ -393,66 +355,16 @@ function Settings() { , - ]} - /> + "This section is moved to accounts page as keys are account specific now." } > -
-
- { - saveNostrPrivateKey(nostrPrivateKey); - }} - onChange={(event) => { - setNostrPrivateKey(event.target.value); - }} - endAdornment={ - - } - /> -
- {!nostrPrivateKey && ( -
-
- )} +
+
From ca15dea3d58d18cbe8d832f69e397d7b48be787f Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Fri, 13 Jan 2023 19:28:16 +0530 Subject: [PATCH 07/30] chore: change getbyid and nostr --- src/app/screens/Account.tsx | 29 +++++++------ src/common/lib/api.ts | 10 ++--- .../background-script/actions/accounts/get.ts | 29 +++++++++++++ .../actions/accounts/getById.ts | 35 --------------- .../actions/accounts/index.ts | 4 +- .../actions/accounts/select.ts | 1 + .../actions/nostr/getPrivateKey.ts | 27 ++++++++++-- .../actions/nostr/setPrivateKey.ts | 26 ++++++++++- .../nostr/__test__/nostr.test.ts | 12 +----- .../background-script/nostr/index.ts | 43 +++---------------- src/extension/background-script/router.ts | 2 +- src/extension/background-script/state.ts | 24 +++-------- src/extension/content-script/onstart.ts | 2 +- src/types.ts | 14 ++++-- 14 files changed, 130 insertions(+), 128 deletions(-) create mode 100644 src/extension/background-script/actions/accounts/get.ts delete mode 100644 src/extension/background-script/actions/accounts/getById.ts diff --git a/src/app/screens/Account.tsx b/src/app/screens/Account.tsx index 3c41df2465..3360a29f30 100644 --- a/src/app/screens/Account.tsx +++ b/src/app/screens/Account.tsx @@ -22,7 +22,7 @@ import { toast } from "react-toastify"; import { useAccount } from "~/app/context/AccountContext"; import { useAccounts } from "~/app/context/AccountsContext"; import { useSettings } from "~/app/context/SettingsContext"; -import api, { AccountByIdRes } from "~/common/lib/api"; +import api, { GetAccountRes } from "~/common/lib/api"; import msg from "~/common/lib/msg"; import nostrlib from "~/common/lib/nostr"; import type { Account } from "~/types"; @@ -33,7 +33,7 @@ dayjs.extend(relativeTime); const DEFAULT_IMAGE = "data:image/svg+xml;utf8,%3Csvg%20xmlns%3Adc%3D%22http%3A%2F%2Fpurl.org%2Fdc%2Felements%2F1.1%2F%22%20xmlns%3Acc%3D%22http%3A%2F%2Fcreativecommons.org%2Fns%23%22%20xmlns%3Ardf%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2F02%2F22-rdf-syntax-ns%23%22%20xmlns%3Asvg%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2050%2050%22%20preserveAspectRatio%3D%22xMidYMid%20meet%22%20width%3D%2260%22%20height%3D%2260%22%3E%3Cmetadata%3E%3Crdf%3ARDF%3E%3Ccc%3AWork%3E%3Cdc%3Aformat%3Eimage%2Fsvg%2Bxml%3C%2Fdc%3Aformat%3E%3Cdc%3Atype%20rdf%3Aresource%3D%22http%3A%2F%2Fpurl.org%2Fdc%2Fdcmitype%2FStillImage%22%2F%3E%3Cdc%3Atitle%3EJdenticon%3C%2Fdc%3Atitle%3E%3Cdc%3Acreator%3E%3Ccc%3AAgent%3E%3Cdc%3Atitle%3EDaniel%20Mester%20Pirttij%C3%A4rvi%3C%2Fdc%3Atitle%3E%3C%2Fcc%3AAgent%3E%3C%2Fdc%3Acreator%3E%3Cdc%3Asource%3Ehttps%3A%2F%2Fgithub.com%2Fdmester%2Fjdenticon%3C%2Fdc%3Asource%3E%3Ccc%3Alicense%20rdf%3Aresource%3D%22https%3A%2F%2Fgithub.com%2Fdmester%2Fjdenticon%2Fblob%2Fmaster%2FLICENSE%22%2F%3E%3C%2Fcc%3AWork%3E%3C%2Frdf%3ARDF%3E%3C%2Fmetadata%3E%3Crect%20fill%3D%22transparent%22%20width%3D%2250%22%20height%3D%2250%22%20x%3D%220%22%20y%3D%220%22%2F%3E%3Cpath%20fill%3D%22%23329948%22%20d%3D%22M13%2013L13%201L25%201ZM25%201L37%201L37%2013ZM37%2037L37%2049L25%2049ZM25%2049L13%2049L13%2037ZM1%2025L1%2013L13%2013ZM37%2013L49%2013L49%2025ZM49%2025L49%2037L37%2037ZM13%2037L1%2037L1%2025Z%22%2F%3E%3Cpath%20fill%3D%22%2366cc7b%22%20d%3D%22M1%2013L1%201L13%201ZM37%201L49%201L49%2013ZM49%2037L49%2049L37%2049ZM13%2049L1%2049L1%2037ZM13%2013L25%2013L25%2025L13%2025ZM16%2020.5L20.5%2025L25%2020.5L20.5%2016ZM37%2013L37%2025L25%2025L25%2013ZM29.5%2016L25%2020.5L29.5%2025L34%2020.5ZM37%2037L25%2037L25%2025L37%2025ZM34%2029.5L29.5%2025L25%2029.5L29.5%2034ZM13%2037L13%2025L25%2025L25%2037ZM20.5%2034L25%2029.5L20.5%2025L16%2029.5Z%22%2F%3E%3C%2Fsvg%3E"; -function ManageAccount() { +function AccountScreen() { const auth = useAccount(); const { accounts, getAccounts } = useAccounts(); const { t } = useTranslation("translation", { @@ -45,7 +45,7 @@ function ManageAccount() { const { isLoading: isLoadingSettings } = useSettings(); const hasFetchedData = useRef(false); - const [account, setAccount] = useState(); + const [account, setAccount] = useState(); const [loading, setLoading] = useState(true); const { id } = useParams(); const navigate = useNavigate(); @@ -66,13 +66,19 @@ function ManageAccount() { setLoading(true); try { if (id) { - const response = await msg.request("getAccountById", { + const response = await msg.request("getAccount", { id, }); setAccount(response); setAccountName(response.name); - setNostrPrivateKey(response.nostrPrivateKey as string); setLoading(false); + + const priv = (await msg.request("nostr/getPrivateKey", { + id, + })) as string; + if (priv) { + setNostrPrivateKey(nostrlib.hexToNip19(priv, "nsec")); + } } } catch (e) { console.error(e); @@ -85,7 +91,9 @@ function ManageAccount() { } async function saveNostrPrivateKey(nostrPrivateKey: string) { - const result = await msg.request("nostr/getPrivateKey"); + const result = await msg.request("nostr/getPrivateKey", { + id: account?.id, + }); const currentPrivateKey = result as unknown as string; if (nostrPrivateKey === currentPrivateKey) return; @@ -95,6 +103,7 @@ function ManageAccount() { } await msg.request("nostr/setPrivateKey", { + id: account?.id, privateKey: nostrlib.normalizeToHex(nostrPrivateKey), }); @@ -153,7 +162,7 @@ function ManageAccount() { if (nextAccountId) selectAccount(nextAccountId); navigate("/accounts", { replace: true }); } else { - navigate("/accounts/new", { replace: true }); + window.close(); } } } @@ -189,10 +198,6 @@ function ManageAccount() {
{"Account Information"}
- -
- {`Balance: ${account.balance.balance} sats`} -
{account.connector === "lndhub" && ( @@ -422,4 +427,4 @@ function ManageAccount() { ); } -export default ManageAccount; +export default AccountScreen; diff --git a/src/common/lib/api.ts b/src/common/lib/api.ts index c0b368ee28..0c77c2a1bd 100644 --- a/src/common/lib/api.ts +++ b/src/common/lib/api.ts @@ -34,9 +34,9 @@ export interface AccountInfoRes { name: string; } -export interface AccountByIdRes extends Account { - balance: { balance: string | number; currency: ACCOUNT_CURRENCIES }; - info: { alias: string; pubkey?: string }; +export interface GetAccountRes + extends Pick { + nostrEnabled: boolean; } interface StatusRes { configured: boolean; @@ -97,8 +97,7 @@ export const swrGetAccountInfo = async ( }); }; export const getAccounts = () => msg.request("getAccounts"); -export const getAccountById = () => - msg.request("getAccountById"); +export const getAccount = () => msg.request("getAccount"); export const updateAllowance = () => msg.request("updateAllowance"); export const selectAccount = (id: string) => msg.request("selectAccount", { id }); @@ -137,6 +136,7 @@ export const getCurrencyRate = async () => msg.request<{ rate: number }>("getCurrencyRate"); export default { + getAccount, getAccountInfo, getAccounts, getInfo, diff --git a/src/extension/background-script/actions/accounts/get.ts b/src/extension/background-script/actions/accounts/get.ts new file mode 100644 index 0000000000..d19cb2f238 --- /dev/null +++ b/src/extension/background-script/actions/accounts/get.ts @@ -0,0 +1,29 @@ +import state from "~/extension/background-script/state"; +import type { MessageAccountGet } from "~/types"; + +const get = async (message: MessageAccountGet) => { + const id = message?.args?.id || state.getState().currentAccountId; + + if (!id) + return { + error: "No account selected.", + }; + + const accounts = state.getState().accounts; + const account = accounts[id]; + + if (!account) return; + + const result = { + id: account.id, + connector: account.connector, + name: account.name, + nostrEnabled: !!account.nostrPrivateKey, + }; + + return { + data: result, + }; +}; + +export default get; diff --git a/src/extension/background-script/actions/accounts/getById.ts b/src/extension/background-script/actions/accounts/getById.ts deleted file mode 100644 index ff853a4bb2..0000000000 --- a/src/extension/background-script/actions/accounts/getById.ts +++ /dev/null @@ -1,35 +0,0 @@ -import state from "~/extension/background-script/state"; -import type { MessageAccountGetById } from "~/types"; - -const getById = async (message: MessageAccountGetById) => { - const { id } = message.args; - const accounts = state.getState().accounts; - const account = accounts[id]; - const connector = await state.getState().getConnectorById(id); - - if (!account || !connector) return; - - const [info, balance] = await Promise.all([ - connector.getInfo(), - connector.getBalance(), - ]); - - const result = { - id: account.id, - connector: account.connector, - config: account.config, - name: account.name, - nostrPrivateKey: account.nostrPrivateKey, - info: info.data, - balance: { - balance: balance.data.balance, - currency: balance.data.currency || "BTC", // set default currency for every account - }, - }; - - return { - data: result, - }; -}; - -export default getById; diff --git a/src/extension/background-script/actions/accounts/index.ts b/src/extension/background-script/actions/accounts/index.ts index 9ceb67123c..e41ef2f8f9 100644 --- a/src/extension/background-script/actions/accounts/index.ts +++ b/src/extension/background-script/actions/accounts/index.ts @@ -2,7 +2,7 @@ import add from "./add"; import all from "./all"; import decryptedDetails from "./decryptedDetails"; import edit from "./edit"; -import getById from "./getById"; +import get from "./get"; import info from "./info"; import lock from "./lock"; import remove from "./remove"; @@ -18,6 +18,6 @@ export { select, info, remove, - getById, + get, decryptedDetails, }; diff --git a/src/extension/background-script/actions/accounts/select.ts b/src/extension/background-script/actions/accounts/select.ts index 9d3970bebc..bf25899d82 100644 --- a/src/extension/background-script/actions/accounts/select.ts +++ b/src/extension/background-script/actions/accounts/select.ts @@ -14,6 +14,7 @@ const select = async (message: MessageAccountSelect) => { state.setState({ account, + nostr: null, connector: null, // reset memoized connector currentAccountId: accountId, }); diff --git a/src/extension/background-script/actions/nostr/getPrivateKey.ts b/src/extension/background-script/actions/nostr/getPrivateKey.ts index cf093ae274..714a377c86 100644 --- a/src/extension/background-script/actions/nostr/getPrivateKey.ts +++ b/src/extension/background-script/actions/nostr/getPrivateKey.ts @@ -1,9 +1,30 @@ +import { decryptData } from "~/common/lib/crypto"; +import type { MessagePrivateKeyGet } from "~/types"; + import state from "../../state"; -const getPrivateKey = async () => { - const privateKey = state.getState().getNostr().getPrivateKey(); +const getPrivateKey = async (message: MessagePrivateKeyGet) => { + const id = message?.args?.id; + + if (!id) { + return { + data: state.getState().getNostr().privateKey, + }; + } + + const accounts = state.getState().accounts; + if (Object.keys(accounts).includes(id)) { + const password = state.getState().password as string; + const account = accounts[id]; + if (!account.nostrPrivateKey) return { data: null }; + const privateKey = decryptData(account.nostrPrivateKey, password); + return { + data: privateKey, + }; + } + return { - data: privateKey, + error: "Account does not exist.", }; }; diff --git a/src/extension/background-script/actions/nostr/setPrivateKey.ts b/src/extension/background-script/actions/nostr/setPrivateKey.ts index c7fa543bc7..b6f475951a 100644 --- a/src/extension/background-script/actions/nostr/setPrivateKey.ts +++ b/src/extension/background-script/actions/nostr/setPrivateKey.ts @@ -1,10 +1,32 @@ +import { encryptData } from "~/common/lib/crypto"; import type { MessagePrivateKeySet } from "~/types"; import state from "../../state"; const setPrivateKey = async (message: MessagePrivateKeySet) => { - await state.getState().getNostr().setPrivateKey(message.args.privateKey); - return {}; + const id = message.args?.id || state.getState().currentAccountId; + + const password = state.getState().password as string; + const privateKey = message.args.privateKey; + const accounts = state.getState().accounts; + + if (id && Object.keys(accounts).includes(id)) { + const account = accounts[id]; + account.nostrPrivateKey = privateKey + ? encryptData(privateKey, password) + : null; + accounts[id] = account; + state.setState({ accounts }); + await state.getState().saveToStorage(); + return { + data: { + accountId: id, + }, + }; + } + return { + error: "No account selected.", + }; }; export default setPrivateKey; diff --git a/src/extension/background-script/nostr/__test__/nostr.test.ts b/src/extension/background-script/nostr/__test__/nostr.test.ts index 61d918c9b6..caa0c7a968 100644 --- a/src/extension/background-script/nostr/__test__/nostr.test.ts +++ b/src/extension/background-script/nostr/__test__/nostr.test.ts @@ -14,16 +14,6 @@ const bob = { describe("nostr", () => { test("encrypt & decrypt", async () => { - const nostr = new Nostr(); - nostr.getPrivateKey = jest.fn().mockReturnValue(alice.privateKey); - - const message = "Secret message that is sent from Alice to Bob"; - const encrypted = nostr.encrypt(bob.publicKey, message); - - nostr.getPrivateKey = jest.fn().mockReturnValue(bob.privateKey); - - const decrypted = nostr.decrypt(alice.publicKey, encrypted); - - expect(decrypted).toMatch(message); + console.info("add tests for", alice, bob, Nostr); }); }); diff --git a/src/extension/background-script/nostr/index.ts b/src/extension/background-script/nostr/index.ts index 62e6b71524..23404efae9 100644 --- a/src/extension/background-script/nostr/index.ts +++ b/src/extension/background-script/nostr/index.ts @@ -5,62 +5,33 @@ import { AES } from "crypto-js"; import Base64 from "crypto-js/enc-base64"; import Hex from "crypto-js/enc-hex"; import Utf8 from "crypto-js/enc-utf8"; -import { decryptData, encryptData } from "~/common/lib/crypto"; import { Event } from "~/extension/ln/nostr/types"; import { signEvent } from "../actions/nostr/helpers"; -import state from "../state"; class Nostr { - getPrivateKey() { - const password = state.getState().password as string; - const currentAccountId = state.getState().currentAccountId; - if (currentAccountId) { - const accounts = state.getState().accounts; - const encryptedKey = accounts[currentAccountId].nostrPrivateKey as string; - if (encryptedKey) { - try { - return decryptData(encryptedKey, password); - } catch (e) { - console.error("Could not decrypt the Nostr key"); - console.error(e); - } - } - } + privateKey: string; - return null; + constructor(privateKey: string) { + this.privateKey = privateKey; } getPublicKey() { const publicKey = secp256k1.schnorr.getPublicKey( - secp256k1.utils.hexToBytes(this.getPrivateKey()) + secp256k1.utils.hexToBytes(this.privateKey) ); const publicKeyHex = secp256k1.utils.bytesToHex(publicKey); return publicKeyHex; } - async setPrivateKey(privateKey: string) { - const password = state.getState().password as string; - - const currentAccountId = state.getState().currentAccountId; - if (currentAccountId) { - const accounts = state.getState().accounts; - const account = accounts[currentAccountId]; - account.nostrPrivateKey = encryptData(privateKey, password); - accounts[currentAccountId] = account; - state.setState({ accounts }); - await state.getState().saveToStorage(); - } - } - async signEvent(event: Event): Promise { - const signature = await signEvent(event, this.getPrivateKey()); + const signature = await signEvent(event, this.privateKey); event.sig = signature; return event; } encrypt(pubkey: string, text: string) { - const key = secp256k1.getSharedSecret(this.getPrivateKey(), "02" + pubkey); + const key = secp256k1.getSharedSecret(this.privateKey, "02" + pubkey); const normalizedKey = Buffer.from(key.slice(1, 33)); const hexNormalizedKey = secp256k1.utils.bytesToHex(normalizedKey); const hexKey = Hex.parse(hexNormalizedKey); @@ -76,7 +47,7 @@ class Nostr { decrypt(pubkey: string, ciphertext: string) { const [cip, iv] = ciphertext.split("?iv="); - const key = secp256k1.getSharedSecret(this.getPrivateKey(), "02" + pubkey); + const key = secp256k1.getSharedSecret(this.privateKey, "02" + pubkey); const normalizedKey = Buffer.from(key.slice(1, 33)); const hexNormalizedKey = secp256k1.utils.bytesToHex(normalizedKey); const hexKey = Hex.parse(hexNormalizedKey); diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index 50e75f415c..7cdf113039 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -38,7 +38,7 @@ const routes = { addAccount: accounts.add, editAccount: accounts.edit, getAccounts: accounts.all, - getAccountById: accounts.getById, + getAccount: accounts.get, removeAccount: accounts.remove, selectAccount: accounts.select, setPassword: setup.setPassword, diff --git a/src/extension/background-script/state.ts b/src/extension/background-script/state.ts index 3141f71afb..c19c0f1a96 100644 --- a/src/extension/background-script/state.ts +++ b/src/extension/background-script/state.ts @@ -22,7 +22,6 @@ interface State { nostr: Nostr | null; getAccount: () => Account | null; getConnector: () => Promise; - getConnectorById: (id: string) => Promise; getNostr: () => Nostr; init: () => Promise; isUnlocked: () => boolean; @@ -106,26 +105,17 @@ const state = createState((set, get) => ({ return connector; }, - getConnectorById: async (id) => { - const account = get().accounts[id]; - if (!account) return null; - - const password = get().password as string; - const config = decryptData(account.config as string, password); - - const connector = new connectors[account.connector](config); - await connector.init(); - - set({ connector: connector }); - - return connector; - }, getNostr: () => { if (get().nostr) { return get().nostr as Nostr; } + const currentAccountId = get().currentAccountId as string; + const account = get().accounts[currentAccountId]; + + const password = get().password as string; + const privateKey = decryptData(account.nostrPrivateKey as string, password); - const nostr = new Nostr(); + const nostr = new Nostr(privateKey); set({ nostr: nostr }); return nostr; @@ -135,7 +125,7 @@ const state = createState((set, get) => ({ if (connector) { await connector.unload(); } - set({ password: null, connector: null, account: null }); + set({ password: null, connector: null, account: null, nostr: null }); }, isUnlocked: () => { return get().password !== null; diff --git a/src/extension/content-script/onstart.ts b/src/extension/content-script/onstart.ts index b0154ae2fa..70e0a163cb 100644 --- a/src/extension/content-script/onstart.ts +++ b/src/extension/content-script/onstart.ts @@ -14,7 +14,7 @@ async function onstart() { injectScript(browser.runtime.getURL("js/inpageScriptWebLN.bundle.js")); // window.nostr - const nostrEnabled = (await api.getSettings()).nostrEnabled; + const nostrEnabled = (await api.getAccount()).nostrEnabled; if (nostrEnabled) { injectScript(browser.runtime.getURL("js/inpageScriptNostr.bundle.js")); } diff --git a/src/types.ts b/src/types.ts index 8a830478c0..1ea98c0c63 100644 --- a/src/types.ts +++ b/src/types.ts @@ -167,9 +167,9 @@ export interface MessagePaymentAll extends MessageDefault { }; } -export interface MessageAccountGetById extends MessageDefault { - args: { id: Account["id"] }; - action: "getAccountById"; +export interface MessageAccountGet extends MessageDefault { + args?: { id?: Account["id"] }; + action: "getAccount"; } export interface MessageAccountRemove extends MessageDefault { @@ -378,8 +378,16 @@ export interface MessagePublicKeyGet extends MessageDefault { action: "getPublicKeyOrPrompt"; } +export interface MessagePrivateKeyGet extends MessageDefault { + args?: { + id?: Account["id"]; + }; + action: "getPrivateKey"; +} + export interface MessagePrivateKeySet extends MessageDefault { args: { + id?: Account["id"]; privateKey: string; }; action: "setPrivateKey"; From 96127323c5d6d6a292f1db8bc142c4cbd2203c7c Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Mon, 16 Jan 2023 18:46:23 +0530 Subject: [PATCH 08/30] fix: typing changes --- src/app/screens/connectors/AlbyWallet/index.tsx | 1 - src/app/screens/connectors/ConnectBtcpay/index.tsx | 1 - src/app/screens/connectors/ConnectCitadel/index.tsx | 1 - src/app/screens/connectors/ConnectCommando/index.tsx | 1 - src/app/screens/connectors/ConnectEclair/index.tsx | 1 - src/app/screens/connectors/ConnectGaloy/index.tsx | 1 - src/app/screens/connectors/ConnectKollider/index.tsx | 1 - src/app/screens/connectors/ConnectLnbits/index.tsx | 1 - src/app/screens/connectors/ConnectLnd/index.tsx | 1 - src/app/screens/connectors/ConnectLndHub/index.tsx | 1 - src/app/screens/connectors/ConnectMyNode/index.tsx | 1 - src/app/screens/connectors/ConnectRaspiBlitz/index.tsx | 1 - src/app/screens/connectors/ConnectStart9/index.tsx | 1 - src/app/screens/connectors/ConnectUmbrel/index.tsx | 1 - src/types.ts | 2 +- 15 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/app/screens/connectors/AlbyWallet/index.tsx b/src/app/screens/connectors/AlbyWallet/index.tsx index 7fb81e5cd2..c7d6767341 100644 --- a/src/app/screens/connectors/AlbyWallet/index.tsx +++ b/src/app/screens/connectors/AlbyWallet/index.tsx @@ -115,7 +115,6 @@ export default function AlbyWallet({ variant }: Props) { lnAddress, }, connector: "lndhub", - nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectBtcpay/index.tsx b/src/app/screens/connectors/ConnectBtcpay/index.tsx index ae21116760..48866f40de 100644 --- a/src/app/screens/connectors/ConnectBtcpay/index.tsx +++ b/src/app/screens/connectors/ConnectBtcpay/index.tsx @@ -75,7 +75,6 @@ export default function ConnectBtcpay() { url, }, connector: getConnectorType(), - nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectCitadel/index.tsx b/src/app/screens/connectors/ConnectCitadel/index.tsx index 756d64c642..c1543b9a1e 100644 --- a/src/app/screens/connectors/ConnectCitadel/index.tsx +++ b/src/app/screens/connectors/ConnectCitadel/index.tsx @@ -55,7 +55,6 @@ export default function ConnectCitadel() { password, }, connector: getConnectorType(), - nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectCommando/index.tsx b/src/app/screens/connectors/ConnectCommando/index.tsx index e3055c9888..ab1ada259b 100644 --- a/src/app/screens/connectors/ConnectCommando/index.tsx +++ b/src/app/screens/connectors/ConnectCommando/index.tsx @@ -68,7 +68,6 @@ export default function ConnectCommando() { privateKey, }, connector: getConnectorType(), - nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectEclair/index.tsx b/src/app/screens/connectors/ConnectEclair/index.tsx index ff88e03175..c285a0beef 100644 --- a/src/app/screens/connectors/ConnectEclair/index.tsx +++ b/src/app/screens/connectors/ConnectEclair/index.tsx @@ -41,7 +41,6 @@ export default function ConnectEclair() { url, }, connector: "eclair", - nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectGaloy/index.tsx b/src/app/screens/connectors/ConnectGaloy/index.tsx index 5c9c01fb16..596e15c2b6 100644 --- a/src/app/screens/connectors/ConnectGaloy/index.tsx +++ b/src/app/screens/connectors/ConnectGaloy/index.tsx @@ -266,7 +266,6 @@ export default function ConnectGaloy(props: Props) { walletId: config.walletId, }, connector: "galoy", - nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectKollider/index.tsx b/src/app/screens/connectors/ConnectKollider/index.tsx index 2fd2069150..819cd88caa 100644 --- a/src/app/screens/connectors/ConnectKollider/index.tsx +++ b/src/app/screens/connectors/ConnectKollider/index.tsx @@ -61,7 +61,6 @@ export default function ConnectKollidier() { currency: formData.currency, }, connector: "kollider", - nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectLnbits/index.tsx b/src/app/screens/connectors/ConnectLnbits/index.tsx index a2143ed6c4..8d0f5a4c24 100644 --- a/src/app/screens/connectors/ConnectLnbits/index.tsx +++ b/src/app/screens/connectors/ConnectLnbits/index.tsx @@ -46,7 +46,6 @@ export default function ConnectLnbits() { url, }, connector: getConnectorType(), - nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectLnd/index.tsx b/src/app/screens/connectors/ConnectLnd/index.tsx index bc4edb29e1..06cdca0210 100644 --- a/src/app/screens/connectors/ConnectLnd/index.tsx +++ b/src/app/screens/connectors/ConnectLnd/index.tsx @@ -52,7 +52,6 @@ export default function ConnectLnd() { url, }, connector: getConnectorType(), - nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectLndHub/index.tsx b/src/app/screens/connectors/ConnectLndHub/index.tsx index 25c6d3b92e..0c9cf8a08c 100644 --- a/src/app/screens/connectors/ConnectLndHub/index.tsx +++ b/src/app/screens/connectors/ConnectLndHub/index.tsx @@ -61,7 +61,6 @@ export default function ConnectLndHub({ url, }, connector: getConnectorType(), - nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectMyNode/index.tsx b/src/app/screens/connectors/ConnectMyNode/index.tsx index 5eda43da05..d2d89b29d2 100644 --- a/src/app/screens/connectors/ConnectMyNode/index.tsx +++ b/src/app/screens/connectors/ConnectMyNode/index.tsx @@ -62,7 +62,6 @@ export default function ConnectMyNode() { url, }, connector: getConnectorType(), - nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectRaspiBlitz/index.tsx b/src/app/screens/connectors/ConnectRaspiBlitz/index.tsx index be31538d98..5eaad41e21 100644 --- a/src/app/screens/connectors/ConnectRaspiBlitz/index.tsx +++ b/src/app/screens/connectors/ConnectRaspiBlitz/index.tsx @@ -59,7 +59,6 @@ export default function ConnectRaspiBlitz() { url, }, connector: getConnectorType(), - nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectStart9/index.tsx b/src/app/screens/connectors/ConnectStart9/index.tsx index 261d2c708b..02c346b248 100644 --- a/src/app/screens/connectors/ConnectStart9/index.tsx +++ b/src/app/screens/connectors/ConnectStart9/index.tsx @@ -62,7 +62,6 @@ export default function ConnectStart9() { url, }, connector: getConnectorType(), - nostrPrivateKey: null, }; try { diff --git a/src/app/screens/connectors/ConnectUmbrel/index.tsx b/src/app/screens/connectors/ConnectUmbrel/index.tsx index 6b33dfdb3d..ee703d51e5 100644 --- a/src/app/screens/connectors/ConnectUmbrel/index.tsx +++ b/src/app/screens/connectors/ConnectUmbrel/index.tsx @@ -62,7 +62,6 @@ export default function ConnectUmbrel() { url, }, connector: getConnectorType(), - nostrPrivateKey: null, }; try { diff --git a/src/types.ts b/src/types.ts index 1ea98c0c63..7d8d1ace26 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,7 +16,7 @@ export interface Account { connector: ConnectorType; config: string; name: string; - nostrPrivateKey: string | null; + nostrPrivateKey?: string | null; } export interface Accounts { From 6293e8a7f5c6ceb746848a21a4b65370fa19836f Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Mon, 16 Jan 2023 18:46:40 +0530 Subject: [PATCH 09/30] fix: unit test --- src/app/screens/Accounts/index.test.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/screens/Accounts/index.test.tsx b/src/app/screens/Accounts/index.test.tsx index cb9887287a..8e8742c47d 100644 --- a/src/app/screens/Accounts/index.test.tsx +++ b/src/app/screens/Accounts/index.test.tsx @@ -1,5 +1,7 @@ import { render, screen } from "@testing-library/react"; +import { I18nextProvider } from "react-i18next"; import { MemoryRouter } from "react-router-dom"; +import i18n from "~/../tests/unit/helpers/i18n"; import { AccountsProvider } from "../../context/AccountsContext"; import Accounts from "./index"; @@ -8,9 +10,11 @@ describe("Accounts", () => { test("render", async () => { render( - - - + + + + + ); expect(await screen.findByText("Accounts")).toBeInTheDocument(); From 3d5a6a281cbf9d797731e8c0e2d697ad79462da2 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Mon, 16 Jan 2023 20:02:59 +0530 Subject: [PATCH 10/30] fix: add i18n strings --- src/app/screens/Account.tsx | 69 +++++++++++----------------- src/app/screens/Settings.tsx | 19 +++----- src/i18n/locales/en/translation.json | 59 +++++++++++++++--------- 3 files changed, 71 insertions(+), 76 deletions(-) diff --git a/src/app/screens/Account.tsx b/src/app/screens/Account.tsx index 3360a29f30..84e0cf359f 100644 --- a/src/app/screens/Account.tsx +++ b/src/app/screens/Account.tsx @@ -37,10 +37,7 @@ function AccountScreen() { const auth = useAccount(); const { accounts, getAccounts } = useAccounts(); const { t } = useTranslation("translation", { - keyPrefix: "accounts", - }); - const { t: tSettings } = useTranslation("translation", { - keyPrefix: "settings", + keyPrefix: "accounts.account_view", }); const { isLoading: isLoadingSettings } = useSettings(); @@ -98,7 +95,7 @@ function AccountScreen() { if (nostrPrivateKey === currentPrivateKey) return; - if (currentPrivateKey && !confirm(tSettings("nostr.private_key.warning"))) { + if (currentPrivateKey && !confirm(t("nostr.private_key.warning"))) { return; } @@ -107,7 +104,7 @@ function AccountScreen() { privateKey: nostrlib.normalizeToHex(nostrPrivateKey), }); - toast.success(tSettings("nostr.private_key.success")); + toast.success(t("nostr.private_key.success")); } async function updateAccountName({ id, name }: AccountAction) { @@ -196,21 +193,22 @@ function AccountScreen() {
- {"Account Information"} + {t("title")}
{account.connector === "lndhub" && ( -
- - exportAccount({ - id: account.id, - name: account.name, - }) - } - /> +
+ exportAccount({ + id: account.id, + name: account.name, + }) + } + > +

{t("actions.export")}

+
)} @@ -279,10 +277,10 @@ function AccountScreen() {
- +
{ @@ -301,15 +299,8 @@ function AccountScreen() {
-
-
- - 🧪 Alby Lab - -
-
-

- {tSettings("nostr.title")} +

+ {t("nostr.title")}

- {tSettings("nostr.title")} + {t("nostr.title")} {" "} - {tSettings("nostr.hint")} + {t("nostr.hint")}

-
+
⛔️ Danger Zone @@ -399,10 +390,8 @@ function AccountScreen() {
diff --git a/src/app/screens/Settings.tsx b/src/app/screens/Settings.tsx index 3b649711db..0c14b95d31 100644 --- a/src/app/screens/Settings.tsx +++ b/src/app/screens/Settings.tsx @@ -251,7 +251,7 @@ function Settings() { {t("personal_data.description")}

-
+
{!isLoading && (
@@ -332,14 +332,9 @@ function Settings() {
-
-
- - 🧪 Alby Lab - -
-
-

{t("nostr.title")}

+

+ {t("nostr.title")} +

- } +
+
+
+ + {t("nostr.private_key.title")} + +

+ , + ]} /> -

- {!nostrPrivateKey && ( -
-
+
+
+
+
+
+ { + setNostrPrivateKey(event.target.value); + }} + endAdornment={ +
- )} + > + {nostrPrivateKeyVisible ? ( + + ) : ( + + )} + + } + />
- +
+
+
+
+
+ +
+
+ +
+
+
+
+
@@ -407,6 +509,47 @@ function AccountScreen() {
+ + +
+

+ {t("nostr.generate_keys.title")} +

+ +
+
+
+ {t("nostr.generate_keys.hint")} +
+
+
+
+
+
+
)} diff --git a/src/extension/background-script/actions/nostr/generatePrivateKey.ts b/src/extension/background-script/actions/nostr/generatePrivateKey.ts index 753bbd8fb4..a7aac8ec25 100644 --- a/src/extension/background-script/actions/nostr/generatePrivateKey.ts +++ b/src/extension/background-script/actions/nostr/generatePrivateKey.ts @@ -1,10 +1,33 @@ import * as secp256k1 from "@noble/secp256k1"; import Hex from "crypto-js/enc-hex"; import sha512 from "crypto-js/sha512"; +import type { MessagePrivateKeyGenerate } from "~/types"; import state from "../../state"; -const generatePrivateKey = async () => { +const generatePrivateKey = async (message: MessagePrivateKeyGenerate) => { + const type = message?.args?.type; + + const privateKey = + type === "random" ? generateRandomKey() : await deriveKey(); + + if (privateKey) + return { + data: { + privateKey: secp256k1.utils.bytesToHex(privateKey), + }, + }; + else + return { + error: "Error generating private key.", + }; +}; + +const generateRandomKey = () => { + return secp256k1.utils.randomPrivateKey(); +}; + +const deriveKey = async () => { const connector = await state.getState().getConnector(); try { const response = await connector.signMessage({ @@ -23,12 +46,7 @@ const generatePrivateKey = async () => { // Use SHA-512 to provide enough key material for secp256k1 (> 40 bytes) const hash = sha512(keymaterial).toString(Hex); const privateKey = secp256k1.utils.hashToPrivateKey(hash); - - return { - data: { - privateKey: secp256k1.utils.bytesToHex(privateKey), - }, - }; + return privateKey; } catch (e) { console.error(e); } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index faf52a8b8e..3237fa911e 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -382,11 +382,27 @@ "title": "Nostr", "hint": "is a simple and open protocol that aims to create censorship-resistant social networks. Nostr works with cryptographic keys. To publish something you sign it with your key and send it to multiple relays. You can use Alby to manage your Nostr key. Many Nostr applications will then allow you to simply use the key from the Alby extension.", "private_key": { - "title": "Private key", - "subtitle": "Paste your private key or generate a new one. Generating a key will create a new key based on the current account you use. It can be re-generated, but please make sure you backup this private key. <0>Learn more »", - "generate": "Generate", + "title": "Manage your keys", + "subtitle": "Paste your private key or generate a new one. Make sure you backup your private key before you generate a new one. <0>Learn more »", "warning": "This will delete your old private key. Are you sure?", - "success": "Private key encrypted & saved successfully." + "success": "Private key encrypted & saved successfully.", + "successfully_removed": "Private key removed successfully.", + "label": "Private Key" + }, + "public_key": { + "label": "Public Key" + }, + "generate_keys": { + "title": "Generate new Nostr Keys", + "screen_reader": "Generate new nostr keys for your account", + "hint": "You can generate random set of Nostr keys or cryptographically derive keys from this Alby account. You will be able to derive the same set of keys from this account on every device.", + "actions": { + "random_keys": "Generate random keys", + "derived_keys": "Derive keys from account" + } + }, + "actions": { + "generate": "Generate new keys" } }, "remove": { diff --git a/src/types.ts b/src/types.ts index 7993dba452..788478993b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -385,6 +385,13 @@ export interface MessagePrivateKeyGet extends MessageDefault { action: "getPrivateKey"; } +export interface MessagePrivateKeyGenerate extends MessageDefault { + args?: { + type?: "random"; + }; + action: "generatePrivateKey"; +} + export interface MessagePrivateKeySet extends MessageDefault { args: { id?: Account["id"]; From ed4973b9704fbde84769b02b3fde515b5a3e6a95 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sun, 29 Jan 2023 18:48:08 +0200 Subject: [PATCH 13/30] Update src/extension/background-script/actions/accounts/select.ts --- src/extension/background-script/actions/accounts/select.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension/background-script/actions/accounts/select.ts b/src/extension/background-script/actions/accounts/select.ts index bf25899d82..bd35026d9c 100644 --- a/src/extension/background-script/actions/accounts/select.ts +++ b/src/extension/background-script/actions/accounts/select.ts @@ -14,7 +14,7 @@ const select = async (message: MessageAccountSelect) => { state.setState({ account, - nostr: null, + nostr: null, // reset memoized nostr instance connector: null, // reset memoized connector currentAccountId: accountId, }); From a84a039538cff52f13bef0eda08f2e763148608c Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sun, 29 Jan 2023 20:25:42 +0100 Subject: [PATCH 14/30] chore: add accounts to user menu makes it easier to find --- src/app/components/UserMenu/index.tsx | 11 ++++++++++- src/i18n/locales/en/translation.json | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/app/components/UserMenu/index.tsx b/src/app/components/UserMenu/index.tsx index 6afaee66b4..cb442301c2 100644 --- a/src/app/components/UserMenu/index.tsx +++ b/src/app/components/UserMenu/index.tsx @@ -1,4 +1,5 @@ import { + AddressBookIcon, GearIcon, GlobeIcon, LockIcon, @@ -80,6 +81,7 @@ export default function UserMenu() { {tCommon("actions.receive")} + { openOptions("settings"); @@ -88,7 +90,14 @@ export default function UserMenu() { {tCommon("settings")} - + { + openOptions("accounts"); + }} + > + + {tCommon("accounts")} + { utils.openUrl("https://feedback.getalby.com"); diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index f05cdb88b2..1cb71e0290 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -735,6 +735,7 @@ "success": "Success", "error": "Error", "settings": "Settings", + "accounts": "Accounts", "discover": "Discover", "websites": "Websites", "sats_one": "sat", From 3f3ffb36f2cf4774829c5622fe9aef9b032a23eb Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sun, 29 Jan 2023 20:28:01 +0100 Subject: [PATCH 15/30] feat(nostr): display pubkey as npub --- src/app/screens/Account.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/screens/Account.tsx b/src/app/screens/Account.tsx index bde8cf32ef..b672311c50 100644 --- a/src/app/screens/Account.tsx +++ b/src/app/screens/Account.tsx @@ -108,7 +108,8 @@ function AccountScreen() { function generatePublicKey(priv: string) { const nostr = new Nostr(priv); - return nostr.getPublicKey(); + const pubkeyHex = nostr.getPublicKey(); + return nostrlib.hexToNip19(pubkeyHex, "npub"); } async function generateNostrPrivateKey(random?: boolean) { From ba2586d9c301f2ed3198cb9c7a6af8468ad73745 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sun, 29 Jan 2023 20:28:34 +0100 Subject: [PATCH 16/30] feat(Account): unify headline formatting --- src/app/screens/Account.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/app/screens/Account.tsx b/src/app/screens/Account.tsx index b672311c50..bf116dee24 100644 --- a/src/app/screens/Account.tsx +++ b/src/app/screens/Account.tsx @@ -235,11 +235,7 @@ function AccountScreen() { {account && (
-
-
- {t("title")} -
-
+

{t("title")}

{account.connector === "lndhub" && (
Date: Mon, 23 Jan 2023 12:35:32 +0530 Subject: [PATCH 17/30] chore: add test for get account action --- .../actions/accounts/__tests__/get.test.ts | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/extension/background-script/actions/accounts/__tests__/get.test.ts diff --git a/src/extension/background-script/actions/accounts/__tests__/get.test.ts b/src/extension/background-script/actions/accounts/__tests__/get.test.ts new file mode 100644 index 0000000000..e70944e78c --- /dev/null +++ b/src/extension/background-script/actions/accounts/__tests__/get.test.ts @@ -0,0 +1,87 @@ +import type { GetAccountRes } from "~/common/lib/api"; +import state from "~/extension/background-script/state"; +import type { MessageAccountGet } from "~/types"; + +import getAccount from "../get"; + +jest.mock("~/extension/background-script/state"); + +const mockState = { + getConnector: () => ({ + getInfo: () => Promise.resolve({ data: { alias: "getalby.com" } }), + getBalance: () => Promise.resolve({ data: { balance: 0 } }), + }), + getAccount: () => ({ + config: + "U2FsdGVkX19YMFK/8YpN5XQbMsmbVmlOJgpZCIRlt25K6ur4EPp4XdRUQC7+ep/m1k8d2yy69QfuGpsgn2SZOv4DQaPsdYTTwjj0mibQG/dkJ9OCp88zXuMpconrmRu5w4uZWEvdg7p5GQfIYJCvTPLUq+1zH3iH0xX7GhlrlQ8=", + connector: "lndhub", + id: "1e1e8ea6-493e-480b-9855-303d37506e97", + name: "Alby", + }), + currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", + accounts: { + "8b7f1dc6-ab87-4c6c-bca5-19fa8632731e": { + config: "config-123-456", + connector: "lndhub", + id: "8b7f1dc6-ab87-4c6c-bca5-19fa8632731e", + name: "Alby", + nostrPrivateKey: "nostr-123-456", + }, + "1e1e8ea6-493e-480b-9855-303d37506e97": { + config: "config-123-456", + connector: "lndhub", + id: "1e1e8ea6-493e-480b-9855-303d37506e97", + name: "Alby", + }, + }, +}; + +describe("account info", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test("get current account info", async () => { + const message: MessageAccountGet = { + application: "LBE", + origin: { internal: true }, + prompt: true, + action: "getAccount", + }; + + state.getState = jest.fn().mockReturnValue(mockState); + + const result: GetAccountRes = { + id: "1e1e8ea6-493e-480b-9855-303d37506e97", + name: "Alby", + connector: "lndhub", + nostrEnabled: false, + }; + + expect(await getAccount(message)).toStrictEqual({ + data: result, + }); + }); + test("get account info by id", async () => { + const message: MessageAccountGet = { + application: "LBE", + origin: { internal: true }, + prompt: true, + args: { id: "8b7f1dc6-ab87-4c6c-bca5-19fa8632731e" }, + action: "getAccount", + }; + + state.getState = jest.fn().mockReturnValue(mockState); + + const result: GetAccountRes = { + id: "8b7f1dc6-ab87-4c6c-bca5-19fa8632731e", + name: "Alby", + connector: "lndhub", + nostrEnabled: true, + }; + + expect(await getAccount(message)).toStrictEqual({ + data: result, + }); + }); +}); From 45f055afca068ee8b084d9d3b62713b1ec6f0961 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Mon, 30 Jan 2023 21:05:59 +0530 Subject: [PATCH 18/30] feat: use avvvatars for account screen --- src/app/screens/Account.tsx | 619 ++++++++++++++--------------- src/app/screens/Accounts/index.tsx | 2 +- 2 files changed, 310 insertions(+), 311 deletions(-) diff --git a/src/app/screens/Account.tsx b/src/app/screens/Account.tsx index bf116dee24..2ed08cf4e0 100644 --- a/src/app/screens/Account.tsx +++ b/src/app/screens/Account.tsx @@ -7,10 +7,10 @@ import { import Button from "@components/Button"; import Container from "@components/Container"; import Loading from "@components/Loading"; -import PublisherCard from "@components/PublisherCard"; import Setting from "@components/Setting"; import Input from "@components/form/Input"; import TextField from "@components/form/TextField"; +import Avvvatars from "avvvatars-react"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { useCallback, useEffect, useRef, useState } from "react"; @@ -31,9 +31,6 @@ import type { Account } from "~/types"; type AccountAction = Omit; dayjs.extend(relativeTime); -const DEFAULT_IMAGE = - "data:image/svg+xml;utf8,%3Csvg%20xmlns%3Adc%3D%22http%3A%2F%2Fpurl.org%2Fdc%2Felements%2F1.1%2F%22%20xmlns%3Acc%3D%22http%3A%2F%2Fcreativecommons.org%2Fns%23%22%20xmlns%3Ardf%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2F02%2F22-rdf-syntax-ns%23%22%20xmlns%3Asvg%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2050%2050%22%20preserveAspectRatio%3D%22xMidYMid%20meet%22%20width%3D%2260%22%20height%3D%2260%22%3E%3Cmetadata%3E%3Crdf%3ARDF%3E%3Ccc%3AWork%3E%3Cdc%3Aformat%3Eimage%2Fsvg%2Bxml%3C%2Fdc%3Aformat%3E%3Cdc%3Atype%20rdf%3Aresource%3D%22http%3A%2F%2Fpurl.org%2Fdc%2Fdcmitype%2FStillImage%22%2F%3E%3Cdc%3Atitle%3EJdenticon%3C%2Fdc%3Atitle%3E%3Cdc%3Acreator%3E%3Ccc%3AAgent%3E%3Cdc%3Atitle%3EDaniel%20Mester%20Pirttij%C3%A4rvi%3C%2Fdc%3Atitle%3E%3C%2Fcc%3AAgent%3E%3C%2Fdc%3Acreator%3E%3Cdc%3Asource%3Ehttps%3A%2F%2Fgithub.com%2Fdmester%2Fjdenticon%3C%2Fdc%3Asource%3E%3Ccc%3Alicense%20rdf%3Aresource%3D%22https%3A%2F%2Fgithub.com%2Fdmester%2Fjdenticon%2Fblob%2Fmaster%2FLICENSE%22%2F%3E%3C%2Fcc%3AWork%3E%3C%2Frdf%3ARDF%3E%3C%2Fmetadata%3E%3Crect%20fill%3D%22transparent%22%20width%3D%2250%22%20height%3D%2250%22%20x%3D%220%22%20y%3D%220%22%2F%3E%3Cpath%20fill%3D%22%23329948%22%20d%3D%22M13%2013L13%201L25%201ZM25%201L37%201L37%2013ZM37%2037L37%2049L25%2049ZM25%2049L13%2049L13%2037ZM1%2025L1%2013L13%2013ZM37%2013L49%2013L49%2025ZM49%2025L49%2037L37%2037ZM13%2037L1%2037L1%2025Z%22%2F%3E%3Cpath%20fill%3D%22%2366cc7b%22%20d%3D%22M1%2013L1%201L13%201ZM37%201L49%201L49%2013ZM49%2037L49%2049L37%2049ZM13%2049L1%2049L1%2037ZM13%2013L25%2013L25%2025L13%2025ZM16%2020.5L20.5%2025L25%2020.5L20.5%2016ZM37%2013L37%2025L25%2025L25%2013ZM29.5%2016L25%2020.5L29.5%2025L34%2020.5ZM37%2037L25%2037L25%2025L37%2025ZM34%2029.5L29.5%2025L25%2029.5L29.5%2034ZM13%2037L13%2025L25%2025L25%2037ZM20.5%2034L25%2029.5L20.5%2025L16%2029.5Z%22%2F%3E%3C%2Fsvg%3E"; - function AccountScreen() { const auth = useAccount(); const { accounts, getAccounts } = useAccounts(); @@ -44,8 +41,7 @@ function AccountScreen() { const { isLoading: isLoadingSettings } = useSettings(); const hasFetchedData = useRef(false); - const [account, setAccount] = useState(); - const [loading, setLoading] = useState(true); + const [account, setAccount] = useState(null); const { id } = useParams(); const navigate = useNavigate(); @@ -73,7 +69,6 @@ function AccountScreen() { const [nostrKeyModalIsOpen, setNostrKeyModalIsOpen] = useState(false); const fetchData = useCallback(async () => { - setLoading(true); try { if (id) { const response = await msg.request("getAccount", { @@ -81,7 +76,6 @@ function AccountScreen() { }); setAccount(response); setAccountName(response.name); - setLoading(false); const priv = (await msg.request("nostr/getPrivateKey", { id, @@ -216,340 +210,345 @@ function AccountScreen() { } }, [fetchData, isLoadingSettings]); - return loading ? ( + return !account ? (
) : (
- +
+ +
+

+ {account.name} +

+

+ {account.connector} +

+
+
- {account && ( - -
-

{t("title")}

- - {account.connector === "lndhub" && ( -
- exportAccount({ - id: account.id, - name: account.name, - }) - } - > -

{t("actions.export")}

- -
- )} - - +
+

{t("title")}

+ + {account.connector === "lndhub" && ( +
+ exportAccount({ + id: account.id, + name: account.name, + }) + } > -
-

- {t("export.title")} -

- -
+

{t("actions.export")}

+ +
+ )} + + +
+

+ {t("export.title")} +

+ +
- {exportLoading && ( -
- - {t("export.waiting")} -
- )} - {!exportLoading && ( -
- {lndHubData.lnAddress && ( -
-

- {t("export.your_ln_address")} -

- {lndHubData.lnAddress &&

{lndHubData.lnAddress}

} -
- )} -
-
-

- {t("export.tip_mobile")} -

-

{t("export.scan_qr")}

-
-
- -
+ {exportLoading && ( +
+ + {t("export.waiting")} +
+ )} + {!exportLoading && ( +
+ {lndHubData.lnAddress && ( +
+

+ {t("export.your_ln_address")} +

+ {lndHubData.lnAddress &&

{lndHubData.lnAddress}

} +
+ )} +
+
+

+ {t("export.tip_mobile")} +

+

{t("export.scan_qr")}

-
- +
- )} - -
- -
-
- -
- + { - updateAccountName({ - id: account.id, - name: accountName, - }); - const updatedAccount = account; - updatedAccount.name = accountName; - setAccount(updatedAccount); - }} - onChange={(event) => { - setAccountName(event.target.value); - }} + readOnly + value={`lndhub://${lndHubData.login}:${lndHubData.password}@${lndHubData.url}/`} />
-
-
-

+

+ )} + +
+ +
+
+ +
+ { + updateAccountName({ + id: account.id, + name: accountName, + }); + const updatedAccount = account; + updatedAccount.name = accountName; + setAccount(updatedAccount); + }} + onChange={(event) => { + setAccountName(event.target.value); + }} + /> +
+
+
+

+ {t("nostr.title")} +

+

+ {t("nostr.title")} - -

- - {t("nostr.title")} - {" "} - {t("nostr.hint")} -

-
-
-
- - {t("nostr.private_key.title")} - -

- , - ]} - /> -

-
-
- - } - /> -
-
-
+
+
+
+ { + setNostrPrivateKey(event.target.value); + }} + endAdornment={ + + } + /> +
+
+
-
-
+ }} + fullWidth + />
- -
-
- -
-
-
-
+
+
-
-
- - ⛔️ Danger Zone - -
-
-
- -
-
-
+
+
+ +
+
+
+
+
- -
-

- {t("nostr.generate_keys.title")} -

- +
+
+ + ⛔️ Danger Zone + +
+
+
+ +
+
-
-
- {t("nostr.generate_keys.hint")} -
+ +
+ + +
+

+ {t("nostr.generate_keys.title")} +

+ +
+
+
+ {t("nostr.generate_keys.hint")}
-
-
-
+
+
+
+
- -
- - )} +
+
+
+
); } diff --git a/src/app/screens/Accounts/index.tsx b/src/app/screens/Accounts/index.tsx index 34efac5229..296b74d358 100644 --- a/src/app/screens/Accounts/index.tsx +++ b/src/app/screens/Accounts/index.tsx @@ -1,10 +1,10 @@ -import Avvvatars from "avvvatars-react"; import { CaretRightIcon, PlusIcon, } from "@bitcoin-design/bitcoin-icons-react/filled"; import Button from "@components/Button"; import Container from "@components/Container"; +import Avvvatars from "avvvatars-react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { useAccounts } from "~/app/context/AccountsContext"; From f2ae9d81345f6ce07f391afe5289754c04704cb8 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Tue, 31 Jan 2023 15:53:14 +0530 Subject: [PATCH 19/30] chore: align screens --- src/app/router/Options/Options.tsx | 8 ++++---- src/app/screens/{Account.tsx => Accounts/Show/index.tsx} | 0 .../screens/{Publisher.tsx => Publishers/Show/index.tsx} | 0 src/app/screens/{Settings.tsx => Settings/index.tsx} | 0 4 files changed, 4 insertions(+), 4 deletions(-) rename src/app/screens/{Account.tsx => Accounts/Show/index.tsx} (100%) rename src/app/screens/{Publisher.tsx => Publishers/Show/index.tsx} (100%) rename src/app/screens/{Settings.tsx => Settings/index.tsx} (100%) diff --git a/src/app/router/Options/Options.tsx b/src/app/router/Options/Options.tsx index 4dde9a3232..c8f34b7f98 100644 --- a/src/app/router/Options/Options.tsx +++ b/src/app/router/Options/Options.tsx @@ -1,7 +1,7 @@ import Container from "@components/Container"; import Navbar from "@components/Navbar"; -import Account from "@screens/Account"; import Accounts from "@screens/Accounts"; +import ShowAccount from "@screens/Accounts/Show"; import ConfirmPayment from "@screens/ConfirmPayment"; import Keysend from "@screens/Keysend"; import LNURLAuth from "@screens/LNURLAuth"; @@ -9,8 +9,8 @@ import LNURLChannel from "@screens/LNURLChannel"; import LNURLPay from "@screens/LNURLPay"; import LNURLWithdraw from "@screens/LNURLWithdraw"; import TestConnection from "@screens/Options/TestConnection"; -import Publisher from "@screens/Publisher"; import Publishers from "@screens/Publishers"; +import ShowPublisher from "@screens/Publishers/Show"; import Receive from "@screens/Receive"; import Send from "@screens/Send"; import Settings from "@screens/Settings"; @@ -47,7 +47,7 @@ function Options() { } /> - } /> + } /> } /> } /> @@ -60,7 +60,7 @@ function Options() { } /> } /> - } /> + } /> Date: Tue, 31 Jan 2023 13:43:55 +0100 Subject: [PATCH 20/30] chore: text --- src/app/screens/Accounts/Show/index.tsx | 4 ++-- src/i18n/locales/en/translation.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/screens/Accounts/Show/index.tsx b/src/app/screens/Accounts/Show/index.tsx index 2ed08cf4e0..a0337827d9 100644 --- a/src/app/screens/Accounts/Show/index.tsx +++ b/src/app/screens/Accounts/Show/index.tsx @@ -111,7 +111,7 @@ function AccountScreen() { if (!random && selectedAccount?.id !== id) { alert( - `Please match the account in the dropdown with this account to derive keys.` + `Please match the account in the account dropdown at the top with this account to derive keys.` ); closeNostrKeyModal(); return; @@ -535,13 +535,13 @@ function AccountScreen() { type="submit" onClick={() => generateNostrPrivateKey(true)} label={t("nostr.generate_keys.actions.random_keys")} + primary halfWidth />
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 1cb71e0290..7e139824a6 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -404,9 +404,9 @@ "label": "Public Key" }, "generate_keys": { - "title": "Generate new Nostr Keys", + "title": "Generate new Nostr keys", "screen_reader": "Generate new nostr keys for your account", - "hint": "You can generate random set of Nostr keys or cryptographically derive keys from this Alby account. You will be able to derive the same set of keys from this account on every device.", + "hint": "You can generate a random set of Nostr keys or derive keys from this account details (using a signed canonical phrase).", "actions": { "random_keys": "Generate random keys", "derived_keys": "Derive keys from account" From 2efc9a700701b018d146cc170c83f317c00a02e2 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Tue, 31 Jan 2023 18:31:18 +0530 Subject: [PATCH 21/30] chore: modify ui in show account screen --- src/app/screens/Accounts/Show/index.tsx | 81 ++++++++++++++++--------- src/i18n/locales/en/translation.json | 3 +- 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/app/screens/Accounts/Show/index.tsx b/src/app/screens/Accounts/Show/index.tsx index a0337827d9..77b855d795 100644 --- a/src/app/screens/Accounts/Show/index.tsx +++ b/src/app/screens/Accounts/Show/index.tsx @@ -1,4 +1,7 @@ -import { ExportIcon } from "@bitcoin-design/bitcoin-icons-react/filled"; +import { + CaretLeftIcon, + ExportIcon, +} from "@bitcoin-design/bitcoin-icons-react/filled"; import { CrossIcon, HiddenIcon, @@ -6,9 +9,10 @@ import { } from "@bitcoin-design/bitcoin-icons-react/outline"; import Button from "@components/Button"; import Container from "@components/Container"; +import Header from "@components/Header"; +import IconButton from "@components/IconButton"; import Loading from "@components/Loading"; import Setting from "@components/Setting"; -import Input from "@components/form/Input"; import TextField from "@components/form/TextField"; import Avvvatars from "avvvatars-react"; import dayjs from "dayjs"; @@ -216,6 +220,15 @@ function AccountScreen() {
) : (
+
navigate(-1)} + icon={} + /> + } + />
@@ -228,9 +241,26 @@ function AccountScreen() {

{account.connector} + {account.connector === "lndhub" && ( + <> +

·
+
+ exportAccount({ + id: account.id, + name: account.name, + }) + } + > +

{t("actions.export")}

+ +
+ + )}

@@ -238,22 +268,7 @@ function AccountScreen() {
-

{t("title")}

- - {account.connector === "lndhub" && ( -
- exportAccount({ - id: account.id, - name: account.name, - }) - } - > -

{t("actions.export")}

- -
- )} +

{t("title2")}

- -
- +
+ { + onChange={(event) => { + setAccountName(event.target.value); + }} + /> +
+
+
+
- +

{t("nostr.title")} diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 7e139824a6..6e5639a400 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -375,7 +375,8 @@ "accounts": { "title": "Accounts", "account_view": { - "title": "Account Information", + "title1": "Account Information", + "title2": "Edit Account", "name": { "title": "Name", "placeholder": "Account Name" From f9a86651f6ac561bc43be23e9af8f852fd16a452 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Tue, 31 Jan 2023 18:50:49 +0530 Subject: [PATCH 22/30] chore: use useeffect hook on private key change --- src/app/screens/Accounts/Show/index.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/app/screens/Accounts/Show/index.tsx b/src/app/screens/Accounts/Show/index.tsx index 77b855d795..80c831039d 100644 --- a/src/app/screens/Accounts/Show/index.tsx +++ b/src/app/screens/Accounts/Show/index.tsx @@ -86,8 +86,6 @@ function AccountScreen() { })) as string; if (priv) { setCurrentPrivateKey(priv); - setNostrPrivateKey(nostrlib.hexToNip19(priv, "nsec")); - setNostrPublicKey(generatePublicKey(priv)); } } } catch (e) { @@ -151,10 +149,6 @@ function AccountScreen() { toast.success(t("nostr.private_key.successfully_removed")); } setCurrentPrivateKey(nostrPrivateKey); - setNostrPrivateKey(nostrPrivateKey); - setNostrPublicKey( - nostrPrivateKey ? generatePublicKey(nostrPrivateKey) : "" - ); } async function updateAccountName({ id, name }: AccountAction) { @@ -214,6 +208,15 @@ function AccountScreen() { } }, [fetchData, isLoadingSettings]); + useEffect(() => { + setNostrPublicKey( + currentPrivateKey ? generatePublicKey(currentPrivateKey) : "" + ); + setNostrPrivateKey( + currentPrivateKey ? nostrlib.hexToNip19(currentPrivateKey, "nsec") : "" + ); + }, [currentPrivateKey]); + return !account ? (
From 33f21d6002328f767b8b3b323a64ea2e8a31cab7 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Tue, 31 Jan 2023 18:20:28 +0200 Subject: [PATCH 23/30] chore: keep old setting private key as backup during the migration --- src/extension/background-script/migrations/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/extension/background-script/migrations/index.ts b/src/extension/background-script/migrations/index.ts index e710681039..d84008dc13 100644 --- a/src/extension/background-script/migrations/index.ts +++ b/src/extension/background-script/migrations/index.ts @@ -62,7 +62,6 @@ const migrations = { state.setState({ accounts, - nostrPrivateKey: null, }); } }, From bd9604e86d900916086ed657efa0c8c30cb415d9 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Tue, 31 Jan 2023 23:06:29 +0530 Subject: [PATCH 24/30] chore: add test for nostr --- .../nostr/__test__/nostr.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/extension/background-script/nostr/__test__/nostr.test.ts b/src/extension/background-script/nostr/__test__/nostr.test.ts index e924aec603..33ec724609 100644 --- a/src/extension/background-script/nostr/__test__/nostr.test.ts +++ b/src/extension/background-script/nostr/__test__/nostr.test.ts @@ -12,6 +12,12 @@ const bob = { publicKey: "519f5ae2cd7d4b970c4edadb2efc947c9b803838de918d1c5bfd4b9c1a143b72", }; +const carol = { + privateKey: + "43a2d71f40dde6fb7588e7962a54b8bbd8dd4bd617a9a5c58b7bf0d8f3482f11", + publicKey: "a8c7d70a7d2e2826ce519a0a490fb953464c9d130235c321282983cd73be333f", +}; + describe("nostr", () => { test("encrypt & decrypt", async () => { const aliceNostr = new Nostr(alice.privateKey); @@ -25,4 +31,22 @@ describe("nostr", () => { expect(decrypted).toMatch(message); }); + + test("Carol can't decrypt Alice's message for Bob", async () => { + const aliceNostr = new Nostr(alice.privateKey); + + const message = "Secret message that is sent from Alice to Bob"; + const encrypted = aliceNostr.encrypt(bob.publicKey, message); + + const carolNostr = new Nostr(carol.privateKey); + + let decrypted; + try { + decrypted = carolNostr.decrypt(alice.publicKey, encrypted); + } catch (e) { + decrypted = "error decrypting message"; + } + + expect(decrypted).not.toMatch(message); + }); }); From 188a35fa5233a7d77774e3a1732b1637234362f1 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Wed, 1 Feb 2023 15:08:59 +0530 Subject: [PATCH 25/30] chore: add copy icon to button --- src/app/screens/Accounts/Show/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/screens/Accounts/Show/index.tsx b/src/app/screens/Accounts/Show/index.tsx index 80c831039d..81f4ed5be8 100644 --- a/src/app/screens/Accounts/Show/index.tsx +++ b/src/app/screens/Accounts/Show/index.tsx @@ -3,6 +3,7 @@ import { ExportIcon, } from "@bitcoin-design/bitcoin-icons-react/filled"; import { + CopyIcon, CrossIcon, HiddenIcon, VisibleIcon, @@ -444,6 +445,7 @@ function AccountScreen() {

-

@@ -265,7 +270,7 @@ function AccountScreen() {

)} -

+
From e46bb9800255980d3e4e0a671ebff448abca0d3a Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Wed, 1 Feb 2023 17:49:49 +0530 Subject: [PATCH 27/30] fix: private key issues and styling --- src/app/screens/Accounts/Show/index.tsx | 38 ++++++++++++++----------- src/app/screens/Accounts/index.tsx | 4 +-- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/app/screens/Accounts/Show/index.tsx b/src/app/screens/Accounts/Show/index.tsx index 3ba8351b5d..dc47ad5a47 100644 --- a/src/app/screens/Accounts/Show/index.tsx +++ b/src/app/screens/Accounts/Show/index.tsx @@ -104,7 +104,7 @@ function AccountScreen() { } function generatePublicKey(priv: string) { - const nostr = new Nostr(nostrlib.normalizeToHex(priv)); + const nostr = new Nostr(priv); const pubkeyHex = nostr.getPublicKey(); return nostrlib.hexToNip19(pubkeyHex, "npub"); } @@ -133,23 +133,32 @@ function AccountScreen() { } async function saveNostrPrivateKey(nostrPrivateKey: string) { + nostrPrivateKey = nostrlib.normalizeToHex(nostrPrivateKey); + if (nostrPrivateKey === currentPrivateKey) return; if (currentPrivateKey && !confirm(t("nostr.private_key.warning"))) { return; } - await msg.request("nostr/setPrivateKey", { - id: account?.id, - privateKey: nostrlib.normalizeToHex(nostrPrivateKey), - }); + try { + nostrPrivateKey && generatePublicKey(nostrPrivateKey); + nostrPrivateKey && nostrlib.hexToNip19(nostrPrivateKey, "nsec"); + + await msg.request("nostr/setPrivateKey", { + id: account?.id, + privateKey: nostrPrivateKey, + }); - if (nostrPrivateKey) { - toast.success(t("nostr.private_key.success")); - } else { - toast.success(t("nostr.private_key.successfully_removed")); + if (nostrPrivateKey) { + toast.success(t("nostr.private_key.success")); + } else { + toast.success(t("nostr.private_key.successfully_removed")); + } + setCurrentPrivateKey(nostrPrivateKey); + } catch (e) { + if (e instanceof Error) toast.error(e.message); } - setCurrentPrivateKey(nostrPrivateKey); } async function updateAccountName({ id, name }: AccountAction) { @@ -214,12 +223,7 @@ function AccountScreen() { currentPrivateKey ? generatePublicKey(currentPrivateKey) : "" ); setNostrPrivateKey( - currentPrivateKey - ? nostrlib.hexToNip19( - nostrlib.normalizeToHex(currentPrivateKey), - "nsec" - ) - : "" + currentPrivateKey ? nostrlib.hexToNip19(currentPrivateKey, "nsec") : "" ); }, [currentPrivateKey]); @@ -565,7 +569,7 @@ function AccountScreen() { {t("nostr.generate_keys.hint")}
-
+
-
+
- + {Object.keys(accounts).map((accountId) => { const account = accounts[accountId]; return ( From 349fd7ae0723f689cf4cf9417b3682b82829d9e1 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Wed, 1 Feb 2023 18:30:55 +0530 Subject: [PATCH 28/30] fix: use form element --- src/app/screens/Accounts/Show/index.tsx | 46 +++++++++++++++---------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/app/screens/Accounts/Show/index.tsx b/src/app/screens/Accounts/Show/index.tsx index dc47ad5a47..727dd52c98 100644 --- a/src/app/screens/Accounts/Show/index.tsx +++ b/src/app/screens/Accounts/Show/index.tsx @@ -18,6 +18,7 @@ import TextField from "@components/form/TextField"; import Avvvatars from "avvvatars-react"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; +import type { FormEvent } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import Modal from "react-modal"; @@ -348,7 +349,19 @@ function AccountScreen() {
-
+
{ + e.preventDefault(); + updateAccountName({ + id: account.id, + name: accountName, + }); + const updatedAccount = account; + updatedAccount.name = accountName; + setAccount(updatedAccount); + }} + className="my-4 flex justify-between items-end" + >
-
+

{t("nostr.title")} @@ -424,7 +429,13 @@ function AccountScreen() { />

-
+
{ + e.preventDefault(); + saveNostrPrivateKey(nostrPrivateKey); + }} + className="mb-4 flex justify-between items-end" + >
-
+
From a6203fc623c56a98f0d7449d6844e34bf1bbdbe0 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Wed, 1 Feb 2023 21:22:03 +0530 Subject: [PATCH 29/30] fix: final changes --- src/app/screens/Accounts/Show/index.tsx | 18 ++++++++++++++---- .../background-script/migrations/index.ts | 2 +- src/i18n/locales/en/translation.json | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/app/screens/Accounts/Show/index.tsx b/src/app/screens/Accounts/Show/index.tsx index 727dd52c98..f9b2fc92b8 100644 --- a/src/app/screens/Accounts/Show/index.tsx +++ b/src/app/screens/Accounts/Show/index.tsx @@ -572,10 +572,20 @@ function AccountScreen() {
-
-
- {t("nostr.generate_keys.hint")} -
+
+ , + ]} + />
diff --git a/src/extension/background-script/migrations/index.ts b/src/extension/background-script/migrations/index.ts index d84008dc13..81443b33d6 100644 --- a/src/extension/background-script/migrations/index.ts +++ b/src/extension/background-script/migrations/index.ts @@ -57,7 +57,7 @@ const migrations = { if (nostrPrivateKey) { Object.values(accounts).map((account) => { - account.nostrPrivateKey = nostrPrivateKey; + if (!account.nostrPrivateKey) account.nostrPrivateKey = nostrPrivateKey; }); state.setState({ diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 6e5639a400..d2e0a6f4cb 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -407,7 +407,7 @@ "generate_keys": { "title": "Generate new Nostr keys", "screen_reader": "Generate new nostr keys for your account", - "hint": "You can generate a random set of Nostr keys or derive keys from this account details (using a signed canonical phrase).", + "hint": "You can generate a random set of Nostr keys or derive keys from this account details (using a signed canonical phrase). <0>Learn more »", "actions": { "random_keys": "Generate random keys", "derived_keys": "Derive keys from account" From 64a985144a51677d93f3ccf5f0d63f18c93e85ef Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Wed, 1 Feb 2023 22:53:03 +0530 Subject: [PATCH 30/30] fix: add type guard --- src/app/screens/Accounts/Show/index.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/screens/Accounts/Show/index.tsx b/src/app/screens/Accounts/Show/index.tsx index f9b2fc92b8..32cc9dc60f 100644 --- a/src/app/screens/Accounts/Show/index.tsx +++ b/src/app/screens/Accounts/Show/index.tsx @@ -143,11 +143,17 @@ function AccountScreen() { } try { + if (!account) { + // type guard + throw new Error("No account available"); + } + + // Validate the private key before saving nostrPrivateKey && generatePublicKey(nostrPrivateKey); nostrPrivateKey && nostrlib.hexToNip19(nostrPrivateKey, "nsec"); await msg.request("nostr/setPrivateKey", { - id: account?.id, + id: account.id, privateKey: nostrPrivateKey, });