From 7aa9a2223eae9eeb610b2a42d2e06f542c98c4fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Sun, 2 Apr 2023 21:54:37 +0200 Subject: [PATCH 001/118] feat: first steps --- .../background-script/actions/webbtc/index.ts | 3 +++ .../actions/webbtc/signPsbt.ts | 26 +++++++++++++++++++ src/extension/background-script/router.ts | 5 ++++ src/extension/ln/webbtc/index.ts | 8 ++++++ 4 files changed, 42 insertions(+) create mode 100644 src/extension/background-script/actions/webbtc/index.ts create mode 100644 src/extension/background-script/actions/webbtc/signPsbt.ts diff --git a/src/extension/background-script/actions/webbtc/index.ts b/src/extension/background-script/actions/webbtc/index.ts new file mode 100644 index 0000000000..d09d839a91 --- /dev/null +++ b/src/extension/background-script/actions/webbtc/index.ts @@ -0,0 +1,3 @@ +import signPsbt from "./signPsbt"; + +export { signPsbt }; diff --git a/src/extension/background-script/actions/webbtc/signPsbt.ts b/src/extension/background-script/actions/webbtc/signPsbt.ts new file mode 100644 index 0000000000..0590425f20 --- /dev/null +++ b/src/extension/background-script/actions/webbtc/signPsbt.ts @@ -0,0 +1,26 @@ +import utils from "~/common/lib/utils"; +import { Message } from "~/types"; + +const signPsbt = async (message: Message) => { + const psbt = message.args.psbt; + if (typeof psbt !== "string") { + return { + error: "PSBT missing.", + }; + } + + try { + const response = await utils.openPrompt({ + ...message, + action: "confirmSignPsbt", + }); + return response; + } catch (e) { + console.error("signPsbt cancelled", e); + if (e instanceof Error) { + return { error: e.message }; + } + } +}; + +export default signPsbt; diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index 05ad0693f4..56d104edae 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -9,6 +9,7 @@ import * as payments from "./actions/payments"; import * as permissions from "./actions/permissions"; import * as settings from "./actions/settings"; import * as setup from "./actions/setup"; +import * as webbtc from "./actions/webbtc"; import * as webln from "./actions/webln"; const routes = { @@ -66,6 +67,10 @@ const routes = { // Public calls that are accessible from the inpage script (through the content script) public: { + webbtc: { + enable: allowances.enable, + getInfo: webbtc.signPsbt, + }, webln: { enable: allowances.enable, getInfo: ln.getInfo, diff --git a/src/extension/ln/webbtc/index.ts b/src/extension/ln/webbtc/index.ts index 61f0321d24..1e4e8149b0 100644 --- a/src/extension/ln/webbtc/index.ts +++ b/src/extension/ln/webbtc/index.ts @@ -45,6 +45,14 @@ export default class WebBTCProvider { return this.execute("signMessageOrPrompt", { message }); } + signPsbt(psbt: string) { + if (!this.enabled) { + throw new Error("Provider must be enabled before calling signMessage"); + } + + return this.execute("signPsbtWithPrompt", { psbt }); + } + verifyMessage(signature: string, message: string) { if (!this.enabled) { throw new Error("Provider must be enabled before calling verifyMessage"); From f0d3544ad72a9fb26ce12bec429789263c66aaef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Tue, 4 Apr 2023 15:51:07 +0200 Subject: [PATCH 002/118] feat: untie webbtc from lightning --- src/app/router/Prompt/Prompt.tsx | 4 + .../actions/webbtc/getInfo.ts | 15 +++ .../background-script/actions/webbtc/index.ts | 3 +- src/extension/background-script/router.ts | 3 +- src/extension/content-script/onendwebbtc.js | 105 ++++++++++++++++++ .../{onend.js => onendwebln.js} | 0 src/extension/content-script/onstart.ts | 4 + src/extension/inpage-script/webbtc.js | 5 + src/extension/inpage-script/webln.js | 2 - src/extension/ln/webbtc/index.ts | 49 +------- src/manifest.json | 7 +- webpack.config.js | 15 ++- 12 files changed, 156 insertions(+), 56 deletions(-) create mode 100644 src/extension/background-script/actions/webbtc/getInfo.ts create mode 100644 src/extension/content-script/onendwebbtc.js rename src/extension/content-script/{onend.js => onendwebln.js} (100%) create mode 100644 src/extension/inpage-script/webbtc.js diff --git a/src/app/router/Prompt/Prompt.tsx b/src/app/router/Prompt/Prompt.tsx index 31a839d556..54ecf72cfa 100644 --- a/src/app/router/Prompt/Prompt.tsx +++ b/src/app/router/Prompt/Prompt.tsx @@ -74,6 +74,10 @@ function Prompt() { path="public/nostr/enable" element={} // prompt will always have an `origin` set, just the type is optional to support usage via PopUp /> + } // prompt will always have an `origin` set, just the type is optional to support usage via PopUp + /> } /> { + const supportedMethods = ["getInfo", "signPsbt"]; + + return { + data: { + version: "Alby", + supports: ["bitcoin"], + methods: supportedMethods, + }, + }; +}; + +export default getInfo; diff --git a/src/extension/background-script/actions/webbtc/index.ts b/src/extension/background-script/actions/webbtc/index.ts index d09d839a91..29be179eda 100644 --- a/src/extension/background-script/actions/webbtc/index.ts +++ b/src/extension/background-script/actions/webbtc/index.ts @@ -1,3 +1,4 @@ +import getInfo from "./getInfo"; import signPsbt from "./signPsbt"; -export { signPsbt }; +export { getInfo, signPsbt }; diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index 56d104edae..7941addfb8 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -69,7 +69,8 @@ const routes = { public: { webbtc: { enable: allowances.enable, - getInfo: webbtc.signPsbt, + getInfo: webbtc.getInfo, + signPsbt: webbtc.signPsbt, }, webln: { enable: allowances.enable, diff --git a/src/extension/content-script/onendwebbtc.js b/src/extension/content-script/onendwebbtc.js new file mode 100644 index 0000000000..276f61af0e --- /dev/null +++ b/src/extension/content-script/onendwebbtc.js @@ -0,0 +1,105 @@ +import browser from "webextension-polyfill"; + +import getOriginData from "./originData"; +import shouldInject from "./shouldInject"; + +// Nostr calls that can be executed from the WebBTC Provider. +// Update when new calls are added +const webbtcCalls = ["webbtc/getInfo", "webbtc/signPsbt"]; +// calls that can be executed when `window.webbtc` is not enabled for the current content page +const disabledCalls = ["webbtc/enable"]; + +let isEnabled = false; // store if nostr is enabled for this content page +let isRejected = false; // store if the nostr enable call failed. if so we do not prompt again +let callActive = false; // store if a nostr call is currently active. Used to prevent multiple calls in parallel + +async function init() { + const inject = await shouldInject(); + if (!inject) { + return; + } + + const SCOPE = "webbtc"; + + // message listener to listen to inpage webln calls + // those calls get passed on to the background script + // (the inpage script can not do that directly, but only the inpage script can make webln available to the page) + window.addEventListener("message", (ev) => { + // Only accept messages from the current window + if ( + ev.source !== window || + ev.data.application !== "LBE" || + ev.data.scope !== SCOPE + ) { + return; + } + + if (ev.data && !ev.data.response) { + // if an enable call railed we ignore the request to prevent spamming the user with prompts + if (isRejected) { + console.error( + "Enable had failed. Rejecting further WebLN calls until the next reload" + ); + return; + } + // if a call is active we ignore the request + if (callActive) { + console.error("nostr call already executing"); + return; + } + + // limit the calls that can be made from window.nostr + // only listed calls can be executed + // if not enabled only enable can be called. + const availableCalls = isEnabled ? webbtcCalls : disabledCalls; + if (!availableCalls.includes(ev.data.action)) { + console.error("Function not available."); + return; + } + + const messageWithOrigin = { + // every call call is scoped in `public` + // this prevents websites from accessing internal actions + action: `public/${ev.data.action}`, + args: ev.data.args, + application: "LBE", + public: true, // indicate that this is a public call from the content script + prompt: true, + origin: getOriginData(), + }; + + const replyFunction = (response) => { + callActive = false; // reset call is active + + if (ev.data.action === `${SCOPE}/enable`) { + isEnabled = response.data?.enabled; + if (response.error) { + console.error(response.error); + console.info("Enable was rejected ignoring further nostr calls"); + isRejected = true; + } + } + + window.postMessage( + { + application: "LBE", + response: true, + data: response, + scope: SCOPE, + }, + "*" // TODO use origin + ); + }; + + callActive = true; + return browser.runtime + .sendMessage(messageWithOrigin) + .then(replyFunction) + .catch(replyFunction); + } + }); +} + +init(); + +export {}; diff --git a/src/extension/content-script/onend.js b/src/extension/content-script/onendwebln.js similarity index 100% rename from src/extension/content-script/onend.js rename to src/extension/content-script/onendwebln.js diff --git a/src/extension/content-script/onstart.ts b/src/extension/content-script/onstart.ts index 70e0a163cb..8bffeaee90 100644 --- a/src/extension/content-script/onstart.ts +++ b/src/extension/content-script/onstart.ts @@ -13,6 +13,10 @@ async function onstart() { // window.webln injectScript(browser.runtime.getURL("js/inpageScriptWebLN.bundle.js")); + // window.webbtc + // TODO: Add check if current account has keys + injectScript(browser.runtime.getURL("js/inpageScriptWebBTC.bundle.js")); + // window.nostr const nostrEnabled = (await api.getAccount()).nostrEnabled; if (nostrEnabled) { diff --git a/src/extension/inpage-script/webbtc.js b/src/extension/inpage-script/webbtc.js new file mode 100644 index 0000000000..61fabcf6b6 --- /dev/null +++ b/src/extension/inpage-script/webbtc.js @@ -0,0 +1,5 @@ +import WebBTCProvider from "../ln/webbtc"; + +if (document) { + window.webbtc = new WebBTCProvider(); +} diff --git a/src/extension/inpage-script/webln.js b/src/extension/inpage-script/webln.js index 64589bb9c2..9adc288270 100644 --- a/src/extension/inpage-script/webln.js +++ b/src/extension/inpage-script/webln.js @@ -1,7 +1,5 @@ -import WebBTCProvider from "../ln/webbtc"; import WebLNProvider from "../ln/webln"; if (document) { window.webln = new WebLNProvider(); - window.webbtc = new WebBTCProvider(); } diff --git a/src/extension/ln/webbtc/index.ts b/src/extension/ln/webbtc/index.ts index 1e4e8149b0..0b2752774c 100644 --- a/src/extension/ln/webbtc/index.ts +++ b/src/extension/ln/webbtc/index.ts @@ -1,11 +1,3 @@ -type RequestInvoiceArgs = { - amount?: string | number; - defaultAmount?: string | number; - minimumAmount?: string | number; - maximumAmount?: string | number; - defaultMemo?: string; -}; - export default class WebBTCProvider { enabled: boolean; isEnabled: boolean; @@ -37,14 +29,6 @@ export default class WebBTCProvider { return this.execute("getInfo"); } - signMessage(message: string) { - if (!this.enabled) { - throw new Error("Provider must be enabled before calling signMessage"); - } - - return this.execute("signMessageOrPrompt", { message }); - } - signPsbt(psbt: string) { if (!this.enabled) { throw new Error("Provider must be enabled before calling signMessage"); @@ -53,31 +37,6 @@ export default class WebBTCProvider { return this.execute("signPsbtWithPrompt", { psbt }); } - verifyMessage(signature: string, message: string) { - if (!this.enabled) { - throw new Error("Provider must be enabled before calling verifyMessage"); - } - throw new Error("Alby does not support `verifyMessage`"); - } - - makeInvoice(args: string | number | RequestInvoiceArgs) { - if (!this.enabled) { - throw new Error("Provider must be enabled before calling makeInvoice"); - } - if (typeof args !== "object") { - args = { amount: args }; - } - - return this.execute("makeInvoice", args); - } - - sendPayment(paymentRequest: string) { - if (!this.enabled) { - throw new Error("Provider must be enabled before calling sendPayment"); - } - return this.execute("sendPaymentOrPrompt", { paymentRequest }); - } - sendTransaction(address: string, amount: string) { if (!this.enabled) { throw new Error( @@ -117,8 +76,8 @@ export default class WebBTCProvider { { application: "LBE", prompt: true, - action: `webln/${action}`, - scope: "webln", + action: `webbtc/${action}`, + scope: "webbtc", args, }, "*" // TODO use origin @@ -130,10 +89,12 @@ export default class WebBTCProvider { if ( !messageEvent.data || !messageEvent.data.response || - messageEvent.data.application !== "LBE" + messageEvent.data.application !== "LBE" || + messageEvent.data.scope !== "webbtc" ) { return; } + if (messageEvent.data.data.error) { reject(new Error(messageEvent.data.data.error)); } else { diff --git a/src/manifest.json b/src/manifest.json index 1803a66670..a206917303 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -16,6 +16,7 @@ "web_accessible_resources": [ "js/inpageScript.bundle.js", "js/inpageScriptWebLN.bundle.js", + "js/inpageScriptWebBTC.bundle.js", "js/inpageScriptNostr.bundle.js" ], @@ -24,6 +25,7 @@ "resources": [ "js/inpageScript.bundle.js", "js/inpageScriptWebLN.bundle.js", + "js/inpageScriptWebBTC.bundle.js", "js/inpageScriptNostr.bundle.js" ], "matches": ["https://*/*"] @@ -167,8 +169,9 @@ "matches": ["*://*/*"], "run_at": "document_end", "js": [ - "js/contentScriptOnEnd.bundle.js", - "js/contentScriptOnEndNostr.bundle.js" + "js/contentScriptOnEndWebLN.bundle.js", + "js/contentScriptOnEndNostr.bundle.js", + "js/contentScriptOnEndWebBTC.bundle.js" ] }, { diff --git a/webpack.config.js b/webpack.config.js index c1f225c8b2..0bde004d50 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,7 +9,8 @@ const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); const WextManifestWebpackPlugin = require("wext-manifest-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin"); const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); -const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const BundleAnalyzerPlugin = + require("webpack-bundle-analyzer").BundleAnalyzerPlugin; // default value is set in the code where it is used if (!process.env.WALLET_CREATE_URL) { @@ -61,11 +62,13 @@ var options = { entry: { manifest: "./src/manifest.json", background: "./src/extension/background-script/index.ts", - contentScriptOnEnd: "./src/extension/content-script/onend.js", + contentScriptOnEndWebLN: "./src/extension/content-script/onendwebln.js", contentScriptOnEndNostr: "./src/extension/content-script/onendnostr.js", + contentScriptOnEndWebBTC: "./src/extension/content-script/onendwebbtc.js", contentScriptOnStart: "./src/extension/content-script/onstart.ts", inpageScript: "./src/extension/inpage-script/index.js", inpageScriptWebLN: "./src/extension/inpage-script/webln.js", + inpageScriptWebBTC: "./src/extension/inpage-script/webbtc.js", inpageScriptNostr: "./src/extension/inpage-script/nostr.js", popup: "./src/app/router/Popup/index.tsx", prompt: "./src/app/router/Prompt/index.tsx", @@ -205,10 +208,10 @@ var options = { patterns: [{ from: "static/assets", to: "assets" }], }), new BundleAnalyzerPlugin({ - generateStatsFile: (nodeEnv !== "development" ? true : false), - analyzerMode: (nodeEnv !== "development" ? 'static' : 'disabled'), - reportFilename: '../bundle-report.html', - statsFilename: '../bundle-stats.json', + generateStatsFile: nodeEnv !== "development" ? true : false, + analyzerMode: nodeEnv !== "development" ? "static" : "disabled", + reportFilename: "../bundle-report.html", + statsFilename: "../bundle-stats.json", openAnalyzer: nodeEnv !== "development", }), ], From c7599eabc21c556c9fd488210b81f2c4430ed641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Tue, 4 Apr 2023 17:15:02 +0200 Subject: [PATCH 003/118] feat: webbtc & prompts for signPsbt --- package.json | 1 + src/app/router/Prompt/Prompt.tsx | 2 + .../screens/ConfirmSignPsbt/index.test.tsx | 52 ++++++++++ src/app/screens/ConfirmSignPsbt/index.tsx | 95 +++++++++++++++++++ src/extension/content-script/onendwebbtc.js | 2 +- src/extension/ln/webbtc/index.ts | 2 +- src/types.ts | 1 + yarn.lock | 31 +++++- 8 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 src/app/screens/ConfirmSignPsbt/index.test.tsx create mode 100644 src/app/screens/ConfirmSignPsbt/index.tsx diff --git a/package.json b/package.json index 31c1495a86..cb336e5cf9 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@headlessui/react": "^1.7.12", "@lightninglabs/lnc-web": "^0.2.2-alpha", "@noble/secp256k1": "^1.7.1", + "@scure/btc-signer": "^0.5.1", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/line-clamp": "^0.4.2", "@vespaiach/axios-fetch-adapter": "^0.3.0", diff --git a/src/app/router/Prompt/Prompt.tsx b/src/app/router/Prompt/Prompt.tsx index 54ecf72cfa..e3519edc43 100644 --- a/src/app/router/Prompt/Prompt.tsx +++ b/src/app/router/Prompt/Prompt.tsx @@ -18,6 +18,7 @@ import { HashRouter, Navigate, Outlet, Route, Routes } from "react-router-dom"; import { ToastContainer } from "react-toastify"; import Providers from "~/app/context/Providers"; import RequireAuth from "~/app/router/RequireAuth"; +import ConfirmSignPsbt from "~/app/screens/ConfirmSignPsbt"; import type { NavigationState, OriginData } from "~/types"; // Parse out the parameters from the querystring. @@ -100,6 +101,7 @@ function Prompt() { } /> } /> } /> + } /> } diff --git a/src/app/screens/ConfirmSignPsbt/index.test.tsx b/src/app/screens/ConfirmSignPsbt/index.test.tsx new file mode 100644 index 0000000000..5ee1a71c8a --- /dev/null +++ b/src/app/screens/ConfirmSignPsbt/index.test.tsx @@ -0,0 +1,52 @@ +import { act, render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import type { OriginData } from "~/types"; + +import ConfirmSignPsbt from "./index"; + +const mockOrigin: OriginData = { + location: "https://getalby.com/demo", + domain: "https://getalby.com", + host: "getalby.com", + pathname: "/demo", + name: "Alby", + description: "", + icon: "https://getalby.com/assets/alby-503261fa1b83c396b7ba8d927db7072d15fea5a84d387a654c5d0a2cefd44604.svg", + metaData: { + title: "Alby Demo", + url: "https://getalby.com/demo", + provider: "Alby", + image: + "https://getalby.com/assets/alby-503261fa1b83c396b7ba8d927db7072d15fea5a84d387a654c5d0a2cefd44604.svg", + icon: "https://getalby.com/favicon.ico", + }, + external: true, +}; + +jest.mock("~/app/hooks/useNavigationState", () => { + return { + useNavigationState: jest.fn(() => ({ + origin: mockOrigin, + args: { + psbt: "psbt", + }, + })), + }; +}); + +describe("ConfirmSignMessage", () => { + test("render", async () => { + await act(async () => { + render( + + + + ); + }); + + expect( + await screen.findByText("This website asks you to sign:") + ).toBeInTheDocument(); + expect(await screen.findByText("psbt")).toBeInTheDocument(); + }); +}); diff --git a/src/app/screens/ConfirmSignPsbt/index.tsx b/src/app/screens/ConfirmSignPsbt/index.tsx new file mode 100644 index 0000000000..32ebaa6b98 --- /dev/null +++ b/src/app/screens/ConfirmSignPsbt/index.tsx @@ -0,0 +1,95 @@ +//import Checkbox from "../../components/Form/Checkbox"; +import ConfirmOrCancel from "@components/ConfirmOrCancel"; +import Container from "@components/Container"; +import ContentMessage from "@components/ContentMessage"; +import PublisherCard from "@components/PublisherCard"; +import SuccessMessage from "@components/SuccessMessage"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import ScreenHeader from "~/app/components/ScreenHeader"; +import { useNavigationState } from "~/app/hooks/useNavigationState"; +import { USER_REJECTED_ERROR } from "~/common/constants"; +import msg from "~/common/lib/msg"; +import type { OriginData } from "~/types"; + +function ConfirmSignPsbt() { + const navState = useNavigationState(); + const { t: tCommon } = useTranslation("common"); + const { t } = useTranslation("translation", { + keyPrefix: "confirm_sign_message", + }); + const navigate = useNavigate(); + + const psbt = navState.args?.psbt as string; + const origin = navState.origin as OriginData; + const [loading, setLoading] = useState(false); + const [successMessage, setSuccessMessage] = useState(""); + + async function confirm() { + try { + setLoading(true); + const response = await msg.request("signPsbt", { psbt }, { origin }); + msg.reply(response); + setSuccessMessage(tCommon("success")); + } catch (e) { + console.error(e); + if (e instanceof Error) toast.error(`${tCommon("error")}: ${e.message}`); + } finally { + setLoading(false); + } + } + + function reject(e: React.MouseEvent) { + e.preventDefault(); + msg.error(USER_REJECTED_ERROR); + } + + function close(e: React.MouseEvent) { + if (navState.isPrompt) { + window.close(); + } else { + e.preventDefault(); + navigate(-1); + } + } + + return ( +
+ + {!successMessage ? ( + +
+ + +
+ +
+ ) : ( + + + + + )} +
+ ); +} + +export default ConfirmSignPsbt; diff --git a/src/extension/content-script/onendwebbtc.js b/src/extension/content-script/onendwebbtc.js index 276f61af0e..6b5027f5a7 100644 --- a/src/extension/content-script/onendwebbtc.js +++ b/src/extension/content-script/onendwebbtc.js @@ -44,7 +44,7 @@ async function init() { } // if a call is active we ignore the request if (callActive) { - console.error("nostr call already executing"); + console.error("WebBTC call already executing"); return; } diff --git a/src/extension/ln/webbtc/index.ts b/src/extension/ln/webbtc/index.ts index 0b2752774c..6e5ca96b9c 100644 --- a/src/extension/ln/webbtc/index.ts +++ b/src/extension/ln/webbtc/index.ts @@ -34,7 +34,7 @@ export default class WebBTCProvider { throw new Error("Provider must be enabled before calling signMessage"); } - return this.execute("signPsbtWithPrompt", { psbt }); + return this.execute("signPsbt", { psbt }); } sendTransaction(address: string, amount: string) { diff --git a/src/types.ts b/src/types.ts index 8e3738e3ad..0bd4aa52d4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -148,6 +148,7 @@ export type NavigationState = { method: string; description: string; }; + psbt?: string; }; isPrompt?: true; // only passed via Prompt.tsx action: string; diff --git a/yarn.lock b/yarn.lock index 2760b7046b..a7fde0d283 100644 --- a/yarn.lock +++ b/yarn.lock @@ -982,7 +982,14 @@ strict-event-emitter "^0.2.4" web-encoding "^1.1.5" -"@noble/hashes@^1.2.0": +"@noble/curves@~0.8.3": + version "0.8.3" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-0.8.3.tgz#ad6d48baf2599cf1d58dcb734c14d5225c8996e0" + integrity sha512-OqaOf4RWDaCRuBKJLDURrgVxjLmneGsiCXGuzYB5y95YithZMA6w4uk34DHSm0rKMrrYiaeZj48/81EvaAScLQ== + dependencies: + "@noble/hashes" "1.3.0" + +"@noble/hashes@1.3.0", "@noble/hashes@^1.2.0", "@noble/hashes@~1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg== @@ -1038,6 +1045,21 @@ resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.3.3.tgz#d6d531d69c0fa3a44fda7dc00b20d49b44549164" integrity sha512-YRHie1yQEj0kqqCTCJEfHqYSSNlZQ696QJG+MMiW4mxSl9I0ojz/eRhJS4fs88Z5i6D1SmoF9d3K99/QOhI8/w== +"@scure/base@~1.1.0", "@scure/base@~1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" + integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== + +"@scure/btc-signer@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@scure/btc-signer/-/btc-signer-0.5.1.tgz#79e2e89a359c5ba9aa944ba5bd487a6ce1b2ad36" + integrity sha512-T8ViYQEwAz79UNdfrdpxUeGuriYlvgxH2EouL7gTJZJ3jAqK/0ft3gL0VsOkrmYx8XfIX+p89tJFxuy/MXhgoA== + dependencies: + "@noble/curves" "~0.8.3" + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + micro-packed "~0.3.2" + "@sinclair/typebox@^0.24.1": version "0.24.20" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.20.tgz#11a657875de6008622d53f56e063a6347c51a6dd" @@ -6633,6 +6655,13 @@ methods@~1.1.2: resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= +micro-packed@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/micro-packed/-/micro-packed-0.3.2.tgz#3679188366c2283cb60a78366ed0416e5472b7cf" + integrity sha512-D1Bq0/lVOzdxhnX5vylCxZpdw5LylH7Vd81py0DfRsKUP36XYpwvy8ZIsECVo3UfnoROn8pdKqkOzL7Cd82sGA== + dependencies: + "@scure/base" "~1.1.1" + micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" From fe0a2aa0479ffe666c2b052a24ec6a2897d69066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Tue, 4 Apr 2023 17:34:27 +0200 Subject: [PATCH 004/118] fix: remove bracket --- src/extension/background-script/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index 7941addfb8..656540b37d 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -107,7 +107,7 @@ const router = (path: FixMe) => { console.warn(`Route not found: ${path}`); // return a function to keep the expected method signature return () => { - return Promise.reject({ error: `${path} not found}` }); + return Promise.reject({ error: `${path} not found` }); }; } return route; From b573f9c1c74d08ff378528fe96aa1175734f7255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Tue, 11 Apr 2023 16:38:19 +0200 Subject: [PATCH 005/118] feat: first steps towards psbt --- .../background-script/actions/webbtc/index.ts | 6 ++- .../actions/webbtc/signPsbt.ts | 46 +++++++++++-------- .../actions/webbtc/signPsbtWithPrompt.ts | 26 +++++++++++ src/extension/background-script/router.ts | 4 +- src/extension/content-script/onendwebbtc.js | 2 +- src/extension/ln/webbtc/index.ts | 2 +- src/types.ts | 7 +++ 7 files changed, 70 insertions(+), 23 deletions(-) create mode 100644 src/extension/background-script/actions/webbtc/signPsbtWithPrompt.ts diff --git a/src/extension/background-script/actions/webbtc/index.ts b/src/extension/background-script/actions/webbtc/index.ts index 29be179eda..a95a6292ed 100644 --- a/src/extension/background-script/actions/webbtc/index.ts +++ b/src/extension/background-script/actions/webbtc/index.ts @@ -1,4 +1,6 @@ +import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; + import getInfo from "./getInfo"; -import signPsbt from "./signPsbt"; +import signPsbtWithPrompt from "./signPsbtWithPrompt"; -export { getInfo, signPsbt }; +export { getInfo, signPsbtWithPrompt, signPsbt }; diff --git a/src/extension/background-script/actions/webbtc/signPsbt.ts b/src/extension/background-script/actions/webbtc/signPsbt.ts index 0590425f20..9cf80acd09 100644 --- a/src/extension/background-script/actions/webbtc/signPsbt.ts +++ b/src/extension/background-script/actions/webbtc/signPsbt.ts @@ -1,25 +1,35 @@ -import utils from "~/common/lib/utils"; -import { Message } from "~/types"; +import * as secp256k1 from "@noble/secp256k1"; +import { Transaction } from "@scure/btc-signer"; +import { MessageSignPsbt } from "~/types"; + +const signPsbt = async (message: MessageSignPsbt) => { + try { + const privateKey = secp256k1.utils.hexToBytes( + "0101010101010101010101010101010101010101010101010101010101010101" + ); + + console.log("🔑 Private key", privateKey); + + const psbtBytes = secp256k1.utils.hexToBytes(message.args.psbt); + + console.log("🔟 psbtBytes", psbtBytes); + + const transaction = Transaction.fromPSBT(psbtBytes); // PSBT tx + + console.log("🔑 Decoded transaction", transaction); + + const result = transaction.sign(privateKey); -const signPsbt = async (message: Message) => { - const psbt = message.args.psbt; - if (typeof psbt !== "string") { return { - error: "PSBT missing.", + data: { + status: "OK", + signed: result, + }, }; - } - - try { - const response = await utils.openPrompt({ - ...message, - action: "confirmSignPsbt", - }); - return response; } catch (e) { - console.error("signPsbt cancelled", e); - if (e instanceof Error) { - return { error: e.message }; - } + return { + error: "signPsbt failed: " + e, + }; } }; diff --git a/src/extension/background-script/actions/webbtc/signPsbtWithPrompt.ts b/src/extension/background-script/actions/webbtc/signPsbtWithPrompt.ts new file mode 100644 index 0000000000..0d0ccef65c --- /dev/null +++ b/src/extension/background-script/actions/webbtc/signPsbtWithPrompt.ts @@ -0,0 +1,26 @@ +import utils from "~/common/lib/utils"; +import { Message } from "~/types"; + +const signPsbtWithPrompt = async (message: Message) => { + const psbt = message.args.psbt; + if (typeof psbt !== "string") { + return { + error: "PSBT missing.", + }; + } + + try { + const response = await utils.openPrompt({ + ...message, + action: "confirmSignPsbt", + }); + return response; + } catch (e) { + console.error("signPsbt cancelled", e); + if (e instanceof Error) { + return { error: e.message }; + } + } +}; + +export default signPsbtWithPrompt; diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index 656540b37d..1295ec389d 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -58,6 +58,8 @@ const routes = { lnurl: lnurl, lnurlAuth: auth, getCurrencyRate: cache.getCurrencyRate, + signPsbt: webbtc.signPsbt, + nostr: { generatePrivateKey: nostr.generatePrivateKey, getPrivateKey: nostr.getPrivateKey, @@ -70,7 +72,7 @@ const routes = { webbtc: { enable: allowances.enable, getInfo: webbtc.getInfo, - signPsbt: webbtc.signPsbt, + signPsbtWithPrompt: webbtc.signPsbtWithPrompt, }, webln: { enable: allowances.enable, diff --git a/src/extension/content-script/onendwebbtc.js b/src/extension/content-script/onendwebbtc.js index 6b5027f5a7..10303abeef 100644 --- a/src/extension/content-script/onendwebbtc.js +++ b/src/extension/content-script/onendwebbtc.js @@ -5,7 +5,7 @@ import shouldInject from "./shouldInject"; // Nostr calls that can be executed from the WebBTC Provider. // Update when new calls are added -const webbtcCalls = ["webbtc/getInfo", "webbtc/signPsbt"]; +const webbtcCalls = ["webbtc/getInfo", "webbtc/signPsbtWithPrompt"]; // calls that can be executed when `window.webbtc` is not enabled for the current content page const disabledCalls = ["webbtc/enable"]; diff --git a/src/extension/ln/webbtc/index.ts b/src/extension/ln/webbtc/index.ts index 6e5ca96b9c..0b2752774c 100644 --- a/src/extension/ln/webbtc/index.ts +++ b/src/extension/ln/webbtc/index.ts @@ -34,7 +34,7 @@ export default class WebBTCProvider { throw new Error("Provider must be enabled before calling signMessage"); } - return this.execute("signPsbt", { psbt }); + return this.execute("signPsbtWithPrompt", { psbt }); } sendTransaction(address: string, amount: string) { diff --git a/src/types.ts b/src/types.ts index 0bd4aa52d4..8a3245239f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -486,6 +486,13 @@ export interface MessageDecryptGet extends MessageDefault { action: "decrypt"; } +export interface MessageSignPsbt extends MessageDefault { + args: { + psbt: string; + }; + action: "signPsbt"; +} + export interface LNURLChannelServiceResponse { uri: string; // Remote node address of form node_key@ip_address:port_number callback: string; // a second-level URL which would initiate an OpenChannel message from target LN node From 7ff1d5da7f5017b05c1a7236c5f6b20e4fc3f283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Wed, 12 Apr 2023 22:37:30 +0200 Subject: [PATCH 006/118] fix: impelemented some basic functionality & tests (wip) --- package.json | 2 + .../actions/webbtc/__tests__/signPsbt.test.ts | 183 ++++++++++++++++++ .../actions/webbtc/signPsbt.ts | 31 +-- tsconfig.json | 2 +- yarn.lock | 24 +++ 5 files changed, 228 insertions(+), 14 deletions(-) create mode 100644 src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts diff --git a/package.json b/package.json index cb336e5cf9..57468dd278 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,8 @@ "@headlessui/react": "^1.7.12", "@lightninglabs/lnc-web": "^0.2.2-alpha", "@noble/secp256k1": "^1.7.1", + "@scure/bip32": "^1.3.0", + "@scure/bip39": "^1.2.0", "@scure/btc-signer": "^0.5.1", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/line-clamp": "^0.4.2", diff --git a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts new file mode 100644 index 0000000000..32faf06cff --- /dev/null +++ b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts @@ -0,0 +1,183 @@ +import * as secp256k1 from "@noble/secp256k1"; +import { hex } from "@scure/base"; +import { HDKey } from "@scure/bip32"; +import * as bip39 from "@scure/bip39"; +import * as btc from "@scure/btc-signer"; +import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; +import state from "~/extension/background-script/state"; +import { allowanceFixture } from "~/fixtures/allowances"; +import type { DbAllowance, MessageSignPsbt } from "~/types"; + +jest.mock("~/extension/background-script/state"); + +// Same as above +const TX_TEST_OUTPUTS: [string, bigint][] = [ + ["1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP", 10n], + ["3H3Kc7aSPP4THLX68k4mQMyf1gvL6AtmDm", 50n], + ["bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", 93n], +]; +const TX_TEST_INPUTS = [ + { + txid: hex.decode( + "c061c23190ed3370ad5206769651eaf6fac6d87d85b5db34e30a74e0c4a6da3e" + ), + index: 0, + amount: 550n, + script: hex.decode("76a91411dbe48cc6b617f9c6adaf4d9ed5f625b1c7cb5988ac"), + }, + { + txid: hex.decode( + "a21965903c938af35e7280ae5779b9fea4f7f01ac256b8a2a53b1b19a4e89a0d" + ), + index: 0, + amount: 600n, + script: hex.decode("76a91411dbe48cc6b617f9c6adaf4d9ed5f625b1c7cb5988ac"), + }, + { + txid: hex.decode( + "fae21e319ca827df32462afc3225c17719338a8e8d3e3b3ddeb0c2387da3a4c7" + ), + index: 0, + amount: 600n, + script: hex.decode("76a91411dbe48cc6b617f9c6adaf4d9ed5f625b1c7cb5988ac"), + }, +]; + +const defaultMockState = { + currentAccountId: "8b7f1dc6-ab87-4c6c-bca5-19fa8632731e", +}; + +const regtest = { + bech32: "bcrt", + pubKeyHash: 0x6f, + scriptHash: 0xc4, + wif: 0, +}; + +const mockState = defaultMockState; +state.getState = jest.fn().mockReturnValue(mockState); + +Date.now = jest.fn(() => 1487076708000); + +const mockAllowances: DbAllowance[] = [allowanceFixture[0]]; + +beforeEach(async () => { + // fill the DB first +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +async function sendPsbtMessage(psbt: string) { + const message: MessageSignPsbt = { + application: "LBE", + prompt: true, + action: "signPsbt", + origin: { + internal: true, + }, + args: { + psbt: psbt, + }, + }; + + return await signPsbt(message); +} + +describe("signPsbt", () => { + const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const seed = bip39.mnemonicToSeedSync(mnemonic); + + const hdkey = HDKey.fromMasterSeed(seed); + if (!hdkey) throw Error("invalid hdkey"); + + const privateKey = hdkey.privateKey; + + if (!privateKey) throw Error("no private key available"); + + test("successfully signed psbt", async () => { + const tx32 = new btc.Transaction({ version: 1 }); + for (const [address, amount] of TX_TEST_OUTPUTS) + tx32.addOutputAddress(address, amount); + for (const inp of TX_TEST_INPUTS) { + tx32.addInput({ + txid: inp.txid, + index: inp.index, + witnessUtxo: { + amount: inp.amount, + script: btc.p2wpkh(secp256k1.getPublicKey(privateKey, true)).script, + }, + }); + } + const psbt = tx32.toPSBT(2); + + expect(tx32.isFinal).toBe(false); + + const result = await sendPsbtMessage(secp256k1.utils.bytesToHex(psbt)); + + expect(result.data).not.toBe(undefined); + expect(result.error).toBe(undefined); + + // expect(result.data?.signed).toBe( + // "010000000001033edaa6c4e0740ae334dbb5857dd8c6faf6ea5196760652ad7033ed9031c261c00000000000ffffffff0d9ae8a4191b3ba5a2b856c21af0f7a4feb97957ae80725ef38a933c906519a20000000000ffffffffc7a4a37d38c2b0de3d3b3e8d8e8a331977c12532fc2a4632df27a89c311ee2fa0000000000ffffffff030a000000000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac320000000000000017a914a860f76561c85551594c18eecceffaee8c4822d7875d00000000000000160014e8df018c7e326cc253faac7e46cdc51e68542c420248304502210089852ee0ca628998de7bd3ca155058196c4c1f66aa3ffb775fd363dafc121c5f0220424ca42eafaa529ac3ff6f1f5af690f45fa2ba294e250c8e91eab0bd37d82a07012103d902f35f560e0470c63313c7369168d9d7df2d49bf295fd9fb7cb109ccee049402483045022100dd99ceb0b087568f62da6de5ac6e875a47c3758f18853dccbafed9c2709892ec022010f1d1dc54fb369a033a57da8a7d0ef897682499efeb216016a265f414546417012103d902f35f560e0470c63313c7369168d9d7df2d49bf295fd9fb7cb109ccee049402483045022100cbad3acff2f56bec89b08496ed2953cc1785282effbe651c4aea79cd601c6b6f02207b0e43638e7ba4933ea13ed562854c893b8e416baa08f2a9ec5ad806bb19aa27012103d902f35f560e0470c63313c7369168d9d7df2d49bf295fd9fb7cb109ccee049400000000" + // ); + + if (result.data?.signed) { + const checkTx = btc.Transaction.fromRaw(hex.decode(result.data?.signed)); + expect(checkTx.isFinal).toBe(true); + } + }); +}); + +// from https://github.com/satoshilabs/slips/blob/master/slip-0132.md +describe("test transaction building", () => { + test("create from mnemonic", async () => { + const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const seed = bip39.mnemonicToSeedSync(mnemonic); + const hdkey = HDKey.fromMasterSeed(seed); + + expect(hdkey.privateExtendedKey).toBe( + "xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu" + ); + + console.log("root", hdkey.privateExtendedKey, hdkey.publicExtendedKey); + + // Check + + // ✅ m/0'/0' + // BIP32 Extended Private key + // xprv9w83TkwTJSpYjV4hWcxttB9bQWHdrFCPzCLnMHKceyd4WGBfsUgijUirvMaHM6TFBqQegpt3hZysUeBP8PFmkjPWitahm71vjNhMLqKmuLb + // ✅ m/44'/0'/0'/0 + // ❌ m/49'/0'/0'/0 + // ❌ m/84'/0'/0'/0 + // zprvAg4yBxbZcJpcLxtXp5kZuh8jC1FXGtZnCjrkG69JPf96KZ1TqSakA1HF3EZkNjt9yC4CTjm7txs4sRD9EoHLgDqwhUE6s1yD9nY4BCNN4hw + + const derivedKey = hdkey.derive("m/84'/0'/0'"); + + const addressKey = hdkey.derive("m/84'/0'/0'/0/0"); + + console.log( + "derived", + derivedKey.privateExtendedKey, + derivedKey.publicExtendedKey + ); + console.log( + "address", + btc.getAddress("wpkh", addressKey.privateKey, btc.NETWORK) + ); + }); +}); + +describe("signPsbt input validation", () => { + test("no inputs signed", async () => { + const result = await sendPsbtMessage("test"); + expect(result.error).not.toBe(null); + }); + test("invalid psbt", async () => { + const result = await sendPsbtMessage("test"); + expect(result.error).not.toBe(null); + }); +}); diff --git a/src/extension/background-script/actions/webbtc/signPsbt.ts b/src/extension/background-script/actions/webbtc/signPsbt.ts index 9cf80acd09..ed6a2868e3 100644 --- a/src/extension/background-script/actions/webbtc/signPsbt.ts +++ b/src/extension/background-script/actions/webbtc/signPsbt.ts @@ -1,29 +1,34 @@ import * as secp256k1 from "@noble/secp256k1"; -import { Transaction } from "@scure/btc-signer"; +import { hex } from "@scure/base"; +import { HDKey } from "@scure/bip32"; +import * as bip39 from "@scure/bip39"; +import * as btc from "@scure/btc-signer"; import { MessageSignPsbt } from "~/types"; +// TODO: Load from account +// TODO: Make network configurable via ENV +const mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; +const seed = bip39.mnemonicToSeedSync(mnemonic); +const hdkey = HDKey.fromMasterSeed(seed); + const signPsbt = async (message: MessageSignPsbt) => { try { - const privateKey = secp256k1.utils.hexToBytes( - "0101010101010101010101010101010101010101010101010101010101010101" - ); - - console.log("🔑 Private key", privateKey); + const privateKey = hdkey.privateKey!; const psbtBytes = secp256k1.utils.hexToBytes(message.args.psbt); + const transaction = btc.Transaction.fromPSBT(psbtBytes); - console.log("🔟 psbtBytes", psbtBytes); - - const transaction = Transaction.fromPSBT(psbtBytes); // PSBT tx + transaction.sign(privateKey); - console.log("🔑 Decoded transaction", transaction); + // TODO: Do we need to finalize() here or should that be done by websites? + transaction.finalize(); - const result = transaction.sign(privateKey); + const signedTransaction = hex.encode(transaction.extract()); return { data: { - status: "OK", - signed: result, + signed: signedTransaction, }, }; } catch (e) { diff --git a/tsconfig.json b/tsconfig.json index 6e4172398b..a58464969c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es6", + "target": "ESNext", "module": "commonjs", "allowJs": true, "jsx": "react-jsx", diff --git a/yarn.lock b/yarn.lock index a7fde0d283..caad51962b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -989,6 +989,13 @@ dependencies: "@noble/hashes" "1.3.0" +"@noble/curves@~1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.0.0.tgz#e40be8c7daf088aaf291887cbc73f43464a92932" + integrity sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw== + dependencies: + "@noble/hashes" "1.3.0" + "@noble/hashes@1.3.0", "@noble/hashes@^1.2.0", "@noble/hashes@~1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" @@ -1050,6 +1057,23 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== +"@scure/bip32@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.0.tgz#6c8d980ef3f290987736acd0ee2e0f0d50068d87" + integrity sha512-bcKpo1oj54hGholplGLpqPHRbIsnbixFtc06nwuNM5/dwSXOq/AAYoIBRsBmnZJSdfeNW5rnff7NTAz3ZCqR9Q== + dependencies: + "@noble/curves" "~1.0.0" + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + +"@scure/bip39@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.0.tgz#a207e2ef96de354de7d0002292ba1503538fc77b" + integrity sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg== + dependencies: + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + "@scure/btc-signer@^0.5.1": version "0.5.1" resolved "https://registry.yarnpkg.com/@scure/btc-signer/-/btc-signer-0.5.1.tgz#79e2e89a359c5ba9aa944ba5bd487a6ce1b2ad36" From 6d9c0ba43d81ec57d9dac90217fbb6bbcf937713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Mon, 17 Apr 2023 14:23:28 +0200 Subject: [PATCH 007/118] fix: remove lib from lockfile --- yarn.lock | 5 ----- 1 file changed, 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index d2478ea303..7b5c5d359f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1065,11 +1065,6 @@ "@scure/base" "~1.1.0" micro-packed "~0.3.2" -"@sinclair/typebox@^0.24.1": - version "0.24.20" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.20.tgz#11a657875de6008622d53f56e063a6347c51a6dd" - integrity sha512-kVaO5aEFZb33nPMTZBxiPEkY+slxiPtqC7QX8f9B3eGOMBvEfuMfxp9DSTTCsRJPumPKjrge4yagyssO4q6qzQ== - "@sinclair/typebox@^0.25.16": version "0.25.21" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.21.tgz#763b05a4b472c93a8db29b2c3e359d55b29ce272" From 8297383b77aac0c1d3267f5bbc36fc33512952c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Mon, 17 Apr 2023 15:09:48 +0200 Subject: [PATCH 008/118] fix: webbtc --- src/extension/content-script/onendwebbtc.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extension/content-script/onendwebbtc.js b/src/extension/content-script/onendwebbtc.js index 10303abeef..f72f5c2beb 100644 --- a/src/extension/content-script/onendwebbtc.js +++ b/src/extension/content-script/onendwebbtc.js @@ -21,7 +21,7 @@ async function init() { const SCOPE = "webbtc"; - // message listener to listen to inpage webln calls + // message listener to listen to inpage webbtc calls // those calls get passed on to the background script // (the inpage script can not do that directly, but only the inpage script can make webln available to the page) window.addEventListener("message", (ev) => { @@ -38,7 +38,7 @@ async function init() { // if an enable call railed we ignore the request to prevent spamming the user with prompts if (isRejected) { console.error( - "Enable had failed. Rejecting further WebLN calls until the next reload" + "Enable had failed. Rejecting further WebBTC calls until the next reload" ); return; } From 99c9b2ab5d6a0553ddeb4447c11e37632fa5c71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Tue, 18 Apr 2023 10:07:12 +0200 Subject: [PATCH 009/118] fix: cleanup test --- .../actions/webbtc/__tests__/signPsbt.test.ts | 74 ++++++++++++------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts index 32faf06cff..92b0568fb0 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts @@ -4,9 +4,7 @@ import { HDKey } from "@scure/bip32"; import * as bip39 from "@scure/bip39"; import * as btc from "@scure/btc-signer"; import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; -import state from "~/extension/background-script/state"; -import { allowanceFixture } from "~/fixtures/allowances"; -import type { DbAllowance, MessageSignPsbt } from "~/types"; +import type { MessageSignPsbt } from "~/types"; jest.mock("~/extension/background-script/state"); @@ -43,10 +41,6 @@ const TX_TEST_INPUTS = [ }, ]; -const defaultMockState = { - currentAccountId: "8b7f1dc6-ab87-4c6c-bca5-19fa8632731e", -}; - const regtest = { bech32: "bcrt", pubKeyHash: 0x6f, @@ -54,13 +48,6 @@ const regtest = { wif: 0, }; -const mockState = defaultMockState; -state.getState = jest.fn().mockReturnValue(mockState); - -Date.now = jest.fn(() => 1487076708000); - -const mockAllowances: DbAllowance[] = [allowanceFixture[0]]; - beforeEach(async () => { // fill the DB first }); @@ -143,10 +130,7 @@ describe("test transaction building", () => { "xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu" ); - console.log("root", hdkey.privateExtendedKey, hdkey.publicExtendedKey); - // Check - // ✅ m/0'/0' // BIP32 Extended Private key // xprv9w83TkwTJSpYjV4hWcxttB9bQWHdrFCPzCLnMHKceyd4WGBfsUgijUirvMaHM6TFBqQegpt3hZysUeBP8PFmkjPWitahm71vjNhMLqKmuLb @@ -154,20 +138,54 @@ describe("test transaction building", () => { // ❌ m/49'/0'/0'/0 // ❌ m/84'/0'/0'/0 // zprvAg4yBxbZcJpcLxtXp5kZuh8jC1FXGtZnCjrkG69JPf96KZ1TqSakA1HF3EZkNjt9yC4CTjm7txs4sRD9EoHLgDqwhUE6s1yD9nY4BCNN4hw + // 49 / 84 doesn't seem to follow the same derivation logic 🤔 - const derivedKey = hdkey.derive("m/84'/0'/0'"); + // const derivedKey = hdkey.derive("m/84'/0'/0'"); + // const addressKey = hdkey.derive("m/84'/0'/0'/0/0"); - const addressKey = hdkey.derive("m/84'/0'/0'/0/0"); + // const nostrKey = hdkey.derive("m/1237'/0'/0"); + // const liquidKey = hdkey.derive("m/1776'/0'/0"); - console.log( - "derived", - derivedKey.privateExtendedKey, - derivedKey.publicExtendedKey - ); - console.log( - "address", - btc.getAddress("wpkh", addressKey.privateKey, btc.NETWORK) - ); + // console.log(nostrKey, liquidKey); + + // console.log( + // "derived", + // derivedKey.privateExtendedKey, + // derivedKey.publicExtendedKey + // ); + // console.log( + // "address", + // btc.getAddress("wpkh", addressKey.privateKey!, btc.NETWORK) + // ); + + // Assemble transaction + // const result1 = btc.WIF(regtest).encode(derivedKey.publicKey!); + // console.log(result1); + + const tx32 = new btc.Transaction({ version: 1 }); + for (const [address, amount] of TX_TEST_OUTPUTS) + tx32.addOutputAddress(address, amount); + for (const inp of TX_TEST_INPUTS) { + tx32.addInput({ + txid: inp.txid, + index: inp.index, + witnessUtxo: { + amount: inp.amount, + script: btc.p2wpkh(secp256k1.getPublicKey(hdkey.privateKey!, true)) + .script, + }, + }); + } + + // Create psbt + const psbt = hex.encode(tx32.toPSBT()); + + // Sign transaction + const result = await sendPsbtMessage(psbt); + + // Check signatures + expect(result.data).not.toBe(undefined); + expect(result.error).toBe(undefined); }); }); From e12139f8f0512f83414db05c25cb1cacb74a7e2d Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Sat, 22 Apr 2023 21:41:47 +0700 Subject: [PATCH 010/118] feat: mnemonic / secret key UI WIP --- src/app/router/Options/Options.tsx | 5 ++ src/app/screens/Accounts/Detail/index.tsx | 36 ++++++++++- .../Accounts/GenerateSecretKey/index.tsx | 62 +++++++++++++++++++ src/types.ts | 1 + 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/app/screens/Accounts/GenerateSecretKey/index.tsx diff --git a/src/app/router/Options/Options.tsx b/src/app/router/Options/Options.tsx index eb587edd96..d57da4c342 100644 --- a/src/app/router/Options/Options.tsx +++ b/src/app/router/Options/Options.tsx @@ -22,6 +22,7 @@ import { ToastContainer } from "react-toastify"; import Providers from "~/app/context/Providers"; import RequireAuth from "~/app/router/RequireAuth"; import getConnectorRoutes from "~/app/router/connectorRoutes"; +import GenerateSecretKey from "~/app/screens/Accounts/GenerateSecretKey"; import Discover from "~/app/screens/Discover"; import AlbyWallet from "~/app/screens/connectors/AlbyWallet"; import ChooseConnector from "~/app/screens/connectors/ChooseConnector"; @@ -63,6 +64,10 @@ function Options() { } /> } /> + } + /> + +

+ {/*t("nostr.title")*/}Secret Key +

+ { + /* TODO: secret key exists &&*/

+ {/*t("nostr.hint")*/}Your Account Secret Key allows you to use + Alby to interact with protocols such as Nostr or Oridinals. +

+ } + +
+
+ {/*t("nostr.private_key.backup")*/}⚠️ Back up your Secret Key! Not + backing it up might result in permanently loosing access to your + Nostr identity or purchased Oridinals. +
+
+
Generate your Secret Key
+ +
+ +
+
+
+

{t("nostr.title")}

diff --git a/src/app/screens/Accounts/GenerateSecretKey/index.tsx b/src/app/screens/Accounts/GenerateSecretKey/index.tsx new file mode 100644 index 0000000000..3375d0f1a5 --- /dev/null +++ b/src/app/screens/Accounts/GenerateSecretKey/index.tsx @@ -0,0 +1,62 @@ +import Container from "@components/Container"; +import Loading from "@components/Loading"; +import * as bip39 from "@scure/bip39"; +import { wordlist } from "@scure/bip39/wordlists/english"; +import React from "react"; +import { toast } from "react-toastify"; +import Button from "~/app/components/Button"; +import { useAccount } from "~/app/context/AccountContext"; + +function GenerateSecretKey() { + const [mnemomic] = React.useState(bip39.generateMnemonic(wordlist, 128)); + const account = useAccount(); + + /*const { t } = useTranslation("translation", { + keyPrefix: "accounts.account_view", + }); + const { t: tCommon } = useTranslation("common");*/ + + async function saveSecretKey() { + try { + // TODO: re-add + if (!account) { + // type guard + throw new Error("No account available"); + } + + alert("Mnemonic: " + mnemomic); + + // TODO: make sure secret key doesn't already exist + + // await msg.request("secretKey/save", { + // id: account.id, + // mnemomic, + // }); + // toast.success(t("nostr.private_key.success")); + // } + } catch (e) { + if (e instanceof Error) toast.error(e.message); + } + } + + return !account ? ( +
+ +
+ ) : ( +
+ +
+ Test +
+
+ ); +} + +export default GenerateSecretKey; diff --git a/src/types.ts b/src/types.ts index 8a3245239f..46392831a0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,7 @@ export interface Account { config: string; name: string; nostrPrivateKey?: string | null; + // mnemonic?: string | null; } export interface Accounts { From eb1536b7f5167f717fa37127344252dc637e3c3a Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 25 Apr 2023 12:44:59 +0700 Subject: [PATCH 011/118] feat: account secret key section styling and copy WIP --- src/app/screens/Accounts/Detail/index.tsx | 111 +++++++++++++++++++--- src/i18n/locales/en/translation.json | 2 +- 2 files changed, 101 insertions(+), 12 deletions(-) diff --git a/src/app/screens/Accounts/Detail/index.tsx b/src/app/screens/Accounts/Detail/index.tsx index f9aa5b5c0e..903edae2ba 100644 --- a/src/app/screens/Accounts/Detail/index.tsx +++ b/src/app/screens/Accounts/Detail/index.tsx @@ -25,6 +25,7 @@ import QRCode from "react-qr-code"; import { Link, useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; import Avatar from "~/app/components/Avatar"; +import MenuDivider from "~/app/components/Menu/MenuDivider"; import { useAccount } from "~/app/context/AccountContext"; import { useAccounts } from "~/app/context/AccountsContext"; import { useSettings } from "~/app/context/SettingsContext"; @@ -37,6 +38,9 @@ import type { Account } from "~/types"; type AccountAction = Omit; dayjs.extend(relativeTime); +// TODO: replace with checking account +const SECRET_KEY_EXISTS = false; + function AccountDetail() { const auth = useAccount(); const { accounts, getAccounts } = useAccounts(); @@ -420,26 +424,48 @@ function AccountDetail() { {/*t("nostr.title")*/}Secret Key { - /* TODO: secret key exists &&*/

+

{/*t("nostr.hint")*/}Your Account Secret Key allows you to use Alby to interact with protocols such as Nostr or Oridinals.

} -
-
- {/*t("nostr.private_key.backup")*/}⚠️ Back up your Secret Key! Not - backing it up might result in permanently loosing access to your - Nostr identity or purchased Oridinals. -
-
-
Generate your Secret Key
+
+ {SECRET_KEY_EXISTS && ( +
+ {/*t("nostr.private_key.backup")*/}⚠️ Backup your Secret Key! + Not backing it up might result in permanently loosing access to + your Nostr identity or purchased Oridinals. +
+ )} + +
+
+

+ {SECRET_KEY_EXISTS + ? "Backup your Secret Key" + : "Generate your Secret Key"} +

+

+ Your Secret Key is a set of 12 words that will allow you to + access your keys to protocols such as Nostr or Oridinals on a + new device or in case you loose access to your account. +

+
- +
+ +
+
+

+ {/*tCommon("actions.save")*/}Import a Secret Key +

+

+ {/*tCommon("actions.save")*/}Use an existing Secret Key to + recover your derived keys. +

+
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+
+

diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 61942ad153..f7a80fc278 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -409,7 +409,7 @@ "label": "Private Key" }, "public_key": { - "label": "Public Key" + "label": "Nostr Public Key" }, "generate_keys": { "title": "Generate a new Nostr key", From e9e59d0d7654df09c0be5e34c29c9987fcff7e7d Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 25 Apr 2023 13:55:23 +0700 Subject: [PATCH 012/118] feat: backup secret key component WIP --- src/app/components/form/Input/index.tsx | 9 +- src/app/router/Options/Options.tsx | 11 +- .../Accounts/BackupSecretKey/index.tsx | 155 ++++++++++++++++++ src/app/screens/Accounts/Detail/index.tsx | 10 +- .../Accounts/GenerateSecretKey/index.tsx | 62 ------- .../Accounts/ImportSecretKey/index.tsx | 5 + 6 files changed, 177 insertions(+), 75 deletions(-) create mode 100644 src/app/screens/Accounts/BackupSecretKey/index.tsx delete mode 100644 src/app/screens/Accounts/GenerateSecretKey/index.tsx create mode 100644 src/app/screens/Accounts/ImportSecretKey/index.tsx diff --git a/src/app/components/form/Input/index.tsx b/src/app/components/form/Input/index.tsx index 2942742200..968ec1880f 100644 --- a/src/app/components/form/Input/index.tsx +++ b/src/app/components/form/Input/index.tsx @@ -5,6 +5,7 @@ import { classNames } from "../../../utils"; type Props = { suffix?: string; endAdornment?: React.ReactNode; + block?: boolean; }; export default function Input({ @@ -26,6 +27,8 @@ export default function Input({ max, suffix, endAdornment, + block = true, + className, }: React.InputHTMLAttributes & Props) { const inputEl = useRef(null); const outerStyles = @@ -38,13 +41,15 @@ export default function Input({ name={name} id={id} className={classNames( - "block w-full placeholder-gray-500 dark:placeholder-neutral-600", + "placeholder-gray-500 dark:placeholder-neutral-600", + block && "block w-full", !suffix && !endAdornment ? `${outerStyles} focus:ring-primary focus:border-primary focus:ring-1` : "pr-0 border-0 focus:ring-0", disabled ? "bg-gray-50 dark:bg-surface-01dp text-gray-500 dark:text-neutral-500" - : "bg-white dark:bg-black dark:text-white" + : "bg-white dark:bg-black dark:text-white", + !!className && className )} placeholder={placeholder} required={required} diff --git a/src/app/router/Options/Options.tsx b/src/app/router/Options/Options.tsx index d57da4c342..f2ae6f9ec5 100644 --- a/src/app/router/Options/Options.tsx +++ b/src/app/router/Options/Options.tsx @@ -22,7 +22,8 @@ import { ToastContainer } from "react-toastify"; import Providers from "~/app/context/Providers"; import RequireAuth from "~/app/router/RequireAuth"; import getConnectorRoutes from "~/app/router/connectorRoutes"; -import GenerateSecretKey from "~/app/screens/Accounts/GenerateSecretKey"; +import BackupSecretKey from "~/app/screens/Accounts/BackupSecretKey"; +import ImportSecretKey from "~/app/screens/Accounts/ImportSecretKey"; import Discover from "~/app/screens/Discover"; import AlbyWallet from "~/app/screens/connectors/AlbyWallet"; import ChooseConnector from "~/app/screens/connectors/ChooseConnector"; @@ -65,8 +66,12 @@ function Options() { } /> } + path=":id/secret-key/backup" + element={} + /> + } /> (); + const account = useAccount(); + const { t: tCommon } = useTranslation("common"); + const [publicKeyCopyLabel, setPublicKeyCopyLabel] = React.useState( + tCommon("actions.copy") as string + ); + + React.useEffect(() => { + // TODO: only generate mnemonic if account doesn't have one yet + setMnemonic(bip39.generateMnemonic(wordlist, 128)); + }, []); + + /*const { t } = useTranslation("translation", { + keyPrefix: "accounts.account_view", + }); + const { t: tCommon } = useTranslation("common");*/ + + async function saveSecretKey() { + try { + // TODO: re-add + if (!account) { + // type guard + throw new Error("No account available"); + } + + alert("Mnemonic: " + mnemomic); + + // TODO: make sure secret key doesn't already exist + + // await msg.request("secretKey/save", { + // id: account.id, + // mnemomic, + // }); + // toast.success(t("nostr.private_key.success")); + // } + } catch (e) { + if (e instanceof Error) toast.error(e.message); + } + } + + return !account ? ( +
+ +
+ ) : ( +
+ +
+

+ {SECRET_KEY_EXISTS + ? "Back up your Secret Key" + : "Generate your Secret Key"} +

+

+ In addition to Bitcoin Lightning Network, Alby allows you to + generate keys and interact with other protocols such as: +

+

+ Secret Key is a set of 12 words that will allow you to access your + keys to those protocols on a new device or in case you loose access + to your account: +

+ + {/* TODO: consider making CopyButton component */} +
+ {!SECRET_KEY_EXISTS && ( +
+
+ )} +
+
+ ); +} + +export default BackupSecretKey; + +// TODO: move to separate file +type MnemonicInputsProps = { + mnemonic?: string; + disabled?: boolean; +}; + +function MnemonicInputs({ + mnemonic, + disabled, + children, +}: React.PropsWithChildren) { + const words = mnemonic?.split(" ") || []; + + return ( +
+

{"Your Secret Key"}

+
+ {[...new Array(12)].map((_, i) => ( +
+ {i + 1}. + +
+ ))} +
+ {children} +
+ ); +} diff --git a/src/app/screens/Accounts/Detail/index.tsx b/src/app/screens/Accounts/Detail/index.tsx index 903edae2ba..663fd9716b 100644 --- a/src/app/screens/Accounts/Detail/index.tsx +++ b/src/app/screens/Accounts/Detail/index.tsx @@ -454,13 +454,7 @@ function AccountDetail() {

- +
- ) : ( -
- -
- Test -
-
- ); -} - -export default GenerateSecretKey; diff --git a/src/app/screens/Accounts/ImportSecretKey/index.tsx b/src/app/screens/Accounts/ImportSecretKey/index.tsx new file mode 100644 index 0000000000..1004209928 --- /dev/null +++ b/src/app/screens/Accounts/ImportSecretKey/index.tsx @@ -0,0 +1,5 @@ +function ImportSecretKey() { + return

TODO

; +} + +export default ImportSecretKey; From be6abfd0f13b58780381bce83162c5e378054527 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 25 Apr 2023 14:06:53 +0700 Subject: [PATCH 013/118] feat: add copy and confirmation to backup secret key component --- .../Accounts/BackupSecretKey/index.tsx | 77 +++++++++++++------ 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/src/app/screens/Accounts/BackupSecretKey/index.tsx b/src/app/screens/Accounts/BackupSecretKey/index.tsx index c3fdc2754f..8849af631b 100644 --- a/src/app/screens/Accounts/BackupSecretKey/index.tsx +++ b/src/app/screens/Accounts/BackupSecretKey/index.tsx @@ -3,10 +3,11 @@ import Container from "@components/Container"; import Loading from "@components/Loading"; import * as bip39 from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english"; -import React from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; import Button from "~/app/components/Button"; +import Checkbox from "~/app/components/form/Checkbox"; import Input from "~/app/components/form/Input"; import { useAccount } from "~/app/context/AccountContext"; @@ -14,12 +15,13 @@ import { useAccount } from "~/app/context/AccountContext"; const SECRET_KEY_EXISTS = false; function BackupSecretKey() { - const [mnemomic, setMnemonic] = React.useState(); + const [mnemomic, setMnemonic] = useState(); const account = useAccount(); const { t: tCommon } = useTranslation("common"); - const [publicKeyCopyLabel, setPublicKeyCopyLabel] = React.useState( + const [publicKeyCopyLabel, setPublicKeyCopyLabel] = useState( tCommon("actions.copy") as string ); + const [hasBackedUp, setBackedUp] = useState(false); React.useEffect(() => { // TODO: only generate mnemonic if account doesn't have one yet @@ -33,6 +35,11 @@ function BackupSecretKey() { async function saveSecretKey() { try { + if (!hasBackedUp) { + throw new Error( + "Please confirm that you have backed up your secret key." + ); + } // TODO: re-add if (!account) { // type guard @@ -77,28 +84,50 @@ function BackupSecretKey() { to your account:

- {/* TODO: consider making CopyButton component */} -
{!SECRET_KEY_EXISTS && ( From 8871fb12265259b99743d0e3072621ea2961dbbf Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 25 Apr 2023 17:21:22 +0700 Subject: [PATCH 014/118] feat: scroll to top on options navigation --- src/app/components/ScrollToTop/index.tsx | 14 ++++++++++++++ src/app/router/Options/Options.tsx | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 src/app/components/ScrollToTop/index.tsx diff --git a/src/app/components/ScrollToTop/index.tsx b/src/app/components/ScrollToTop/index.tsx new file mode 100644 index 0000000000..35e53a28af --- /dev/null +++ b/src/app/components/ScrollToTop/index.tsx @@ -0,0 +1,14 @@ +import { useEffect } from "react"; +import { useLocation } from "react-router-dom"; + +function ScrollToTop() { + const location = useLocation(); + // Scroll to top if path changes + useEffect(() => { + window.scrollTo(0, 0); + }, [location.pathname]); + + return null; +} + +export default ScrollToTop; diff --git a/src/app/router/Options/Options.tsx b/src/app/router/Options/Options.tsx index f2ae6f9ec5..4366b76912 100644 --- a/src/app/router/Options/Options.tsx +++ b/src/app/router/Options/Options.tsx @@ -19,6 +19,7 @@ import Unlock from "@screens/Unlock"; import { useTranslation } from "react-i18next"; import { HashRouter, Navigate, Outlet, Route, Routes } from "react-router-dom"; import { ToastContainer } from "react-toastify"; +import ScrollToTop from "~/app/components/ScrollToTop"; import Providers from "~/app/context/Providers"; import RequireAuth from "~/app/router/RequireAuth"; import getConnectorRoutes from "~/app/router/connectorRoutes"; @@ -32,10 +33,10 @@ import i18n from "~/i18n/i18nConfig"; function Options() { const connectorRoutes = getConnectorRoutes(); - return ( + Date: Tue, 25 Apr 2023 17:24:07 +0700 Subject: [PATCH 015/118] feat: improve styling in BackupSecretKey screen --- src/app/icons/LiquidIcon.tsx | 44 +++++++++++++++++++ src/app/icons/NostrIcon.tsx | 18 ++++++++ src/app/icons/OrdinalsIcon.tsx | 34 ++++++++++++++ .../Accounts/BackupSecretKey/index.tsx | 33 +++++++++++--- 4 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 src/app/icons/LiquidIcon.tsx create mode 100644 src/app/icons/NostrIcon.tsx create mode 100644 src/app/icons/OrdinalsIcon.tsx diff --git a/src/app/icons/LiquidIcon.tsx b/src/app/icons/LiquidIcon.tsx new file mode 100644 index 0000000000..5f2a1de74b --- /dev/null +++ b/src/app/icons/LiquidIcon.tsx @@ -0,0 +1,44 @@ +import { SVGProps } from "react"; + +const LiquidIcon = (props: SVGProps) => ( + + + + + + + +); +export default LiquidIcon; diff --git a/src/app/icons/NostrIcon.tsx b/src/app/icons/NostrIcon.tsx new file mode 100644 index 0000000000..44ea4163bb --- /dev/null +++ b/src/app/icons/NostrIcon.tsx @@ -0,0 +1,18 @@ +import { SVGProps } from "react"; + +const NostrIcon = (props: SVGProps) => ( + + + +); +export default NostrIcon; diff --git a/src/app/icons/OrdinalsIcon.tsx b/src/app/icons/OrdinalsIcon.tsx new file mode 100644 index 0000000000..9fefebe8ce --- /dev/null +++ b/src/app/icons/OrdinalsIcon.tsx @@ -0,0 +1,34 @@ +import { SVGProps } from "react"; + +const OrdinalsIcon = (props: SVGProps) => ( + + + + + + + + + + + + + + + + +); +export default OrdinalsIcon; diff --git a/src/app/screens/Accounts/BackupSecretKey/index.tsx b/src/app/screens/Accounts/BackupSecretKey/index.tsx index 8849af631b..b690c1e947 100644 --- a/src/app/screens/Accounts/BackupSecretKey/index.tsx +++ b/src/app/screens/Accounts/BackupSecretKey/index.tsx @@ -3,13 +3,15 @@ import Container from "@components/Container"; import Loading from "@components/Loading"; import * as bip39 from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; import Button from "~/app/components/Button"; import Checkbox from "~/app/components/form/Checkbox"; import Input from "~/app/components/form/Input"; import { useAccount } from "~/app/context/AccountContext"; +import NostrIcon from "~/app/icons/NostrIcon"; +import OrdinalsIcon from "~/app/icons/OrdinalsIcon"; // TODO: replace with checking account const SECRET_KEY_EXISTS = false; @@ -23,7 +25,7 @@ function BackupSecretKey() { ); const [hasBackedUp, setBackedUp] = useState(false); - React.useEffect(() => { + useEffect(() => { // TODO: only generate mnemonic if account doesn't have one yet setMnemonic(bip39.generateMnemonic(wordlist, 128)); }, []); @@ -56,6 +58,8 @@ function BackupSecretKey() { // }); // toast.success(t("nostr.private_key.success")); // } + toast.success(/*t("nostr.private_key.success")*/ "Secret Key saved"); + history.back(); } catch (e) { if (e instanceof Error) toast.error(e.message); } @@ -68,17 +72,23 @@ function BackupSecretKey() { ) : (
-
-

+
+

{SECRET_KEY_EXISTS ? "Back up your Secret Key" : "Generate your Secret Key"}

-

+

In addition to Bitcoin Lightning Network, Alby allows you to generate keys and interact with other protocols such as:

-

+

+ } title="Nostr protocol" /> + } title="Ordinals" /> + {/* } title="Liquid" /> */} +
+ +

Secret Key is a set of 12 words that will allow you to access your keys to those protocols on a new device or in case you loose access to your account: @@ -146,6 +156,17 @@ function BackupSecretKey() { export default BackupSecretKey; +type ProtocolListItemProps = { icon: React.ReactNode; title: string }; + +function ProtocolListItem({ icon, title }: ProtocolListItemProps) { + return ( +

+ {icon} + {title} +
+ ); +} + // TODO: move to separate file type MnemonicInputsProps = { mnemonic?: string; From 96a6a20c9f80428adf742a460b18467ca675e0f1 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 25 Apr 2023 17:54:14 +0700 Subject: [PATCH 016/118] feat: import secret key screen --- src/app/components/MnemonicInputs/index.tsx | 47 ++++++++ .../Accounts/BackupSecretKey/index.tsx | 56 ++------- .../Accounts/ImportSecretKey/index.tsx | 108 +++++++++++++++++- src/i18n/locales/en/translation.json | 5 + 4 files changed, 168 insertions(+), 48 deletions(-) create mode 100644 src/app/components/MnemonicInputs/index.tsx diff --git a/src/app/components/MnemonicInputs/index.tsx b/src/app/components/MnemonicInputs/index.tsx new file mode 100644 index 0000000000..76ca0056ac --- /dev/null +++ b/src/app/components/MnemonicInputs/index.tsx @@ -0,0 +1,47 @@ +import { wordlist } from "@scure/bip39/wordlists/english"; +import Input from "~/app/components/form/Input"; + +type MnemonicInputsProps = { + mnemonic?: string; + setMnemonic?(mnemonic: string): void; + disabled?: boolean; +}; + +export default function MnemonicInputs({ + mnemonic, + setMnemonic, + disabled, + children, +}: React.PropsWithChildren) { + const words = mnemonic?.split(" ") || []; + while (words.length < 12) { + words.push(""); + } + + return ( +
+

{"Your Secret Key"}

+
+ {[...new Array(12)].map((_, i) => ( +
+ {i + 1}. + { + words[i] = e.target.value; + setMnemonic?.(words.join(" ")); + }} + /> +
+ ))} +
+ {children} +
+ ); +} diff --git a/src/app/screens/Accounts/BackupSecretKey/index.tsx b/src/app/screens/Accounts/BackupSecretKey/index.tsx index b690c1e947..efa5b19227 100644 --- a/src/app/screens/Accounts/BackupSecretKey/index.tsx +++ b/src/app/screens/Accounts/BackupSecretKey/index.tsx @@ -7,8 +7,8 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; import Button from "~/app/components/Button"; +import MnemonicInputs from "~/app/components/MnemonicInputs"; import Checkbox from "~/app/components/form/Checkbox"; -import Input from "~/app/components/form/Input"; import { useAccount } from "~/app/context/AccountContext"; import NostrIcon from "~/app/icons/NostrIcon"; import OrdinalsIcon from "~/app/icons/OrdinalsIcon"; @@ -17,11 +17,11 @@ import OrdinalsIcon from "~/app/icons/OrdinalsIcon"; const SECRET_KEY_EXISTS = false; function BackupSecretKey() { - const [mnemomic, setMnemonic] = useState(); + const [mnemonic, setMnemonic] = useState(); const account = useAccount(); const { t: tCommon } = useTranslation("common"); const [publicKeyCopyLabel, setPublicKeyCopyLabel] = useState( - tCommon("actions.copy") as string + tCommon("actions.copy_clipboard") as string ); const [hasBackedUp, setBackedUp] = useState(false); @@ -32,8 +32,7 @@ function BackupSecretKey() { /*const { t } = useTranslation("translation", { keyPrefix: "accounts.account_view", - }); - const { t: tCommon } = useTranslation("common");*/ + });*/ async function saveSecretKey() { try { @@ -48,7 +47,7 @@ function BackupSecretKey() { throw new Error("No account available"); } - alert("Mnemonic: " + mnemomic); + alert("Mnemonic: " + mnemonic); // TODO: make sure secret key doesn't already exist @@ -93,7 +92,7 @@ function BackupSecretKey() { keys to those protocols on a new device or in case you loose access to your account:

- + <> {/* TODO: consider making CopyButton component */}
); } - -// TODO: move to separate file -type MnemonicInputsProps = { - mnemonic?: string; - disabled?: boolean; -}; - -function MnemonicInputs({ - mnemonic, - disabled, - children, -}: React.PropsWithChildren) { - const words = mnemonic?.split(" ") || []; - - return ( -
-

{"Your Secret Key"}

-
- {[...new Array(12)].map((_, i) => ( -
- {i + 1}. - -
- ))} -
- {children} -
- ); -} diff --git a/src/app/screens/Accounts/ImportSecretKey/index.tsx b/src/app/screens/Accounts/ImportSecretKey/index.tsx index 1004209928..2da16f83fe 100644 --- a/src/app/screens/Accounts/ImportSecretKey/index.tsx +++ b/src/app/screens/Accounts/ImportSecretKey/index.tsx @@ -1,5 +1,111 @@ +import { CopyIcon } from "@bitcoin-design/bitcoin-icons-react/filled"; +import Container from "@components/Container"; +import Loading from "@components/Loading"; +import * as bip39 from "@scure/bip39"; +import { wordlist } from "@scure/bip39/wordlists/english"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "react-toastify"; +import Button from "~/app/components/Button"; +import MnemonicInputs from "~/app/components/MnemonicInputs"; +import { useAccount } from "~/app/context/AccountContext"; + function ImportSecretKey() { - return

TODO

; + const [mnemonic, setMnemonic] = useState(""); + const account = useAccount(); + const { t: tCommon } = useTranslation("common"); + const [importPasteLabel, setImportPasteLabel] = useState( + tCommon("actions.paste_clipboard") as string + ); + + /*const { t } = useTranslation("translation", { + keyPrefix: "accounts.account_view", + });*/ + + function cancelImport() { + history.back(); + } + + async function importKey() { + try { + // TODO: re-add + if (!account) { + // type guard + throw new Error("No account available"); + } + + if ( + mnemonic.split(" ").length !== 12 || + !bip39.validateMnemonic(mnemonic, wordlist) + ) { + throw new Error("Invalid mnemonic"); + } + + alert("Mnemonic: " + mnemonic); + + // TODO: make sure secret key doesn't already exist + + // await msg.request("secretKey/save", { + // id: account.id, + // mnemomic, + // }); + // toast.success(t("nostr.private_key.success")); + // } + toast.success(/*t("nostr.private_key.success")*/ "Secret Key saved"); + history.back(); + } catch (e) { + if (e instanceof Error) toast.error(e.message); + } + } + + return !account ? ( +
+ +
+ ) : ( +
+ +
+

Import Secret Key

+

+ Use existing Secret Key to derive protocol keys: +

+ + + <> +
+ +
+
+
+
+ ); } export default ImportSecretKey; diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index f7a80fc278..23693358c0 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -793,6 +793,7 @@ "optional": "Optional", "feedback": "Feedback", "copied": "Copied!", + "pasted": "Pasted!", "description": "Description", "description_full": "Full Description", "success_message": "{{amount}}{{fiatAmount}} sent to {{destination}}", @@ -814,6 +815,7 @@ "unlock": "Unlock", "send": "Send", "save": "Save", + "import": "Import", "receive": "Receive", "receive_again": "Receive another payment", "transactions": "Transactions", @@ -821,6 +823,9 @@ "export": "Export", "remove": "Remove", "copy": "Copy", + "copy_clipboard": "Copy to clipboard", + "paste": "Paste", + "paste_clipboard": "Paste from clipboard", "log_in": "Log in", "remember": "Remember my choice and don't ask again" }, From 6e558a348a955400c984bf2b27e8b38c9d9194d3 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 26 Apr 2023 18:36:10 +0700 Subject: [PATCH 017/118] chore: display nostr key origin WIP --- src/app/components/Badge/index.tsx | 2 +- .../Accounts/BackupSecretKey/index.tsx | 3 + src/app/screens/Accounts/Detail/index.tsx | 87 +++++++++---------- .../Accounts/ImportSecretKey/index.tsx | 3 + .../actions/nostr/getKeyOrigin.ts | 25 ++++++ .../background-script/actions/nostr/index.ts | 2 + src/extension/background-script/router.ts | 1 + src/i18n/locales/en/translation.json | 4 +- src/types.ts | 7 ++ 9 files changed, 88 insertions(+), 46 deletions(-) create mode 100644 src/extension/background-script/actions/nostr/getKeyOrigin.ts diff --git a/src/app/components/Badge/index.tsx b/src/app/components/Badge/index.tsx index a0c23ae60a..b9c87c1fdf 100644 --- a/src/app/components/Badge/index.tsx +++ b/src/app/components/Badge/index.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; type Props = { - label: "active" | "auth"; + label: "active" | "auth" | "derived" | "unknown"; color: string; textColor: string; small?: boolean; diff --git a/src/app/screens/Accounts/BackupSecretKey/index.tsx b/src/app/screens/Accounts/BackupSecretKey/index.tsx index efa5b19227..ae7779dcc2 100644 --- a/src/app/screens/Accounts/BackupSecretKey/index.tsx +++ b/src/app/screens/Accounts/BackupSecretKey/index.tsx @@ -50,6 +50,9 @@ function BackupSecretKey() { alert("Mnemonic: " + mnemonic); // TODO: make sure secret key doesn't already exist + // TODO: check if nostr key exists and warn about replacement - where should this happen? + // TODO: save key and regenerate derived keys + // TODO: this code should be shared between Import & Backup // await msg.request("secretKey/save", { // id: account.id, diff --git a/src/app/screens/Accounts/Detail/index.tsx b/src/app/screens/Accounts/Detail/index.tsx index 663fd9716b..cdfa5e6167 100644 --- a/src/app/screens/Accounts/Detail/index.tsx +++ b/src/app/screens/Accounts/Detail/index.tsx @@ -1,5 +1,6 @@ import { CaretLeftIcon, + CopyIcon as CopyFilledIcon, ExportIcon, } from "@bitcoin-design/bitcoin-icons-react/filled"; import { @@ -25,6 +26,7 @@ import QRCode from "react-qr-code"; import { Link, useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; import Avatar from "~/app/components/Avatar"; +import Badge from "~/app/components/Badge"; import MenuDivider from "~/app/components/Menu/MenuDivider"; import { useAccount } from "~/app/context/AccountContext"; import { useAccounts } from "~/app/context/AccountsContext"; @@ -66,13 +68,14 @@ function AccountDetail() { const [currentPrivateKey, setCurrentPrivateKey] = useState(""); const [nostrPrivateKey, setNostrPrivateKey] = useState(""); const [nostrPublicKey, setNostrPublicKey] = useState(""); + const [nostrKeyOrigin, setNostrKeyOrigin] = useState<"derived" | "unknown">( + "unknown" + ); const [nostrPrivateKeyVisible, setNostrPrivateKeyVisible] = useState(false); const [privateKeyCopyLabel, setPrivateKeyCopyLabel] = useState( tCommon("actions.copy") as string ); - const [publicKeyCopyLabel, setPublicKeyCopyLabel] = useState( - tCommon("actions.copy") as string - ); + const [publicKeyCopied, setPublicKeyCopied] = useState(false); const [exportLoading, setExportLoading] = useState(false); const [exportModalIsOpen, setExportModalIsOpen] = useState(false); @@ -98,6 +101,10 @@ function AccountDetail() { if (priv) { setCurrentPrivateKey(priv); } + const keyOrigin = (await msg.request("nostr/getKeyOrigin", { + id, + })) as FixMe; + setNostrKeyOrigin(keyOrigin); } } catch (e) { console.error(e); @@ -491,33 +498,44 @@ function AccountDetail() {

-
+
-
-
-
@@ -526,7 +544,7 @@ function AccountDetail() { label={/*tCommon("actions.save")*/ "Advanced Settings"} primary fullWidth - onClick={() => alert("TODO")} + onClick={() => setNostrKeyModalIsOpen(true)} />
@@ -657,26 +675,7 @@ function AccountDetail() { disabled />
-
-
+
diff --git a/src/app/screens/Accounts/ImportSecretKey/index.tsx b/src/app/screens/Accounts/ImportSecretKey/index.tsx index 2da16f83fe..90d4e9368c 100644 --- a/src/app/screens/Accounts/ImportSecretKey/index.tsx +++ b/src/app/screens/Accounts/ImportSecretKey/index.tsx @@ -44,6 +44,9 @@ function ImportSecretKey() { alert("Mnemonic: " + mnemonic); // TODO: make sure secret key doesn't already exist + // TODO: check if nostr key exists and warn about replacement - where should this happen? + // TODO: save key and regenerate derived keys + // TODO: this code should be shared between Import & Backup // await msg.request("secretKey/save", { // id: account.id, diff --git a/src/extension/background-script/actions/nostr/getKeyOrigin.ts b/src/extension/background-script/actions/nostr/getKeyOrigin.ts new file mode 100644 index 0000000000..bb654ba76a --- /dev/null +++ b/src/extension/background-script/actions/nostr/getKeyOrigin.ts @@ -0,0 +1,25 @@ +import { MessageKeyOrigin } from "~/types"; + +import generatePrivateKey from "./generatePrivateKey"; +import getPrivateKey from "./getPrivateKey"; + +const getKeyOrigin = async (message: MessageKeyOrigin) => { + const privateKey = await getPrivateKey({ + ...message, + action: "getPrivateKey", + }); + // TODO: check against secret key + const derivedKey = await generatePrivateKey({ + ...message, + action: "generatePrivateKey", + args: { + type: undefined, + }, + }); + return { + data: + derivedKey.data?.privateKey === privateKey.data ? "derived" : "unknown", + }; +}; + +export default getKeyOrigin; diff --git a/src/extension/background-script/actions/nostr/index.ts b/src/extension/background-script/actions/nostr/index.ts index 7a0896fe6c..3e08c24fe5 100644 --- a/src/extension/background-script/actions/nostr/index.ts +++ b/src/extension/background-script/actions/nostr/index.ts @@ -1,6 +1,7 @@ import decryptOrPrompt from "./decryptOrPrompt"; import encryptOrPrompt from "./encryptOrPrompt"; import generatePrivateKey from "./generatePrivateKey"; +import getKeyOrigin from "./getKeyOrigin"; import getPrivateKey from "./getPrivateKey"; import getPublicKeyOrPrompt from "./getPublicKeyOrPrompt"; import getRelays from "./getRelays"; @@ -12,6 +13,7 @@ import signSchnorrOrPrompt from "./signSchnorrOrPrompt"; export { generatePrivateKey, getPrivateKey, + getKeyOrigin, removePrivateKey, setPrivateKey, getPublicKeyOrPrompt, diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index 1295ec389d..783597b97a 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -63,6 +63,7 @@ const routes = { nostr: { generatePrivateKey: nostr.generatePrivateKey, getPrivateKey: nostr.getPrivateKey, + getKeyOrigin: nostr.getKeyOrigin, removePrivateKey: nostr.removePrivateKey, setPrivateKey: nostr.setPrivateKey, }, diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 23693358c0..edc352ba43 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -936,7 +936,9 @@ "badge": { "label": { "active": "ACTIVE", - "auth": "LOGIN" + "auth": "LOGIN", + "derived": "DERIVED", + "unknown": "UNKNOWN" } } }, diff --git a/src/types.ts b/src/types.ts index 46392831a0..dd2d98722d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -428,6 +428,13 @@ export interface MessagePublicKeyGet extends MessageDefault { action: "getPublicKeyOrPrompt"; } +export interface MessageKeyOrigin extends MessageDefault { + args?: { + id?: Account["id"]; + }; + action: "getKeyOrigin"; +} + export interface MessagePrivateKeyGet extends MessageDefault { args?: { id?: Account["id"]; From 8f6fa22590d9316c065f288b1c3e692b14f4553c Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 26 Apr 2023 20:46:07 +0700 Subject: [PATCH 018/118] feat: new advanced nostr settings screen --- src/app/components/Badge/index.tsx | 2 +- src/app/components/InputCopyButton/index.tsx | 41 +++ src/app/router/Options/Options.tsx | 2 + src/app/screens/Accounts/Detail/index.tsx | 336 ++---------------- .../Accounts/NostrAdvancedSettings/index.tsx | 240 +++++++++++++ src/i18n/locales/en/translation.json | 5 +- 6 files changed, 309 insertions(+), 317 deletions(-) create mode 100644 src/app/components/InputCopyButton/index.tsx create mode 100644 src/app/screens/Accounts/NostrAdvancedSettings/index.tsx diff --git a/src/app/components/Badge/index.tsx b/src/app/components/Badge/index.tsx index b9c87c1fdf..810b4dfcdf 100644 --- a/src/app/components/Badge/index.tsx +++ b/src/app/components/Badge/index.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; type Props = { - label: "active" | "auth" | "derived" | "unknown"; + label: "active" | "auth" | "imported"; color: string; textColor: string; small?: boolean; diff --git a/src/app/components/InputCopyButton/index.tsx b/src/app/components/InputCopyButton/index.tsx new file mode 100644 index 0000000000..e8b8d63daa --- /dev/null +++ b/src/app/components/InputCopyButton/index.tsx @@ -0,0 +1,41 @@ +import { CopyIcon as CopyFilledIcon } from "@bitcoin-design/bitcoin-icons-react/filled"; +import { CopyIcon } from "@bitcoin-design/bitcoin-icons-react/outline"; +import { useState } from "react"; +import { toast } from "react-toastify"; +import { classNames } from "~/app/utils"; + +type Props = { + value: string; + className?: string; +}; + +function InputCopyButton({ value, className }: Props) { + const [copied, setCopied] = useState(false); + const CurrentIcon = copied ? CopyFilledIcon : CopyIcon; + return ( + + ); +} +export default InputCopyButton; diff --git a/src/app/router/Options/Options.tsx b/src/app/router/Options/Options.tsx index 4366b76912..41ab44e10f 100644 --- a/src/app/router/Options/Options.tsx +++ b/src/app/router/Options/Options.tsx @@ -25,6 +25,7 @@ import RequireAuth from "~/app/router/RequireAuth"; import getConnectorRoutes from "~/app/router/connectorRoutes"; import BackupSecretKey from "~/app/screens/Accounts/BackupSecretKey"; import ImportSecretKey from "~/app/screens/Accounts/ImportSecretKey"; +import NostrAdvancedSettings from "~/app/screens/Accounts/NostrAdvancedSettings"; import Discover from "~/app/screens/Discover"; import AlbyWallet from "~/app/screens/connectors/AlbyWallet"; import ChooseConnector from "~/app/screens/connectors/ChooseConnector"; @@ -74,6 +75,7 @@ function Options() { path=":id/secret-key/import" element={} /> + } /> ( - "unknown" - ); - const [nostrPrivateKeyVisible, setNostrPrivateKeyVisible] = useState(false); - const [privateKeyCopyLabel, setPrivateKeyCopyLabel] = useState( - tCommon("actions.copy") as string - ); - const [publicKeyCopied, setPublicKeyCopied] = useState(false); + const [nostrKeyOrigin, setNostrKeyOrigin] = useState< + "derived" | "unknown" | "secret-key" + >("unknown"); const [exportLoading, setExportLoading] = useState(false); const [exportModalIsOpen, setExportModalIsOpen] = useState(false); - const [nostrKeyModalIsOpen, setNostrKeyModalIsOpen] = useState(false); const fetchData = useCallback(async () => { try { @@ -87,11 +75,7 @@ function AccountDetail() { const response = await msg.request("getAccount", { id, }); - // for backwards compatibility - // TODO: remove. if you ask when, then it's probably now. - if (!response.id) { - response.id = id; - } + setAccount(response); setAccountName(response.name); @@ -116,82 +100,14 @@ function AccountDetail() { setExportModalIsOpen(false); } - function closeNostrKeyModal() { - setNostrKeyModalIsOpen(false); - } - + // TODO: make utility function function generatePublicKey(priv: string) { + // FIXME: this is using code from background script const nostr = new Nostr(priv); const pubkeyHex = nostr.getPublicKey(); return nostrlib.hexToNip19(pubkeyHex, "npub"); } - async function generateNostrPrivateKey(random?: boolean) { - const selectedAccount = await auth.fetchAccountInfo(); - - if (!random && selectedAccount?.id !== id) { - alert( - `Please match the account in the account dropdown at the top with this account to derive keys.` - ); - closeNostrKeyModal(); - return; - } - // check with current selected account - const result = await msg.request( - "nostr/generatePrivateKey", - random - ? { - type: "random", - } - : undefined - ); - saveNostrPrivateKey(result.privateKey as string); - closeNostrKeyModal(); - } - - async function saveNostrPrivateKey(nostrPrivateKey: string) { - nostrPrivateKey = nostrlib.normalizeToHex(nostrPrivateKey); - - if (nostrPrivateKey === currentPrivateKey) return; - - if ( - currentPrivateKey && - prompt(t("nostr.private_key.warning"))?.toLowerCase() !== - account?.name?.toLowerCase() - ) { - toast.error(t("nostr.private_key.failed_to_remove")); - return; - } - - try { - if (!account) { - // type guard - throw new Error("No account available"); - } - - if (nostrPrivateKey) { - // Validate the private key before saving - generatePublicKey(nostrPrivateKey); - nostrlib.hexToNip19(nostrPrivateKey, "nsec"); - - await msg.request("nostr/setPrivateKey", { - id: account.id, - privateKey: nostrPrivateKey, - }); - toast.success(t("nostr.private_key.success")); - } else { - await msg.request("nostr/removePrivateKey", { - id: account.id, - }); - toast.success(t("nostr.private_key.successfully_removed")); - } - - setCurrentPrivateKey(nostrPrivateKey); - } catch (e) { - if (e instanceof Error) toast.error(e.message); - } - } - async function updateAccountName({ id, name }: AccountAction) { await msg.request("editAccount", { name, @@ -254,9 +170,6 @@ function AccountDetail() { setNostrPublicKey( currentPrivateKey ? generatePublicKey(currentPrivateKey) : "" ); - setNostrPrivateKey( - currentPrivateKey ? nostrlib.hexToNip19(currentPrivateKey, "nsec") : "" - ); } catch (e) { if (e instanceof Error) toast.error( @@ -505,178 +418,26 @@ function AccountDetail() { type="text" value={nostrPublicKey} disabled - endAdornment={ - - } - /> - -
-
-
-
- - -
-
- +
- -
@@ -703,57 +464,6 @@ function AccountDetail() { - - -
-

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

- -
-
- , - ]} - /> -
-
-
-
-
-
diff --git a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx new file mode 100644 index 0000000000..597b65b04e --- /dev/null +++ b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx @@ -0,0 +1,240 @@ +import { + HiddenIcon, + VisibleIcon, +} from "@bitcoin-design/bitcoin-icons-react/filled"; +import Container from "@components/Container"; +import Loading from "@components/Loading"; +import { FormEvent, useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useParams } from "react-router-dom"; +import { toast } from "react-toastify"; +import Button from "~/app/components/Button"; +import InputCopyButton from "~/app/components/InputCopyButton"; +import TextField from "~/app/components/form/TextField"; +import { useAccount } from "~/app/context/AccountContext"; +import msg from "~/common/lib/msg"; +import nostrlib from "~/common/lib/nostr"; +import Nostr from "~/extension/background-script/nostr"; + +// import { GetAccountRes } from "~/common/lib/api"; + +function NostrAdvancedSettings() { + const account = useAccount(); + //const [account, setAccount] = useState(null); + const { t: tCommon } = useTranslation("common"); + // TODO: move these translations to the correct place + const { t } = useTranslation("translation", { + keyPrefix: "accounts.account_view", + }); + const [currentPrivateKey, setCurrentPrivateKey] = useState(""); + const [nostrPrivateKey, setNostrPrivateKey] = useState(""); + const [nostrPrivateKeyVisible, setNostrPrivateKeyVisible] = useState(false); + const [nostrPublicKey, setNostrPublicKey] = useState(""); + const [nostrKeyOrigin, setNostrKeyOrigin] = useState< + "derived" | "unknown" | "secret-key" + >("unknown"); + const { id } = useParams(); + + const fetchData = useCallback(async () => { + try { + if (id) { + const priv = (await msg.request("nostr/getPrivateKey", { + id, + })) as string; + if (priv) { + setCurrentPrivateKey(priv); + } + const keyOrigin = (await msg.request("nostr/getKeyOrigin", { + id, + })) as FixMe; + setNostrKeyOrigin(keyOrigin); + } + } catch (e) { + console.error(e); + if (e instanceof Error) toast.error(`Error: ${e.message}`); + } + }, [id]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + // TODO: make utility function + function generatePublicKey(priv: string) { + // FIXME: this is using code from background script + const nostr = new Nostr(priv); + const pubkeyHex = nostr.getPublicKey(); + return nostrlib.hexToNip19(pubkeyHex, "npub"); + } + + useEffect(() => { + try { + setNostrPublicKey( + currentPrivateKey ? generatePublicKey(currentPrivateKey) : "" + ); + setNostrPrivateKey( + currentPrivateKey ? nostrlib.hexToNip19(currentPrivateKey, "nsec") : "" + ); + } catch (e) { + if (e instanceof Error) + toast.error( +

+ {t("nostr.errors.failed_to_load")} +
+ {e.message} +

+ ); + } + }, [currentPrivateKey, t]); + + function onCancel() { + history.back(); + } + + function deriveNostrKeyFromSecretKey() { + alert("TODO"); + } + + async function saveNostrPrivateKey(nostrPrivateKey: string) { + nostrPrivateKey = nostrlib.normalizeToHex(nostrPrivateKey); + + if (nostrPrivateKey === currentPrivateKey) return; + + if ( + currentPrivateKey && + prompt(t("nostr.private_key.warning"))?.toLowerCase() !== + account?.account?.name?.toLowerCase() + ) { + toast.error(t("nostr.private_key.failed_to_remove")); + return; + } + + try { + if (!account) { + // type guard + throw new Error("No account available"); + } + + if (nostrPrivateKey) { + // Validate the private key before saving + generatePublicKey(nostrPrivateKey); + nostrlib.hexToNip19(nostrPrivateKey, "nsec"); + + await msg.request("nostr/setPrivateKey", { + id, + privateKey: nostrPrivateKey, + }); + toast.success(t("nostr.private_key.success")); + } else { + await msg.request("nostr/removePrivateKey", { + id, + }); + toast.success(t("nostr.private_key.successfully_removed")); + } + history.back(); + } catch (e) { + if (e instanceof Error) toast.error(e.message); + } + } + + return !account ? ( +
+ +
+ ) : ( +
+
{ + e.preventDefault(); + saveNostrPrivateKey(nostrPrivateKey); + }} + > + +
+

+ {/*{t("nostr.generate_keys.title")}*/}Advanced Nostr Settings +

+

+ {/*{t("nostr.generate_keys.title")}*/}Derive Nostr keys from your + Secret Key or import your existing private key by pasting it in + “Nostr Private Key” field. +

+ + {currentPrivateKey && nostrKeyOrigin !== "secret-key" ? ( +
+ {/*t("nostr.private_key.backup")*/} + {nostrKeyOrigin === "unknown" + ? "⚠️ You’re currently using an imported or randomly generated Nostr key which cannot be restored by your Secret Key, so remember to back up your Nostr private key." + : "⚠️ You’re currently using a Nostr key derived from your account (legacy) which cannot be restored by your Secret Key, so remember to back up your Nostr private key."} +
+ ) : nostrKeyOrigin === "secret-key" ? ( +
+ {/*t("nostr.private_key.backup")*/} + {"✅ Nostr key derived from your secret key"} +
+ ) : null} + { + setNostrPrivateKey(event.target.value.trim()); + }} + endAdornment={ +
+ + +
+ } + /> + + } + /> + {nostrKeyOrigin !== "secret-key" && ( +
+
+ )} +
+
+
+
+
+
+ ); +} + +export default NostrAdvancedSettings; diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index edc352ba43..daf126130f 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -406,7 +406,7 @@ "success": "Private key encrypted & saved successfully.", "failed_to_remove": "The entered account name didn't match, your old private has been restored.", "successfully_removed": "Private key removed successfully.", - "label": "Private Key" + "label": "Nostr Private Key" }, "public_key": { "label": "Nostr Public Key" @@ -937,8 +937,7 @@ "label": { "active": "ACTIVE", "auth": "LOGIN", - "derived": "DERIVED", - "unknown": "UNKNOWN" + "imported": "IMPORTED" } } }, From 1d1b1ae78ce74178334cab92e07c8522d0f0fb03 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 26 Apr 2023 22:33:08 +0700 Subject: [PATCH 019/118] feat: store mnemonic and generate nostr derived key WIP --- .../Accounts/BackupSecretKey/index.tsx | 19 +++++----- src/app/screens/Accounts/Detail/index.tsx | 21 ++++++---- .../Accounts/ImportSecretKey/index.tsx | 4 +- .../Accounts/NostrAdvancedSettings/index.tsx | 38 +++++++++++++++++-- .../actions/mnemonic/getMnemonic.ts | 30 +++++++++++++++ .../actions/mnemonic/index.ts | 4 ++ .../actions/mnemonic/setMnemonic.ts | 35 +++++++++++++++++ .../actions/nostr/getKeyOrigin.ts | 37 ++++++++++++++++-- src/extension/background-script/router.ts | 3 ++ src/types.ts | 23 ++++++++++- 10 files changed, 187 insertions(+), 27 deletions(-) create mode 100644 src/extension/background-script/actions/mnemonic/getMnemonic.ts create mode 100644 src/extension/background-script/actions/mnemonic/index.ts create mode 100644 src/extension/background-script/actions/mnemonic/setMnemonic.ts diff --git a/src/app/screens/Accounts/BackupSecretKey/index.tsx b/src/app/screens/Accounts/BackupSecretKey/index.tsx index ae7779dcc2..6a3d52b4b5 100644 --- a/src/app/screens/Accounts/BackupSecretKey/index.tsx +++ b/src/app/screens/Accounts/BackupSecretKey/index.tsx @@ -5,6 +5,7 @@ import * as bip39 from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useParams } from "react-router-dom"; import { toast } from "react-toastify"; import Button from "~/app/components/Button"; import MnemonicInputs from "~/app/components/MnemonicInputs"; @@ -12,6 +13,7 @@ import Checkbox from "~/app/components/form/Checkbox"; import { useAccount } from "~/app/context/AccountContext"; import NostrIcon from "~/app/icons/NostrIcon"; import OrdinalsIcon from "~/app/icons/OrdinalsIcon"; +import msg from "~/common/lib/msg"; // TODO: replace with checking account const SECRET_KEY_EXISTS = false; @@ -29,6 +31,7 @@ function BackupSecretKey() { // TODO: only generate mnemonic if account doesn't have one yet setMnemonic(bip39.generateMnemonic(wordlist, 128)); }, []); + const { id } = useParams(); /*const { t } = useTranslation("translation", { keyPrefix: "accounts.account_view", @@ -47,19 +50,15 @@ function BackupSecretKey() { throw new Error("No account available"); } - alert("Mnemonic: " + mnemonic); - // TODO: make sure secret key doesn't already exist - // TODO: check if nostr key exists and warn about replacement - where should this happen? - // TODO: save key and regenerate derived keys + // TODO: check if nostr key exists and warn nostr key not replaced + // TODO: save key and regenerate derived keys that don't exist // TODO: this code should be shared between Import & Backup - // await msg.request("secretKey/save", { - // id: account.id, - // mnemomic, - // }); - // toast.success(t("nostr.private_key.success")); - // } + await msg.request("setMnemonic", { + id, + mnemonic, + }); toast.success(/*t("nostr.private_key.success")*/ "Secret Key saved"); history.back(); } catch (e) { diff --git a/src/app/screens/Accounts/Detail/index.tsx b/src/app/screens/Accounts/Detail/index.tsx index a267326f4f..5bc39c53cb 100644 --- a/src/app/screens/Accounts/Detail/index.tsx +++ b/src/app/screens/Accounts/Detail/index.tsx @@ -35,9 +35,6 @@ import type { Account } from "~/types"; type AccountAction = Omit; dayjs.extend(relativeTime); -// TODO: replace with checking account -const SECRET_KEY_EXISTS = false; - function AccountDetail() { const auth = useAccount(); const { accounts, getAccounts } = useAccounts(); @@ -60,6 +57,8 @@ function AccountDetail() { }); const [accountName, setAccountName] = useState(""); + // TODO: add hooks useMnemonic, useNostrPrivateKey, ... + const [mnemonic, setMnemonic] = useState(""); const [currentPrivateKey, setCurrentPrivateKey] = useState(""); const [nostrPublicKey, setNostrPublicKey] = useState(""); const [nostrKeyOrigin, setNostrKeyOrigin] = useState< @@ -89,6 +88,12 @@ function AccountDetail() { id, })) as FixMe; setNostrKeyOrigin(keyOrigin); + const accountMnemonic = (await msg.request("getMnemonic", { + id, + })) as string; + if (accountMnemonic) { + setMnemonic(accountMnemonic); + } } } catch (e) { console.error(e); @@ -351,7 +356,7 @@ function AccountDetail() { }
- {SECRET_KEY_EXISTS && ( + {mnemonic && (
{/*t("nostr.private_key.backup")*/}⚠️ Backup your Secret Key! Not backing it up might result in permanently loosing access to @@ -362,7 +367,7 @@ function AccountDetail() {

- {SECRET_KEY_EXISTS + {mnemonic ? "Backup your Secret Key" : "Generate your Secret Key"}

@@ -377,9 +382,9 @@ function AccountDetail() {
+ )} + {debug && hasMnemonic && ( +
+
)} diff --git a/src/app/screens/Accounts/ImportSecretKey/index.tsx b/src/app/screens/Accounts/ImportSecretKey/index.tsx index ab5a7d3613..bbd247ba83 100644 --- a/src/app/screens/Accounts/ImportSecretKey/index.tsx +++ b/src/app/screens/Accounts/ImportSecretKey/index.tsx @@ -3,12 +3,15 @@ import Container from "@components/Container"; import Loading from "@components/Loading"; import * as bip39 from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english"; -import { useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useParams } from "react-router-dom"; import { toast } from "react-toastify"; import Button from "~/app/components/Button"; import MnemonicInputs from "~/app/components/MnemonicInputs"; import { useAccount } from "~/app/context/AccountContext"; +import { saveMnemonic } from "~/app/utils/saveMnemonic"; +import msg from "~/common/lib/msg"; function ImportSecretKey() { const [mnemonic, setMnemonic] = useState(""); @@ -18,6 +21,32 @@ function ImportSecretKey() { tCommon("actions.paste_clipboard") as string ); + // TODO: useMnemonic hook + const [hasFetchedData, setHasFetchedData] = useState(false); + const [hasMnemonic, setHasMnemonic] = useState(false); + const { id } = useParams(); + + const fetchData = useCallback(async () => { + try { + if (id) { + const accountMnemonic = (await msg.request("getMnemonic", { + id, + })) as string; + if (accountMnemonic) { + setHasMnemonic(true); + } + setHasFetchedData(true); + } + } catch (e) { + console.error(e); + if (e instanceof Error) toast.error(`Error: ${e.message}`); + } + }, [id]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + /*const { t } = useTranslation("translation", { keyPrefix: "accounts.account_view", });*/ @@ -28,8 +57,16 @@ function ImportSecretKey() { async function importKey() { try { - // TODO: re-add - if (!account) { + if (hasMnemonic) { + if ( + !window.confirm( + "You already have a secret key, are you sure you want to overwrite it?" + ) + ) { + return; + } + } + if (!account || !id) { // type guard throw new Error("No account available"); } @@ -41,27 +78,13 @@ function ImportSecretKey() { throw new Error("Invalid mnemonic"); } - alert("Mnemonic: " + mnemonic); - - // TODO: make sure secret key doesn't already exist - // TODO: check if nostr key exists and warn nostr key not replaced - // TODO: save key and regenerate derived keys that don't exist - // TODO: this code should be shared between Import & Backup - - // await msg.request("secretKey/save", { - // id: account.id, - // mnemomic, - // }); - // toast.success(t("nostr.private_key.success")); - // } - toast.success(/*t("nostr.private_key.success")*/ "Secret Key saved"); - history.back(); + await saveMnemonic(id, mnemonic); } catch (e) { if (e instanceof Error) toast.error(e.message); } } - return !account ? ( + return !account || !hasFetchedData ? (
diff --git a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx index 4e1b613e6b..c7f38d20ff 100644 --- a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx +++ b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx @@ -4,8 +4,6 @@ import { } from "@bitcoin-design/bitcoin-icons-react/filled"; import Container from "@components/Container"; import Loading from "@components/Loading"; -import { HDKey } from "@scure/bip32"; -import * as bip39 from "@scure/bip39"; import { FormEvent, useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; @@ -14,6 +12,7 @@ import Button from "~/app/components/Button"; import InputCopyButton from "~/app/components/InputCopyButton"; import TextField from "~/app/components/form/TextField"; import { useAccount } from "~/app/context/AccountContext"; +import { deriveNostrKeyFromSecretKey } from "~/app/utils/deriveNostrKeyFromSecretKey"; import msg from "~/common/lib/msg"; import nostrlib from "~/common/lib/nostr"; import Nostr from "~/extension/background-script/nostr"; @@ -102,25 +101,14 @@ function NostrAdvancedSettings() { history.back(); } - async function deriveNostrKeyFromSecretKey() { + async function onDeriveNostrKeyFromSecretKey() { // TODO: if no mnemonic, go to backup secret key page if (!mnemonic) { toast.error("You haven't setup your secret key yet"); return; } - // TODO: move this functionality somewhere else - const seed = bip39.mnemonicToSeedSync(mnemonic); - const hdkey = HDKey.fromMasterSeed(seed); - if (!hdkey) { - throw new Error("invalid hdkey"); - } - const nostrPrivateKeyBytes = hdkey.derive("m/1237'/0'/0").privateKey; - if (!nostrPrivateKeyBytes) { - throw new Error("invalid derived private key"); - } - const nostrPrivateKey = Buffer.from(nostrPrivateKeyBytes).toString("hex"); - + const nostrPrivateKey = await deriveNostrKeyFromSecretKey(mnemonic); saveNostrPrivateKey(nostrPrivateKey); } @@ -247,7 +235,7 @@ function NostrAdvancedSettings() {
)} diff --git a/src/app/utils/deriveNostrKeyFromSecretKey.ts b/src/app/utils/deriveNostrKeyFromSecretKey.ts new file mode 100644 index 0000000000..0726be7d21 --- /dev/null +++ b/src/app/utils/deriveNostrKeyFromSecretKey.ts @@ -0,0 +1,19 @@ +import { HDKey } from "@scure/bip32"; +import * as bip39 from "@scure/bip39"; + +export async function deriveNostrKeyFromSecretKey(mnemonic: string) { + if (!mnemonic) { + throw new Error("You haven't setup your secret key yet"); + } + + const seed = bip39.mnemonicToSeedSync(mnemonic); + const hdkey = HDKey.fromMasterSeed(seed); + if (!hdkey) { + throw new Error("invalid hdkey"); + } + const nostrPrivateKeyBytes = hdkey.derive("m/1237'/0'/0").privateKey; + if (!nostrPrivateKeyBytes) { + throw new Error("invalid derived private key"); + } + return Buffer.from(nostrPrivateKeyBytes).toString("hex"); +} diff --git a/src/app/utils/saveMnemonic.ts b/src/app/utils/saveMnemonic.ts new file mode 100644 index 0000000000..f6a7e7d549 --- /dev/null +++ b/src/app/utils/saveMnemonic.ts @@ -0,0 +1,26 @@ +import { toast } from "react-toastify"; +import msg from "~/common/lib/msg"; + +export async function saveMnemonic(id: string, mnemonic: string) { + const priv = (await msg.request("nostr/getPrivateKey", { + id, + })) as string; + const hasNostrPrivateKey = !!priv; + + if (hasNostrPrivateKey) { + alert( + "This account already has a nostr private key set. Your nostr private key will not be replaced." + ); + } + await msg.request("setMnemonic", { + id, + mnemonic, + }); + + if (!hasNostrPrivateKey) { + alert("TODO derive nostr key"); + } + + toast.success(/*t("nostr.private_key.success")*/ "Secret Key saved"); + history.back(); +} diff --git a/src/extension/background-script/actions/nostr/getKeyOrigin.ts b/src/extension/background-script/actions/nostr/getKeyOrigin.ts index 2ed68cb765..54c417c122 100644 --- a/src/extension/background-script/actions/nostr/getKeyOrigin.ts +++ b/src/extension/background-script/actions/nostr/getKeyOrigin.ts @@ -1,5 +1,4 @@ -import { HDKey } from "@scure/bip32"; -import * as bip39 from "@scure/bip39"; +import { deriveNostrKeyFromSecretKey } from "~/app/utils/deriveNostrKeyFromSecretKey"; import { getMnemonic } from "~/extension/background-script/actions/mnemonic"; import { MessageKeyOrigin } from "~/types"; @@ -18,18 +17,9 @@ const getKeyOrigin = async (message: MessageKeyOrigin) => { }); if (mnemonic.data) { - // FIXME: this is all copied from NostrAdvancedSettings - const seed = bip39.mnemonicToSeedSync(mnemonic.data); - const hdkey = HDKey.fromMasterSeed(seed); - if (!hdkey) { - throw new Error("invalid hdkey"); - } - const nostrPrivateKeyBytes = hdkey.derive("m/1237'/0'/0").privateKey; - if (!nostrPrivateKeyBytes) { - throw new Error("invalid derived private key"); - } - const mnemonicDerivedPrivateKey = - Buffer.from(nostrPrivateKeyBytes).toString("hex"); + const mnemonicDerivedPrivateKey = await deriveNostrKeyFromSecretKey( + mnemonic.data + ); if (mnemonicDerivedPrivateKey === privateKey.data) { return { From 3b2ca0718ea83a85bd01f54fbc6d1f40d1cc7b00 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 27 Apr 2023 20:49:32 +0700 Subject: [PATCH 022/118] chore: update nostr key not replaced warning --- src/app/utils/saveMnemonic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/utils/saveMnemonic.ts b/src/app/utils/saveMnemonic.ts index f6a7e7d549..77d1848cd3 100644 --- a/src/app/utils/saveMnemonic.ts +++ b/src/app/utils/saveMnemonic.ts @@ -9,7 +9,7 @@ export async function saveMnemonic(id: string, mnemonic: string) { if (hasNostrPrivateKey) { alert( - "This account already has a nostr private key set. Your nostr private key will not be replaced." + "This account already has a nostr private key set and will not be derived from this secret key. You can manage your nostr key from your account settings." ); } await msg.request("setMnemonic", { From 96cfcb4e646d6997d1af1891b5a13d9eb6d788d3 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 1 May 2023 20:14:26 +0700 Subject: [PATCH 023/118] chore: mnemonic code restructure --- src/app/components/form/Input/index.tsx | 2 +- src/app/components/form/TextField/index.tsx | 2 +- .../Accounts/BackupSecretKey/index.tsx | 4 + src/app/screens/Accounts/Detail/index.tsx | 39 ++++---- .../Accounts/ImportSecretKey/index.tsx | 4 + .../Accounts/NostrAdvancedSettings/index.tsx | 93 +++++++++---------- src/app/utils/deriveNostrKeyFromSecretKey.ts | 19 ---- src/app/utils/getNostrKeyOrigin.ts | 29 ++++++ src/app/utils/saveMnemonic.ts | 15 ++- src/app/utils/saveNostrPrivateKey.ts | 24 +++++ src/common/lib/mnemonic.ts | 18 ++++ src/common/lib/nostr.ts | 10 ++ .../actions/nostr/getKeyOrigin.ts | 46 --------- .../background-script/actions/nostr/index.ts | 2 - src/extension/background-script/router.ts | 1 - src/i18n/locales/en/translation.json | 14 +-- src/types.ts | 8 -- 17 files changed, 165 insertions(+), 165 deletions(-) delete mode 100644 src/app/utils/deriveNostrKeyFromSecretKey.ts create mode 100644 src/app/utils/getNostrKeyOrigin.ts create mode 100644 src/app/utils/saveNostrPrivateKey.ts create mode 100644 src/common/lib/mnemonic.ts delete mode 100644 src/extension/background-script/actions/nostr/getKeyOrigin.ts diff --git a/src/app/components/form/Input/index.tsx b/src/app/components/form/Input/index.tsx index 968ec1880f..3601b3942d 100644 --- a/src/app/components/form/Input/index.tsx +++ b/src/app/components/form/Input/index.tsx @@ -32,7 +32,7 @@ export default function Input({ }: React.InputHTMLAttributes & Props) { const inputEl = useRef(null); const outerStyles = - "rounded-md border border-gray-300 dark:border-neutral-800 transition duration-300"; + "rounded-md border border-gray-300 dark:border-neutral-800 transition duration-300 flex-1"; const inputNode = ( -
+
; @@ -61,9 +64,8 @@ function AccountDetail() { const [mnemonic, setMnemonic] = useState(""); const [currentPrivateKey, setCurrentPrivateKey] = useState(""); const [nostrPublicKey, setNostrPublicKey] = useState(""); - const [nostrKeyOrigin, setNostrKeyOrigin] = useState< - "derived" | "unknown" | "secret-key" - >("unknown"); + const [nostrKeyOrigin, setNostrKeyOrigin] = + useState("unknown"); const [exportLoading, setExportLoading] = useState(false); const [exportModalIsOpen, setExportModalIsOpen] = useState(false); @@ -84,16 +86,17 @@ function AccountDetail() { if (priv) { setCurrentPrivateKey(priv); } - const keyOrigin = (await msg.request("nostr/getKeyOrigin", { - id, - })) as FixMe; - setNostrKeyOrigin(keyOrigin); + const accountMnemonic = (await msg.request("getMnemonic", { id, })) as string; if (accountMnemonic) { setMnemonic(accountMnemonic); } + if (priv) { + const keyOrigin = await getNostrKeyOrigin(priv, accountMnemonic); + setNostrKeyOrigin(keyOrigin); + } } } catch (e) { console.error(e); @@ -105,14 +108,6 @@ function AccountDetail() { setExportModalIsOpen(false); } - // TODO: make utility function - function generatePublicKey(priv: string) { - // FIXME: this is using code from background script - const nostr = new Nostr(priv); - const pubkeyHex = nostr.getPublicKey(); - return nostrlib.hexToNip19(pubkeyHex, "npub"); - } - async function updateAccountName({ id, name }: AccountAction) { await msg.request("editAccount", { name, @@ -173,7 +168,7 @@ function AccountDetail() { useEffect(() => { try { setNostrPublicKey( - currentPrivateKey ? generatePublicKey(currentPrivateKey) : "" + currentPrivateKey ? nostr.generatePublicKey(currentPrivateKey) : "" ); } catch (e) { if (e instanceof Error) @@ -416,16 +411,18 @@ function AccountDetail() {
-
+
} + endAdornment={ + nostrPublicKey && + } /> - {nostrKeyOrigin !== "secret-key" && ( + {nostrPublicKey && nostrKeyOrigin !== "secret-key" && ( ("unknown"); + const [nostrKeyOrigin, setNostrKeyOrigin] = + useState("unknown"); const { id } = useParams(); const fetchData = useCallback(async () => { @@ -47,10 +50,6 @@ function NostrAdvancedSettings() { if (priv) { setCurrentPrivateKey(priv); } - const keyOrigin = (await msg.request("nostr/getKeyOrigin", { - id, - })) as FixMe; - setNostrKeyOrigin(keyOrigin); const accountMnemonic = (await msg.request("getMnemonic", { id, @@ -58,6 +57,11 @@ function NostrAdvancedSettings() { if (accountMnemonic) { setMnemonic(accountMnemonic); } + + if (priv) { + const keyOrigin = await getNostrKeyOrigin(priv, accountMnemonic); + setNostrKeyOrigin(keyOrigin); + } } } catch (e) { console.error(e); @@ -69,18 +73,10 @@ function NostrAdvancedSettings() { fetchData(); }, [fetchData]); - // TODO: make utility function - function generatePublicKey(priv: string) { - // FIXME: this is using code from background script - const nostr = new Nostr(priv); - const pubkeyHex = nostr.getPublicKey(); - return nostrlib.hexToNip19(pubkeyHex, "npub"); - } - useEffect(() => { try { setNostrPublicKey( - currentPrivateKey ? generatePublicKey(currentPrivateKey) : "" + currentPrivateKey ? nostr.generatePublicKey(currentPrivateKey) : "" ); setNostrPrivateKey( currentPrivateKey ? nostrlib.hexToNip19(currentPrivateKey, "nsec") : "" @@ -101,20 +97,25 @@ function NostrAdvancedSettings() { history.back(); } - async function onDeriveNostrKeyFromSecretKey() { - // TODO: if no mnemonic, go to backup secret key page + async function handleDeriveNostrKeyFromSecretKey() { + if (!id) { + throw new Error("No id set"); + } + if (!mnemonic) { toast.error("You haven't setup your secret key yet"); return; } - const nostrPrivateKey = await deriveNostrKeyFromSecretKey(mnemonic); - saveNostrPrivateKey(nostrPrivateKey); - } + const nostrPrivateKey = await deriveNostrPrivateKey(mnemonic); - async function saveNostrPrivateKey(nostrPrivateKey: string) { - nostrPrivateKey = nostrlib.normalizeToHex(nostrPrivateKey); + await handleSaveNostrPrivateKey(nostrPrivateKey); + } + async function handleSaveNostrPrivateKey(nostrPrivateKey: string) { + if (!id) { + throw new Error("No id set"); + } if (nostrPrivateKey === currentPrivateKey) { toast.error("Your private key hasn't changed"); return; @@ -130,31 +131,21 @@ function NostrAdvancedSettings() { } try { - if (!account) { - // type guard - throw new Error("No account available"); - } - - if (nostrPrivateKey) { - // Validate the private key before saving - generatePublicKey(nostrPrivateKey); - nostrlib.hexToNip19(nostrPrivateKey, "nsec"); - - await msg.request("nostr/setPrivateKey", { - id, - privateKey: nostrPrivateKey, - }); - toast.success(t("nostr.private_key.success")); - } else { - await msg.request("nostr/removePrivateKey", { - id, - }); - toast.success(t("nostr.private_key.successfully_removed")); - } - history.back(); + saveNostrPrivateKey(id, nostrPrivateKey); + toast.success( + t( + nostrPrivateKey + ? "nostr.private_key.success" + : "nostr.private_key.successfully_removed" + ) + ); } catch (e) { - if (e instanceof Error) toast.error(e.message); + console.error(e); + if (e instanceof Error) { + toast.error(e.message); + } } + history.back(); } return !account ? ( @@ -166,7 +157,7 @@ function NostrAdvancedSettings() {
{ e.preventDefault(); - saveNostrPrivateKey(nostrPrivateKey); + handleSaveNostrPrivateKey(nostrPrivateKey); }} > @@ -235,7 +226,7 @@ function NostrAdvancedSettings() {
)} diff --git a/src/app/utils/deriveNostrKeyFromSecretKey.ts b/src/app/utils/deriveNostrKeyFromSecretKey.ts deleted file mode 100644 index 0726be7d21..0000000000 --- a/src/app/utils/deriveNostrKeyFromSecretKey.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { HDKey } from "@scure/bip32"; -import * as bip39 from "@scure/bip39"; - -export async function deriveNostrKeyFromSecretKey(mnemonic: string) { - if (!mnemonic) { - throw new Error("You haven't setup your secret key yet"); - } - - const seed = bip39.mnemonicToSeedSync(mnemonic); - const hdkey = HDKey.fromMasterSeed(seed); - if (!hdkey) { - throw new Error("invalid hdkey"); - } - const nostrPrivateKeyBytes = hdkey.derive("m/1237'/0'/0").privateKey; - if (!nostrPrivateKeyBytes) { - throw new Error("invalid derived private key"); - } - return Buffer.from(nostrPrivateKeyBytes).toString("hex"); -} diff --git a/src/app/utils/getNostrKeyOrigin.ts b/src/app/utils/getNostrKeyOrigin.ts new file mode 100644 index 0000000000..aa115f19ea --- /dev/null +++ b/src/app/utils/getNostrKeyOrigin.ts @@ -0,0 +1,29 @@ +import { deriveNostrPrivateKey } from "~/common/lib/mnemonic"; +import msg from "~/common/lib/msg"; + +export type NostrKeyOrigin = + | "legacy-account-derived" + | "unknown" + | "secret-key"; + +export async function getNostrKeyOrigin( + nostrPrivateKey: string, + mnemonic: string | null +): Promise { + if (mnemonic) { + const mnemonicDerivedPrivateKey = await deriveNostrPrivateKey(mnemonic); + + if (mnemonicDerivedPrivateKey === nostrPrivateKey) { + return "secret-key"; + } + } + + // TODO: consider removing this at some point and just returning "unknown" + const legacyAccountDerivedPrivateKey = ( + await msg.request("nostr/generatePrivateKey") + ).privateKey; + + return legacyAccountDerivedPrivateKey === nostrPrivateKey + ? "legacy-account-derived" + : "unknown"; +} diff --git a/src/app/utils/saveMnemonic.ts b/src/app/utils/saveMnemonic.ts index 77d1848cd3..ffade4052a 100644 --- a/src/app/utils/saveMnemonic.ts +++ b/src/app/utils/saveMnemonic.ts @@ -1,9 +1,10 @@ -import { toast } from "react-toastify"; +import { saveNostrPrivateKey } from "~/app/utils/saveNostrPrivateKey"; +import { deriveNostrPrivateKey } from "~/common/lib/mnemonic"; import msg from "~/common/lib/msg"; -export async function saveMnemonic(id: string, mnemonic: string) { +export async function saveMnemonic(accountId: string, mnemonic: string) { const priv = (await msg.request("nostr/getPrivateKey", { - id, + id: accountId, })) as string; const hasNostrPrivateKey = !!priv; @@ -13,14 +14,12 @@ export async function saveMnemonic(id: string, mnemonic: string) { ); } await msg.request("setMnemonic", { - id, + id: accountId, mnemonic, }); if (!hasNostrPrivateKey) { - alert("TODO derive nostr key"); + const nostrPrivateKey = await deriveNostrPrivateKey(mnemonic); + await saveNostrPrivateKey(accountId, nostrPrivateKey); } - - toast.success(/*t("nostr.private_key.success")*/ "Secret Key saved"); - history.back(); } diff --git a/src/app/utils/saveNostrPrivateKey.ts b/src/app/utils/saveNostrPrivateKey.ts new file mode 100644 index 0000000000..d38fef22b1 --- /dev/null +++ b/src/app/utils/saveNostrPrivateKey.ts @@ -0,0 +1,24 @@ +import msg from "~/common/lib/msg"; +import { default as nostr, default as nostrlib } from "~/common/lib/nostr"; + +export async function saveNostrPrivateKey( + accountId: string, + nostrPrivateKey: string +) { + nostrPrivateKey = nostrlib.normalizeToHex(nostrPrivateKey); + + if (nostrPrivateKey) { + // Validate the private key before saving + nostr.generatePublicKey(nostrPrivateKey); + nostrlib.hexToNip19(nostrPrivateKey, "nsec"); + + await msg.request("nostr/setPrivateKey", { + id: accountId, + privateKey: nostrPrivateKey, + }); + } else { + await msg.request("nostr/removePrivateKey", { + id: accountId, + }); + } +} diff --git a/src/common/lib/mnemonic.ts b/src/common/lib/mnemonic.ts new file mode 100644 index 0000000000..8fb24272d1 --- /dev/null +++ b/src/common/lib/mnemonic.ts @@ -0,0 +1,18 @@ +import { HDKey } from "@scure/bip32"; +import * as bip39 from "@scure/bip39"; + +export function deriveNostrPrivateKey(mnemonic: string) { + return deriveKey(mnemonic, 1237); +} +export function deriveKey(mnemonic: string, coinType: number) { + const seed = bip39.mnemonicToSeedSync(mnemonic); + const hdkey = HDKey.fromMasterSeed(seed); + if (!hdkey) { + throw new Error("invalid hdkey"); + } + const privateKeyBytes = hdkey.derive(`m/${coinType}'/0'/0`).privateKey; + if (!privateKeyBytes) { + throw new Error("invalid derived private key"); + } + return Buffer.from(privateKeyBytes).toString("hex"); +} diff --git a/src/common/lib/nostr.ts b/src/common/lib/nostr.ts index 0e58e95e2c..9f35aff673 100644 --- a/src/common/lib/nostr.ts +++ b/src/common/lib/nostr.ts @@ -1,3 +1,6 @@ +import * as secp256k1 from "@noble/secp256k1"; +import nostrlib from "~/common/lib/nostr"; + import { bech32Decode, bech32Encode } from "../utils/helpers"; const nostr = { @@ -17,6 +20,13 @@ const nostr = { hexToNip19(hex: string, prefix = "nsec") { return bech32Encode(prefix, hex); }, + generatePublicKey(privateKey: string) { + const publicKey = secp256k1.schnorr.getPublicKey( + secp256k1.utils.hexToBytes(privateKey) + ); + const publicKeyHex = secp256k1.utils.bytesToHex(publicKey); + return nostrlib.hexToNip19(publicKeyHex, "npub"); + }, }; export default nostr; diff --git a/src/extension/background-script/actions/nostr/getKeyOrigin.ts b/src/extension/background-script/actions/nostr/getKeyOrigin.ts deleted file mode 100644 index 54c417c122..0000000000 --- a/src/extension/background-script/actions/nostr/getKeyOrigin.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { deriveNostrKeyFromSecretKey } from "~/app/utils/deriveNostrKeyFromSecretKey"; -import { getMnemonic } from "~/extension/background-script/actions/mnemonic"; -import { MessageKeyOrigin } from "~/types"; - -import generatePrivateKey from "./generatePrivateKey"; -import getPrivateKey from "./getPrivateKey"; - -// TODO: should this function exist in the background script? -const getKeyOrigin = async (message: MessageKeyOrigin) => { - const privateKey = await getPrivateKey({ - ...message, - action: "getPrivateKey", - }); - const mnemonic = await getMnemonic({ - ...message, - action: "getMnemonic", - }); - - if (mnemonic.data) { - const mnemonicDerivedPrivateKey = await deriveNostrKeyFromSecretKey( - mnemonic.data - ); - - if (mnemonicDerivedPrivateKey === privateKey.data) { - return { - data: "secret-key", - }; - } - } - - const legacyAccountDerivedPrivateKey = await generatePrivateKey({ - ...message, - action: "generatePrivateKey", - args: { - type: undefined, - }, - }); - return { - data: - legacyAccountDerivedPrivateKey.data?.privateKey === privateKey.data - ? "derived" - : "unknown", - }; -}; - -export default getKeyOrigin; diff --git a/src/extension/background-script/actions/nostr/index.ts b/src/extension/background-script/actions/nostr/index.ts index 3e08c24fe5..7a0896fe6c 100644 --- a/src/extension/background-script/actions/nostr/index.ts +++ b/src/extension/background-script/actions/nostr/index.ts @@ -1,7 +1,6 @@ import decryptOrPrompt from "./decryptOrPrompt"; import encryptOrPrompt from "./encryptOrPrompt"; import generatePrivateKey from "./generatePrivateKey"; -import getKeyOrigin from "./getKeyOrigin"; import getPrivateKey from "./getPrivateKey"; import getPublicKeyOrPrompt from "./getPublicKeyOrPrompt"; import getRelays from "./getRelays"; @@ -13,7 +12,6 @@ import signSchnorrOrPrompt from "./signSchnorrOrPrompt"; export { generatePrivateKey, getPrivateKey, - getKeyOrigin, removePrivateKey, setPrivateKey, getPublicKeyOrPrompt, diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index a4436575cf..98be78c2c4 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -66,7 +66,6 @@ const routes = { nostr: { generatePrivateKey: nostr.generatePrivateKey, getPrivateKey: nostr.getPrivateKey, - getKeyOrigin: nostr.getKeyOrigin, removePrivateKey: nostr.removePrivateKey, setPrivateKey: nostr.setPrivateKey, }, diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index daf126130f..7ab7d62528 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -399,13 +399,13 @@ "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": "Manage your key", - "subtitle": "Paste your private key or generate a new one. <0>Learn more »", - "backup": "⚠️ Don't forget to back up your private key! Not backing up your key might result in losing access.", - "warning": "Please enter the name of the account to confirm the deletion of the private key:", - "success": "Private key encrypted & saved successfully.", - "failed_to_remove": "The entered account name didn't match, your old private has been restored.", - "successfully_removed": "Private key removed successfully.", + "title": "Manage your Nostr private key", + "subtitle": "Paste your nostr private key or generate a new one. <0>Learn more »", + "backup": "⚠️ Don't forget to back up your nostr private key! Not backing up your key might result in losing access.", + "warning": "Please enter the name of the account to confirm the deletion of your nostr private key:", + "success": "Nostr private key encrypted & saved successfully.", + "failed_to_remove": "The entered account name didn't match, your old Nostr private key has been restored.", + "successfully_removed": "Nostr private key removed successfully.", "label": "Nostr Private Key" }, "public_key": { diff --git a/src/types.ts b/src/types.ts index 9f1cf75c74..278d22dabd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -429,14 +429,6 @@ export interface MessagePublicKeyGet extends MessageDefault { action: "getPublicKeyOrPrompt"; } -// TODO: add Nostr Prefix -export interface MessageKeyOrigin extends MessageDefault { - args?: { - id?: Account["id"]; - }; - action: "getKeyOrigin"; -} - // TODO: add Nostr Prefix export interface MessagePrivateKeyGet extends MessageDefault { args?: { From c6230c28cef2cf22459fdc7814bf885b3cb3d7f0 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 1 May 2023 20:44:33 +0700 Subject: [PATCH 024/118] chore: display existing nostr key alert in backup and import secret key pages --- .../screens/Accounts/BackupSecretKey/index.tsx | 18 ++++++++++++++++++ .../screens/Accounts/ImportSecretKey/index.tsx | 17 +++++++++++++++++ src/app/utils/saveMnemonic.ts | 5 ----- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/app/screens/Accounts/BackupSecretKey/index.tsx b/src/app/screens/Accounts/BackupSecretKey/index.tsx index fad662a66a..d3f7dcd9c6 100644 --- a/src/app/screens/Accounts/BackupSecretKey/index.tsx +++ b/src/app/screens/Accounts/BackupSecretKey/index.tsx @@ -28,14 +28,24 @@ function BackupSecretKey() { const [hasConfirmedBackup, setHasConfirmedBackup] = useState(false); // TODO: useMnemonic hook const [hasMnemonic, setHasMnemonic] = useState(false); + // TODO: useNostrPrivateKey hook + const [currentPrivateKey, setCurrentPrivateKey] = useState(""); + const { id } = useParams(); const fetchData = useCallback(async () => { try { if (id) { + const priv = (await msg.request("nostr/getPrivateKey", { + id, + })) as string; + if (priv) { + setCurrentPrivateKey(priv); + } const accountMnemonic = (await msg.request("getMnemonic", { id, })) as string; + if (accountMnemonic) { setMnemonic(accountMnemonic); setHasMnemonic(true); @@ -158,6 +168,14 @@ function BackupSecretKey() { )} + {!hasMnemonic && currentPrivateKey && ( +
+ {/*t("nostr.private_key.backup")*/} + { + "⚠️ This account already has a nostr private key set and will not be derived from this secret key. You can manage your nostr key from your account settings." + } +
+ )}
{!hasMnemonic && (
diff --git a/src/app/screens/Accounts/ImportSecretKey/index.tsx b/src/app/screens/Accounts/ImportSecretKey/index.tsx index 7397525448..5a4e0cba23 100644 --- a/src/app/screens/Accounts/ImportSecretKey/index.tsx +++ b/src/app/screens/Accounts/ImportSecretKey/index.tsx @@ -24,11 +24,20 @@ function ImportSecretKey() { // TODO: useMnemonic hook const [hasFetchedData, setHasFetchedData] = useState(false); const [hasMnemonic, setHasMnemonic] = useState(false); + // TODO: useNostrPrivateKey hook + const [currentPrivateKey, setCurrentPrivateKey] = useState(""); const { id } = useParams(); const fetchData = useCallback(async () => { try { if (id) { + const priv = (await msg.request("nostr/getPrivateKey", { + id, + })) as string; + if (priv) { + setCurrentPrivateKey(priv); + } + const accountMnemonic = (await msg.request("getMnemonic", { id, })) as string; @@ -123,6 +132,14 @@ function ImportSecretKey() { /> + {currentPrivateKey && ( +
+ {/*t("nostr.private_key.backup")*/} + { + "⚠️ This account already has a nostr private key set and will not be derived from this secret key. You can manage your nostr key from your account settings." + } +
+ )}
diff --git a/src/app/utils/saveMnemonic.ts b/src/app/utils/saveMnemonic.ts index ffade4052a..f95b1d0334 100644 --- a/src/app/utils/saveMnemonic.ts +++ b/src/app/utils/saveMnemonic.ts @@ -8,11 +8,6 @@ export async function saveMnemonic(accountId: string, mnemonic: string) { })) as string; const hasNostrPrivateKey = !!priv; - if (hasNostrPrivateKey) { - alert( - "This account already has a nostr private key set and will not be derived from this secret key. You can manage your nostr key from your account settings." - ); - } await msg.request("setMnemonic", { id: accountId, mnemonic, From 0c5c7460056b9a8d1ba0d267560dce29deea8e5f Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 1 May 2023 22:51:49 +0700 Subject: [PATCH 025/118] chore: backup secret key translations WIP --- src/app/components/MnemonicInputs/index.tsx | 7 ++- .../Accounts/BackupSecretKey/index.tsx | 55 +++++++------------ src/app/screens/Accounts/Detail/index.tsx | 9 +-- .../Accounts/NostrAdvancedSettings/index.tsx | 33 +++++++---- src/i18n/locales/en/translation.json | 25 +++++++++ 5 files changed, 78 insertions(+), 51 deletions(-) diff --git a/src/app/components/MnemonicInputs/index.tsx b/src/app/components/MnemonicInputs/index.tsx index 76ca0056ac..8aed0f63b9 100644 --- a/src/app/components/MnemonicInputs/index.tsx +++ b/src/app/components/MnemonicInputs/index.tsx @@ -1,4 +1,5 @@ import { wordlist } from "@scure/bip39/wordlists/english"; +import { useTranslation } from "react-i18next"; import Input from "~/app/components/form/Input"; type MnemonicInputsProps = { @@ -13,6 +14,10 @@ export default function MnemonicInputs({ disabled, children, }: React.PropsWithChildren) { + const { t } = useTranslation("translation", { + keyPrefix: "accounts.account_view.mnemonic", + }); + const words = mnemonic?.split(" ") || []; while (words.length < 12) { words.push(""); @@ -20,7 +25,7 @@ export default function MnemonicInputs({ return (
-

{"Your Secret Key"}

+

{t("inputs.title")}

{[...new Array(12)].map((_, i) => (
diff --git a/src/app/screens/Accounts/BackupSecretKey/index.tsx b/src/app/screens/Accounts/BackupSecretKey/index.tsx index d3f7dcd9c6..43e7a55ff7 100644 --- a/src/app/screens/Accounts/BackupSecretKey/index.tsx +++ b/src/app/screens/Accounts/BackupSecretKey/index.tsx @@ -22,6 +22,9 @@ function BackupSecretKey() { const [mnemonic, setMnemonic] = useState(); const account = useAccount(); const { t: tCommon } = useTranslation("common"); + const { t } = useTranslation("translation", { + keyPrefix: "accounts.account_view.mnemonic", + }); const [publicKeyCopyLabel, setPublicKeyCopyLabel] = useState( tCommon("actions.copy_clipboard") as string ); @@ -64,18 +67,11 @@ function BackupSecretKey() { fetchData(); }, [fetchData]); - /*const { t } = useTranslation("translation", { - keyPrefix: "accounts.account_view", - });*/ - async function backupSecretKey() { try { if (!hasConfirmedBackup) { - throw new Error( - "Please confirm that you have backed up your secret key." - ); + throw new Error(t("backup.error_confirm")); } - // TODO: re-add if (!account || !id) { // type guard throw new Error("No account available"); @@ -85,9 +81,7 @@ function BackupSecretKey() { } await saveMnemonic(id, mnemonic); - toast.success( - /*t("nostr.private_key.success")*/ "Secret Key encrypted & saved successfully." - ); + toast.success(t("success")); history.back(); } catch (e) { if (e instanceof Error) toast.error(e.message); @@ -103,25 +97,22 @@ function BackupSecretKey() {

- {hasMnemonic - ? "Back up your Secret Key" - : "Generate your Secret Key"} + {hasMnemonic ? t("backup.title") : t("generate.title")}

-

- In addition to Bitcoin Lightning Network, Alby allows you to - generate keys and interact with other protocols such as: -

+

{t("backup.description1")}

- } title="Nostr protocol" /> - } title="Ordinals" /> + } + title={t("backup.protocols.nostr")} + /> + } + title={t("backup.protocols.ordinals")} + /> {/* } title="Liquid" /> */}
-

- Secret Key is a set of 12 words that will allow you to access your - keys to those protocols on a new device or in case you loose access - to your account: -

+

{t("backup.description2")}

<> {/* TODO: consider making CopyButton component */} @@ -160,9 +151,7 @@ function BackupSecretKey() { htmlFor="has_backed_up" className="cursor-pointer ml-2 block text-sm text-gray-900 font-medium dark:text-white" > - { - /*tCommon("actions.remember")*/ "I’ve backed my account’s Secret Key in a private and secure place" - } + {t("backup.confirm")}
)} @@ -170,26 +159,24 @@ function BackupSecretKey() { {!hasMnemonic && currentPrivateKey && (
- {/*t("nostr.private_key.backup")*/} - { - "⚠️ This account already has a nostr private key set and will not be derived from this secret key. You can manage your nostr key from your account settings." - } + {t("existing_nostr_key")}
)}
{!hasMnemonic && (
)} + {/* TODO: remove - only for testing */} {debug && hasMnemonic && (

- {/*t("nostr.title")*/}Secret Key + {t("mnemonic.title")}

{

- {/*t("nostr.hint")*/}Your Account Secret Key allows you to use - Alby to interact with protocols such as Nostr or Oridinals. + {t("mnemonic.description")}

}
{mnemonic && (
- {/*t("nostr.private_key.backup")*/}⚠️ Backup your Secret Key! - Not backing it up might result in permanently loosing access to - your Nostr identity or purchased Oridinals. + {t("mnemonic.backup_warning")}
)} diff --git a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx index e59e9968d1..7184e90168 100644 --- a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx +++ b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx @@ -6,7 +6,7 @@ import Container from "@components/Container"; import Loading from "@components/Loading"; import { FormEvent, useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useParams } from "react-router-dom"; +import { Link, useParams } from "react-router-dom"; import { toast } from "react-toastify"; import Button from "~/app/components/Button"; import InputCopyButton from "~/app/components/InputCopyButton"; @@ -221,15 +221,28 @@ function NostrAdvancedSettings() { disabled endAdornment={} /> - {nostrKeyOrigin !== "secret-key" && ( -
-
- )} + {nostrKeyOrigin !== "secret-key" && + (mnemonic ? ( +
+
+ ) : ( +

+ You {"don't"} have a secret key yet.{" "} + + Click here + {" "} + to create your secret key and derive your nostr keys. +

+ ))}
diff --git a/src/app/screens/Accounts/Detail/index.tsx b/src/app/screens/Accounts/Detail/index.tsx index 08edc22abb..5809de396d 100644 --- a/src/app/screens/Accounts/Detail/index.tsx +++ b/src/app/screens/Accounts/Detail/index.tsx @@ -320,6 +320,7 @@ function AccountDetail() { { @@ -352,32 +353,32 @@ function AccountDetail() {
{mnemonic && (
- {t("mnemonic.backup_warning")} + {t("mnemonic.backup.warning")}
)}

- {mnemonic - ? "Backup your Secret Key" - : "Generate your Secret Key"} + {t( + mnemonic + ? "mnemonic.backup.title" + : "mnemonic.generate.title" + )}

- Your Secret Key is a set of 12 words that will allow you to - access your keys to protocols such as Nostr or Oridinals on a - new device or in case you loose access to your account. + {t("mnemonic.description2")}

diff --git a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx index 7184e90168..97a1dfde7a 100644 --- a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx +++ b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx @@ -5,7 +5,7 @@ import { import Container from "@components/Container"; import Loading from "@components/Loading"; import { FormEvent, useCallback, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { Link, useParams } from "react-router-dom"; import { toast } from "react-toastify"; import Button from "~/app/components/Button"; @@ -21,13 +21,9 @@ import { deriveNostrPrivateKey } from "~/common/lib/mnemonic"; import msg from "~/common/lib/msg"; import { default as nostr, default as nostrlib } from "~/common/lib/nostr"; -// import { GetAccountRes } from "~/common/lib/api"; - function NostrAdvancedSettings() { const account = useAccount(); - //const [account, setAccount] = useState(null); const { t: tCommon } = useTranslation("common"); - // TODO: move these translations to the correct place const { t } = useTranslation("translation", { keyPrefix: "accounts.account_view", }); @@ -103,8 +99,7 @@ function NostrAdvancedSettings() { } if (!mnemonic) { - toast.error("You haven't setup your secret key yet"); - return; + throw new Error("No mnemonic exists"); } const nostrPrivateKey = await deriveNostrPrivateKey(mnemonic); @@ -117,8 +112,7 @@ function NostrAdvancedSettings() { throw new Error("No id set"); } if (nostrPrivateKey === currentPrivateKey) { - toast.error("Your private key hasn't changed"); - return; + throw new Error("private key hasn't changed"); } if ( @@ -163,25 +157,27 @@ function NostrAdvancedSettings() {

- {/*{t("nostr.generate_keys.title")}*/}Advanced Nostr Settings + {t("nostr.advanced_settings.title")}

- {/*{t("nostr.generate_keys.title")}*/}Derive Nostr keys from your - Secret Key or import your existing private key by pasting it in - “Nostr Private Key” field. + {t("nostr.advanced_settings.description")}

{currentPrivateKey && nostrKeyOrigin !== "secret-key" ? ( + // TODO: extract to Alert component
- {/*t("nostr.private_key.backup")*/} - {nostrKeyOrigin === "unknown" - ? "⚠️ You’re currently using an imported or randomly generated Nostr key which cannot be restored by your Secret Key, so remember to back up your Nostr private key." - : "⚠️ You’re currently using a Nostr key derived from your account (legacy) which cannot be restored by your Secret Key, so remember to back up your Nostr private key."} +

+ {t( + nostrKeyOrigin === "unknown" + ? "nostr.advanced_settings.imported_key_warning" + : "nostr.advanced_settings.legacy_derived_key_warning" + )} +

) : nostrKeyOrigin === "secret-key" ? ( -
- {/*t("nostr.private_key.backup")*/} - {"✅ Nostr key derived from your secret key"} + // TODO: extract to Alert component +
+

{t("nostr.advanced_settings.can_restore")}

) : null} } /> {nostrKeyOrigin !== "secret-key" && - (mnemonic ? ( + (mnemonic || !currentPrivateKey) && (
-
- ) : ( -

- You {"don't"} have a secret key yet.{" "} - - Click here - {" "} - to create your secret key and derive your nostr keys. -

- ))} + )}
to create your secret key and derive your nostr keys." + }, "private_key": { "title": "Manage your Nostr private key", "subtitle": "Paste your nostr private key or generate a new one. <0>Learn more »", - "backup": "⚠️ Don't forget to back up your nostr private key! Not backing up your key might result in losing access.", "warning": "Please enter the name of the account to confirm the deletion of your nostr private key:", "success": "Nostr private key encrypted & saved successfully.", "failed_to_remove": "The entered account name didn't match, your old Nostr private key has been restored.", @@ -950,7 +968,7 @@ "add": "Add a new account", "manage": "Manage accounts", "account_settings": "Account settings", - "go_to_web_wallet": "Manage your Alby Account" + "go_to_web_wallet": "Manage your web account" } } }, From 5a594566d8dcf9dce8faa1b6537d3a85f93e36e2 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 2 May 2023 13:32:16 +0700 Subject: [PATCH 027/118] chore: secret key dark mode styling --- src/app/components/MnemonicInputs/index.tsx | 15 +++++++++++-- src/app/components/form/Input/index.tsx | 12 ++++++++-- src/app/icons/LiquidIcon.tsx | 10 ++++----- src/app/icons/NostrIcon.tsx | 2 +- src/app/icons/OrdinalsIcon.tsx | 4 ++-- .../Accounts/BackupSecretKey/index.tsx | 22 +++++++++++++------ src/app/screens/Accounts/Detail/index.tsx | 10 +++++---- .../Accounts/ImportSecretKey/index.tsx | 8 +++++-- .../Accounts/NostrAdvancedSettings/index.tsx | 4 ++-- src/app/styles/index.css | 17 ++++++++------ src/i18n/locales/en/translation.json | 2 +- 11 files changed, 71 insertions(+), 35 deletions(-) diff --git a/src/app/components/MnemonicInputs/index.tsx b/src/app/components/MnemonicInputs/index.tsx index 8aed0f63b9..09f3b1dc0b 100644 --- a/src/app/components/MnemonicInputs/index.tsx +++ b/src/app/components/MnemonicInputs/index.tsx @@ -25,11 +25,13 @@ export default function MnemonicInputs({ return (
-

{t("inputs.title")}

+

{t("inputs.title")}

{[...new Array(12)].map((_, i) => (
- {i + 1}. + + {i + 1}. + { words[i] = e.target.value; @@ -46,6 +50,13 @@ export default function MnemonicInputs({
))}
+ {!disabled && ( + + {wordlist.map((word) => ( + + )} {children}
); diff --git a/src/app/components/form/Input/index.tsx b/src/app/components/form/Input/index.tsx index 3601b3942d..260058e565 100644 --- a/src/app/components/form/Input/index.tsx +++ b/src/app/components/form/Input/index.tsx @@ -29,6 +29,7 @@ export default function Input({ endAdornment, block = true, className, + ...otherProps }: React.InputHTMLAttributes & Props) { const inputEl = useRef(null); const outerStyles = @@ -64,6 +65,7 @@ export default function Input({ disabled={disabled} min={min} max={max} + {...otherProps} /> ); @@ -73,7 +75,8 @@ export default function Input({
@@ -89,7 +92,12 @@ export default function Input({ )} {endAdornment && ( - + {endAdornment} )} diff --git a/src/app/icons/LiquidIcon.tsx b/src/app/icons/LiquidIcon.tsx index 5f2a1de74b..cf65dde751 100644 --- a/src/app/icons/LiquidIcon.tsx +++ b/src/app/icons/LiquidIcon.tsx @@ -13,31 +13,31 @@ const LiquidIcon = (props: SVGProps) => ( fillRule="evenodd" clipRule="evenodd" d="M6.74222 16.2186C6.32091 16.2186 5.92036 15.9726 5.74509 15.5628C5.14954 14.1707 4.84802 12.6916 4.84802 11.1669C4.84802 5.49621 9.11048 0.698581 14.7631 0.00793613C15.3577 -0.0634632 15.897 0.354781 15.9702 0.945264C16.0434 1.53541 15.6211 2.07276 15.0275 2.14518C10.4588 2.70385 7.01379 6.58209 7.01379 11.1669C7.01379 12.4006 7.2578 13.5961 7.73832 14.7199C7.97246 15.2671 7.71586 15.8999 7.16557 16.1323C7.02706 16.1909 6.88345 16.2186 6.74222 16.2186Z" - fill="#374151" + fill="currentColor" /> ); diff --git a/src/app/icons/NostrIcon.tsx b/src/app/icons/NostrIcon.tsx index 44ea4163bb..0ec7e13cbf 100644 --- a/src/app/icons/NostrIcon.tsx +++ b/src/app/icons/NostrIcon.tsx @@ -11,7 +11,7 @@ const NostrIcon = (props: SVGProps) => ( > ); diff --git a/src/app/icons/OrdinalsIcon.tsx b/src/app/icons/OrdinalsIcon.tsx index 9fefebe8ce..952689bc7e 100644 --- a/src/app/icons/OrdinalsIcon.tsx +++ b/src/app/icons/OrdinalsIcon.tsx @@ -13,11 +13,11 @@ const OrdinalsIcon = (props: SVGProps) => ( diff --git a/src/app/screens/Accounts/BackupSecretKey/index.tsx b/src/app/screens/Accounts/BackupSecretKey/index.tsx index 7d310255e6..5fc953f0db 100644 --- a/src/app/screens/Accounts/BackupSecretKey/index.tsx +++ b/src/app/screens/Accounts/BackupSecretKey/index.tsx @@ -96,23 +96,30 @@ function BackupSecretKey() {
-

+

{hasMnemonic ? t("backup.title") : t("generate.title")}

-

{t("backup.description1")}

+

+ {t("backup.description1")} +

} + icon={ + + } title={t("backup.protocols.nostr")} /> } + icon={ + + } title={t("backup.protocols.ordinals")} /> - {/* } title="Liquid" /> */}
-

{t("backup.description2")}

+

+ {t("backup.description2")} +

<> {/* TODO: consider making CopyButton component */} @@ -158,6 +165,7 @@ function BackupSecretKey() { {!hasMnemonic && currentPrivateKey && ( + // TODO: extract to Alert component
{t("existing_nostr_key_notice")}
@@ -202,7 +210,7 @@ function ProtocolListItem({ icon, title }: ProtocolListItemProps) { return (
{icon} - {title} + {title}
); } diff --git a/src/app/screens/Accounts/Detail/index.tsx b/src/app/screens/Accounts/Detail/index.tsx index 5809de396d..261f8b5712 100644 --- a/src/app/screens/Accounts/Detail/index.tsx +++ b/src/app/screens/Accounts/Detail/index.tsx @@ -359,14 +359,14 @@ function AccountDetail() {
-

+

{t( mnemonic ? "mnemonic.backup.title" : "mnemonic.generate.title" )}

-

+

{t("mnemonic.description2")}

@@ -388,8 +388,10 @@ function AccountDetail() {
-

{t("mnemonic.import.title")}

-

+

+ {t("mnemonic.import.title")} +

+

{t("mnemonic.import.description")}

diff --git a/src/app/screens/Accounts/ImportSecretKey/index.tsx b/src/app/screens/Accounts/ImportSecretKey/index.tsx index 079f412dc6..cf252f073d 100644 --- a/src/app/screens/Accounts/ImportSecretKey/index.tsx +++ b/src/app/screens/Accounts/ImportSecretKey/index.tsx @@ -98,8 +98,12 @@ function ImportSecretKey() {
-

{t("import.title")}

-

{t("import.description")}

+

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

+

+ {t("import.description")} +

<> diff --git a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx index 97a1dfde7a..be0181969e 100644 --- a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx +++ b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx @@ -156,10 +156,10 @@ function NostrAdvancedSettings() { >
-

+

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

-

+

{t("nostr.advanced_settings.description")}

diff --git a/src/app/styles/index.css b/src/app/styles/index.css index b955f36db4..a9012a39fe 100644 --- a/src/app/styles/index.css +++ b/src/app/styles/index.css @@ -15,25 +15,25 @@ } @font-face { - font-family: 'Inter var'; + font-family: "Inter var"; font-weight: 100 900; font-display: swap; font-style: normal; - font-named-instance: 'Regular'; + font-named-instance: "Regular"; src: url("./fonts/Inter-roman.var.woff2?v=3.18") format("woff2"); } @font-face { - font-family: 'Inter var'; + font-family: "Inter var"; font-weight: 100 900; font-display: swap; font-style: italic; - font-named-instance: 'Italic'; + font-named-instance: "Italic"; src: url("./fonts/Inter-italic.var.woff2?v=3.18") format("woff2"); } body { font-size: 100%; - font-family: 'Inter var'; + font-family: "Inter var"; font-weight: normal; } @@ -50,7 +50,6 @@ body { opacity: 0; } - /* Hide number spinner */ /* Webkit */ input.dual-currency-field::-webkit-outer-spin-button, @@ -60,6 +59,10 @@ input.dual-currency-field::-webkit-inner-spin-button { } /* Firefox */ -input.dual-currency-field[type=number] { +input.dual-currency-field[type="number"] { -moz-appearance: textfield; } + +input[type="text"]::-webkit-calendar-picker-indicator { + display: none !important; +} diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 9c2d272377..1a90d23caf 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -413,7 +413,7 @@ "button": "Backup Secret Key", "warning": "⚠️ Backup your Secret Key! Not backing it up might result in permanently loosing access to your Nostr identity or purchased Oridinals.", "description1": "In addition to the Bitcoin Lightning Network, Alby allows you to generate keys and interact with other protocols such as:", - "description2": "Secret Key is a set of 12 words that will allow you to access your keys to those protocols on a new device or in case you loose access to your account:", + "description2": "Your Secret Key is a set of 12 words that will allow you to access your keys to those protocols on a new device or in case you loose access to your account:", "protocols": { "nostr": "Nostr protocol", "ordinals": "Ordinals" From a46f9ad902b7bad8b8dffe523ebe783b2de60f00 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 3 May 2023 14:14:35 +0700 Subject: [PATCH 028/118] fix: onboarding tab behaviour --- src/app/components/Button/index.tsx | 2 ++ src/app/components/PasswordForm/index.tsx | 2 -- src/app/components/form/Input/index.tsx | 2 +- .../connectors/ChooseConnectorPath/index.tsx | 17 ++++++++++++++--- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/app/components/Button/index.tsx b/src/app/components/Button/index.tsx index cb887cce07..fd83d969bb 100644 --- a/src/app/components/Button/index.tsx +++ b/src/app/components/Button/index.tsx @@ -32,11 +32,13 @@ const Button = forwardRef( loading = false, flex = false, className, + ...otherProps }: Props, ref: Ref ) => { return ( -

{title}

- - {uiDescription} - - {!!buttons?.length && ( -
{buttons}
- )} -
- ); -} diff --git a/src/app/components/CloseableCard/index.test.tsx b/src/app/components/TipCard/index.test.tsx similarity index 65% rename from src/app/components/CloseableCard/index.test.tsx rename to src/app/components/TipCard/index.test.tsx index 0bc0452546..7c477d383d 100644 --- a/src/app/components/CloseableCard/index.test.tsx +++ b/src/app/components/TipCard/index.test.tsx @@ -1,20 +1,24 @@ import { render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; +import BuyBitcoinTipCardIcon from "~/app/icons/BuyBitcoinTipCardIcon"; import type { Props } from "./index"; -import CloseableCard from "./index"; +import TipCard from "./index"; const props: Props = { title: "Card Title", description: "Card description", handleClose: () => ({}), + arrowClassName: "text-orange-500", + backgroundIcon: , + className: "border-orange-500", }; describe("CloseableCard", () => { test("render label", async () => { render( - + ); diff --git a/src/app/components/TipCard/index.tsx b/src/app/components/TipCard/index.tsx new file mode 100644 index 0000000000..9c0af57413 --- /dev/null +++ b/src/app/components/TipCard/index.tsx @@ -0,0 +1,59 @@ +import { + ArrowRightIcon, + CrossIcon, +} from "@bitcoin-design/bitcoin-icons-react/filled"; +import React from "react"; +import { classNames } from "~/app/utils"; + +export type Props = { + title: string; + description: string; + className: string; + arrowClassName: string; + backgroundIcon: React.ReactNode; + handleClose: React.MouseEventHandler; +}; + +export default function TipCard({ + title, + description, + handleClose, + className, + arrowClassName, + backgroundIcon, +}: Props) { + return ( +
+ {backgroundIcon && ( +
{backgroundIcon}
+ )} +
+ { + + } +
+ +

+ {title} +

+

+ {description} +

+
+ ); +} diff --git a/src/app/components/Tips/index.test.tsx b/src/app/components/Tips/index.test.tsx index 9399408690..cc82693f6e 100644 --- a/src/app/components/Tips/index.test.tsx +++ b/src/app/components/Tips/index.test.tsx @@ -7,12 +7,12 @@ import { TIPS } from "~/common/constants"; jest.mock("~/app/hooks/useTips", () => ({ useTips: () => ({ - tips: [TIPS.TOP_UP_WALLET, TIPS.DEMO], + tips: Object.values(TIPS), }), })); describe("Tips", () => { - test("should have 2 tips", async () => { + test("should have 3 tips", async () => { render( @@ -21,9 +21,8 @@ describe("Tips", () => { ); - expect( - await screen.findByText("⚡️ Top up your wallet") - ).toBeInTheDocument(); - expect(await screen.findByText("🕹️ Try out Alby Demo")).toBeInTheDocument(); + expect(await screen.findByText("Buy Bitcoin")).toBeInTheDocument(); + expect(await screen.findByText("Alby Demo")).toBeInTheDocument(); + expect(await screen.findByText("Nostr and Ordinals")).toBeInTheDocument(); }); }); diff --git a/src/app/components/Tips/index.tsx b/src/app/components/Tips/index.tsx index 2a3536c040..7d073d8d99 100644 --- a/src/app/components/Tips/index.tsx +++ b/src/app/components/Tips/index.tsx @@ -1,16 +1,52 @@ -import Button from "@components/Button"; -import CloseableCard from "@components/CloseableCard"; +import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; +import { Link } from "react-router-dom"; +// import { useNavigate } from "react-router-dom"; +import TipCard from "~/app/components/TipCard"; +import { useAccount } from "~/app/context/AccountContext"; import { useTips } from "~/app/hooks/useTips"; +import BuyBitcoinTipCardIcon from "~/app/icons/BuyBitcoinTipCardIcon"; +import DemoTipCardIcon from "~/app/icons/DemoTipCardIcon"; +import MnemonicTipCardIcon from "~/app/icons/MnemonicTipCardIcon"; +import { classNames } from "~/app/utils"; import { TIPS } from "~/common/constants"; export default function Tips() { const { t } = useTranslation("translation", { keyPrefix: "discover.tips", }); + const accountContext = useAccount(); + const accountId = accountContext?.account?.id; - const navigate = useNavigate(); + const tipCardConfigs = useMemo( + () => + ({ + [TIPS.TOP_UP_WALLET]: { + background: false, + border: "border-orange-500", + arrow: "text-orange-500", + backgroundIcon: , + link: "https://getalby.com/topup", + }, + [TIPS.DEMO]: { + background: false, + border: "border-yellow-500", + arrow: "text-yellow-500", + backgroundIcon: , + link: "https://getalby.com/demo", + }, + [TIPS.MNEMONIC]: { + background: "bg-purple-50", + border: "border-purple-500", + arrow: "text-purple-500", + backgroundIcon: , + link: `/accounts/${accountId}/secret-key/backup`, + }, + } as const), + [accountId] + ); + + // const navigate = useNavigate(); const { tips, closeTip } = useTips(); @@ -18,73 +54,32 @@ export default function Tips() { return tips.includes(id); } - const tipElements = [] as JSX.Element[]; - - if (hasTip(TIPS.TOP_UP_WALLET)) { - tipElements.push( - closeTip(TIPS.TOP_UP_WALLET)} - title={t("top_up_wallet.title")} - description={t("top_up_wallet.description")} - buttons={[ -

{title} diff --git a/src/app/components/Tips/index.tsx b/src/app/components/Tips/index.tsx index 7d073d8d99..f29dcf3b10 100644 --- a/src/app/components/Tips/index.tsx +++ b/src/app/components/Tips/index.tsx @@ -22,21 +22,21 @@ export default function Tips() { () => ({ [TIPS.TOP_UP_WALLET]: { - background: false, + background: "bg-white dark:bg-surface-02dp", border: "border-orange-500", arrow: "text-orange-500", backgroundIcon: , link: "https://getalby.com/topup", }, [TIPS.DEMO]: { - background: false, + background: "bg-white dark:bg-surface-02dp", border: "border-yellow-500", arrow: "text-yellow-500", backgroundIcon: , link: "https://getalby.com/demo", }, [TIPS.MNEMONIC]: { - background: "bg-purple-50", + background: "bg-purple-50 dark:bg-purple-950", border: "border-purple-500", arrow: "text-purple-500", backgroundIcon: , From 4cd039b9c964e3b984678bc4b5c789c32d43b882 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 3 May 2023 22:24:22 +0700 Subject: [PATCH 031/118] chore: fix webbtc postmessage --- src/extension/content-script/onendwebbtc.js | 33 +++++++++++++-------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/extension/content-script/onendwebbtc.js b/src/extension/content-script/onendwebbtc.js index f72f5c2beb..68763a676a 100644 --- a/src/extension/content-script/onendwebbtc.js +++ b/src/extension/content-script/onendwebbtc.js @@ -5,7 +5,11 @@ import shouldInject from "./shouldInject"; // Nostr calls that can be executed from the WebBTC Provider. // Update when new calls are added -const webbtcCalls = ["webbtc/getInfo", "webbtc/signPsbtWithPrompt"]; +const webbtcCalls = [ + "webbtc/enable", + "webbtc/getInfo", + "webbtc/signPsbtWithPrompt", +]; // calls that can be executed when `window.webbtc` is not enabled for the current content page const disabledCalls = ["webbtc/enable"]; @@ -13,14 +17,14 @@ let isEnabled = false; // store if nostr is enabled for this content page let isRejected = false; // store if the nostr enable call failed. if so we do not prompt again let callActive = false; // store if a nostr call is currently active. Used to prevent multiple calls in parallel +const SCOPE = "webbtc"; + async function init() { const inject = await shouldInject(); if (!inject) { return; } - const SCOPE = "webbtc"; - // message listener to listen to inpage webbtc calls // those calls get passed on to the background script // (the inpage script can not do that directly, but only the inpage script can make webln available to the page) @@ -80,15 +84,7 @@ async function init() { } } - window.postMessage( - { - application: "LBE", - response: true, - data: response, - scope: SCOPE, - }, - "*" // TODO use origin - ); + postMessage(ev, response); }; callActive = true; @@ -102,4 +98,17 @@ async function init() { init(); +function postMessage(ev, response) { + window.postMessage( + { + id: ev.data.id, + application: "LBE", + response: true, + data: response, + scope: SCOPE, + }, + "*" + ); +} + export {}; From bf7da6b8da43d007564d0465f9c0289f628d6f17 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 3 May 2023 22:26:30 +0700 Subject: [PATCH 032/118] feat: use mnemonic from account in signPsbt --- src/common/lib/mnemonic.ts | 19 +++++++++--- .../actions/webbtc/signPsbt.ts | 30 +++++++++++++------ src/types.ts | 1 + 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/common/lib/mnemonic.ts b/src/common/lib/mnemonic.ts index 8fb24272d1..ceba82d5e0 100644 --- a/src/common/lib/mnemonic.ts +++ b/src/common/lib/mnemonic.ts @@ -1,18 +1,29 @@ +import * as secp256k1 from "@noble/secp256k1"; import { HDKey } from "@scure/bip32"; import * as bip39 from "@scure/bip39"; +const debug = process.env.NODE_ENV === "development"; + export function deriveNostrPrivateKey(mnemonic: string) { return deriveKey(mnemonic, 1237); } -export function deriveKey(mnemonic: string, coinType: number) { +export function deriveBitcoinPrivateKey(mnemonic: string, testnet = debug) { + return deriveKey(mnemonic, testnet ? 1 : 0); +} +export function getRootPrivateKey(mnemonic: string) { const seed = bip39.mnemonicToSeedSync(mnemonic); const hdkey = HDKey.fromMasterSeed(seed); - if (!hdkey) { - throw new Error("invalid hdkey"); + if (!hdkey.privateKey) { + throw new Error("invalid key"); } + return secp256k1.utils.bytesToHex(hdkey.privateKey); +} +export function deriveKey(mnemonic: string, coinType: number) { + const seed = bip39.mnemonicToSeedSync(mnemonic); + const hdkey = HDKey.fromMasterSeed(seed); const privateKeyBytes = hdkey.derive(`m/${coinType}'/0'/0`).privateKey; if (!privateKeyBytes) { throw new Error("invalid derived private key"); } - return Buffer.from(privateKeyBytes).toString("hex"); + return secp256k1.utils.bytesToHex(privateKeyBytes); } diff --git a/src/extension/background-script/actions/webbtc/signPsbt.ts b/src/extension/background-script/actions/webbtc/signPsbt.ts index ed6a2868e3..0554353ddd 100644 --- a/src/extension/background-script/actions/webbtc/signPsbt.ts +++ b/src/extension/background-script/actions/webbtc/signPsbt.ts @@ -1,20 +1,32 @@ import * as secp256k1 from "@noble/secp256k1"; import { hex } from "@scure/base"; -import { HDKey } from "@scure/bip32"; -import * as bip39 from "@scure/bip39"; import * as btc from "@scure/btc-signer"; +import { decryptData } from "~/common/lib/crypto"; +import { getRootPrivateKey } from "~/common/lib/mnemonic"; +import state from "~/extension/background-script/state"; import { MessageSignPsbt } from "~/types"; -// TODO: Load from account -// TODO: Make network configurable via ENV -const mnemonic = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; -const seed = bip39.mnemonicToSeedSync(mnemonic); -const hdkey = HDKey.fromMasterSeed(seed); +// TODO: Make network (mainnet or testnet) configurable via ENV const signPsbt = async (message: MessageSignPsbt) => { try { - const privateKey = hdkey.privateKey!; + // TODO: is this the correct way to decrypt the mnmenonic? + const password = await state.getState().password(); + if (!password) { + throw new Error("No password set"); + } + const account = await state.getState().getAccount(); + if (!account) { + throw new Error("No account selected"); + } + if (!account.mnemonic) { + throw new Error("No mnemonic set"); + } + const mnemonic = decryptData(account.mnemonic, password); + const privateKey = secp256k1.utils.hexToBytes( + //deriveBitcoinPrivateKey(mnemonic, message.args.testnet) + getRootPrivateKey(mnemonic) + ); const psbtBytes = secp256k1.utils.hexToBytes(message.args.psbt); const transaction = btc.Transaction.fromPSBT(psbtBytes); diff --git a/src/types.ts b/src/types.ts index fe07960cba..85645c131b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -510,6 +510,7 @@ export interface MessageDecryptGet extends MessageDefault { export interface MessageSignPsbt extends MessageDefault { args: { psbt: string; + testnet?: boolean; // TODO: review }; action: "signPsbt"; } From 35edbf8ddb62e8f4926307c1795e39c752ceeb48 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 4 May 2023 14:10:33 +0700 Subject: [PATCH 033/118] fix: textfield flex direction --- src/app/components/form/TextField/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/form/TextField/index.tsx b/src/app/components/form/TextField/index.tsx index afe660e3f3..5f7a45bcc6 100644 --- a/src/app/components/form/TextField/index.tsx +++ b/src/app/components/form/TextField/index.tsx @@ -36,7 +36,7 @@ const TextField = ({ {label} -
+
Date: Fri, 12 May 2023 14:03:55 +0700 Subject: [PATCH 034/118] fix: signpsbt --- src/app/components/MnemonicInputs/index.tsx | 7 +- .../Accounts/ImportSecretKey/index.tsx | 1 + src/app/screens/ConfirmSignPsbt/index.tsx | 7 +- src/common/lib/mnemonic.ts | 27 +-- .../actions/webbtc/__tests__/signPsbt.test.ts | 206 +++++------------- .../actions/webbtc/signPsbt.ts | 17 +- src/extension/providers/webbtc/index.ts | 4 +- src/types.ts | 3 +- 8 files changed, 90 insertions(+), 182 deletions(-) diff --git a/src/app/components/MnemonicInputs/index.tsx b/src/app/components/MnemonicInputs/index.tsx index 09f3b1dc0b..5683f234c5 100644 --- a/src/app/components/MnemonicInputs/index.tsx +++ b/src/app/components/MnemonicInputs/index.tsx @@ -44,7 +44,12 @@ export default function MnemonicInputs({ value={words[i]} onChange={(e) => { words[i] = e.target.value; - setMnemonic?.(words.join(" ")); + setMnemonic?.( + words + .map((word) => word.trim()) + .join(" ") + .trim() + ); }} />
diff --git a/src/app/screens/Accounts/ImportSecretKey/index.tsx b/src/app/screens/Accounts/ImportSecretKey/index.tsx index cf252f073d..333d75ddda 100644 --- a/src/app/screens/Accounts/ImportSecretKey/index.tsx +++ b/src/app/screens/Accounts/ImportSecretKey/index.tsx @@ -79,6 +79,7 @@ function ImportSecretKey() { mnemonic.split(" ").length !== 12 || !bip39.validateMnemonic(mnemonic, wordlist) ) { + console.error("Invalid mnemonic: '" + mnemonic + "'"); throw new Error("Invalid mnemonic"); } diff --git a/src/app/screens/ConfirmSignPsbt/index.tsx b/src/app/screens/ConfirmSignPsbt/index.tsx index 32ebaa6b98..aa4f9a56ca 100644 --- a/src/app/screens/ConfirmSignPsbt/index.tsx +++ b/src/app/screens/ConfirmSignPsbt/index.tsx @@ -23,6 +23,7 @@ function ConfirmSignPsbt() { const navigate = useNavigate(); const psbt = navState.args?.psbt as string; + const derivationPath = navState.args?.derivationPath as string | undefined; const origin = navState.origin as OriginData; const [loading, setLoading] = useState(false); const [successMessage, setSuccessMessage] = useState(""); @@ -30,7 +31,11 @@ function ConfirmSignPsbt() { async function confirm() { try { setLoading(true); - const response = await msg.request("signPsbt", { psbt }, { origin }); + const response = await msg.request( + "signPsbt", + { psbt, derivationPath }, + { origin } + ); msg.reply(response); setSuccessMessage(tCommon("success")); } catch (e) { diff --git a/src/common/lib/mnemonic.ts b/src/common/lib/mnemonic.ts index ceba82d5e0..98021944e3 100644 --- a/src/common/lib/mnemonic.ts +++ b/src/common/lib/mnemonic.ts @@ -2,26 +2,23 @@ import * as secp256k1 from "@noble/secp256k1"; import { HDKey } from "@scure/bip32"; import * as bip39 from "@scure/bip39"; -const debug = process.env.NODE_ENV === "development"; +export const NOSTR_DERIVATION_PATH = "m/44'/1237'/0'/0/0"; // NIP-06 +export const BTC_TAPROOT_DERIVATION_PATH = "m/86'/0'/0'/0/0"; +// Segwit +// m/84'/0'/0'/0/0 +// Nested segwit +// m/84'/0'/0'/0/0 +// Legacy +// m/44'/0'/0'/0/0 export function deriveNostrPrivateKey(mnemonic: string) { - return deriveKey(mnemonic, 1237); + return derivePrivateKey(mnemonic, NOSTR_DERIVATION_PATH); } -export function deriveBitcoinPrivateKey(mnemonic: string, testnet = debug) { - return deriveKey(mnemonic, testnet ? 1 : 0); -} -export function getRootPrivateKey(mnemonic: string) { - const seed = bip39.mnemonicToSeedSync(mnemonic); - const hdkey = HDKey.fromMasterSeed(seed); - if (!hdkey.privateKey) { - throw new Error("invalid key"); - } - return secp256k1.utils.bytesToHex(hdkey.privateKey); -} -export function deriveKey(mnemonic: string, coinType: number) { + +export function derivePrivateKey(mnemonic: string, path: string) { const seed = bip39.mnemonicToSeedSync(mnemonic); const hdkey = HDKey.fromMasterSeed(seed); - const privateKeyBytes = hdkey.derive(`m/${coinType}'/0'/0`).privateKey; + const privateKeyBytes = hdkey.derive(path).privateKey; if (!privateKeyBytes) { throw new Error("invalid derived private key"); } diff --git a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts index 92b0568fb0..eb3c60a4a9 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts @@ -1,53 +1,45 @@ -import * as secp256k1 from "@noble/secp256k1"; import { hex } from "@scure/base"; -import { HDKey } from "@scure/bip32"; -import * as bip39 from "@scure/bip39"; import * as btc from "@scure/btc-signer"; import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; +import state from "~/extension/background-script/state"; import type { MessageSignPsbt } from "~/types"; -jest.mock("~/extension/background-script/state"); - -// Same as above -const TX_TEST_OUTPUTS: [string, bigint][] = [ - ["1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP", 10n], - ["3H3Kc7aSPP4THLX68k4mQMyf1gvL6AtmDm", 50n], - ["bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", 93n], -]; -const TX_TEST_INPUTS = [ - { - txid: hex.decode( - "c061c23190ed3370ad5206769651eaf6fac6d87d85b5db34e30a74e0c4a6da3e" - ), - index: 0, - amount: 550n, - script: hex.decode("76a91411dbe48cc6b617f9c6adaf4d9ed5f625b1c7cb5988ac"), - }, - { - txid: hex.decode( - "a21965903c938af35e7280ae5779b9fea4f7f01ac256b8a2a53b1b19a4e89a0d" - ), - index: 0, - amount: 600n, - script: hex.decode("76a91411dbe48cc6b617f9c6adaf4d9ed5f625b1c7cb5988ac"), - }, - { - txid: hex.decode( - "fae21e319ca827df32462afc3225c17719338a8e8d3e3b3ddeb0c2387da3a4c7" - ), - index: 0, - amount: 600n, - script: hex.decode("76a91411dbe48cc6b617f9c6adaf4d9ed5f625b1c7cb5988ac"), - }, -]; - -const regtest = { - bech32: "bcrt", - pubKeyHash: 0x6f, - scriptHash: 0xc4, - wif: 0, +const passwordMock = jest.fn; + +// generated in sparrow wallet using mock mnemonic below, +// native segwit derivation: m/84'/1'/0' - 1 input ("m/84'/1'/0'/0/0" - first receive address), 2 outputs, saved as binary PSBT file +// imported using `cat "filename.psbt" | xxd -p -c 1000` +const regtestSegwitPsbt = + "70736274ff0100710200000001fe1204e9e35f90c356bb6fe1d8944a46b0c5ac57160f707f6f5ca728bf1ab5490000000000fdffffff0280969800000000001600146fa016500a3c6a737ebb260e2ddca78ba9234558f5ecfa0200000000160014744c9993900c8e098d599b315a9f667777e4f82a1e0100004f01043587cf030ef4b1af800000003c8c2037ee4c1621da0d348db51163709a622d0d2838dde6d8419c51f6301c6203b88e0fbe3f646337ed93bc0c0f3b843fcf7d2589e5ec884754e6402027a890b41073c5da0a5400008001000080000000800001005202000000010380d5854dfa1e789fe3cb615e834b0cef9f1aad732db4bac886eb8750497a180000000000fdffffff010284930300000000160014d0c4a3ef09e997b6e99e397e518fe3e41a118ca11401000001011f0284930300000000160014d0c4a3ef09e997b6e99e397e518fe3e41a118ca101030401000000220602e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c3191873c5da0a540000800100008000000080000000000000000000220203eeed205a69022fed4a62a02457f3699b19c06bf74bf801acc6d9ae84bc16a9e11873c5da0a540000800100008000000080000000000100000000220202e6c60079372951c3024a033ecf6584579ebf2f7927ae99c42633e805596f29351873c5da0a540000800100008000000080010000000400000000"; + +// signed PSBT and verified by importing in sparrow and broadcasting transaction +const regtestSegwitSignedPsbt = + "02000000000101fe1204e9e35f90c356bb6fe1d8944a46b0c5ac57160f707f6f5ca728bf1ab5490000000000fdffffff0280969800000000001600146fa016500a3c6a737ebb260e2ddca78ba9234558f5ecfa0200000000160014744c9993900c8e098d599b315a9f667777e4f82a02473044022065255b047fecc1b5a0afdf095a367c538c6afde33a1d6fb9f5fe28638aa7dbcf022072de13455179e876c336b32cc525c1b862f7199913e8b67c0663566489fcd2c0012102e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c3191e010000"; + +const regtestSegwitDerivationPath = "m/84'/1'/0'/0/0"; + +const mockMnemnoic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + +const mockState = { + password: passwordMock, + currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", + getAccount: () => ({ + mnemonic: mockMnemnoic, + }), + getConnector: jest.fn(), }; +state.getState = jest.fn().mockReturnValue(mockState); + +jest.mock("~/common/lib/crypto", () => { + return { + decryptData: jest.fn(() => { + return mockMnemnoic; + }), + }; +}); + beforeEach(async () => { // fill the DB first }); @@ -56,7 +48,7 @@ afterEach(() => { jest.clearAllMocks(); }); -async function sendPsbtMessage(psbt: string) { +async function sendPsbtMessage(psbt: string, derivationPath?: string) { const message: MessageSignPsbt = { application: "LBE", prompt: true, @@ -65,7 +57,8 @@ async function sendPsbtMessage(psbt: string) { internal: true, }, args: { - psbt: psbt, + psbt, + derivationPath, }, }; @@ -73,127 +66,26 @@ async function sendPsbtMessage(psbt: string) { } describe("signPsbt", () => { - const mnemonic = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - const seed = bip39.mnemonicToSeedSync(mnemonic); - - const hdkey = HDKey.fromMasterSeed(seed); - if (!hdkey) throw Error("invalid hdkey"); - - const privateKey = hdkey.privateKey; - - if (!privateKey) throw Error("no private key available"); - - test("successfully signed psbt", async () => { - const tx32 = new btc.Transaction({ version: 1 }); - for (const [address, amount] of TX_TEST_OUTPUTS) - tx32.addOutputAddress(address, amount); - for (const inp of TX_TEST_INPUTS) { - tx32.addInput({ - txid: inp.txid, - index: inp.index, - witnessUtxo: { - amount: inp.amount, - script: btc.p2wpkh(secp256k1.getPublicKey(privateKey, true)).script, - }, - }); - } - const psbt = tx32.toPSBT(2); - - expect(tx32.isFinal).toBe(false); - - const result = await sendPsbtMessage(secp256k1.utils.bytesToHex(psbt)); - - expect(result.data).not.toBe(undefined); - expect(result.error).toBe(undefined); - - // expect(result.data?.signed).toBe( - // "010000000001033edaa6c4e0740ae334dbb5857dd8c6faf6ea5196760652ad7033ed9031c261c00000000000ffffffff0d9ae8a4191b3ba5a2b856c21af0f7a4feb97957ae80725ef38a933c906519a20000000000ffffffffc7a4a37d38c2b0de3d3b3e8d8e8a331977c12532fc2a4632df27a89c311ee2fa0000000000ffffffff030a000000000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac320000000000000017a914a860f76561c85551594c18eecceffaee8c4822d7875d00000000000000160014e8df018c7e326cc253faac7e46cdc51e68542c420248304502210089852ee0ca628998de7bd3ca155058196c4c1f66aa3ffb775fd363dafc121c5f0220424ca42eafaa529ac3ff6f1f5af690f45fa2ba294e250c8e91eab0bd37d82a07012103d902f35f560e0470c63313c7369168d9d7df2d49bf295fd9fb7cb109ccee049402483045022100dd99ceb0b087568f62da6de5ac6e875a47c3758f18853dccbafed9c2709892ec022010f1d1dc54fb369a033a57da8a7d0ef897682499efeb216016a265f414546417012103d902f35f560e0470c63313c7369168d9d7df2d49bf295fd9fb7cb109ccee049402483045022100cbad3acff2f56bec89b08496ed2953cc1785282effbe651c4aea79cd601c6b6f02207b0e43638e7ba4933ea13ed562854c893b8e416baa08f2a9ec5ad806bb19aa27012103d902f35f560e0470c63313c7369168d9d7df2d49bf295fd9fb7cb109ccee049400000000" - // ); - - if (result.data?.signed) { - const checkTx = btc.Transaction.fromRaw(hex.decode(result.data?.signed)); - expect(checkTx.isFinal).toBe(true); - } - }); -}); - -// from https://github.com/satoshilabs/slips/blob/master/slip-0132.md -describe("test transaction building", () => { - test("create from mnemonic", async () => { - const mnemonic = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - const seed = bip39.mnemonicToSeedSync(mnemonic); - const hdkey = HDKey.fromMasterSeed(seed); - - expect(hdkey.privateExtendedKey).toBe( - "xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu" + test("1 input, segwit, regtest", async () => { + const result = await sendPsbtMessage( + regtestSegwitPsbt, + regtestSegwitDerivationPath ); - - // Check - // ✅ m/0'/0' - // BIP32 Extended Private key - // xprv9w83TkwTJSpYjV4hWcxttB9bQWHdrFCPzCLnMHKceyd4WGBfsUgijUirvMaHM6TFBqQegpt3hZysUeBP8PFmkjPWitahm71vjNhMLqKmuLb - // ✅ m/44'/0'/0'/0 - // ❌ m/49'/0'/0'/0 - // ❌ m/84'/0'/0'/0 - // zprvAg4yBxbZcJpcLxtXp5kZuh8jC1FXGtZnCjrkG69JPf96KZ1TqSakA1HF3EZkNjt9yC4CTjm7txs4sRD9EoHLgDqwhUE6s1yD9nY4BCNN4hw - // 49 / 84 doesn't seem to follow the same derivation logic 🤔 - - // const derivedKey = hdkey.derive("m/84'/0'/0'"); - // const addressKey = hdkey.derive("m/84'/0'/0'/0/0"); - - // const nostrKey = hdkey.derive("m/1237'/0'/0"); - // const liquidKey = hdkey.derive("m/1776'/0'/0"); - - // console.log(nostrKey, liquidKey); - - // console.log( - // "derived", - // derivedKey.privateExtendedKey, - // derivedKey.publicExtendedKey - // ); - // console.log( - // "address", - // btc.getAddress("wpkh", addressKey.privateKey!, btc.NETWORK) - // ); - - // Assemble transaction - // const result1 = btc.WIF(regtest).encode(derivedKey.publicKey!); - // console.log(result1); - - const tx32 = new btc.Transaction({ version: 1 }); - for (const [address, amount] of TX_TEST_OUTPUTS) - tx32.addOutputAddress(address, amount); - for (const inp of TX_TEST_INPUTS) { - tx32.addInput({ - txid: inp.txid, - index: inp.index, - witnessUtxo: { - amount: inp.amount, - script: btc.p2wpkh(secp256k1.getPublicKey(hdkey.privateKey!, true)) - .script, - }, - }); + if (!result.data) { + throw new Error("Result should have data"); } - // Create psbt - const psbt = hex.encode(tx32.toPSBT()); - - // Sign transaction - const result = await sendPsbtMessage(psbt); - - // Check signatures expect(result.data).not.toBe(undefined); + expect(result.data?.signed).not.toBe(undefined); expect(result.error).toBe(undefined); + + const checkTx = btc.Transaction.fromRaw(hex.decode(result.data.signed)); + expect(checkTx.isFinal).toBe(true); + expect(result.data?.signed).toBe(regtestSegwitSignedPsbt); }); }); describe("signPsbt input validation", () => { - test("no inputs signed", async () => { - const result = await sendPsbtMessage("test"); - expect(result.error).not.toBe(null); - }); test("invalid psbt", async () => { const result = await sendPsbtMessage("test"); expect(result.error).not.toBe(null); diff --git a/src/extension/background-script/actions/webbtc/signPsbt.ts b/src/extension/background-script/actions/webbtc/signPsbt.ts index 0554353ddd..d34467eaef 100644 --- a/src/extension/background-script/actions/webbtc/signPsbt.ts +++ b/src/extension/background-script/actions/webbtc/signPsbt.ts @@ -2,12 +2,13 @@ import * as secp256k1 from "@noble/secp256k1"; import { hex } from "@scure/base"; import * as btc from "@scure/btc-signer"; import { decryptData } from "~/common/lib/crypto"; -import { getRootPrivateKey } from "~/common/lib/mnemonic"; +import { + BTC_TAPROOT_DERIVATION_PATH, + derivePrivateKey, +} from "~/common/lib/mnemonic"; import state from "~/extension/background-script/state"; import { MessageSignPsbt } from "~/types"; -// TODO: Make network (mainnet or testnet) configurable via ENV - const signPsbt = async (message: MessageSignPsbt) => { try { // TODO: is this the correct way to decrypt the mnmenonic? @@ -24,16 +25,22 @@ const signPsbt = async (message: MessageSignPsbt) => { } const mnemonic = decryptData(account.mnemonic, password); const privateKey = secp256k1.utils.hexToBytes( - //deriveBitcoinPrivateKey(mnemonic, message.args.testnet) - getRootPrivateKey(mnemonic) + // TODO: allow account to specify derivation path + derivePrivateKey( + mnemonic, + message.args.derivationPath || BTC_TAPROOT_DERIVATION_PATH + ) ); const psbtBytes = secp256k1.utils.hexToBytes(message.args.psbt); const transaction = btc.Transaction.fromPSBT(psbtBytes); + // this only works with a single input + // TODO: support signing individual inputs transaction.sign(privateKey); // TODO: Do we need to finalize() here or should that be done by websites? + // if signing individual inputs, each should be finalized individually transaction.finalize(); const signedTransaction = hex.encode(transaction.extract()); diff --git a/src/extension/providers/webbtc/index.ts b/src/extension/providers/webbtc/index.ts index 865b62e40b..18362098d6 100644 --- a/src/extension/providers/webbtc/index.ts +++ b/src/extension/providers/webbtc/index.ts @@ -37,12 +37,12 @@ export default class WebBTCProvider { return this.execute("getInfo"); } - signPsbt(psbt: string) { + signPsbt(psbt: string, derivationPath?: string) { if (!this.enabled) { throw new Error("Provider must be enabled before calling signPsbt"); } - return this.execute("signPsbtWithPrompt", { psbt }); + return this.execute("signPsbtWithPrompt", { psbt, derivationPath }); } sendTransaction(address: string, amount: string) { diff --git a/src/types.ts b/src/types.ts index 85645c131b..6969c316e3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -150,6 +150,7 @@ export type NavigationState = { description: string; }; psbt?: string; + derivationPath?: string; }; isPrompt?: true; // only passed via Prompt.tsx action: string; @@ -510,7 +511,7 @@ export interface MessageDecryptGet extends MessageDefault { export interface MessageSignPsbt extends MessageDefault { args: { psbt: string; - testnet?: boolean; // TODO: review + derivationPath?: string; // custom derivation path }; action: "signPsbt"; } From f7a288f30387d3465120d37ed952fe3bc5eb0bbd Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 12 May 2023 22:24:47 +0700 Subject: [PATCH 035/118] feat: sign psbt preview (wip) --- package.json | 1 + src/app/components/ContentMessage/index.tsx | 2 +- src/app/screens/ConfirmSignPsbt/index.tsx | 50 +++++++++++++- src/common/lib/psbt.ts | 67 +++++++++++++++++++ .../actions/webbtc/__tests__/signPsbt.test.ts | 65 +++++++++++++++++- yarn.lock | 21 +++++- 6 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 src/common/lib/psbt.ts diff --git a/package.json b/package.json index d3036398e1..6a9515803d 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@vespaiach/axios-fetch-adapter": "^0.3.0", "axios": "^0.27.2", "bech32": "^2.0.0", + "bitcoinjs-lib": "^6.1.0", "bolt11": "^1.4.1", "crypto-js": "^4.1.1", "dayjs": "^1.11.7", diff --git a/src/app/components/ContentMessage/index.tsx b/src/app/components/ContentMessage/index.tsx index 0de09f5b92..fc1ecd0541 100644 --- a/src/app/components/ContentMessage/index.tsx +++ b/src/app/components/ContentMessage/index.tsx @@ -8,7 +8,7 @@ function ContentMessage({ heading, content }: Props) { <>
{heading}
-
+
{content}
diff --git a/src/app/screens/ConfirmSignPsbt/index.tsx b/src/app/screens/ConfirmSignPsbt/index.tsx index aa4f9a56ca..f1d8abd7ed 100644 --- a/src/app/screens/ConfirmSignPsbt/index.tsx +++ b/src/app/screens/ConfirmSignPsbt/index.tsx @@ -4,14 +4,16 @@ import Container from "@components/Container"; import ContentMessage from "@components/ContentMessage"; import PublisherCard from "@components/PublisherCard"; import SuccessMessage from "@components/SuccessMessage"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; +import Loading from "~/app/components/Loading"; import ScreenHeader from "~/app/components/ScreenHeader"; import { useNavigationState } from "~/app/hooks/useNavigationState"; import { USER_REJECTED_ERROR } from "~/common/constants"; import msg from "~/common/lib/msg"; +import { PsbtPreview, getPsbtPreview } from "~/common/lib/psbt"; import type { OriginData } from "~/types"; function ConfirmSignPsbt() { @@ -27,6 +29,12 @@ function ConfirmSignPsbt() { const origin = navState.origin as OriginData; const [loading, setLoading] = useState(false); const [successMessage, setSuccessMessage] = useState(""); + const [preview, setPreview] = useState(undefined); + + useEffect(() => { + // FIXME: do not hardcode the network type + setPreview(getPsbtPreview(psbt, "regtest")); + }, [psbt]); async function confirm() { try { @@ -60,9 +68,13 @@ function ConfirmSignPsbt() { } } + if (!preview) { + return ; + } + return (
- + {!successMessage ? (
@@ -71,6 +83,40 @@ function ConfirmSignPsbt() { image={origin.icon} url={origin.host} /> + + input.address.substring(0, 5) + + "..." + + input.address.substring( + input.address.length - 5, + input.address.length + ) + + ": " + + input.amount + + " sats" + ) + .join("\n")} + /> + + output.address.substring(0, 5) + + "..." + + output.address.substring( + output.address.length - 5, + output.address.length + ) + + ": " + + output.amount + + " sats" + ) + .join("\n")} + /> 0) { + throw new Error("Multiple inputs currently unsupported"); + } + const inputType = unsignedPsbt.getInputType(i); + if (inputType !== "witnesspubkeyhash") { + throw new Error("Unsupported input type: " + inputType); + } + const bip32Derivation = unsignedPsbt.data.inputs[i].bip32Derivation; + if (!bip32Derivation) { + throw new Error("No bip32Derivation in input " + i); + } + const address = payments.p2wpkh({ + pubkey: bip32Derivation[0].pubkey, + network, + }).address; + + if (!address) { + throw new Error("No address found in input " + i); + } + const witnessUtxo = unsignedPsbt.data.inputs[i].witnessUtxo; + if (!witnessUtxo) { + throw new Error("No witnessUtxo in input " + i); + } + + preview.inputs.push({ + amount: witnessUtxo.value, + address, + }); + } + for (let i = 0; i < unsignedPsbt.txOutputs.length; i++) { + const txOutput = unsignedPsbt.txOutputs[i]; + if (!txOutput.address) { + throw new Error("No address in output " + i); + } + const output: Address = { + amount: txOutput.value, + address: txOutput.address, + }; + preview.outputs.push(output); + } + return preview; +} diff --git a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts index eb3c60a4a9..c6dca95634 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts @@ -1,5 +1,7 @@ import { hex } from "@scure/base"; import * as btc from "@scure/btc-signer"; +import { Psbt, networks, payments } from "bitcoinjs-lib"; +import { getPsbtPreview } from "~/common/lib/psbt"; import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; import state from "~/extension/background-script/state"; import type { MessageSignPsbt } from "~/types"; @@ -69,7 +71,7 @@ describe("signPsbt", () => { test("1 input, segwit, regtest", async () => { const result = await sendPsbtMessage( regtestSegwitPsbt, - regtestSegwitDerivationPath + regtestSegwitDerivationPath // TODO: move to account btc derivation path ); if (!result.data) { throw new Error("Result should have data"); @@ -91,3 +93,64 @@ describe("signPsbt input validation", () => { expect(result.error).not.toBe(null); }); }); + +describe("decode psbt", () => { + test("manually decode segwit transaction", async () => { + const unsignedPsbt = Psbt.fromHex(regtestSegwitPsbt, { + network: networks.regtest, + }); + expect(unsignedPsbt.data.inputs[0].witnessUtxo?.value).toBe(59999234); + + expect(unsignedPsbt.getInputType(0)).toBe("witnesspubkeyhash"); + expect( + hex.encode( + unsignedPsbt.data.inputs[0].bip32Derivation?.[0].pubkey ?? + new Uint8Array() + ) + ).toBe( + "02e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c319" + ); + const address = payments.p2wpkh({ + pubkey: unsignedPsbt.data.inputs[0].bip32Derivation?.[0].pubkey, + network: networks.regtest, + }).address; + expect(address).toBe("bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk"); + expect(unsignedPsbt.txOutputs[0].address).toBe( + "bcrt1qd7spv5q28348xl4myc8zmh983w5jx32cs707jh" + ); + expect(unsignedPsbt.txOutputs[0].value).toBe(10000000); + expect(unsignedPsbt.txOutputs[1].address).toBe( + "bcrt1qw3xfnyuspj8qnr2envc448mxwam7f7p249rqs0" + ); + expect(unsignedPsbt.txOutputs[1].value).toBe(49999093); + + console.info( + hex.encode(unsignedPsbt.txInputs[0].hash), + unsignedPsbt.data.inputs[0] + ); + + // fee should be 141 sats + // input should be bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk 59,999,234 sats + // output 1 should be bcrt1qd7spv5q28348xl4myc8zmh983w5jx32cs707jh 10,000,000 sats + // output 2 should be bcrt1qw3xfnyuspj8qnr2envc448mxwam7f7p249rqs0 49,999,093 sats + }); + + test("get segwit transaction preview", async () => { + const preview = getPsbtPreview(regtestSegwitPsbt, "regtest"); + expect(preview.inputs.length).toBe(1); + expect(preview.inputs[0].address).toBe( + "bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk" + ); + expect(preview.inputs[0].amount).toBe(59999234); + expect(preview.outputs.length).toBe(2); + + expect(preview.outputs[0].address).toBe( + "bcrt1qd7spv5q28348xl4myc8zmh983w5jx32cs707jh" + ); + expect(preview.outputs[0].amount).toBe(10000000); + expect(preview.outputs[1].address).toBe( + "bcrt1qw3xfnyuspj8qnr2envc448mxwam7f7p249rqs0" + ); + expect(preview.outputs[1].amount).toBe(49999093); + }); +}); diff --git a/yarn.lock b/yarn.lock index c2d3741b6d..9abf4c8255 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2542,6 +2542,11 @@ bip174@^2.0.1: resolved "https://registry.npmjs.org/bip174/-/bip174-2.0.1.tgz" integrity sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ== +bip174@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/bip174/-/bip174-2.1.0.tgz#cd3402581feaa5116f0f00a0eaee87a5843a2d30" + integrity sha512-lkc0XyiX9E9KiVAS1ZiOqK1xfiwvf4FXDDdkDq5crcDzOq+xGytY+14qCsqz7kCiy8rpN1CRNfacRhf9G3JNSA== + bitcoinjs-lib@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-6.0.1.tgz#4fa9438bb86a0449451ac58607e83d9b5a7732e6" @@ -2555,6 +2560,20 @@ bitcoinjs-lib@^6.0.0: varuint-bitcoin "^1.1.2" wif "^2.0.1" +bitcoinjs-lib@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-6.1.0.tgz#2e3123d63eab5e8e752fd7e2f237314f35ed738f" + integrity sha512-eupi1FBTJmPuAZdChnzTXLv2HBqFW2AICpzXZQLniP0V9FWWeeUQSMKES6sP8isy/xO0ijDexbgkdEyFVrsuJw== + dependencies: + bech32 "^2.0.0" + bip174 "^2.1.0" + bs58check "^2.1.2" + create-hash "^1.1.0" + ripemd160 "^2.0.2" + typeforce "^1.11.3" + varuint-bitcoin "^1.1.2" + wif "^2.0.1" + bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -8398,7 +8417,7 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -ripemd160@^2.0.1: +ripemd160@^2.0.1, ripemd160@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz" integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== From c3f8b59283873530f9b47eec53b2b962772eb3d3 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 15 May 2023 14:10:42 +0700 Subject: [PATCH 036/118] feat: add webbtc functions to get addresses or single address --- src/app/components/ContentMessage/index.tsx | 2 +- src/app/router/Prompt/Prompt.tsx | 5 + .../ConfirmGetAddresses/index.test.tsx | 52 +++++++++ src/app/screens/ConfirmGetAddresses/index.tsx | 110 ++++++++++++++++++ src/common/lib/btc.ts | 38 ++++++ src/common/lib/mnemonic.ts | 10 ++ .../actions/webbtc/getAddresses.ts | 81 +++++++++++++ .../actions/webbtc/getAddressesWithPrompt.ts | 41 +++++++ .../actions/webbtc/getInfo.ts | 7 +- .../background-script/actions/webbtc/index.ts | 10 +- src/extension/background-script/router.ts | 2 + src/extension/content-script/onendwebbtc.js | 1 + src/extension/providers/webbtc/index.ts | 24 +++- src/types.ts | 15 ++- 14 files changed, 388 insertions(+), 10 deletions(-) create mode 100644 src/app/screens/ConfirmGetAddresses/index.test.tsx create mode 100644 src/app/screens/ConfirmGetAddresses/index.tsx create mode 100644 src/common/lib/btc.ts create mode 100644 src/extension/background-script/actions/webbtc/getAddresses.ts create mode 100644 src/extension/background-script/actions/webbtc/getAddressesWithPrompt.ts diff --git a/src/app/components/ContentMessage/index.tsx b/src/app/components/ContentMessage/index.tsx index fc1ecd0541..7d637e8f7a 100644 --- a/src/app/components/ContentMessage/index.tsx +++ b/src/app/components/ContentMessage/index.tsx @@ -8,7 +8,7 @@ function ContentMessage({ heading, content }: Props) { <>
{heading}
-
+
{content}
diff --git a/src/app/router/Prompt/Prompt.tsx b/src/app/router/Prompt/Prompt.tsx index 195da081c5..625b882794 100644 --- a/src/app/router/Prompt/Prompt.tsx +++ b/src/app/router/Prompt/Prompt.tsx @@ -18,6 +18,7 @@ import { HashRouter, Navigate, Outlet, Route, Routes } from "react-router-dom"; import { ToastContainer } from "react-toastify"; import Providers from "~/app/context/Providers"; import RequireAuth from "~/app/router/RequireAuth"; +import ConfirmGetAddresses from "~/app/screens/ConfirmGetAddresses"; import ConfirmSignPsbt from "~/app/screens/ConfirmSignPsbt"; import type { NavigationState, OriginData } from "~/types"; @@ -102,6 +103,10 @@ function Prompt() { } /> } /> } /> + } + /> } diff --git a/src/app/screens/ConfirmGetAddresses/index.test.tsx b/src/app/screens/ConfirmGetAddresses/index.test.tsx new file mode 100644 index 0000000000..0bc724391c --- /dev/null +++ b/src/app/screens/ConfirmGetAddresses/index.test.tsx @@ -0,0 +1,52 @@ +import { act, render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import type { OriginData } from "~/types"; + +import ConfirmGetAddresses from "./index"; + +const mockOrigin: OriginData = { + location: "https://getalby.com/demo", + domain: "https://getalby.com", + host: "getalby.com", + pathname: "/demo", + name: "Alby", + description: "", + icon: "https://getalby.com/assets/alby-503261fa1b83c396b7ba8d927db7072d15fea5a84d387a654c5d0a2cefd44604.svg", + metaData: { + title: "Alby Demo", + url: "https://getalby.com/demo", + provider: "Alby", + image: + "https://getalby.com/assets/alby-503261fa1b83c396b7ba8d927db7072d15fea5a84d387a654c5d0a2cefd44604.svg", + icon: "https://getalby.com/favicon.ico", + }, + external: true, +}; + +jest.mock("~/app/hooks/useNavigationState", () => { + return { + useNavigationState: jest.fn(() => ({ + origin: mockOrigin, + args: { + psbt: "psbt", + }, + })), + }; +}); + +describe("ConfirmGetAddresses", () => { + test("render", async () => { + await act(async () => { + render( + + + + ); + }); + + expect( + await screen.findByText("This website asks you to sign:") + ).toBeInTheDocument(); + expect(await screen.findByText("psbt")).toBeInTheDocument(); + }); +}); diff --git a/src/app/screens/ConfirmGetAddresses/index.tsx b/src/app/screens/ConfirmGetAddresses/index.tsx new file mode 100644 index 0000000000..e412385acd --- /dev/null +++ b/src/app/screens/ConfirmGetAddresses/index.tsx @@ -0,0 +1,110 @@ +//import Checkbox from "../../components/Form/Checkbox"; +import ConfirmOrCancel from "@components/ConfirmOrCancel"; +import Container from "@components/Container"; +import ContentMessage from "@components/ContentMessage"; +import PublisherCard from "@components/PublisherCard"; +import SuccessMessage from "@components/SuccessMessage"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import ScreenHeader from "~/app/components/ScreenHeader"; +import { useNavigationState } from "~/app/hooks/useNavigationState"; +import { USER_REJECTED_ERROR } from "~/common/constants"; +import msg from "~/common/lib/msg"; +import type { OriginData } from "~/types"; + +function ConfirmGetAddresses() { + const navState = useNavigationState(); + const { t: tCommon } = useTranslation("common"); + const { t } = useTranslation("translation", { + keyPrefix: "confirm_sign_message", + }); + const navigate = useNavigate(); + + const index = navState.args?.index as number; + const num = navState.args?.num as number; + const change = navState.args?.change as boolean; + const derivationPath = navState.args?.derivationPath as string | undefined; + + const origin = navState.origin as OriginData; + const [loading, setLoading] = useState(false); + const [successMessage, setSuccessMessage] = useState(""); + + async function confirm() { + try { + setLoading(true); + const response = await msg.request( + "getAddresses", + { index, num, change, derivationPath }, + { origin } + ); + msg.reply(response); + setSuccessMessage(tCommon("success")); + } catch (e) { + console.error(e); + if (e instanceof Error) toast.error(`${tCommon("error")}: ${e.message}`); + } finally { + setLoading(false); + } + } + + function reject(e: React.MouseEvent) { + e.preventDefault(); + msg.error(USER_REJECTED_ERROR); + } + + function close(e: React.MouseEvent) { + if (navState.isPrompt) { + window.close(); + } else { + e.preventDefault(); + navigate(-1); + } + } + + return ( +
+ + {!successMessage ? ( + +
+ + + +
+ +
+ ) : ( + + + + + )} +
+ ); +} + +export default ConfirmGetAddresses; diff --git a/src/common/lib/btc.ts b/src/common/lib/btc.ts new file mode 100644 index 0000000000..3efee334ab --- /dev/null +++ b/src/common/lib/btc.ts @@ -0,0 +1,38 @@ +import { networks, payments } from "bitcoinjs-lib"; + +type SupportedAddressType = "witnesspubkeyhash"; + +export function getAddressType(purpose: string): SupportedAddressType { + switch (purpose) { + case "84'": + return "witnesspubkeyhash"; + default: + throw new Error("Unsupported purpose: " + purpose); + } +} + +export function getAddressFromPubkey( + pubkey: string, + addressType: SupportedAddressType, + networkType?: keyof typeof networks +) { + const network = networkType ? networks[networkType] : undefined; + + let address: string | undefined; + switch (addressType) { + case "witnesspubkeyhash": + address = payments.p2wpkh({ + pubkey: Buffer.from(pubkey, "hex"), + network, + }).address; + break; + default: + throw new Error("Unsupported address type: " + addressType); + } + if (!address) { + throw new Error( + "No address found for " + pubkey + " (" + addressType + ")" + ); + } + return address; +} diff --git a/src/common/lib/mnemonic.ts b/src/common/lib/mnemonic.ts index 98021944e3..dbd25e4a2e 100644 --- a/src/common/lib/mnemonic.ts +++ b/src/common/lib/mnemonic.ts @@ -24,3 +24,13 @@ export function derivePrivateKey(mnemonic: string, path: string) { } return secp256k1.utils.bytesToHex(privateKeyBytes); } + +export function getPublicKey(mnemonic: string, path: string) { + const seed = bip39.mnemonicToSeedSync(mnemonic); + const hdkey = HDKey.fromMasterSeed(seed); + const publicKeyBytes = hdkey.derive(path).publicKey; + if (!publicKeyBytes) { + throw new Error("invalid derived public key"); + } + return secp256k1.utils.bytesToHex(publicKeyBytes); +} diff --git a/src/extension/background-script/actions/webbtc/getAddresses.ts b/src/extension/background-script/actions/webbtc/getAddresses.ts new file mode 100644 index 0000000000..230899f5e7 --- /dev/null +++ b/src/extension/background-script/actions/webbtc/getAddresses.ts @@ -0,0 +1,81 @@ +import { getAddressFromPubkey, getAddressType } from "~/common/lib/btc"; +import { decryptData } from "~/common/lib/crypto"; +import { + BTC_TAPROOT_DERIVATION_PATH, + getPublicKey, +} from "~/common/lib/mnemonic"; +import state from "~/extension/background-script/state"; +import { MessageGetAddresses } from "~/types"; + +const getAddresses = async (message: MessageGetAddresses) => { + try { + // TODO: is this the correct way to decrypt the mnmenonic? + const password = await state.getState().password(); + if (!password) { + throw new Error("No password set"); + } + const account = await state.getState().getAccount(); + if (!account) { + throw new Error("No account selected"); + } + if (!account.mnemonic) { + throw new Error("No mnemonic set"); + } + const mnemonic = decryptData(account.mnemonic, password); + + const addresses: { + publicKey: string; + derivationPath: string; + index: number; + address: string; + }[] = []; + + // TODO: derivation path should come from the user's account + const accountDerivationPath = + message.args.derivationPath || BTC_TAPROOT_DERIVATION_PATH; + const accountDerivationPathParts = accountDerivationPath + .split("/") + .slice(0, 4); + if (accountDerivationPathParts.length !== 4) { + throw new Error( + "Invalid account derivation path: " + accountDerivationPath + ); + } + + for (let i = 0; i < message.args.num; i++) { + const index = message.args.index + i; + const derivationPath = + accountDerivationPath + + "/" + + (message.args.change ? 1 : 0) + + "/" + + index; + const publicKey = getPublicKey(mnemonic, derivationPath); + const purpose = accountDerivationPathParts[1]; + const coinType = accountDerivationPathParts[2]; + const addressType = getAddressType(purpose); + + const address = getAddressFromPubkey( + publicKey, + addressType, + coinType === "0'" ? "bitcoin" : "regtest" + ); + addresses.push({ + publicKey, + derivationPath, + index, + address, + }); + } + + return { + data: addresses, + }; + } catch (e) { + return { + error: "getAddresses failed: " + e, + }; + } +}; + +export default getAddresses; diff --git a/src/extension/background-script/actions/webbtc/getAddressesWithPrompt.ts b/src/extension/background-script/actions/webbtc/getAddressesWithPrompt.ts new file mode 100644 index 0000000000..07572dfbc3 --- /dev/null +++ b/src/extension/background-script/actions/webbtc/getAddressesWithPrompt.ts @@ -0,0 +1,41 @@ +import utils from "~/common/lib/utils"; +import { Message } from "~/types"; + +const getAddressesWithPrompt = async (message: Message) => { + message.args.index = message.args.index || 0; + message.args.num = message.args.num || 1; + message.args.change = message.args.change || false; + + if (typeof message.args.index !== "number") { + return { + error: "index missing.", + }; + } + + if (typeof message.args.num !== "number") { + return { + error: "num missing.", + }; + } + + if (typeof message.args.change !== "boolean") { + return { + error: "change missing.", + }; + } + + try { + const response = await utils.openPrompt({ + ...message, + action: "confirmGetAddresses", + }); + return response; + } catch (e) { + console.error("GetAddresses cancelled", e); + if (e instanceof Error) { + return { error: e.message }; + } + } +}; + +export default getAddressesWithPrompt; diff --git a/src/extension/background-script/actions/webbtc/getInfo.ts b/src/extension/background-script/actions/webbtc/getInfo.ts index 65d9c5825b..341195ba68 100644 --- a/src/extension/background-script/actions/webbtc/getInfo.ts +++ b/src/extension/background-script/actions/webbtc/getInfo.ts @@ -1,7 +1,12 @@ import { MessageGetInfo } from "~/types"; const getInfo = async (message: MessageGetInfo) => { - const supportedMethods = ["getInfo", "signPsbt"]; + const supportedMethods = [ + "getInfo", + "signPsbt", + "getAddress", + "getAddresses", + ]; return { data: { diff --git a/src/extension/background-script/actions/webbtc/index.ts b/src/extension/background-script/actions/webbtc/index.ts index a95a6292ed..ca5505ef6f 100644 --- a/src/extension/background-script/actions/webbtc/index.ts +++ b/src/extension/background-script/actions/webbtc/index.ts @@ -1,6 +1,14 @@ +import getAddresses from "~/extension/background-script/actions/webbtc/getAddresses"; +import getAddressesWithPrompt from "~/extension/background-script/actions/webbtc/getAddressesWithPrompt"; import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; import getInfo from "./getInfo"; import signPsbtWithPrompt from "./signPsbtWithPrompt"; -export { getInfo, signPsbtWithPrompt, signPsbt }; +export { + getInfo, + signPsbtWithPrompt, + signPsbt, + getAddressesWithPrompt, + getAddresses, +}; diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index 98be78c2c4..ed03f9e14b 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -60,6 +60,7 @@ const routes = { lnurlAuth: auth, getCurrencyRate: cache.getCurrencyRate, signPsbt: webbtc.signPsbt, + getAddresses: webbtc.getAddresses, setMnemonic: mnemonic.setMnemonic, getMnemonic: mnemonic.getMnemonic, @@ -76,6 +77,7 @@ const routes = { enable: allowances.enable, getInfo: webbtc.getInfo, signPsbtWithPrompt: webbtc.signPsbtWithPrompt, + getAddressesWithPrompt: webbtc.getAddressesWithPrompt, }, webln: { enable: allowances.enable, diff --git a/src/extension/content-script/onendwebbtc.js b/src/extension/content-script/onendwebbtc.js index 68763a676a..e5ddb9f8a2 100644 --- a/src/extension/content-script/onendwebbtc.js +++ b/src/extension/content-script/onendwebbtc.js @@ -9,6 +9,7 @@ const webbtcCalls = [ "webbtc/enable", "webbtc/getInfo", "webbtc/signPsbtWithPrompt", + "webbtc/getAddressesWithPrompt", ]; // calls that can be executed when `window.webbtc` is not enabled for the current content page const disabledCalls = ["webbtc/enable"]; diff --git a/src/extension/providers/webbtc/index.ts b/src/extension/providers/webbtc/index.ts index 18362098d6..5fffc97814 100644 --- a/src/extension/providers/webbtc/index.ts +++ b/src/extension/providers/webbtc/index.ts @@ -54,11 +54,26 @@ export default class WebBTCProvider { throw new Error("Alby does not support `sendTransaction`"); } - getAddress(index: number, num: number, change: boolean) { + async getAddress(index?: number, change?: boolean, derivationPath?: string) { + const addresses = await this.getAddresses(index, 1, change, derivationPath); + return addresses[0]; + } + + getAddresses( + index?: number, + num?: number, + change?: boolean, + derivationPath?: string + ) { if (!this.enabled) { throw new Error("Provider must be enabled before calling getAddress"); } - throw new Error("Alby does not support `getAddress`"); + return this.execute("getAddressesWithPrompt", { + index, + num, + change, + derivationPath, + }); } request(method: string, params: Record) { @@ -66,10 +81,7 @@ export default class WebBTCProvider { throw new Error("Provider must be enabled before calling request"); } - return this.execute("request", { - method, - params, - }); + throw new Error("Alby does not support `request`"); } // NOTE: new call `action`s must be specified also in the content script diff --git a/src/types.ts b/src/types.ts index 6969c316e3..496290b5be 100644 --- a/src/types.ts +++ b/src/types.ts @@ -151,6 +151,9 @@ export type NavigationState = { }; psbt?: string; derivationPath?: string; + index?: number; + num?: number; + change?: boolean; }; isPrompt?: true; // only passed via Prompt.tsx action: string; @@ -511,11 +514,21 @@ export interface MessageDecryptGet extends MessageDefault { export interface MessageSignPsbt extends MessageDefault { args: { psbt: string; - derivationPath?: string; // custom derivation path + derivationPath?: string; // custom derivation path TODO: move to account }; action: "signPsbt"; } +export interface MessageGetAddresses extends MessageDefault { + args: { + index: number; + num: number; + change: boolean; + derivationPath?: string; // custom derivation path TODO: move to account + }; + action: "getAddresses"; +} + export interface LNURLChannelServiceResponse { uri: string; // Remote node address of form node_key@ip_address:port_number callback: string; // a second-level URL which would initiate an OpenChannel message from target LN node From 671f07d1abe8611de94ab1dbf6a3fb0405fbeb01 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 15 May 2023 19:30:54 +0700 Subject: [PATCH 037/118] chore: add tests for webbtc getAddresses --- .../webbtc/__tests__/getAddresses.test.ts | 112 ++++++++++++++++++ .../actions/webbtc/getAddresses.ts | 17 ++- 2 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 src/extension/background-script/actions/webbtc/__tests__/getAddresses.test.ts diff --git a/src/extension/background-script/actions/webbtc/__tests__/getAddresses.test.ts b/src/extension/background-script/actions/webbtc/__tests__/getAddresses.test.ts new file mode 100644 index 0000000000..d89e19330d --- /dev/null +++ b/src/extension/background-script/actions/webbtc/__tests__/getAddresses.test.ts @@ -0,0 +1,112 @@ +import getAddresses from "~/extension/background-script/actions/webbtc/getAddresses"; +import state from "~/extension/background-script/state"; +import type { MessageGetAddresses } from "~/types"; + +const passwordMock = jest.fn; + +const regtestSegwitDerivationPath = "m/84'/1'/0'/0/0"; + +const mockMnemnoic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + +const mockState = { + password: passwordMock, + currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", + getAccount: () => ({ + mnemonic: mockMnemnoic, + }), + getConnector: jest.fn(), +}; + +state.getState = jest.fn().mockReturnValue(mockState); + +jest.mock("~/common/lib/crypto", () => { + return { + decryptData: jest.fn(() => { + return mockMnemnoic; + }), + }; +}); + +beforeEach(async () => { + // fill the DB first +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +async function sendGetAddressesMessage( + index: number, + num: number, + change: boolean, + derivationPath?: string +) { + const message: MessageGetAddresses = { + application: "LBE", + prompt: true, + action: "getAddresses", + origin: { + internal: true, + }, + args: { + index, + num, + change, + derivationPath, + }, + }; + + return await getAddresses(message); +} + +describe("getAddresses", () => { + test("get one segwit address", async () => { + const result = await sendGetAddressesMessage( + 0, + 1, + false, + regtestSegwitDerivationPath // TODO: move to account btc derivation path + ); + if (!result.data) { + throw new Error("Result should have data"); + } + + expect(result.data[0]).toMatchObject({ + publicKey: + "02e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c319", + derivationPath: "m/84'/1'/0'/0/0", + index: 0, + address: "bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk", + }); + }); + + test("get two change addresses from index 1", async () => { + const result = await sendGetAddressesMessage( + 1, + 2, + true, + regtestSegwitDerivationPath // TODO: move to account btc derivation path + ); + if (!result.data) { + throw new Error("Result should have data"); + } + + expect(result.data).toMatchObject([ + { + publicKey: + "03f37f9607be4661510885f4f960954dadfc0af91ea722fe2935ca39c1e54c2948", + derivationPath: "m/84'/1'/0'/1/1", + index: 1, + address: "bcrt1qkwgskuzmmwwvqajnyr7yp9hgvh5y45kg984qvy", + }, + { + publicKey: + "033adaeff018387bf52875cfd0a82ff29680f042e15010cb5b716b297673669836", + derivationPath: "m/84'/1'/0'/1/2", + index: 2, + address: "bcrt1q2vma00td2g9llw8hwa8ny3r774rtt7ae3q2e44", + }, + ]); + }); +}); diff --git a/src/extension/background-script/actions/webbtc/getAddresses.ts b/src/extension/background-script/actions/webbtc/getAddresses.ts index 230899f5e7..040417f474 100644 --- a/src/extension/background-script/actions/webbtc/getAddresses.ts +++ b/src/extension/background-script/actions/webbtc/getAddresses.ts @@ -31,26 +31,23 @@ const getAddresses = async (message: MessageGetAddresses) => { }[] = []; // TODO: derivation path should come from the user's account - const accountDerivationPath = + const derivationPath = message.args.derivationPath || BTC_TAPROOT_DERIVATION_PATH; - const accountDerivationPathParts = accountDerivationPath - .split("/") - .slice(0, 4); + const accountDerivationPathParts = derivationPath.split("/").slice(0, 4); if (accountDerivationPathParts.length !== 4) { - throw new Error( - "Invalid account derivation path: " + accountDerivationPath - ); + throw new Error("Invalid account derivation path: " + derivationPath); } + const accountDerivationPath = accountDerivationPathParts.join("/"); for (let i = 0; i < message.args.num; i++) { const index = message.args.index + i; - const derivationPath = + const indexDerivationPath = accountDerivationPath + "/" + (message.args.change ? 1 : 0) + "/" + index; - const publicKey = getPublicKey(mnemonic, derivationPath); + const publicKey = getPublicKey(mnemonic, indexDerivationPath); const purpose = accountDerivationPathParts[1]; const coinType = accountDerivationPathParts[2]; const addressType = getAddressType(purpose); @@ -62,7 +59,7 @@ const getAddresses = async (message: MessageGetAddresses) => { ); addresses.push({ publicKey, - derivationPath, + derivationPath: indexDerivationPath, index, address, }); From 39012c47f03c734cf84e3a10e6b661083c2074f4 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 16 May 2023 19:55:55 +0700 Subject: [PATCH 038/118] chore: fix broken tests --- src/app/screens/ConfirmGetAddresses/index.test.tsx | 9 +++++++-- src/app/screens/ConfirmGetAddresses/index.tsx | 5 +++-- src/app/screens/ConfirmSignPsbt/index.test.tsx | 11 +++++++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/app/screens/ConfirmGetAddresses/index.test.tsx b/src/app/screens/ConfirmGetAddresses/index.test.tsx index 0bc724391c..1d80003b8a 100644 --- a/src/app/screens/ConfirmGetAddresses/index.test.tsx +++ b/src/app/screens/ConfirmGetAddresses/index.test.tsx @@ -28,7 +28,9 @@ jest.mock("~/app/hooks/useNavigationState", () => { useNavigationState: jest.fn(() => ({ origin: mockOrigin, args: { - psbt: "psbt", + index: 0, + num: 1, + change: false, }, })), }; @@ -44,9 +46,12 @@ describe("ConfirmGetAddresses", () => { ); }); + // TODO: update copy expect( await screen.findByText("This website asks you to sign:") ).toBeInTheDocument(); - expect(await screen.findByText("psbt")).toBeInTheDocument(); + expect( + await screen.findByText("Get 1 external addresses from index 0") + ).toBeInTheDocument(); }); }); diff --git a/src/app/screens/ConfirmGetAddresses/index.tsx b/src/app/screens/ConfirmGetAddresses/index.tsx index e412385acd..9177f1f958 100644 --- a/src/app/screens/ConfirmGetAddresses/index.tsx +++ b/src/app/screens/ConfirmGetAddresses/index.tsx @@ -81,8 +81,9 @@ function ConfirmGetAddresses() { content={`Get ${num} ${ change ? "change" : "external" } addresses from index ${index}${ - derivationPath && - ` with custom derivation path: ${derivationPath}` + derivationPath + ? ` with custom derivation path: ${derivationPath}` + : "" }`} />
diff --git a/src/app/screens/ConfirmSignPsbt/index.test.tsx b/src/app/screens/ConfirmSignPsbt/index.test.tsx index 5ee1a71c8a..7cc0aa1546 100644 --- a/src/app/screens/ConfirmSignPsbt/index.test.tsx +++ b/src/app/screens/ConfirmSignPsbt/index.test.tsx @@ -4,6 +4,12 @@ import type { OriginData } from "~/types"; import ConfirmSignPsbt from "./index"; +// generated in sparrow wallet using mock mnemonic below, +// native segwit derivation: m/84'/1'/0' - 1 input ("m/84'/1'/0'/0/0" - first receive address), 2 outputs, saved as binary PSBT file +// imported using `cat "filename.psbt" | xxd -p -c 1000` +const regtestSegwitPsbt = + "70736274ff0100710200000001fe1204e9e35f90c356bb6fe1d8944a46b0c5ac57160f707f6f5ca728bf1ab5490000000000fdffffff0280969800000000001600146fa016500a3c6a737ebb260e2ddca78ba9234558f5ecfa0200000000160014744c9993900c8e098d599b315a9f667777e4f82a1e0100004f01043587cf030ef4b1af800000003c8c2037ee4c1621da0d348db51163709a622d0d2838dde6d8419c51f6301c6203b88e0fbe3f646337ed93bc0c0f3b843fcf7d2589e5ec884754e6402027a890b41073c5da0a5400008001000080000000800001005202000000010380d5854dfa1e789fe3cb615e834b0cef9f1aad732db4bac886eb8750497a180000000000fdffffff010284930300000000160014d0c4a3ef09e997b6e99e397e518fe3e41a118ca11401000001011f0284930300000000160014d0c4a3ef09e997b6e99e397e518fe3e41a118ca101030401000000220602e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c3191873c5da0a540000800100008000000080000000000000000000220203eeed205a69022fed4a62a02457f3699b19c06bf74bf801acc6d9ae84bc16a9e11873c5da0a540000800100008000000080000000000100000000220202e6c60079372951c3024a033ecf6584579ebf2f7927ae99c42633e805596f29351873c5da0a540000800100008000000080010000000400000000"; + const mockOrigin: OriginData = { location: "https://getalby.com/demo", domain: "https://getalby.com", @@ -28,7 +34,7 @@ jest.mock("~/app/hooks/useNavigationState", () => { useNavigationState: jest.fn(() => ({ origin: mockOrigin, args: { - psbt: "psbt", + psbt: regtestSegwitPsbt, }, })), }; @@ -44,9 +50,10 @@ describe("ConfirmSignMessage", () => { ); }); + // TODO: update copy expect( await screen.findByText("This website asks you to sign:") ).toBeInTheDocument(); - expect(await screen.findByText("psbt")).toBeInTheDocument(); + expect(await screen.findByText(regtestSegwitPsbt)).toBeInTheDocument(); }); }); From dd0b6a6099e9ba6fc6b6243ad30941ed1ef8097b Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 16 May 2023 22:19:32 +0700 Subject: [PATCH 039/118] chore: remove hardcoded network type in confirm sign psbt dialog --- .../screens/ConfirmSignPsbt/index.test.tsx | 19 ++++++++++++ src/app/screens/ConfirmSignPsbt/index.tsx | 19 ++++++++++-- .../actions/webbtc/getAddresses.ts | 9 ++---- .../actions/webbtc/getDerivationPath.ts | 31 +++++++++++++++++++ .../background-script/actions/webbtc/index.ts | 2 ++ src/extension/background-script/router.ts | 1 + src/types.ts | 14 +++++++++ 7 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 src/extension/background-script/actions/webbtc/getDerivationPath.ts diff --git a/src/app/screens/ConfirmSignPsbt/index.test.tsx b/src/app/screens/ConfirmSignPsbt/index.test.tsx index 7cc0aa1546..24b4611422 100644 --- a/src/app/screens/ConfirmSignPsbt/index.test.tsx +++ b/src/app/screens/ConfirmSignPsbt/index.test.tsx @@ -1,5 +1,6 @@ import { act, render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; +import msg from "~/common/lib/msg"; import type { OriginData } from "~/types"; import ConfirmSignPsbt from "./index"; @@ -40,6 +41,10 @@ jest.mock("~/app/hooks/useNavigationState", () => { }; }); +const regtestSegwitDerivationPath = "m/84'/1'/0'/0/0"; +// mock getDerivationPath to return regtestSegwitDerivationPath +msg.request = jest.fn().mockReturnValue(regtestSegwitDerivationPath); + describe("ConfirmSignMessage", () => { test("render", async () => { await act(async () => { @@ -55,5 +60,19 @@ describe("ConfirmSignMessage", () => { await screen.findByText("This website asks you to sign:") ).toBeInTheDocument(); expect(await screen.findByText(regtestSegwitPsbt)).toBeInTheDocument(); + // check input and outputs + expect( + await screen.findByText("bcrt1...3cppk: 59999234 sats") + ).toBeInTheDocument(); + expect( + await screen.findByText("bcrt1...707jh: 10000000 sats", { + exact: false, + }) + ).toBeInTheDocument(); + expect( + await screen.findByText("bcrt1...9rqs0: 49999093 sats", { + exact: false, + }) + ).toBeInTheDocument(); }); }); diff --git a/src/app/screens/ConfirmSignPsbt/index.tsx b/src/app/screens/ConfirmSignPsbt/index.tsx index f1d8abd7ed..431d23e713 100644 --- a/src/app/screens/ConfirmSignPsbt/index.tsx +++ b/src/app/screens/ConfirmSignPsbt/index.tsx @@ -32,9 +32,22 @@ function ConfirmSignPsbt() { const [preview, setPreview] = useState(undefined); useEffect(() => { - // FIXME: do not hardcode the network type - setPreview(getPsbtPreview(psbt, "regtest")); - }, [psbt]); + (async () => { + const derivationPath = (await msg.request( + "getDerivationPath", + {}, + { origin } + )) as string; + setPreview( + getPsbtPreview( + psbt, + derivationPath && derivationPath.split("/")[2] === "1'" + ? "regtest" + : undefined + ) + ); + })(); + }, [origin, psbt]); async function confirm() { try { diff --git a/src/extension/background-script/actions/webbtc/getAddresses.ts b/src/extension/background-script/actions/webbtc/getAddresses.ts index 040417f474..54b4a22d6c 100644 --- a/src/extension/background-script/actions/webbtc/getAddresses.ts +++ b/src/extension/background-script/actions/webbtc/getAddresses.ts @@ -5,7 +5,7 @@ import { getPublicKey, } from "~/common/lib/mnemonic"; import state from "~/extension/background-script/state"; -import { MessageGetAddresses } from "~/types"; +import { BitcoinAddress, MessageGetAddresses } from "~/types"; const getAddresses = async (message: MessageGetAddresses) => { try { @@ -23,12 +23,7 @@ const getAddresses = async (message: MessageGetAddresses) => { } const mnemonic = decryptData(account.mnemonic, password); - const addresses: { - publicKey: string; - derivationPath: string; - index: number; - address: string; - }[] = []; + const addresses: BitcoinAddress[] = []; // TODO: derivation path should come from the user's account const derivationPath = diff --git a/src/extension/background-script/actions/webbtc/getDerivationPath.ts b/src/extension/background-script/actions/webbtc/getDerivationPath.ts new file mode 100644 index 0000000000..0d2b866d7f --- /dev/null +++ b/src/extension/background-script/actions/webbtc/getDerivationPath.ts @@ -0,0 +1,31 @@ +import { decryptData } from "~/common/lib/crypto"; +import state from "~/extension/background-script/state"; +import { MessageGetDerivationPath } from "~/types"; + +const getDerivationPath = async (message: MessageGetDerivationPath) => { + try { + // TODO: is this the correct way to decrypt the mnmenonic? + const password = await state.getState().password(); + if (!password) { + throw new Error("No password set"); + } + const account = await state.getState().getAccount(); + if (!account) { + throw new Error("No account selected"); + } + let derivationPath: string | undefined; + + if (account.bip32DerivationPath) { + derivationPath = decryptData(account.bip32DerivationPath, password); + } + return { + data: derivationPath, + }; + } catch (e) { + return { + error: "getDerivationPath failed: " + e, + }; + } +}; + +export default getDerivationPath; diff --git a/src/extension/background-script/actions/webbtc/index.ts b/src/extension/background-script/actions/webbtc/index.ts index ca5505ef6f..253ea53ee0 100644 --- a/src/extension/background-script/actions/webbtc/index.ts +++ b/src/extension/background-script/actions/webbtc/index.ts @@ -1,5 +1,6 @@ import getAddresses from "~/extension/background-script/actions/webbtc/getAddresses"; import getAddressesWithPrompt from "~/extension/background-script/actions/webbtc/getAddressesWithPrompt"; +import getDerivationPath from "~/extension/background-script/actions/webbtc/getDerivationPath"; import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; import getInfo from "./getInfo"; @@ -11,4 +12,5 @@ export { signPsbt, getAddressesWithPrompt, getAddresses, + getDerivationPath, }; diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index ed03f9e14b..c2e3797011 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -60,6 +60,7 @@ const routes = { lnurlAuth: auth, getCurrencyRate: cache.getCurrencyRate, signPsbt: webbtc.signPsbt, + getDerivationPath: webbtc.getDerivationPath, getAddresses: webbtc.getAddresses, setMnemonic: mnemonic.setMnemonic, getMnemonic: mnemonic.getMnemonic, diff --git a/src/types.ts b/src/types.ts index 496290b5be..b039255591 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,6 +18,7 @@ export interface Account { name: string; nostrPrivateKey?: string | null; mnemonic?: string | null; + bip32DerivationPath?: string | null; } export interface Accounts { @@ -529,6 +530,12 @@ export interface MessageGetAddresses extends MessageDefault { action: "getAddresses"; } +export interface MessageGetDerivationPath extends MessageDefault { + // eslint-disable-next-line @typescript-eslint/ban-types + args: {}; + action: "getDerivationPath"; +} + export interface LNURLChannelServiceResponse { uri: string; // Remote node address of form node_key@ip_address:port_number callback: string; // a second-level URL which would initiate an OpenChannel message from target LN node @@ -821,3 +828,10 @@ export interface DeferredPromise { } export type Theme = "dark" | "light"; + +export type BitcoinAddress = { + publicKey: string; + derivationPath: string; + index: number; + address: string; +}; From 4acb8b7edacc63a94f4a07a43b5850f6a2545d5e Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 17 May 2023 13:08:27 +0700 Subject: [PATCH 040/118] feat: use derivation path from account instead of passing parameter in webbtc methods --- src/app/screens/ConfirmGetAddresses/index.tsx | 9 ++------ src/app/screens/ConfirmSignPsbt/index.tsx | 7 +----- .../webbtc/__tests__/getAddresses.test.ts | 23 +++++-------------- .../actions/webbtc/__tests__/signPsbt.test.ts | 20 +++++++--------- .../actions/webbtc/getAddresses.ts | 7 +++--- .../actions/webbtc/signPsbt.ts | 8 +++---- src/extension/providers/webbtc/index.ts | 16 ++++--------- src/types.ts | 2 -- 8 files changed, 30 insertions(+), 62 deletions(-) diff --git a/src/app/screens/ConfirmGetAddresses/index.tsx b/src/app/screens/ConfirmGetAddresses/index.tsx index 9177f1f958..8c02ca728f 100644 --- a/src/app/screens/ConfirmGetAddresses/index.tsx +++ b/src/app/screens/ConfirmGetAddresses/index.tsx @@ -25,7 +25,6 @@ function ConfirmGetAddresses() { const index = navState.args?.index as number; const num = navState.args?.num as number; const change = navState.args?.change as boolean; - const derivationPath = navState.args?.derivationPath as string | undefined; const origin = navState.origin as OriginData; const [loading, setLoading] = useState(false); @@ -36,7 +35,7 @@ function ConfirmGetAddresses() { setLoading(true); const response = await msg.request( "getAddresses", - { index, num, change, derivationPath }, + { index, num, change }, { origin } ); msg.reply(response); @@ -80,11 +79,7 @@ function ConfirmGetAddresses() { heading={t("content", { host: origin.host })} content={`Get ${num} ${ change ? "change" : "external" - } addresses from index ${index}${ - derivationPath - ? ` with custom derivation path: ${derivationPath}` - : "" - }`} + } addresses from index ${index}`} />
({ mnemonic: mockMnemnoic, + bip32DerivationPath: regtestSegwitDerivationPath, }), getConnector: jest.fn(), }; @@ -22,8 +23,8 @@ state.getState = jest.fn().mockReturnValue(mockState); jest.mock("~/common/lib/crypto", () => { return { - decryptData: jest.fn(() => { - return mockMnemnoic; + decryptData: jest.fn((encrypted, _password) => { + return encrypted; }), }; }); @@ -39,8 +40,7 @@ afterEach(() => { async function sendGetAddressesMessage( index: number, num: number, - change: boolean, - derivationPath?: string + change: boolean ) { const message: MessageGetAddresses = { application: "LBE", @@ -53,7 +53,6 @@ async function sendGetAddressesMessage( index, num, change, - derivationPath, }, }; @@ -62,12 +61,7 @@ async function sendGetAddressesMessage( describe("getAddresses", () => { test("get one segwit address", async () => { - const result = await sendGetAddressesMessage( - 0, - 1, - false, - regtestSegwitDerivationPath // TODO: move to account btc derivation path - ); + const result = await sendGetAddressesMessage(0, 1, false); if (!result.data) { throw new Error("Result should have data"); } @@ -82,12 +76,7 @@ describe("getAddresses", () => { }); test("get two change addresses from index 1", async () => { - const result = await sendGetAddressesMessage( - 1, - 2, - true, - regtestSegwitDerivationPath // TODO: move to account btc derivation path - ); + const result = await sendGetAddressesMessage(1, 2, true); if (!result.data) { throw new Error("Result should have data"); } diff --git a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts index c6dca95634..5662957f8b 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts @@ -1,6 +1,7 @@ import { hex } from "@scure/base"; import * as btc from "@scure/btc-signer"; import { Psbt, networks, payments } from "bitcoinjs-lib"; +import msg from "~/common/lib/msg"; import { getPsbtPreview } from "~/common/lib/psbt"; import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; import state from "~/extension/background-script/state"; @@ -28,6 +29,7 @@ const mockState = { currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", getAccount: () => ({ mnemonic: mockMnemnoic, + bip32DerivationPath: regtestSegwitDerivationPath, }), getConnector: jest.fn(), }; @@ -36,12 +38,15 @@ state.getState = jest.fn().mockReturnValue(mockState); jest.mock("~/common/lib/crypto", () => { return { - decryptData: jest.fn(() => { - return mockMnemnoic; + decryptData: jest.fn((encrypted, _password) => { + return encrypted; }), }; }); +// mock getDerivationPath to return regtestSegwitDerivationPath +msg.request = jest.fn().mockReturnValue(regtestSegwitDerivationPath); + beforeEach(async () => { // fill the DB first }); @@ -60,7 +65,6 @@ async function sendPsbtMessage(psbt: string, derivationPath?: string) { }, args: { psbt, - derivationPath, }, }; @@ -69,10 +73,7 @@ async function sendPsbtMessage(psbt: string, derivationPath?: string) { describe("signPsbt", () => { test("1 input, segwit, regtest", async () => { - const result = await sendPsbtMessage( - regtestSegwitPsbt, - regtestSegwitDerivationPath // TODO: move to account btc derivation path - ); + const result = await sendPsbtMessage(regtestSegwitPsbt); if (!result.data) { throw new Error("Result should have data"); } @@ -124,11 +125,6 @@ describe("decode psbt", () => { ); expect(unsignedPsbt.txOutputs[1].value).toBe(49999093); - console.info( - hex.encode(unsignedPsbt.txInputs[0].hash), - unsignedPsbt.data.inputs[0] - ); - // fee should be 141 sats // input should be bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk 59,999,234 sats // output 1 should be bcrt1qd7spv5q28348xl4myc8zmh983w5jx32cs707jh 10,000,000 sats diff --git a/src/extension/background-script/actions/webbtc/getAddresses.ts b/src/extension/background-script/actions/webbtc/getAddresses.ts index 54b4a22d6c..70a7676a11 100644 --- a/src/extension/background-script/actions/webbtc/getAddresses.ts +++ b/src/extension/background-script/actions/webbtc/getAddresses.ts @@ -25,9 +25,10 @@ const getAddresses = async (message: MessageGetAddresses) => { const addresses: BitcoinAddress[] = []; - // TODO: derivation path should come from the user's account - const derivationPath = - message.args.derivationPath || BTC_TAPROOT_DERIVATION_PATH; + const derivationPath = account.bip32DerivationPath + ? decryptData(account.bip32DerivationPath, password) + : BTC_TAPROOT_DERIVATION_PATH; + const accountDerivationPathParts = derivationPath.split("/").slice(0, 4); if (accountDerivationPathParts.length !== 4) { throw new Error("Invalid account derivation path: " + derivationPath); diff --git a/src/extension/background-script/actions/webbtc/signPsbt.ts b/src/extension/background-script/actions/webbtc/signPsbt.ts index d34467eaef..2f32b94b1a 100644 --- a/src/extension/background-script/actions/webbtc/signPsbt.ts +++ b/src/extension/background-script/actions/webbtc/signPsbt.ts @@ -24,12 +24,12 @@ const signPsbt = async (message: MessageSignPsbt) => { throw new Error("No mnemonic set"); } const mnemonic = decryptData(account.mnemonic, password); + const derivationPath = account.bip32DerivationPath + ? decryptData(account.bip32DerivationPath, password) + : undefined; const privateKey = secp256k1.utils.hexToBytes( // TODO: allow account to specify derivation path - derivePrivateKey( - mnemonic, - message.args.derivationPath || BTC_TAPROOT_DERIVATION_PATH - ) + derivePrivateKey(mnemonic, derivationPath || BTC_TAPROOT_DERIVATION_PATH) ); const psbtBytes = secp256k1.utils.hexToBytes(message.args.psbt); diff --git a/src/extension/providers/webbtc/index.ts b/src/extension/providers/webbtc/index.ts index 5fffc97814..8cd3ad39e6 100644 --- a/src/extension/providers/webbtc/index.ts +++ b/src/extension/providers/webbtc/index.ts @@ -37,12 +37,12 @@ export default class WebBTCProvider { return this.execute("getInfo"); } - signPsbt(psbt: string, derivationPath?: string) { + signPsbt(psbt: string) { if (!this.enabled) { throw new Error("Provider must be enabled before calling signPsbt"); } - return this.execute("signPsbtWithPrompt", { psbt, derivationPath }); + return this.execute("signPsbtWithPrompt", { psbt }); } sendTransaction(address: string, amount: string) { @@ -54,17 +54,12 @@ export default class WebBTCProvider { throw new Error("Alby does not support `sendTransaction`"); } - async getAddress(index?: number, change?: boolean, derivationPath?: string) { - const addresses = await this.getAddresses(index, 1, change, derivationPath); + async getAddress(index?: number, change?: boolean) { + const addresses = await this.getAddresses(index, 1, change); return addresses[0]; } - getAddresses( - index?: number, - num?: number, - change?: boolean, - derivationPath?: string - ) { + getAddresses(index?: number, num?: number, change?: boolean) { if (!this.enabled) { throw new Error("Provider must be enabled before calling getAddress"); } @@ -72,7 +67,6 @@ export default class WebBTCProvider { index, num, change, - derivationPath, }); } diff --git a/src/types.ts b/src/types.ts index b039255591..9470cd849d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -515,7 +515,6 @@ export interface MessageDecryptGet extends MessageDefault { export interface MessageSignPsbt extends MessageDefault { args: { psbt: string; - derivationPath?: string; // custom derivation path TODO: move to account }; action: "signPsbt"; } @@ -525,7 +524,6 @@ export interface MessageGetAddresses extends MessageDefault { index: number; num: number; change: boolean; - derivationPath?: string; // custom derivation path TODO: move to account }; action: "getAddresses"; } From c88e6867b3fa1332db4a1db1607819a09b7a7cd0 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 17 May 2023 13:59:47 +0700 Subject: [PATCH 041/118] chore: replace getDerivationPath with bitcoin network option --- .../screens/ConfirmSignPsbt/index.test.tsx | 22 ++++++++++-- src/app/screens/ConfirmSignPsbt/index.tsx | 16 ++------- src/app/screens/Settings/index.tsx | 34 +++++++++++++++++++ src/common/constants.ts | 1 + src/common/lib/mnemonic.ts | 7 +--- .../webbtc/__tests__/getAddresses.test.ts | 6 ++-- .../actions/webbtc/__tests__/signPsbt.test.ts | 10 ++---- .../actions/webbtc/getAddresses.ts | 9 +++-- .../actions/webbtc/getDerivationPath.ts | 31 ----------------- .../background-script/actions/webbtc/index.ts | 2 -- .../actions/webbtc/signPsbt.ts | 14 +++++--- .../events/__test__/notifications.test.ts | 1 + src/extension/background-script/router.ts | 1 - src/i18n/locales/en/translation.json | 12 +++++++ src/types.ts | 8 +---- 15 files changed, 93 insertions(+), 81 deletions(-) delete mode 100644 src/extension/background-script/actions/webbtc/getDerivationPath.ts diff --git a/src/app/screens/ConfirmSignPsbt/index.test.tsx b/src/app/screens/ConfirmSignPsbt/index.test.tsx index 24b4611422..5c9dd76487 100644 --- a/src/app/screens/ConfirmSignPsbt/index.test.tsx +++ b/src/app/screens/ConfirmSignPsbt/index.test.tsx @@ -1,6 +1,7 @@ import { act, render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import msg from "~/common/lib/msg"; +import state from "~/extension/background-script/state"; import type { OriginData } from "~/types"; import ConfirmSignPsbt from "./index"; @@ -41,9 +42,24 @@ jest.mock("~/app/hooks/useNavigationState", () => { }; }); -const regtestSegwitDerivationPath = "m/84'/1'/0'/0/0"; -// mock getDerivationPath to return regtestSegwitDerivationPath -msg.request = jest.fn().mockReturnValue(regtestSegwitDerivationPath); +const passwordMock = jest.fn; + +const mockState = { + password: passwordMock, + currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", + getAccount: () => ({}), + getConnector: jest.fn(), + settings: { + bitcoinNetwork: "regtest", + }, +}; + +state.getState = jest.fn().mockReturnValue(mockState); + +// mock get settings +msg.request = jest.fn().mockReturnValue({ + bitcoinNetwork: "regtest", +}); describe("ConfirmSignMessage", () => { test("render", async () => { diff --git a/src/app/screens/ConfirmSignPsbt/index.tsx b/src/app/screens/ConfirmSignPsbt/index.tsx index 5ef49e1d37..415946363f 100644 --- a/src/app/screens/ConfirmSignPsbt/index.tsx +++ b/src/app/screens/ConfirmSignPsbt/index.tsx @@ -12,6 +12,7 @@ import Loading from "~/app/components/Loading"; import ScreenHeader from "~/app/components/ScreenHeader"; import { useNavigationState } from "~/app/hooks/useNavigationState"; import { USER_REJECTED_ERROR } from "~/common/constants"; +import api from "~/common/lib/api"; import msg from "~/common/lib/msg"; import { PsbtPreview, getPsbtPreview } from "~/common/lib/psbt"; import type { OriginData } from "~/types"; @@ -32,19 +33,8 @@ function ConfirmSignPsbt() { useEffect(() => { (async () => { - const derivationPath = (await msg.request( - "getDerivationPath", - {}, - { origin } - )) as string; - setPreview( - getPsbtPreview( - psbt, - derivationPath && derivationPath.split("/")[2] === "1'" - ? "regtest" - : undefined - ) - ); + const settings = await api.getSettings(); + setPreview(getPsbtPreview(psbt, settings.bitcoinNetwork)); })(); }, [origin, psbt]); diff --git a/src/app/screens/Settings/index.tsx b/src/app/screens/Settings/index.tsx index 2d37891cab..1afaf43edf 100644 --- a/src/app/screens/Settings/index.tsx +++ b/src/app/screens/Settings/index.tsx @@ -369,6 +369,40 @@ function Settings() {
+

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

+

+ {t("bitcoin.hint")} +

+
+ + {!isLoading && ( +
+ +
+ )} +
+
+
diff --git a/src/common/constants.ts b/src/common/constants.ts index 9bb3a0b736..9eb14301b3 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -184,4 +184,5 @@ export const DEFAULT_SETTINGS: SettingsStorage = { exchange: "alby", nostrEnabled: false, closedTips: [], + bitcoinNetwork: "bitcoin", }; diff --git a/src/common/lib/mnemonic.ts b/src/common/lib/mnemonic.ts index dbd25e4a2e..75db45102c 100644 --- a/src/common/lib/mnemonic.ts +++ b/src/common/lib/mnemonic.ts @@ -4,12 +4,7 @@ import * as bip39 from "@scure/bip39"; export const NOSTR_DERIVATION_PATH = "m/44'/1237'/0'/0/0"; // NIP-06 export const BTC_TAPROOT_DERIVATION_PATH = "m/86'/0'/0'/0/0"; -// Segwit -// m/84'/0'/0'/0/0 -// Nested segwit -// m/84'/0'/0'/0/0 -// Legacy -// m/44'/0'/0'/0/0 +export const BTC_TAPROOT_DERIVATION_PATH_REGTEST = "m/84'/1'/0'/0/0"; //"m/86'/1'/0'/0/0"; // FIXME: export function deriveNostrPrivateKey(mnemonic: string) { return derivePrivateKey(mnemonic, NOSTR_DERIVATION_PATH); diff --git a/src/extension/background-script/actions/webbtc/__tests__/getAddresses.test.ts b/src/extension/background-script/actions/webbtc/__tests__/getAddresses.test.ts index b1da49f03c..1ef95bfe15 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/getAddresses.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/getAddresses.test.ts @@ -4,8 +4,6 @@ import type { MessageGetAddresses } from "~/types"; const passwordMock = jest.fn; -const regtestSegwitDerivationPath = "m/84'/1'/0'/0/0"; - const mockMnemnoic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; @@ -14,9 +12,11 @@ const mockState = { currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", getAccount: () => ({ mnemonic: mockMnemnoic, - bip32DerivationPath: regtestSegwitDerivationPath, }), getConnector: jest.fn(), + settings: { + bitcoinNetwork: "regtest", + }, }; state.getState = jest.fn().mockReturnValue(mockState); diff --git a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts index 5662957f8b..c8f28954fc 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts @@ -1,7 +1,6 @@ import { hex } from "@scure/base"; import * as btc from "@scure/btc-signer"; import { Psbt, networks, payments } from "bitcoinjs-lib"; -import msg from "~/common/lib/msg"; import { getPsbtPreview } from "~/common/lib/psbt"; import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; import state from "~/extension/background-script/state"; @@ -19,8 +18,6 @@ const regtestSegwitPsbt = const regtestSegwitSignedPsbt = "02000000000101fe1204e9e35f90c356bb6fe1d8944a46b0c5ac57160f707f6f5ca728bf1ab5490000000000fdffffff0280969800000000001600146fa016500a3c6a737ebb260e2ddca78ba9234558f5ecfa0200000000160014744c9993900c8e098d599b315a9f667777e4f82a02473044022065255b047fecc1b5a0afdf095a367c538c6afde33a1d6fb9f5fe28638aa7dbcf022072de13455179e876c336b32cc525c1b862f7199913e8b67c0663566489fcd2c0012102e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c3191e010000"; -const regtestSegwitDerivationPath = "m/84'/1'/0'/0/0"; - const mockMnemnoic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; @@ -29,9 +26,11 @@ const mockState = { currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", getAccount: () => ({ mnemonic: mockMnemnoic, - bip32DerivationPath: regtestSegwitDerivationPath, }), getConnector: jest.fn(), + settings: { + bitcoinNetwork: "regtest", + }, }; state.getState = jest.fn().mockReturnValue(mockState); @@ -44,9 +43,6 @@ jest.mock("~/common/lib/crypto", () => { }; }); -// mock getDerivationPath to return regtestSegwitDerivationPath -msg.request = jest.fn().mockReturnValue(regtestSegwitDerivationPath); - beforeEach(async () => { // fill the DB first }); diff --git a/src/extension/background-script/actions/webbtc/getAddresses.ts b/src/extension/background-script/actions/webbtc/getAddresses.ts index 70a7676a11..d653bbd38a 100644 --- a/src/extension/background-script/actions/webbtc/getAddresses.ts +++ b/src/extension/background-script/actions/webbtc/getAddresses.ts @@ -2,6 +2,7 @@ import { getAddressFromPubkey, getAddressType } from "~/common/lib/btc"; import { decryptData } from "~/common/lib/crypto"; import { BTC_TAPROOT_DERIVATION_PATH, + BTC_TAPROOT_DERIVATION_PATH_REGTEST, getPublicKey, } from "~/common/lib/mnemonic"; import state from "~/extension/background-script/state"; @@ -22,12 +23,14 @@ const getAddresses = async (message: MessageGetAddresses) => { throw new Error("No mnemonic set"); } const mnemonic = decryptData(account.mnemonic, password); + const settings = state.getState().settings; const addresses: BitcoinAddress[] = []; - const derivationPath = account.bip32DerivationPath - ? decryptData(account.bip32DerivationPath, password) - : BTC_TAPROOT_DERIVATION_PATH; + const derivationPath = + settings.bitcoinNetwork === "bitcoin" + ? BTC_TAPROOT_DERIVATION_PATH + : BTC_TAPROOT_DERIVATION_PATH_REGTEST; const accountDerivationPathParts = derivationPath.split("/").slice(0, 4); if (accountDerivationPathParts.length !== 4) { diff --git a/src/extension/background-script/actions/webbtc/getDerivationPath.ts b/src/extension/background-script/actions/webbtc/getDerivationPath.ts deleted file mode 100644 index 0d2b866d7f..0000000000 --- a/src/extension/background-script/actions/webbtc/getDerivationPath.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { decryptData } from "~/common/lib/crypto"; -import state from "~/extension/background-script/state"; -import { MessageGetDerivationPath } from "~/types"; - -const getDerivationPath = async (message: MessageGetDerivationPath) => { - try { - // TODO: is this the correct way to decrypt the mnmenonic? - const password = await state.getState().password(); - if (!password) { - throw new Error("No password set"); - } - const account = await state.getState().getAccount(); - if (!account) { - throw new Error("No account selected"); - } - let derivationPath: string | undefined; - - if (account.bip32DerivationPath) { - derivationPath = decryptData(account.bip32DerivationPath, password); - } - return { - data: derivationPath, - }; - } catch (e) { - return { - error: "getDerivationPath failed: " + e, - }; - } -}; - -export default getDerivationPath; diff --git a/src/extension/background-script/actions/webbtc/index.ts b/src/extension/background-script/actions/webbtc/index.ts index 253ea53ee0..ca5505ef6f 100644 --- a/src/extension/background-script/actions/webbtc/index.ts +++ b/src/extension/background-script/actions/webbtc/index.ts @@ -1,6 +1,5 @@ import getAddresses from "~/extension/background-script/actions/webbtc/getAddresses"; import getAddressesWithPrompt from "~/extension/background-script/actions/webbtc/getAddressesWithPrompt"; -import getDerivationPath from "~/extension/background-script/actions/webbtc/getDerivationPath"; import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; import getInfo from "./getInfo"; @@ -12,5 +11,4 @@ export { signPsbt, getAddressesWithPrompt, getAddresses, - getDerivationPath, }; diff --git a/src/extension/background-script/actions/webbtc/signPsbt.ts b/src/extension/background-script/actions/webbtc/signPsbt.ts index 2f32b94b1a..5452bb887a 100644 --- a/src/extension/background-script/actions/webbtc/signPsbt.ts +++ b/src/extension/background-script/actions/webbtc/signPsbt.ts @@ -4,6 +4,7 @@ import * as btc from "@scure/btc-signer"; import { decryptData } from "~/common/lib/crypto"; import { BTC_TAPROOT_DERIVATION_PATH, + BTC_TAPROOT_DERIVATION_PATH_REGTEST, derivePrivateKey, } from "~/common/lib/mnemonic"; import state from "~/extension/background-script/state"; @@ -24,12 +25,15 @@ const signPsbt = async (message: MessageSignPsbt) => { throw new Error("No mnemonic set"); } const mnemonic = decryptData(account.mnemonic, password); - const derivationPath = account.bip32DerivationPath - ? decryptData(account.bip32DerivationPath, password) - : undefined; + const settings = state.getState().settings; + + const derivationPath = + settings.bitcoinNetwork === "bitcoin" + ? BTC_TAPROOT_DERIVATION_PATH + : BTC_TAPROOT_DERIVATION_PATH_REGTEST; + const privateKey = secp256k1.utils.hexToBytes( - // TODO: allow account to specify derivation path - derivePrivateKey(mnemonic, derivationPath || BTC_TAPROOT_DERIVATION_PATH) + derivePrivateKey(mnemonic, derivationPath) ); const psbtBytes = secp256k1.utils.hexToBytes(message.args.psbt); diff --git a/src/extension/background-script/events/__test__/notifications.test.ts b/src/extension/background-script/events/__test__/notifications.test.ts index 9f92db3416..fa8a8d04ca 100644 --- a/src/extension/background-script/events/__test__/notifications.test.ts +++ b/src/extension/background-script/events/__test__/notifications.test.ts @@ -31,6 +31,7 @@ const settings: SettingsStorage = { websiteEnhancements: true, nostrEnabled: false, closedTips: [], + bitcoinNetwork: "bitcoin", }; const mockState = { diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index c2e3797011..ed03f9e14b 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -60,7 +60,6 @@ const routes = { lnurlAuth: auth, getCurrencyRate: cache.getCurrencyRate, signPsbt: webbtc.signPsbt, - getDerivationPath: webbtc.getDerivationPath, getAddresses: webbtc.getAddresses, setMnemonic: mnemonic.setMnemonic, getMnemonic: mnemonic.getMnemonic, diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 80417beed8..59467fcec7 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -553,6 +553,18 @@ "system": "System" } }, + "bitcoin": { + "title": "Bitcoin", + "hint": "Manage your Bitcoin settings for WebBTC-enabled apps", + "network": { + "title": "Network", + "subtitle": "Choose network to derive addresses and decode transactions", + "options": { + "bitcoin": "Mainnet", + "regtest": "Regtest" + } + } + }, "show_fiat": { "title": "Sats to Fiat", "subtitle": "Always convert into selected currency from selected exchange" diff --git a/src/types.ts b/src/types.ts index 9470cd849d..e6773998b2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,7 +18,6 @@ export interface Account { name: string; nostrPrivateKey?: string | null; mnemonic?: string | null; - bip32DerivationPath?: string | null; } export interface Accounts { @@ -528,12 +527,6 @@ export interface MessageGetAddresses extends MessageDefault { action: "getAddresses"; } -export interface MessageGetDerivationPath extends MessageDefault { - // eslint-disable-next-line @typescript-eslint/ban-types - args: {}; - action: "getDerivationPath"; -} - export interface LNURLChannelServiceResponse { uri: string; // Remote node address of form node_key@ip_address:port_number callback: string; // a second-level URL which would initiate an OpenChannel message from target LN node @@ -762,6 +755,7 @@ export interface SettingsStorage { exchange: SupportedExchanges; nostrEnabled: boolean; closedTips: TIPS[]; + bitcoinNetwork: "bitcoin" | "regtest"; } export interface Badge { From f09b13d592d60fc7757878bc3da815703bd26af5 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 17 May 2023 14:27:58 +0700 Subject: [PATCH 042/118] chore: use getAddress instead of getAddresses function --- src/app/router/Prompt/Prompt.tsx | 7 +- .../index.test.tsx | 11 +- .../index.tsx | 26 ++--- .../webbtc/__tests__/getAddress.test.ts | 69 ++++++++++++ .../webbtc/__tests__/getAddresses.test.ts | 101 ------------------ .../actions/webbtc/getAddress.ts | 61 +++++++++++ .../actions/webbtc/getAddressWithPrompt.ts | 19 ++++ .../actions/webbtc/getAddresses.ts | 77 ------------- .../actions/webbtc/getAddressesWithPrompt.ts | 41 ------- .../actions/webbtc/getInfo.ts | 7 +- .../background-script/actions/webbtc/index.ts | 8 +- src/extension/background-script/router.ts | 4 +- src/extension/content-script/onendwebbtc.js | 2 +- src/extension/providers/webbtc/index.ts | 13 +-- src/i18n/locales/en/translation.json | 5 + src/types.ts | 11 +- 16 files changed, 181 insertions(+), 281 deletions(-) rename src/app/screens/{ConfirmGetAddresses => ConfirmGetAddress}/index.test.tsx (81%) rename src/app/screens/{ConfirmGetAddresses => ConfirmGetAddress}/index.tsx (77%) create mode 100644 src/extension/background-script/actions/webbtc/__tests__/getAddress.test.ts delete mode 100644 src/extension/background-script/actions/webbtc/__tests__/getAddresses.test.ts create mode 100644 src/extension/background-script/actions/webbtc/getAddress.ts create mode 100644 src/extension/background-script/actions/webbtc/getAddressWithPrompt.ts delete mode 100644 src/extension/background-script/actions/webbtc/getAddresses.ts delete mode 100644 src/extension/background-script/actions/webbtc/getAddressesWithPrompt.ts diff --git a/src/app/router/Prompt/Prompt.tsx b/src/app/router/Prompt/Prompt.tsx index 625b882794..9de080a6c5 100644 --- a/src/app/router/Prompt/Prompt.tsx +++ b/src/app/router/Prompt/Prompt.tsx @@ -18,7 +18,7 @@ import { HashRouter, Navigate, Outlet, Route, Routes } from "react-router-dom"; import { ToastContainer } from "react-toastify"; import Providers from "~/app/context/Providers"; import RequireAuth from "~/app/router/RequireAuth"; -import ConfirmGetAddresses from "~/app/screens/ConfirmGetAddresses"; +import ConfirmGetAddress from "~/app/screens/ConfirmGetAddress"; import ConfirmSignPsbt from "~/app/screens/ConfirmSignPsbt"; import type { NavigationState, OriginData } from "~/types"; @@ -103,10 +103,7 @@ function Prompt() { } /> } /> } /> - } - /> + } /> } diff --git a/src/app/screens/ConfirmGetAddresses/index.test.tsx b/src/app/screens/ConfirmGetAddress/index.test.tsx similarity index 81% rename from src/app/screens/ConfirmGetAddresses/index.test.tsx rename to src/app/screens/ConfirmGetAddress/index.test.tsx index 1d80003b8a..53bb52087d 100644 --- a/src/app/screens/ConfirmGetAddresses/index.test.tsx +++ b/src/app/screens/ConfirmGetAddress/index.test.tsx @@ -2,7 +2,7 @@ import { act, render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import type { OriginData } from "~/types"; -import ConfirmGetAddresses from "./index"; +import ConfirmGetAddress from "./index"; const mockOrigin: OriginData = { location: "https://getalby.com/demo", @@ -36,22 +36,21 @@ jest.mock("~/app/hooks/useNavigationState", () => { }; }); -describe("ConfirmGetAddresses", () => { +describe("ConfirmGetAddress", () => { test("render", async () => { await act(async () => { render( - + ); }); - // TODO: update copy expect( - await screen.findByText("This website asks you to sign:") + await screen.findByText("This website asks you to read:") ).toBeInTheDocument(); expect( - await screen.findByText("Get 1 external addresses from index 0") + await screen.findByText("Your Bitcoin receive address") ).toBeInTheDocument(); }); }); diff --git a/src/app/screens/ConfirmGetAddresses/index.tsx b/src/app/screens/ConfirmGetAddress/index.tsx similarity index 77% rename from src/app/screens/ConfirmGetAddresses/index.tsx rename to src/app/screens/ConfirmGetAddress/index.tsx index 8c02ca728f..fe9d0cf8f7 100644 --- a/src/app/screens/ConfirmGetAddresses/index.tsx +++ b/src/app/screens/ConfirmGetAddress/index.tsx @@ -14,18 +14,14 @@ import { USER_REJECTED_ERROR } from "~/common/constants"; import msg from "~/common/lib/msg"; import type { OriginData } from "~/types"; -function ConfirmGetAddresses() { +function ConfirmGetAddress() { const navState = useNavigationState(); const { t: tCommon } = useTranslation("common"); const { t } = useTranslation("translation", { - keyPrefix: "confirm_sign_message", + keyPrefix: "confirm_get_address", }); const navigate = useNavigate(); - const index = navState.args?.index as number; - const num = navState.args?.num as number; - const change = navState.args?.change as boolean; - const origin = navState.origin as OriginData; const [loading, setLoading] = useState(false); const [successMessage, setSuccessMessage] = useState(""); @@ -33,11 +29,7 @@ function ConfirmGetAddresses() { async function confirm() { try { setLoading(true); - const response = await msg.request( - "getAddresses", - { index, num, change }, - { origin } - ); + const response = await msg.request("getAddress", {}, { origin }); msg.reply(response); setSuccessMessage(tCommon("success")); } catch (e) { @@ -64,7 +56,7 @@ function ConfirmGetAddresses() { return (
- + {!successMessage ? (
@@ -74,13 +66,7 @@ function ConfirmGetAddresses() { url={origin.host} /> - +
({ + mnemonic: mockMnemnoic, + }), + getConnector: jest.fn(), + settings: { + bitcoinNetwork: "regtest", + }, +}; + +state.getState = jest.fn().mockReturnValue(mockState); + +jest.mock("~/common/lib/crypto", () => { + return { + decryptData: jest.fn((encrypted, _password) => { + return encrypted; + }), + }; +}); + +beforeEach(async () => { + // fill the DB first +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +async function sendGetAddressMessage() { + const message: MessageGetAddress = { + application: "LBE", + prompt: true, + action: "getAddress", + origin: { + internal: true, + }, + args: {}, + }; + + return await getAddress(message); +} + +describe("getAddress", () => { + test("get one segwit address", async () => { + const result = await sendGetAddressMessage(); + if (!result.data) { + throw new Error("Result should have data"); + } + + expect(result.data).toMatchObject({ + publicKey: + "02e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c319", + derivationPath: "m/84'/1'/0'/0/0", + index: 0, + address: "bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk", + }); + }); +}); diff --git a/src/extension/background-script/actions/webbtc/__tests__/getAddresses.test.ts b/src/extension/background-script/actions/webbtc/__tests__/getAddresses.test.ts deleted file mode 100644 index 1ef95bfe15..0000000000 --- a/src/extension/background-script/actions/webbtc/__tests__/getAddresses.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import getAddresses from "~/extension/background-script/actions/webbtc/getAddresses"; -import state from "~/extension/background-script/state"; -import type { MessageGetAddresses } from "~/types"; - -const passwordMock = jest.fn; - -const mockMnemnoic = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - -const mockState = { - password: passwordMock, - currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", - getAccount: () => ({ - mnemonic: mockMnemnoic, - }), - getConnector: jest.fn(), - settings: { - bitcoinNetwork: "regtest", - }, -}; - -state.getState = jest.fn().mockReturnValue(mockState); - -jest.mock("~/common/lib/crypto", () => { - return { - decryptData: jest.fn((encrypted, _password) => { - return encrypted; - }), - }; -}); - -beforeEach(async () => { - // fill the DB first -}); - -afterEach(() => { - jest.clearAllMocks(); -}); - -async function sendGetAddressesMessage( - index: number, - num: number, - change: boolean -) { - const message: MessageGetAddresses = { - application: "LBE", - prompt: true, - action: "getAddresses", - origin: { - internal: true, - }, - args: { - index, - num, - change, - }, - }; - - return await getAddresses(message); -} - -describe("getAddresses", () => { - test("get one segwit address", async () => { - const result = await sendGetAddressesMessage(0, 1, false); - if (!result.data) { - throw new Error("Result should have data"); - } - - expect(result.data[0]).toMatchObject({ - publicKey: - "02e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c319", - derivationPath: "m/84'/1'/0'/0/0", - index: 0, - address: "bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk", - }); - }); - - test("get two change addresses from index 1", async () => { - const result = await sendGetAddressesMessage(1, 2, true); - if (!result.data) { - throw new Error("Result should have data"); - } - - expect(result.data).toMatchObject([ - { - publicKey: - "03f37f9607be4661510885f4f960954dadfc0af91ea722fe2935ca39c1e54c2948", - derivationPath: "m/84'/1'/0'/1/1", - index: 1, - address: "bcrt1qkwgskuzmmwwvqajnyr7yp9hgvh5y45kg984qvy", - }, - { - publicKey: - "033adaeff018387bf52875cfd0a82ff29680f042e15010cb5b716b297673669836", - derivationPath: "m/84'/1'/0'/1/2", - index: 2, - address: "bcrt1q2vma00td2g9llw8hwa8ny3r774rtt7ae3q2e44", - }, - ]); - }); -}); diff --git a/src/extension/background-script/actions/webbtc/getAddress.ts b/src/extension/background-script/actions/webbtc/getAddress.ts new file mode 100644 index 0000000000..cf649773de --- /dev/null +++ b/src/extension/background-script/actions/webbtc/getAddress.ts @@ -0,0 +1,61 @@ +import { getAddressFromPubkey, getAddressType } from "~/common/lib/btc"; +import { decryptData } from "~/common/lib/crypto"; +import { + BTC_TAPROOT_DERIVATION_PATH, + BTC_TAPROOT_DERIVATION_PATH_REGTEST, + getPublicKey, +} from "~/common/lib/mnemonic"; +import state from "~/extension/background-script/state"; +import { BitcoinAddress, MessageGetAddress } from "~/types"; + +const getAddress = async (message: MessageGetAddress) => { + try { + // TODO: is this the correct way to decrypt the mnmenonic? + const password = await state.getState().password(); + if (!password) { + throw new Error("No password set"); + } + const account = await state.getState().getAccount(); + if (!account) { + throw new Error("No account selected"); + } + if (!account.mnemonic) { + throw new Error("No mnemonic set"); + } + const mnemonic = decryptData(account.mnemonic, password); + const settings = state.getState().settings; + + const derivationPath = + settings.bitcoinNetwork === "bitcoin" + ? BTC_TAPROOT_DERIVATION_PATH + : BTC_TAPROOT_DERIVATION_PATH_REGTEST; + + const derivationPathParts = derivationPath.split("/"); + const publicKey = getPublicKey(mnemonic, derivationPath); + const purpose = derivationPathParts[1]; + const addressType = getAddressType(purpose); + + const address = getAddressFromPubkey( + publicKey, + addressType, + settings.bitcoinNetwork + ); + + const data: BitcoinAddress = { + publicKey, + derivationPath, + index: 0, + address, + }; + + return { + data, + }; + } catch (e) { + return { + error: "getAddress failed: " + e, + }; + } +}; + +export default getAddress; diff --git a/src/extension/background-script/actions/webbtc/getAddressWithPrompt.ts b/src/extension/background-script/actions/webbtc/getAddressWithPrompt.ts new file mode 100644 index 0000000000..04223d1b33 --- /dev/null +++ b/src/extension/background-script/actions/webbtc/getAddressWithPrompt.ts @@ -0,0 +1,19 @@ +import utils from "~/common/lib/utils"; +import { Message } from "~/types"; + +const getAddressWithPrompt = async (message: Message) => { + try { + const response = await utils.openPrompt({ + ...message, + action: "confirmGetAddress", + }); + return response; + } catch (e) { + console.error("getAddress cancelled", e); + if (e instanceof Error) { + return { error: e.message }; + } + } +}; + +export default getAddressWithPrompt; diff --git a/src/extension/background-script/actions/webbtc/getAddresses.ts b/src/extension/background-script/actions/webbtc/getAddresses.ts deleted file mode 100644 index d653bbd38a..0000000000 --- a/src/extension/background-script/actions/webbtc/getAddresses.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { getAddressFromPubkey, getAddressType } from "~/common/lib/btc"; -import { decryptData } from "~/common/lib/crypto"; -import { - BTC_TAPROOT_DERIVATION_PATH, - BTC_TAPROOT_DERIVATION_PATH_REGTEST, - getPublicKey, -} from "~/common/lib/mnemonic"; -import state from "~/extension/background-script/state"; -import { BitcoinAddress, MessageGetAddresses } from "~/types"; - -const getAddresses = async (message: MessageGetAddresses) => { - try { - // TODO: is this the correct way to decrypt the mnmenonic? - const password = await state.getState().password(); - if (!password) { - throw new Error("No password set"); - } - const account = await state.getState().getAccount(); - if (!account) { - throw new Error("No account selected"); - } - if (!account.mnemonic) { - throw new Error("No mnemonic set"); - } - const mnemonic = decryptData(account.mnemonic, password); - const settings = state.getState().settings; - - const addresses: BitcoinAddress[] = []; - - const derivationPath = - settings.bitcoinNetwork === "bitcoin" - ? BTC_TAPROOT_DERIVATION_PATH - : BTC_TAPROOT_DERIVATION_PATH_REGTEST; - - const accountDerivationPathParts = derivationPath.split("/").slice(0, 4); - if (accountDerivationPathParts.length !== 4) { - throw new Error("Invalid account derivation path: " + derivationPath); - } - const accountDerivationPath = accountDerivationPathParts.join("/"); - - for (let i = 0; i < message.args.num; i++) { - const index = message.args.index + i; - const indexDerivationPath = - accountDerivationPath + - "/" + - (message.args.change ? 1 : 0) + - "/" + - index; - const publicKey = getPublicKey(mnemonic, indexDerivationPath); - const purpose = accountDerivationPathParts[1]; - const coinType = accountDerivationPathParts[2]; - const addressType = getAddressType(purpose); - - const address = getAddressFromPubkey( - publicKey, - addressType, - coinType === "0'" ? "bitcoin" : "regtest" - ); - addresses.push({ - publicKey, - derivationPath: indexDerivationPath, - index, - address, - }); - } - - return { - data: addresses, - }; - } catch (e) { - return { - error: "getAddresses failed: " + e, - }; - } -}; - -export default getAddresses; diff --git a/src/extension/background-script/actions/webbtc/getAddressesWithPrompt.ts b/src/extension/background-script/actions/webbtc/getAddressesWithPrompt.ts deleted file mode 100644 index 07572dfbc3..0000000000 --- a/src/extension/background-script/actions/webbtc/getAddressesWithPrompt.ts +++ /dev/null @@ -1,41 +0,0 @@ -import utils from "~/common/lib/utils"; -import { Message } from "~/types"; - -const getAddressesWithPrompt = async (message: Message) => { - message.args.index = message.args.index || 0; - message.args.num = message.args.num || 1; - message.args.change = message.args.change || false; - - if (typeof message.args.index !== "number") { - return { - error: "index missing.", - }; - } - - if (typeof message.args.num !== "number") { - return { - error: "num missing.", - }; - } - - if (typeof message.args.change !== "boolean") { - return { - error: "change missing.", - }; - } - - try { - const response = await utils.openPrompt({ - ...message, - action: "confirmGetAddresses", - }); - return response; - } catch (e) { - console.error("GetAddresses cancelled", e); - if (e instanceof Error) { - return { error: e.message }; - } - } -}; - -export default getAddressesWithPrompt; diff --git a/src/extension/background-script/actions/webbtc/getInfo.ts b/src/extension/background-script/actions/webbtc/getInfo.ts index 341195ba68..8b0fbd91b5 100644 --- a/src/extension/background-script/actions/webbtc/getInfo.ts +++ b/src/extension/background-script/actions/webbtc/getInfo.ts @@ -1,12 +1,7 @@ import { MessageGetInfo } from "~/types"; const getInfo = async (message: MessageGetInfo) => { - const supportedMethods = [ - "getInfo", - "signPsbt", - "getAddress", - "getAddresses", - ]; + const supportedMethods = ["getInfo", "signPsbt", "getAddress", "getAddress"]; return { data: { diff --git a/src/extension/background-script/actions/webbtc/index.ts b/src/extension/background-script/actions/webbtc/index.ts index ca5505ef6f..953227ad9e 100644 --- a/src/extension/background-script/actions/webbtc/index.ts +++ b/src/extension/background-script/actions/webbtc/index.ts @@ -1,5 +1,5 @@ -import getAddresses from "~/extension/background-script/actions/webbtc/getAddresses"; -import getAddressesWithPrompt from "~/extension/background-script/actions/webbtc/getAddressesWithPrompt"; +import getAddress from "~/extension/background-script/actions/webbtc/getAddress"; +import getAddressWithPrompt from "~/extension/background-script/actions/webbtc/getAddressWithPrompt"; import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; import getInfo from "./getInfo"; @@ -9,6 +9,6 @@ export { getInfo, signPsbtWithPrompt, signPsbt, - getAddressesWithPrompt, - getAddresses, + getAddressWithPrompt, + getAddress, }; diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index ed03f9e14b..cd2cddc74c 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -60,7 +60,7 @@ const routes = { lnurlAuth: auth, getCurrencyRate: cache.getCurrencyRate, signPsbt: webbtc.signPsbt, - getAddresses: webbtc.getAddresses, + getAddress: webbtc.getAddress, setMnemonic: mnemonic.setMnemonic, getMnemonic: mnemonic.getMnemonic, @@ -77,7 +77,7 @@ const routes = { enable: allowances.enable, getInfo: webbtc.getInfo, signPsbtWithPrompt: webbtc.signPsbtWithPrompt, - getAddressesWithPrompt: webbtc.getAddressesWithPrompt, + getAddressWithPrompt: webbtc.getAddressWithPrompt, }, webln: { enable: allowances.enable, diff --git a/src/extension/content-script/onendwebbtc.js b/src/extension/content-script/onendwebbtc.js index e5ddb9f8a2..d8caab5594 100644 --- a/src/extension/content-script/onendwebbtc.js +++ b/src/extension/content-script/onendwebbtc.js @@ -9,7 +9,7 @@ const webbtcCalls = [ "webbtc/enable", "webbtc/getInfo", "webbtc/signPsbtWithPrompt", - "webbtc/getAddressesWithPrompt", + "webbtc/getAddressWithPrompt", ]; // calls that can be executed when `window.webbtc` is not enabled for the current content page const disabledCalls = ["webbtc/enable"]; diff --git a/src/extension/providers/webbtc/index.ts b/src/extension/providers/webbtc/index.ts index 8cd3ad39e6..9bfaf376d1 100644 --- a/src/extension/providers/webbtc/index.ts +++ b/src/extension/providers/webbtc/index.ts @@ -54,20 +54,11 @@ export default class WebBTCProvider { throw new Error("Alby does not support `sendTransaction`"); } - async getAddress(index?: number, change?: boolean) { - const addresses = await this.getAddresses(index, 1, change); - return addresses[0]; - } - - getAddresses(index?: number, num?: number, change?: boolean) { + getAddress() { if (!this.enabled) { throw new Error("Provider must be enabled before calling getAddress"); } - return this.execute("getAddressesWithPrompt", { - index, - num, - change, - }); + return this.execute("getAddressWithPrompt", {}); } request(method: string, params: Record) { diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 59467fcec7..d7f7e042b6 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -752,6 +752,11 @@ "title": "Sign", "content": "This website asks you to sign:" }, + "confirm_get_address": { + "title": "Get Address", + "heading": "This website asks you to read:", + "content": "Your Bitcoin receive address" + }, "confirm_keysend": { "title": "Approve Payment", "success": "Payment sent! Preimage: {{preimage}}", diff --git a/src/types.ts b/src/types.ts index e6773998b2..ea3abc27a4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -518,13 +518,10 @@ export interface MessageSignPsbt extends MessageDefault { action: "signPsbt"; } -export interface MessageGetAddresses extends MessageDefault { - args: { - index: number; - num: number; - change: boolean; - }; - action: "getAddresses"; +export interface MessageGetAddress extends MessageDefault { + // eslint-disable-next-line @typescript-eslint/ban-types + args: {}; + action: "getAddress"; } export interface LNURLChannelServiceResponse { From 25530d9490b210a55c2a4a4178c3cd10304a110f Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 17 May 2023 23:28:34 +0700 Subject: [PATCH 043/118] feat: use taproot with signpsbt and getaddress --- package.json | 2 + .../screens/ConfirmSignPsbt/index.test.tsx | 19 ++--- src/common/lib/btc.ts | 35 ++------ src/common/lib/mnemonic.ts | 4 +- src/common/lib/psbt.ts | 43 ++++++---- .../webbtc/__tests__/getAddress.test.ts | 15 ++-- .../actions/webbtc/__tests__/signPsbt.test.ts | 74 +++------------- .../actions/webbtc/getAddress.ts | 17 ++-- .../actions/webbtc/signPsbt.ts | 85 ++++++++++++++++--- src/fixtures/btc.ts | 15 ++++ yarn.lock | 25 +++++- 11 files changed, 184 insertions(+), 150 deletions(-) create mode 100644 src/fixtures/btc.ts diff --git a/package.json b/package.json index 6a9515803d..bc5b4e09eb 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "crypto-js": "^4.1.1", "dayjs": "^1.11.7", "dexie": "^3.2.3", + "ecpair": "^2.1.0", "elliptic": "^6.5.4", "html5-qrcode": "^2.3.8", "i18next": "^22.4.15", @@ -71,6 +72,7 @@ "react-toastify": "^9.1.2", "stream": "^0.0.2", "tailwindcss": "^3.3.2", + "tiny-secp256k1": "^2.2.1", "uuid": "^9.0.0", "webextension-polyfill": "^0.10.0", "zustand": "^3.7.2" diff --git a/src/app/screens/ConfirmSignPsbt/index.test.tsx b/src/app/screens/ConfirmSignPsbt/index.test.tsx index 5c9dd76487..0e17b5874f 100644 --- a/src/app/screens/ConfirmSignPsbt/index.test.tsx +++ b/src/app/screens/ConfirmSignPsbt/index.test.tsx @@ -2,16 +2,11 @@ import { act, render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import msg from "~/common/lib/msg"; import state from "~/extension/background-script/state"; +import { btcFixture } from "~/fixtures/btc"; import type { OriginData } from "~/types"; import ConfirmSignPsbt from "./index"; -// generated in sparrow wallet using mock mnemonic below, -// native segwit derivation: m/84'/1'/0' - 1 input ("m/84'/1'/0'/0/0" - first receive address), 2 outputs, saved as binary PSBT file -// imported using `cat "filename.psbt" | xxd -p -c 1000` -const regtestSegwitPsbt = - "70736274ff0100710200000001fe1204e9e35f90c356bb6fe1d8944a46b0c5ac57160f707f6f5ca728bf1ab5490000000000fdffffff0280969800000000001600146fa016500a3c6a737ebb260e2ddca78ba9234558f5ecfa0200000000160014744c9993900c8e098d599b315a9f667777e4f82a1e0100004f01043587cf030ef4b1af800000003c8c2037ee4c1621da0d348db51163709a622d0d2838dde6d8419c51f6301c6203b88e0fbe3f646337ed93bc0c0f3b843fcf7d2589e5ec884754e6402027a890b41073c5da0a5400008001000080000000800001005202000000010380d5854dfa1e789fe3cb615e834b0cef9f1aad732db4bac886eb8750497a180000000000fdffffff010284930300000000160014d0c4a3ef09e997b6e99e397e518fe3e41a118ca11401000001011f0284930300000000160014d0c4a3ef09e997b6e99e397e518fe3e41a118ca101030401000000220602e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c3191873c5da0a540000800100008000000080000000000000000000220203eeed205a69022fed4a62a02457f3699b19c06bf74bf801acc6d9ae84bc16a9e11873c5da0a540000800100008000000080000000000100000000220202e6c60079372951c3024a033ecf6584579ebf2f7927ae99c42633e805596f29351873c5da0a540000800100008000000080010000000400000000"; - const mockOrigin: OriginData = { location: "https://getalby.com/demo", domain: "https://getalby.com", @@ -36,7 +31,7 @@ jest.mock("~/app/hooks/useNavigationState", () => { useNavigationState: jest.fn(() => ({ origin: mockOrigin, args: { - psbt: regtestSegwitPsbt, + psbt: btcFixture.regtestTaprootPsbt, }, })), }; @@ -75,18 +70,20 @@ describe("ConfirmSignMessage", () => { expect( await screen.findByText("This website asks you to sign:") ).toBeInTheDocument(); - expect(await screen.findByText(regtestSegwitPsbt)).toBeInTheDocument(); + expect( + await screen.findByText(btcFixture.regtestTaprootPsbt) + ).toBeInTheDocument(); // check input and outputs expect( - await screen.findByText("bcrt1...3cppk: 59999234 sats") + await screen.findByText("bcrt1...eprhg: 10000000 sats") ).toBeInTheDocument(); expect( - await screen.findByText("bcrt1...707jh: 10000000 sats", { + await screen.findByText("bcrt1...c7l22: 4999845 sats", { exact: false, }) ).toBeInTheDocument(); expect( - await screen.findByText("bcrt1...9rqs0: 49999093 sats", { + await screen.findByText("bcrt1...cyx0f: 5000000 sats", { exact: false, }) ).toBeInTheDocument(); diff --git a/src/common/lib/btc.ts b/src/common/lib/btc.ts index 3efee334ab..e476818bca 100644 --- a/src/common/lib/btc.ts +++ b/src/common/lib/btc.ts @@ -1,38 +1,15 @@ -import { networks, payments } from "bitcoinjs-lib"; +import * as btc from "@scure/btc-signer"; +import { networks } from "bitcoinjs-lib"; -type SupportedAddressType = "witnesspubkeyhash"; - -export function getAddressType(purpose: string): SupportedAddressType { - switch (purpose) { - case "84'": - return "witnesspubkeyhash"; - default: - throw new Error("Unsupported purpose: " + purpose); - } -} - -export function getAddressFromPubkey( - pubkey: string, - addressType: SupportedAddressType, +export function getTaprootAddressFromPrivateKey( + privateKey: string, networkType?: keyof typeof networks ) { const network = networkType ? networks[networkType] : undefined; - let address: string | undefined; - switch (addressType) { - case "witnesspubkeyhash": - address = payments.p2wpkh({ - pubkey: Buffer.from(pubkey, "hex"), - network, - }).address; - break; - default: - throw new Error("Unsupported address type: " + addressType); - } + const address = btc.getAddress("tr", Buffer.from(privateKey, "hex"), network); if (!address) { - throw new Error( - "No address found for " + pubkey + " (" + addressType + ")" - ); + throw new Error("No taproot address found from private key"); } return address; } diff --git a/src/common/lib/mnemonic.ts b/src/common/lib/mnemonic.ts index 75db45102c..5548699b00 100644 --- a/src/common/lib/mnemonic.ts +++ b/src/common/lib/mnemonic.ts @@ -4,7 +4,7 @@ import * as bip39 from "@scure/bip39"; export const NOSTR_DERIVATION_PATH = "m/44'/1237'/0'/0/0"; // NIP-06 export const BTC_TAPROOT_DERIVATION_PATH = "m/86'/0'/0'/0/0"; -export const BTC_TAPROOT_DERIVATION_PATH_REGTEST = "m/84'/1'/0'/0/0"; //"m/86'/1'/0'/0/0"; // FIXME: +export const BTC_TAPROOT_DERIVATION_PATH_REGTEST = "m/86'/1'/0'/0/0"; export function deriveNostrPrivateKey(mnemonic: string) { return derivePrivateKey(mnemonic, NOSTR_DERIVATION_PATH); @@ -20,7 +20,7 @@ export function derivePrivateKey(mnemonic: string, path: string) { return secp256k1.utils.bytesToHex(privateKeyBytes); } -export function getPublicKey(mnemonic: string, path: string) { +export function derivePublicKey(mnemonic: string, path: string) { const seed = bip39.mnemonicToSeedSync(mnemonic); const hdkey = HDKey.fromMasterSeed(seed); const publicKeyBytes = hdkey.derive(path).publicKey; diff --git a/src/common/lib/psbt.ts b/src/common/lib/psbt.ts index 3626b8e85a..f31d8c92e2 100644 --- a/src/common/lib/psbt.ts +++ b/src/common/lib/psbt.ts @@ -1,4 +1,5 @@ -import { Psbt, networks, payments } from "bitcoinjs-lib"; +import * as btc from "@scure/btc-signer"; +import { Psbt, networks } from "bitcoinjs-lib"; type Address = { amount: number; address: string }; @@ -26,18 +27,16 @@ export function getPsbtPreview( if (i > 0) { throw new Error("Multiple inputs currently unsupported"); } - const inputType = unsignedPsbt.getInputType(i); - if (inputType !== "witnesspubkeyhash") { - throw new Error("Unsupported input type: " + inputType); - } - const bip32Derivation = unsignedPsbt.data.inputs[i].bip32Derivation; - if (!bip32Derivation) { + + const tapBip32Derivation = unsignedPsbt.data.inputs[i].tapBip32Derivation; + if (!tapBip32Derivation) { throw new Error("No bip32Derivation in input " + i); } - const address = payments.p2wpkh({ - pubkey: bip32Derivation[0].pubkey, - network, - }).address; + const address = btc.p2tr( + tapBip32Derivation[0].pubkey, + undefined, + network + ).address; if (!address) { throw new Error("No address found in input " + i); @@ -52,16 +51,26 @@ export function getPsbtPreview( address, }); } - for (let i = 0; i < unsignedPsbt.txOutputs.length; i++) { + for (let i = 0; i < unsignedPsbt.data.outputs.length; i++) { const txOutput = unsignedPsbt.txOutputs[i]; - if (!txOutput.address) { - throw new Error("No address in output " + i); + const output = unsignedPsbt.data.outputs[i]; + if (!output.tapBip32Derivation) { + throw new Error("No tapBip32Derivation in output"); + } + const address = btc.p2tr( + output.tapBip32Derivation[0].pubkey, + undefined, + network + ).address; + if (!address) { + throw new Error("No address found in output " + i); } - const output: Address = { + + const previewOutput: Address = { amount: txOutput.value, - address: txOutput.address, + address, }; - preview.outputs.push(output); + preview.outputs.push(previewOutput); } return preview; } diff --git a/src/extension/background-script/actions/webbtc/__tests__/getAddress.test.ts b/src/extension/background-script/actions/webbtc/__tests__/getAddress.test.ts index 8bfb7670e2..6736ac62e4 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/getAddress.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/getAddress.test.ts @@ -1,17 +1,15 @@ import getAddress from "~/extension/background-script/actions/webbtc/getAddress"; import state from "~/extension/background-script/state"; +import { btcFixture } from "~/fixtures/btc"; import type { MessageGetAddress } from "~/types"; const passwordMock = jest.fn; -const mockMnemnoic = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - const mockState = { password: passwordMock, currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", getAccount: () => ({ - mnemonic: mockMnemnoic, + mnemonic: btcFixture.mnemnoic, }), getConnector: jest.fn(), settings: { @@ -52,7 +50,7 @@ async function sendGetAddressMessage() { } describe("getAddress", () => { - test("get one segwit address", async () => { + test("get taproot address", async () => { const result = await sendGetAddressMessage(); if (!result.data) { throw new Error("Result should have data"); @@ -60,10 +58,11 @@ describe("getAddress", () => { expect(result.data).toMatchObject({ publicKey: - "02e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c319", - derivationPath: "m/84'/1'/0'/0/0", + "0255355ca83c973f1d97ce0e3843c85d78905af16b4dc531bc488e57212d230116", + derivationPath: "m/86'/1'/0'/0/0", index: 0, - address: "bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk", + address: + "bcrt1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqjeprhg", }); }); }); diff --git a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts index c8f28954fc..58ecd040db 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts @@ -1,31 +1,18 @@ import { hex } from "@scure/base"; import * as btc from "@scure/btc-signer"; -import { Psbt, networks, payments } from "bitcoinjs-lib"; import { getPsbtPreview } from "~/common/lib/psbt"; import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; import state from "~/extension/background-script/state"; +import { btcFixture } from "~/fixtures/btc"; import type { MessageSignPsbt } from "~/types"; const passwordMock = jest.fn; -// generated in sparrow wallet using mock mnemonic below, -// native segwit derivation: m/84'/1'/0' - 1 input ("m/84'/1'/0'/0/0" - first receive address), 2 outputs, saved as binary PSBT file -// imported using `cat "filename.psbt" | xxd -p -c 1000` -const regtestSegwitPsbt = - "70736274ff0100710200000001fe1204e9e35f90c356bb6fe1d8944a46b0c5ac57160f707f6f5ca728bf1ab5490000000000fdffffff0280969800000000001600146fa016500a3c6a737ebb260e2ddca78ba9234558f5ecfa0200000000160014744c9993900c8e098d599b315a9f667777e4f82a1e0100004f01043587cf030ef4b1af800000003c8c2037ee4c1621da0d348db51163709a622d0d2838dde6d8419c51f6301c6203b88e0fbe3f646337ed93bc0c0f3b843fcf7d2589e5ec884754e6402027a890b41073c5da0a5400008001000080000000800001005202000000010380d5854dfa1e789fe3cb615e834b0cef9f1aad732db4bac886eb8750497a180000000000fdffffff010284930300000000160014d0c4a3ef09e997b6e99e397e518fe3e41a118ca11401000001011f0284930300000000160014d0c4a3ef09e997b6e99e397e518fe3e41a118ca101030401000000220602e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c3191873c5da0a540000800100008000000080000000000000000000220203eeed205a69022fed4a62a02457f3699b19c06bf74bf801acc6d9ae84bc16a9e11873c5da0a540000800100008000000080000000000100000000220202e6c60079372951c3024a033ecf6584579ebf2f7927ae99c42633e805596f29351873c5da0a540000800100008000000080010000000400000000"; - -// signed PSBT and verified by importing in sparrow and broadcasting transaction -const regtestSegwitSignedPsbt = - "02000000000101fe1204e9e35f90c356bb6fe1d8944a46b0c5ac57160f707f6f5ca728bf1ab5490000000000fdffffff0280969800000000001600146fa016500a3c6a737ebb260e2ddca78ba9234558f5ecfa0200000000160014744c9993900c8e098d599b315a9f667777e4f82a02473044022065255b047fecc1b5a0afdf095a367c538c6afde33a1d6fb9f5fe28638aa7dbcf022072de13455179e876c336b32cc525c1b862f7199913e8b67c0663566489fcd2c0012102e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c3191e010000"; - -const mockMnemnoic = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - const mockState = { password: passwordMock, currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", getAccount: () => ({ - mnemonic: mockMnemnoic, + mnemonic: btcFixture.mnemnoic, }), getConnector: jest.fn(), settings: { @@ -68,8 +55,8 @@ async function sendPsbtMessage(psbt: string, derivationPath?: string) { } describe("signPsbt", () => { - test("1 input, segwit, regtest", async () => { - const result = await sendPsbtMessage(regtestSegwitPsbt); + test("1 input, taproot, regtest", async () => { + const result = await sendPsbtMessage(btcFixture.regtestTaprootPsbt); if (!result.data) { throw new Error("Result should have data"); } @@ -80,7 +67,7 @@ describe("signPsbt", () => { const checkTx = btc.Transaction.fromRaw(hex.decode(result.data.signed)); expect(checkTx.isFinal).toBe(true); - expect(result.data?.signed).toBe(regtestSegwitSignedPsbt); + expect(result.data?.signed).toBe(btcFixture.regtestTaprootSignedPsbt); }); }); @@ -92,57 +79,22 @@ describe("signPsbt input validation", () => { }); describe("decode psbt", () => { - test("manually decode segwit transaction", async () => { - const unsignedPsbt = Psbt.fromHex(regtestSegwitPsbt, { - network: networks.regtest, - }); - expect(unsignedPsbt.data.inputs[0].witnessUtxo?.value).toBe(59999234); - - expect(unsignedPsbt.getInputType(0)).toBe("witnesspubkeyhash"); - expect( - hex.encode( - unsignedPsbt.data.inputs[0].bip32Derivation?.[0].pubkey ?? - new Uint8Array() - ) - ).toBe( - "02e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c319" - ); - const address = payments.p2wpkh({ - pubkey: unsignedPsbt.data.inputs[0].bip32Derivation?.[0].pubkey, - network: networks.regtest, - }).address; - expect(address).toBe("bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk"); - expect(unsignedPsbt.txOutputs[0].address).toBe( - "bcrt1qd7spv5q28348xl4myc8zmh983w5jx32cs707jh" - ); - expect(unsignedPsbt.txOutputs[0].value).toBe(10000000); - expect(unsignedPsbt.txOutputs[1].address).toBe( - "bcrt1qw3xfnyuspj8qnr2envc448mxwam7f7p249rqs0" - ); - expect(unsignedPsbt.txOutputs[1].value).toBe(49999093); - - // fee should be 141 sats - // input should be bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk 59,999,234 sats - // output 1 should be bcrt1qd7spv5q28348xl4myc8zmh983w5jx32cs707jh 10,000,000 sats - // output 2 should be bcrt1qw3xfnyuspj8qnr2envc448mxwam7f7p249rqs0 49,999,093 sats - }); - - test("get segwit transaction preview", async () => { - const preview = getPsbtPreview(regtestSegwitPsbt, "regtest"); + test("get taproot transaction preview", async () => { + const preview = getPsbtPreview(btcFixture.regtestTaprootPsbt, "regtest"); expect(preview.inputs.length).toBe(1); expect(preview.inputs[0].address).toBe( - "bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk" + "bcrt1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqjeprhg" ); - expect(preview.inputs[0].amount).toBe(59999234); + expect(preview.inputs[0].amount).toBe(10_000_000); expect(preview.outputs.length).toBe(2); expect(preview.outputs[0].address).toBe( - "bcrt1qd7spv5q28348xl4myc8zmh983w5jx32cs707jh" + "bcrt1p6uav7en8k7zsumsqugdmg5j6930zmzy4dg7jcddshsr0fvxlqx7qnc7l22" ); - expect(preview.outputs[0].amount).toBe(10000000); + expect(preview.outputs[0].amount).toBe(4_999_845); expect(preview.outputs[1].address).toBe( - "bcrt1qw3xfnyuspj8qnr2envc448mxwam7f7p249rqs0" + "bcrt1p90h6z3p36n9hrzy7580h5l429uwchyg8uc9sz4jwzhdtuhqdl5eqkcyx0f" ); - expect(preview.outputs[1].amount).toBe(49999093); + expect(preview.outputs[1].amount).toBe(5_000_000); }); }); diff --git a/src/extension/background-script/actions/webbtc/getAddress.ts b/src/extension/background-script/actions/webbtc/getAddress.ts index cf649773de..c017ce8cff 100644 --- a/src/extension/background-script/actions/webbtc/getAddress.ts +++ b/src/extension/background-script/actions/webbtc/getAddress.ts @@ -1,9 +1,10 @@ -import { getAddressFromPubkey, getAddressType } from "~/common/lib/btc"; +import { getTaprootAddressFromPrivateKey } from "~/common/lib/btc"; import { decryptData } from "~/common/lib/crypto"; import { BTC_TAPROOT_DERIVATION_PATH, BTC_TAPROOT_DERIVATION_PATH_REGTEST, - getPublicKey, + derivePrivateKey, + derivePublicKey, } from "~/common/lib/mnemonic"; import state from "~/extension/background-script/state"; import { BitcoinAddress, MessageGetAddress } from "~/types"; @@ -30,14 +31,11 @@ const getAddress = async (message: MessageGetAddress) => { ? BTC_TAPROOT_DERIVATION_PATH : BTC_TAPROOT_DERIVATION_PATH_REGTEST; - const derivationPathParts = derivationPath.split("/"); - const publicKey = getPublicKey(mnemonic, derivationPath); - const purpose = derivationPathParts[1]; - const addressType = getAddressType(purpose); + const privateKey = derivePrivateKey(mnemonic, derivationPath); + const publicKey = derivePublicKey(mnemonic, derivationPath); - const address = getAddressFromPubkey( - publicKey, - addressType, + const address = getTaprootAddressFromPrivateKey( + privateKey, settings.bitcoinNetwork ); @@ -52,6 +50,7 @@ const getAddress = async (message: MessageGetAddress) => { data, }; } catch (e) { + console.error("getAddress failed: ", e); return { error: "getAddress failed: " + e, }; diff --git a/src/extension/background-script/actions/webbtc/signPsbt.ts b/src/extension/background-script/actions/webbtc/signPsbt.ts index 5452bb887a..0ef9e884cb 100644 --- a/src/extension/background-script/actions/webbtc/signPsbt.ts +++ b/src/extension/background-script/actions/webbtc/signPsbt.ts @@ -1,6 +1,14 @@ import * as secp256k1 from "@noble/secp256k1"; -import { hex } from "@scure/base"; -import * as btc from "@scure/btc-signer"; +import { + Network, + Psbt, + Signer, + crypto, + initEccLib, + networks, +} from "bitcoinjs-lib"; +import ECPairFactory, { ECPairAPI } from "ecpair"; +import * as tinysecp from "tiny-secp256k1"; import { decryptData } from "~/common/lib/crypto"; import { BTC_TAPROOT_DERIVATION_PATH, @@ -36,18 +44,33 @@ const signPsbt = async (message: MessageSignPsbt) => { derivePrivateKey(mnemonic, derivationPath) ); - const psbtBytes = secp256k1.utils.hexToBytes(message.args.psbt); - const transaction = btc.Transaction.fromPSBT(psbtBytes); + const taprootPsbt = Psbt.fromHex(message.args.psbt, { + network: networks[settings.bitcoinNetwork], + }); + + initEccLib(tinysecp); + const ECPair: ECPairAPI = ECPairFactory(tinysecp); + + const keyPair = tweakSigner( + ECPair, + ECPair.fromPrivateKey(Buffer.from(privateKey), { + network: networks[settings.bitcoinNetwork], + }), + { + network: networks[settings.bitcoinNetwork], + } + ); - // this only works with a single input - // TODO: support signing individual inputs - transaction.sign(privateKey); + // Step 1: Sign the Taproot PSBT inputs + taprootPsbt.data.inputs.forEach((input, index) => { + taprootPsbt.signTaprootInput(index, keyPair); + }); - // TODO: Do we need to finalize() here or should that be done by websites? - // if signing individual inputs, each should be finalized individually - transaction.finalize(); + // Step 2: Finalize the Taproot PSBT + taprootPsbt.finalizeAllInputs(); - const signedTransaction = hex.encode(transaction.extract()); + // Step 3: Get the finalized transaction + const signedTransaction = taprootPsbt.extractTransaction().toHex(); return { data: { @@ -55,6 +78,7 @@ const signPsbt = async (message: MessageSignPsbt) => { }, }; } catch (e) { + console.error("signPsbt failed: ", e); return { error: "signPsbt failed: " + e, }; @@ -62,3 +86,42 @@ const signPsbt = async (message: MessageSignPsbt) => { }; export default signPsbt; + +// Below code taken from https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/taproot.spec.ts#L636 +const toXOnly = (pubKey: Buffer) => + pubKey.length === 32 ? pubKey : pubKey.slice(1, 33); + +function tweakSigner( + ECPair: ECPairAPI, + signer: Signer, + opts: { network: Network; tweakHash?: Buffer | undefined } +): Signer { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + let privateKey: Uint8Array | undefined = signer.privateKey; + if (!privateKey) { + throw new Error("Private key is required for tweaking signer!"); + } + if (signer.publicKey[0] === 3) { + privateKey = tinysecp.privateNegate(privateKey); + } + + const tweakedPrivateKey = tinysecp.privateAdd( + privateKey, + tapTweakHash(toXOnly(signer.publicKey), opts.tweakHash) + ); + if (!tweakedPrivateKey) { + throw new Error("Invalid tweaked private key!"); + } + + return ECPair.fromPrivateKey(Buffer.from(tweakedPrivateKey), { + network: opts.network, + }); +} + +function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer { + return crypto.taggedHash( + "TapTweak", + Buffer.concat(h ? [pubKey, h] : [pubKey]) + ); +} diff --git a/src/fixtures/btc.ts b/src/fixtures/btc.ts new file mode 100644 index 0000000000..5b6f46ce88 --- /dev/null +++ b/src/fixtures/btc.ts @@ -0,0 +1,15 @@ +export const btcFixture = { + // generated in sparrow wallet using mock mnemonic below, + // native taproot derivation: m/86'/1'/0' - 1 input ("m/86'/1'/0'/0/0" - first receive address), 2 outputs, saved as binary PSBT file + // imported using `cat taproot.psbt | xxd -p -c 1000` + regtestTaprootPsbt: + "70736274ff0100890200000001b58806ecf5f7a2677dce4d357cc3ef14c59586db851f253e7d495b6506e7c8210100000000fdffffff02a54a4c0000000000225120d73acf6667b7850e6e00e21bb4525a2c5e2d88956a3d2c35b0bc06f4b0df01bc404b4c00000000002251202befa14431d4cb71889ea1df7a7eaa2f1d8b9107e60b01564e15dabe5c0dfd32340100004f01043587cf03e017d1bb8000000001835c0b51218376c61455428a9f47bfcce1f6ce8397ead7b00387e9d9ea568302cfbd7100311e0e85844c3738728314394eb8302a6b5070d692e41b14ba8180901073c5da0a5600008001000080000000800001007d020000000184d4669ffd8232e83b7bf70fd8425b913f83e8664ab7128f196b70c33afc8d9e0100000000fdffffff02dc556202000000001600147d221583ec7f1023a7188ce4e8d2836ff96aac1380969800000000002251203b82b2b2a9185315da6f80da5f06d0440d8a5e1457fa93387c2d919c86ec87862a01000001012b80969800000000002251203b82b2b2a9185315da6f80da5f06d0440d8a5e1457fa93387c2d919c86ec878601030400000000211655355ca83c973f1d97ce0e3843c85d78905af16b4dc531bc488e57212d230116190073c5da0a560000800100008000000080000000000000000001172055355ca83c973f1d97ce0e3843c85d78905af16b4dc531bc488e57212d230116002107b10ac97f676cf1f3ccdacb0b78171282bbe94a94df143201700dc59bcc15f368190073c5da0a5600008001000080000000800100000000000000010520b10ac97f676cf1f3ccdacb0b78171282bbe94a94df143201700dc59bcc15f3680021073058679f6d60b87ef921d98a2a9a1f1e0779dae27bedbd1cdb2f147a07835ac9190073c5da0a56000080010000800000008000000000010000000105203058679f6d60b87ef921d98a2a9a1f1e0779dae27bedbd1cdb2f147a07835ac900", + + // signed PSBT and verified by importing in sparrow and broadcasting transaction + // echo hex | xxd -r -p > taproot_signed.psbt + regtestTaprootSignedPsbt: + "02000000000101b58806ecf5f7a2677dce4d357cc3ef14c59586db851f253e7d495b6506e7c8210100000000fdffffff02a54a4c0000000000225120d73acf6667b7850e6e00e21bb4525a2c5e2d88956a3d2c35b0bc06f4b0df01bc404b4c00000000002251202befa14431d4cb71889ea1df7a7eaa2f1d8b9107e60b01564e15dabe5c0dfd32014091d48b7c4bb1dc7cb4d0da360dfd0ca35ea1e73ca6f1891c25a6a3bd90a6269eaa2ee97bca15969181981eb1abb1c9ab8574add9453355b00b521069dca7dc1634010000", + + mnemnoic: + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", +}; diff --git a/yarn.lock b/yarn.lock index 9abf4c8255..aabd7a7b63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3905,6 +3905,15 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +ecpair@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ecpair/-/ecpair-2.1.0.tgz#673f826b1d80d5eb091b8e2010c6b588e8d2cb45" + integrity sha512-cL/mh3MtJutFOvFc27GPZE2pWL3a3k4YvzUWEOvilnfZVlH3Jwgx/7d6tlD7/75tNk8TG2m+7Kgtz0SI1tWcqw== + dependencies: + randombytes "^2.1.0" + typeforce "^1.18.0" + wif "^2.0.6" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" @@ -9245,6 +9254,13 @@ thunky@^1.0.2: resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== +tiny-secp256k1@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tiny-secp256k1/-/tiny-secp256k1-2.2.1.tgz#a61d4791b7031aa08a9453178a131349c3e10f9b" + integrity sha512-/U4xfVqnVxJXN4YVsru0E6t5wVncu2uunB8+RVR40fYUxkKYUPS10f+ePQZgFBoE/Jbf9H1NBveupF2VmB58Ng== + dependencies: + uint8array-tools "0.0.7" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -9458,7 +9474,7 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typeforce@^1.11.3: +typeforce@^1.11.3, typeforce@^1.18.0: version "1.18.0" resolved "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz" integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g== @@ -9487,6 +9503,11 @@ typeson@^6.0.0, typeson@^6.1.0: resolved "https://registry.yarnpkg.com/typeson/-/typeson-6.1.0.tgz#5b2a53705a5f58ff4d6f82f965917cabd0d7448b" integrity sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA== +uint8array-tools@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/uint8array-tools/-/uint8array-tools-0.0.7.tgz#a7a2bb5d8836eae2fade68c771454e6a438b390d" + integrity sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ== + unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz" @@ -9939,7 +9960,7 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -wif@^2.0.1: +wif@^2.0.1, wif@^2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz" integrity sha1-CNP1IFbGZnkplyb63g1DKudLRwQ= From 6865847ce39f5bfe751abc501405dd040557378c Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 18 May 2023 12:21:34 +0700 Subject: [PATCH 044/118] fix: signPsbt in service worker --- .../actions/webbtc/signPsbt.ts | 33 +++++++++---------- webpack.config.js | 4 +++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/extension/background-script/actions/webbtc/signPsbt.ts b/src/extension/background-script/actions/webbtc/signPsbt.ts index 0ef9e884cb..4a02ce900b 100644 --- a/src/extension/background-script/actions/webbtc/signPsbt.ts +++ b/src/extension/background-script/actions/webbtc/signPsbt.ts @@ -1,12 +1,5 @@ import * as secp256k1 from "@noble/secp256k1"; -import { - Network, - Psbt, - Signer, - crypto, - initEccLib, - networks, -} from "bitcoinjs-lib"; +import * as bitcoin from "bitcoinjs-lib"; import ECPairFactory, { ECPairAPI } from "ecpair"; import * as tinysecp from "tiny-secp256k1"; import { decryptData } from "~/common/lib/crypto"; @@ -44,20 +37,26 @@ const signPsbt = async (message: MessageSignPsbt) => { derivePrivateKey(mnemonic, derivationPath) ); - const taprootPsbt = Psbt.fromHex(message.args.psbt, { - network: networks[settings.bitcoinNetwork], + const taprootPsbt = bitcoin.Psbt.fromHex(message.args.psbt, { + network: bitcoin.networks[settings.bitcoinNetwork], }); - initEccLib(tinysecp); + // fix usages of window (unavailable in service worker) + globalThis.window = globalThis.window || {}; + if (!globalThis.window.crypto) { + globalThis.window.crypto = crypto; + } + + bitcoin.initEccLib(tinysecp); const ECPair: ECPairAPI = ECPairFactory(tinysecp); const keyPair = tweakSigner( ECPair, ECPair.fromPrivateKey(Buffer.from(privateKey), { - network: networks[settings.bitcoinNetwork], + network: bitcoin.networks[settings.bitcoinNetwork], }), { - network: networks[settings.bitcoinNetwork], + network: bitcoin.networks[settings.bitcoinNetwork], } ); @@ -93,9 +92,9 @@ const toXOnly = (pubKey: Buffer) => function tweakSigner( ECPair: ECPairAPI, - signer: Signer, - opts: { network: Network; tweakHash?: Buffer | undefined } -): Signer { + signer: bitcoin.Signer, + opts: { network: bitcoin.Network; tweakHash?: Buffer | undefined } +): bitcoin.Signer { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore let privateKey: Uint8Array | undefined = signer.privateKey; @@ -120,7 +119,7 @@ function tweakSigner( } function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer { - return crypto.taggedHash( + return bitcoin.crypto.taggedHash( "TapTweak", Buffer.concat(h ? [pubKey, h] : [pubKey]) ); diff --git a/webpack.config.js b/webpack.config.js index 0bde004d50..a94738057c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -58,6 +58,10 @@ var options = { }, mode: nodeEnv, + experiments: { + // TODO: remove along with tiny-secp256k1 + asyncWebAssembly: true, + }, entry: { manifest: "./src/manifest.json", From 321959173a441be9fb1beb4137da75b99488a7f7 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 18 May 2023 13:30:11 +0700 Subject: [PATCH 045/118] chore: improve sign psbt modal ui --- src/app/components/ContentMessage/index.tsx | 2 +- src/app/screens/ConfirmSignPsbt/index.tsx | 116 ++++++++++++-------- src/common/lib/psbt.ts | 2 +- src/i18n/locales/en/translation.json | 12 ++ 4 files changed, 87 insertions(+), 45 deletions(-) diff --git a/src/app/components/ContentMessage/index.tsx b/src/app/components/ContentMessage/index.tsx index 7d637e8f7a..0de09f5b92 100644 --- a/src/app/components/ContentMessage/index.tsx +++ b/src/app/components/ContentMessage/index.tsx @@ -8,7 +8,7 @@ function ContentMessage({ heading, content }: Props) { <>
{heading}
-
+
{content}
diff --git a/src/app/screens/ConfirmSignPsbt/index.tsx b/src/app/screens/ConfirmSignPsbt/index.tsx index 415946363f..f109b27da6 100644 --- a/src/app/screens/ConfirmSignPsbt/index.tsx +++ b/src/app/screens/ConfirmSignPsbt/index.tsx @@ -1,27 +1,28 @@ //import Checkbox from "../../components/Form/Checkbox"; import ConfirmOrCancel from "@components/ConfirmOrCancel"; import Container from "@components/Container"; -import ContentMessage from "@components/ContentMessage"; import PublisherCard from "@components/PublisherCard"; import SuccessMessage from "@components/SuccessMessage"; +import { TFunction } from "i18next"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; +import Hyperlink from "~/app/components/Hyperlink"; import Loading from "~/app/components/Loading"; import ScreenHeader from "~/app/components/ScreenHeader"; import { useNavigationState } from "~/app/hooks/useNavigationState"; import { USER_REJECTED_ERROR } from "~/common/constants"; import api from "~/common/lib/api"; import msg from "~/common/lib/msg"; -import { PsbtPreview, getPsbtPreview } from "~/common/lib/psbt"; +import { Address, PsbtPreview, getPsbtPreview } from "~/common/lib/psbt"; import type { OriginData } from "~/types"; function ConfirmSignPsbt() { const navState = useNavigationState(); const { t: tCommon } = useTranslation("common"); const { t } = useTranslation("translation", { - keyPrefix: "confirm_sign_message", + keyPrefix: "confirm_sign_psbt", }); const navigate = useNavigate(); @@ -30,6 +31,8 @@ function ConfirmSignPsbt() { const [loading, setLoading] = useState(false); const [successMessage, setSuccessMessage] = useState(""); const [preview, setPreview] = useState(undefined); + const [showAddresses, setShowAddresses] = useState(false); + const [showHex, setShowHex] = useState(false); useEffect(() => { (async () => { @@ -66,59 +69,69 @@ function ConfirmSignPsbt() { } } + function toggleShowAddresses() { + setShowAddresses((current) => !current); + } + function toggleShowHex() { + setShowHex((current) => !current); + } + if (!preview) { return ; } return (
- + {!successMessage ? ( -
+
- - input.address.substring(0, 5) + - "..." + - input.address.substring( - input.address.length - 5, - input.address.length - ) + - ": " + - input.amount + - " sats" - ) - .join("\n")} - /> - - output.address.substring(0, 5) + - "..." + - output.address.substring( - output.address.length - 5, - output.address.length - ) + - ": " + - output.amount + - " sats" - ) - .join("\n")} - /> - +
+ {t("warning")} +
+
+

+ {t("allow_sign", { host: origin.host })} +

+
+ + {showAddresses ? t("hide_addresses") : t("view_addresses")} + + {"•"} + + {showHex ? t("hide_hex") : t("view_hex")} + +
+ + {showAddresses && ( +
+

{t("input")}

+ +
+ )} + + {showAddresses && ( +
+

{t("outputs")}

+
+ {preview.outputs.map((output) => ( + + ))} +
+
+ )} +
+ + {showHex && ( +
+ {psbt} +
+ )}
; +}) { + return ( +
+

{address}

+

+ {t("amount", { amount })} +

+
+ ); +} + export default ConfirmSignPsbt; diff --git a/src/common/lib/psbt.ts b/src/common/lib/psbt.ts index f31d8c92e2..b92366005c 100644 --- a/src/common/lib/psbt.ts +++ b/src/common/lib/psbt.ts @@ -1,7 +1,7 @@ import * as btc from "@scure/btc-signer"; import { Psbt, networks } from "bitcoinjs-lib"; -type Address = { amount: number; address: string }; +export type Address = { amount: number; address: string }; export type PsbtPreview = { inputs: Address[]; diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index d7f7e042b6..ac07496501 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -752,6 +752,18 @@ "title": "Sign", "content": "This website asks you to sign:" }, + "confirm_sign_psbt": { + "warning": "⚠️ Signing a PSBT may result in loosing your funds or Oridinals.", + "title": "Sign PSBT", + "allow_sign": "Allow {{host}} to sign a Partially Signed Bitcoin Transaction:", + "view_addresses": "View addresses", + "hide_addresses": "Hide addresses", + "view_hex": "View PSBT hex", + "hide_hex": "Hide PSBT hex", + "input": "Input", + "outputs": "Outputs", + "amount": "{{amount}} sats" + }, "confirm_get_address": { "title": "Get Address", "heading": "This website asks you to read:", From 881ed18a094a9e73d837f0de2ea84163fdae0356 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 18 May 2023 14:05:02 +0700 Subject: [PATCH 046/118] chore: remove placeholder in mnemonic inputs --- src/app/components/MnemonicInputs/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/components/MnemonicInputs/index.tsx b/src/app/components/MnemonicInputs/index.tsx index 5683f234c5..84a2a7c0a7 100644 --- a/src/app/components/MnemonicInputs/index.tsx +++ b/src/app/components/MnemonicInputs/index.tsx @@ -35,7 +35,6 @@ export default function MnemonicInputs({ Date: Tue, 30 May 2023 10:32:04 +0200 Subject: [PATCH 047/118] fix: updated box paddings --- src/app/screens/Accounts/Detail/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/screens/Accounts/Detail/index.tsx b/src/app/screens/Accounts/Detail/index.tsx index 261f8b5712..a6e7538240 100644 --- a/src/app/screens/Accounts/Detail/index.tsx +++ b/src/app/screens/Accounts/Detail/index.tsx @@ -350,7 +350,7 @@ function AccountDetail() {

} -
+
{mnemonic && (
{t("mnemonic.backup.warning")} From 0f9dfbc75d6512108934e75f7ee09131927f6842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Tue, 30 May 2023 10:32:38 +0200 Subject: [PATCH 048/118] fix: added hover states for onboarding cards --- src/app/components/Tips/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/components/Tips/index.tsx b/src/app/components/Tips/index.tsx index f29dcf3b10..f7e20b5934 100644 --- a/src/app/components/Tips/index.tsx +++ b/src/app/components/Tips/index.tsx @@ -22,21 +22,21 @@ export default function Tips() { () => ({ [TIPS.TOP_UP_WALLET]: { - background: "bg-white dark:bg-surface-02dp", + background: "bg-white dark:bg-surface-02dp hover:bg-orange-50", border: "border-orange-500", arrow: "text-orange-500", backgroundIcon: , link: "https://getalby.com/topup", }, [TIPS.DEMO]: { - background: "bg-white dark:bg-surface-02dp", + background: "bg-white dark:bg-surface-02dp hover:bg-yellow-50", border: "border-yellow-500", arrow: "text-yellow-500", backgroundIcon: , link: "https://getalby.com/demo", }, [TIPS.MNEMONIC]: { - background: "bg-purple-50 dark:bg-purple-950", + background: "bg-white dark:bg-purple-950 hover:bg-purple-50", border: "border-purple-500", arrow: "text-purple-500", backgroundIcon: , From 0689051e65f08ad5c23e7c76d0ef9346e84499e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Tue, 30 May 2023 10:34:30 +0200 Subject: [PATCH 049/118] fix: remove color from copy button --- src/app/screens/Accounts/BackupSecretKey/index.tsx | 2 +- src/app/screens/Accounts/ImportSecretKey/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/screens/Accounts/BackupSecretKey/index.tsx b/src/app/screens/Accounts/BackupSecretKey/index.tsx index 5fc953f0db..77db4795e6 100644 --- a/src/app/screens/Accounts/BackupSecretKey/index.tsx +++ b/src/app/screens/Accounts/BackupSecretKey/index.tsx @@ -125,7 +125,7 @@ function BackupSecretKey() { {/* TODO: consider making CopyButton component */} + +
+ } + /> +
+ +
+ } + /> + {nostrKeyOrigin !== "secret-key" && + (mnemonic || !currentPrivateKey) && ( +
+ {mnemonic ? ( + - -
- } - /> - - } - /> - {nostrKeyOrigin !== "secret-key" && - (mnemonic || !currentPrivateKey) && ( -
- {mnemonic ? ( -
- )} +
+ )} +
- )}
); diff --git a/src/app/screens/Accounts/Detail/index.tsx b/src/app/screens/Accounts/Detail/index.tsx index a6e7538240..10d4b2e080 100644 --- a/src/app/screens/Accounts/Detail/index.tsx +++ b/src/app/screens/Accounts/Detail/index.tsx @@ -137,7 +137,10 @@ function AccountDetail() { } async function removeAccount({ id, name }: AccountAction) { - if (window.confirm(t("remove.confirm", { name }))) { + if ( + window.prompt(t("remove.confirm", { name }))?.toLowerCase() == + accountName.toLowerCase() + ) { let nextAccountId; let accountIds = Object.keys(accounts); if (auth.account?.id === id && accountIds.length > 1) { @@ -154,6 +157,23 @@ function AccountDetail() { } else { window.close(); } + } else { + toast.error(t("remove.error")); + } + } + async function removeMnemonic({ id, name }: AccountAction) { + if ( + window.prompt(t("remove_secretkey.confirm", { name }))?.toLowerCase() == + accountName.toLowerCase() + ) { + await msg.request("setMnemonic", { + id, + mnemonic: null, + }); + setMnemonic(""); + toast.success(t("remove_secretkey.success")); + } else { + toast.error(t("remove.error")); } } @@ -448,6 +468,25 @@ function AccountDetail() {
+ {mnemonic && ( + +
+
+
+ )}
{currentPrivateKey && nostrKeyOrigin !== "secret-key" ? ( - // TODO: extract to Alert component -
-

- {t( - nostrKeyOrigin === "unknown" - ? "nostr.advanced_settings.imported_key_warning" - : "nostr.advanced_settings.legacy_derived_key_warning" - )} -

-
+ + {t( + nostrKeyOrigin === "unknown" + ? "nostr.advanced_settings.imported_key_warning" + : "nostr.advanced_settings.legacy_derived_key_warning" + )} + ) : nostrKeyOrigin === "secret-key" ? ( - // TODO: extract to Alert component -
-

{t("nostr.advanced_settings.can_restore")}

-
+ + {t("nostr.advanced_settings.can_restore")} + ) : null}
@@ -233,23 +230,20 @@ function NostrAdvancedSettings() { onClick={handleDeriveNostrKeyFromSecretKey} /> ) : ( - // TODO: extract to Alert component -
-

- , - ]} - /> -

-
+ + , + ]} + /> + )}
)} From 1b7db172f5647ca4fc17153612a2d488792393a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Fri, 16 Jun 2023 15:08:02 +0200 Subject: [PATCH 061/118] fix: rename nostr to webbtc --- src/extension/content-script/onendwebbtc.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/extension/content-script/onendwebbtc.js b/src/extension/content-script/onendwebbtc.js index 38650f7883..ffec3824b2 100644 --- a/src/extension/content-script/onendwebbtc.js +++ b/src/extension/content-script/onendwebbtc.js @@ -3,7 +3,7 @@ import browser from "webextension-polyfill"; import getOriginData from "./originData"; import shouldInject from "./shouldInject"; -// Nostr calls that can be executed from the WebBTC Provider. +// WebBTC calls that can be executed from the WebBTC Provider. // Update when new calls are added const webbtcCalls = [ "webbtc/enable", @@ -13,9 +13,9 @@ const webbtcCalls = [ // calls that can be executed when `window.webbtc` is not enabled for the current content page const disabledCalls = ["webbtc/enable"]; -let isEnabled = false; // store if nostr is enabled for this content page -let isRejected = false; // store if the nostr enable call failed. if so we do not prompt again -let callActive = false; // store if a nostr call is currently active. Used to prevent multiple calls in parallel +let isEnabled = false; // store if webbtc is enabled for this content page +let isRejected = false; // store if the webbtc enable call failed. if so we do not prompt again +let callActive = false; // store if a webbtc call is currently active. Used to prevent multiple calls in parallel const SCOPE = "webbtc"; @@ -52,7 +52,7 @@ async function init() { return; } - // limit the calls that can be made from window.nostr + // limit the calls that can be made from window.webbtc // only listed calls can be executed // if not enabled only enable can be called. const availableCalls = isEnabled ? webbtcCalls : disabledCalls; @@ -79,7 +79,7 @@ async function init() { isEnabled = response.data?.enabled; if (response.error) { console.error(response.error); - console.info("Enable was rejected ignoring further nostr calls"); + console.info("Enable was rejected ignoring further webbtc calls"); isRejected = true; } } From 1c3b0fc0fae5489b5b0c9d7264f7ab58c321f303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Fri, 16 Jun 2023 15:22:38 +0200 Subject: [PATCH 062/118] fix: update usages of alert component --- src/app/components/Alert/index.tsx | 14 ++++++++++++-- src/app/screens/Accounts/Detail/index.tsx | 5 ++--- src/app/screens/Accounts/ImportSecretKey/index.tsx | 5 ++--- .../Accounts/NostrAdvancedSettings/index.tsx | 2 +- .../screens/connectors/ConnectKollider/index.tsx | 7 ++----- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/app/components/Alert/index.tsx b/src/app/components/Alert/index.tsx index bf5aecca18..c7ee5bc7fc 100644 --- a/src/app/components/Alert/index.tsx +++ b/src/app/components/Alert/index.tsx @@ -1,11 +1,21 @@ +import { classNames } from "~/app/utils"; + type Props = { - type: "warn"; + type: "warn" | "info"; children: React.ReactNode; }; export default function Alert({ type, children }: Props) { return ( -
+

{children}

); diff --git a/src/app/screens/Accounts/Detail/index.tsx b/src/app/screens/Accounts/Detail/index.tsx index 912318105c..efe7af12c7 100644 --- a/src/app/screens/Accounts/Detail/index.tsx +++ b/src/app/screens/Accounts/Detail/index.tsx @@ -19,6 +19,7 @@ import Modal from "react-modal"; import QRCode from "react-qr-code"; import { Link, useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; +import Alert from "~/app/components/Alert"; import Avatar from "~/app/components/Avatar"; import Badge from "~/app/components/Badge"; import InputCopyButton from "~/app/components/InputCopyButton"; @@ -370,9 +371,7 @@ function AccountDetail() {
{mnemonic && ( -
- {t("mnemonic.backup.warning")} -
+ {t("mnemonic.backup.warning")} )}
diff --git a/src/app/screens/Accounts/ImportSecretKey/index.tsx b/src/app/screens/Accounts/ImportSecretKey/index.tsx index 0585c84a5d..d5ed48e92d 100644 --- a/src/app/screens/Accounts/ImportSecretKey/index.tsx +++ b/src/app/screens/Accounts/ImportSecretKey/index.tsx @@ -7,6 +7,7 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; import { toast } from "react-toastify"; +import Alert from "~/app/components/Alert"; import Button from "~/app/components/Button"; import MnemonicInputs from "~/app/components/MnemonicInputs"; import { useAccount } from "~/app/context/AccountContext"; @@ -129,9 +130,7 @@ function ImportSecretKey() { {currentPrivateKey && ( -
- {t("existing_nostr_key_notice")} -
+ {t("existing_nostr_key_notice")} )}
diff --git a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx index 60a989eaca..32ce8da843 100644 --- a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx +++ b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx @@ -175,7 +175,7 @@ function NostrAdvancedSettings() { )} ) : nostrKeyOrigin === "secret-key" ? ( - + {t("nostr.advanced_settings.can_restore")} ) : null} diff --git a/src/app/screens/connectors/ConnectKollider/index.tsx b/src/app/screens/connectors/ConnectKollider/index.tsx index ae46fc7d29..ab47179ffb 100644 --- a/src/app/screens/connectors/ConnectKollider/index.tsx +++ b/src/app/screens/connectors/ConnectKollider/index.tsx @@ -9,6 +9,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; +import Alert from "~/app/components/Alert"; import { ACCOUNT_CURRENCIES } from "~/common/constants"; import msg from "~/common/lib/msg"; @@ -170,11 +171,7 @@ export default function ConnectKollidier({ variant }: Props) { } onSubmit={handleSubmit} > - {variant === "create" && ( -
- {t("warning")} -
- )} + {variant === "create" && {t("warning")}}
Date: Mon, 19 Jun 2023 13:24:27 +0700 Subject: [PATCH 063/118] chore: remove ordinals from secret key backup screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: René Aaron <100827540+reneaaron@users.noreply.github.com> --- src/app/screens/Accounts/BackupSecretKey/index.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/app/screens/Accounts/BackupSecretKey/index.tsx b/src/app/screens/Accounts/BackupSecretKey/index.tsx index 7cca91cb40..e3cd36efd4 100644 --- a/src/app/screens/Accounts/BackupSecretKey/index.tsx +++ b/src/app/screens/Accounts/BackupSecretKey/index.tsx @@ -108,12 +108,6 @@ function BackupSecretKey() { } title={t("backup.protocols.nostr")} /> - - } - title={t("backup.protocols.ordinals")} - />

From 78b2369042788efe7787f1bf91d2da13f9aed4fd Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 19 Jun 2023 13:33:36 +0700 Subject: [PATCH 064/118] chore: remove unnecessary try-catch --- .../Accounts/NostrAdvancedSettings/index.tsx | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx index 32ce8da843..9f6e7e816c 100644 --- a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx +++ b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx @@ -39,30 +39,25 @@ function NostrAdvancedSettings() { const { id } = useParams(); const fetchData = useCallback(async () => { - try { - if (id) { - const priv = (await msg.request("nostr/getPrivateKey", { - id, - })) as string; - if (priv) { - setCurrentPrivateKey(priv); - } + if (id) { + const priv = (await msg.request("nostr/getPrivateKey", { + id, + })) as string; + if (priv) { + setCurrentPrivateKey(priv); + } - const accountMnemonic = (await msg.request("getMnemonic", { - id, - })) as string; - if (accountMnemonic) { - setMnemonic(accountMnemonic); - } + const accountMnemonic = (await msg.request("getMnemonic", { + id, + })) as string; + if (accountMnemonic) { + setMnemonic(accountMnemonic); + } - if (priv) { - const keyOrigin = await getNostrKeyOrigin(priv, accountMnemonic); - setNostrKeyOrigin(keyOrigin); - } + if (priv) { + const keyOrigin = await getNostrKeyOrigin(priv, accountMnemonic); + setNostrKeyOrigin(keyOrigin); } - } catch (e) { - console.error(e); - if (e instanceof Error) toast.error(`Error: ${e.message}`); } }, [id]); From 7bcbc664534873af9e7ab08f81ab6f4f583ff014 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 19 Jun 2023 13:36:56 +0700 Subject: [PATCH 065/118] chore: revert tsconfig target --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index a58464969c..6e4172398b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ESNext", + "target": "es6", "module": "commonjs", "allowJs": true, "jsx": "react-jsx", From 7d115ff177c25f3c3db2f67c0c9c39fb75b71c75 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 19 Jun 2023 13:58:14 +0700 Subject: [PATCH 066/118] chore: remove insecure copy secret key button --- .../Accounts/BackupSecretKey/index.tsx | 44 +------------------ 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/src/app/screens/Accounts/BackupSecretKey/index.tsx b/src/app/screens/Accounts/BackupSecretKey/index.tsx index f9716f90ab..ea4595a2c2 100644 --- a/src/app/screens/Accounts/BackupSecretKey/index.tsx +++ b/src/app/screens/Accounts/BackupSecretKey/index.tsx @@ -1,4 +1,3 @@ -import { CopyIcon } from "@bitcoin-design/bitcoin-icons-react/filled"; import Container from "@components/Container"; import Loading from "@components/Loading"; import * as bip39 from "@scure/bip39"; @@ -13,23 +12,17 @@ import MnemonicInputs from "~/app/components/MnemonicInputs"; import Checkbox from "~/app/components/form/Checkbox"; import { useAccount } from "~/app/context/AccountContext"; import NostrIcon from "~/app/icons/NostrIcon"; -import OrdinalsIcon from "~/app/icons/OrdinalsIcon"; import { saveMnemonic } from "~/app/utils/saveMnemonic"; import msg from "~/common/lib/msg"; function BackupSecretKey() { const [mnemonic, setMnemonic] = useState(); const account = useAccount(); - const { t: tCommon } = useTranslation("common"); const { t } = useTranslation("translation", { keyPrefix: "accounts.account_view.mnemonic", }); - const [publicKeyCopyLabel, setPublicKeyCopyLabel] = useState( - tCommon("actions.copy_clipboard") as string - ); const [hasConfirmedBackup, setHasConfirmedBackup] = useState(false); - const [confirmedCopyToClipboard, setHasConfirmedCopyToClipboard] = - useState(false); + useState(false); // TODO: useMnemonic hook const [hasMnemonic, setHasMnemonic] = useState(false); // TODO: useNostrPrivateKey hook @@ -117,41 +110,6 @@ function BackupSecretKey() {

<> - {/* TODO: consider making CopyButton component */} - + } + /> +
+
+ ); + })}
- {!disabled && ( + {!readOnly && ( {wordlist.map((word) => (

diff --git a/src/app/utils/getNostrKeyOrigin.ts b/src/app/utils/getNostrKeyOrigin.ts deleted file mode 100644 index 662ae35e37..0000000000 --- a/src/app/utils/getNostrKeyOrigin.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { deriveNostrPrivateKey } from "~/common/lib/mnemonic"; -import msg from "~/common/lib/msg"; - -export type NostrKeyOrigin = - | "legacy-account-derived" - | "unknown" - | "secret-key"; - -export async function getNostrKeyOrigin( - nostrPrivateKey: string, - mnemonic: string | null -): Promise { - if (mnemonic) { - const mnemonicDerivedPrivateKey = await deriveNostrPrivateKey(mnemonic); - - if (mnemonicDerivedPrivateKey === nostrPrivateKey) { - return "secret-key"; - } - } - - // TODO: consider removing this at some point and just returning "unknown" - const legacyAccountDerivedPrivateKeyResponse = await msg.request( - "nostr/generatePrivateKey" - ); - const legacyAccountDerivedPrivateKey = - legacyAccountDerivedPrivateKeyResponse.privateKey; - - return legacyAccountDerivedPrivateKey === nostrPrivateKey - ? "legacy-account-derived" - : "unknown"; -} diff --git a/src/common/lib/api.ts b/src/common/lib/api.ts index 080203fa69..8edd236a8e 100644 --- a/src/common/lib/api.ts +++ b/src/common/lib/api.ts @@ -37,7 +37,8 @@ export interface AccountInfoRes { export interface GetAccountRes extends Pick { nostrEnabled: boolean; - hasSecretKey: boolean; + hasMnemonic: boolean; + hasImportedNostrKey: boolean; } interface StatusRes { configured: boolean; diff --git a/src/common/lib/btc.ts b/src/common/lib/btc.ts index a56a462d43..cab96cdedf 100644 --- a/src/common/lib/btc.ts +++ b/src/common/lib/btc.ts @@ -1,5 +1,8 @@ import * as btc from "@scure/btc-signer"; +// TODO: move these functions to new Bitcoin object + +// TODO: private key should not be needed to be passed in export function getTaprootAddressFromPrivateKey( privateKey: string, networkType?: keyof typeof networks diff --git a/src/common/lib/mnemonic.ts b/src/common/lib/mnemonic.ts index 5548699b00..5448b3548f 100644 --- a/src/common/lib/mnemonic.ts +++ b/src/common/lib/mnemonic.ts @@ -6,10 +6,14 @@ export const NOSTR_DERIVATION_PATH = "m/44'/1237'/0'/0/0"; // NIP-06 export const BTC_TAPROOT_DERIVATION_PATH = "m/86'/0'/0'/0/0"; export const BTC_TAPROOT_DERIVATION_PATH_REGTEST = "m/86'/1'/0'/0/0"; +// TODO: move these functions to new Mnemonic object + +// TODO: mnemonic should not be needed to be passed in export function deriveNostrPrivateKey(mnemonic: string) { return derivePrivateKey(mnemonic, NOSTR_DERIVATION_PATH); } +// TODO: mnemonic should not be needed to be passed in export function derivePrivateKey(mnemonic: string, path: string) { const seed = bip39.mnemonicToSeedSync(mnemonic); const hdkey = HDKey.fromMasterSeed(seed); @@ -20,6 +24,7 @@ export function derivePrivateKey(mnemonic: string, path: string) { return secp256k1.utils.bytesToHex(privateKeyBytes); } +// TODO: mnemonic should not be needed to be passed in export function derivePublicKey(mnemonic: string, path: string) { const seed = bip39.mnemonicToSeedSync(mnemonic); const hdkey = HDKey.fromMasterSeed(seed); diff --git a/src/extension/background-script/actions/accounts/__tests__/get.test.ts b/src/extension/background-script/actions/accounts/__tests__/get.test.ts index 747b7359b2..22a5720826 100644 --- a/src/extension/background-script/actions/accounts/__tests__/get.test.ts +++ b/src/extension/background-script/actions/accounts/__tests__/get.test.ts @@ -58,7 +58,8 @@ describe("account info", () => { name: "Alby", connector: "lndhub", nostrEnabled: false, - hasSecretKey: false, + hasMnemonic: false, + hasImportedNostrKey: true, }; expect(await getAccount(message)).toStrictEqual({ @@ -81,7 +82,8 @@ describe("account info", () => { name: "Alby", connector: "lndhub", nostrEnabled: true, - hasSecretKey: true, + hasMnemonic: true, + hasImportedNostrKey: true, }; expect(await getAccount(message)).toStrictEqual({ diff --git a/src/extension/background-script/actions/accounts/get.ts b/src/extension/background-script/actions/accounts/get.ts index da03199305..bfe044f4a0 100644 --- a/src/extension/background-script/actions/accounts/get.ts +++ b/src/extension/background-script/actions/accounts/get.ts @@ -19,7 +19,9 @@ const get = async (message: MessageAccountGet) => { connector: account.connector, name: account.name, nostrEnabled: !!account.nostrPrivateKey, - hasSecretKey: !!account.mnemonic, + hasMnemonic: !!account.mnemonic, + // Note: undefined (default for new accounts) it is also considered imported + hasImportedNostrKey: account.hasImportedNostrKey !== false, }; return { diff --git a/src/extension/background-script/actions/mnemonic/setMnemonic.ts b/src/extension/background-script/actions/mnemonic/setMnemonic.ts index adfe4f0906..af89d71259 100644 --- a/src/extension/background-script/actions/mnemonic/setMnemonic.ts +++ b/src/extension/background-script/actions/mnemonic/setMnemonic.ts @@ -18,6 +18,7 @@ const setMnemonic = async (message: MessageMnemonicSet) => { if (id && Object.keys(accounts).includes(id)) { const account = accounts[id]; account.mnemonic = mnemonic ? encryptData(mnemonic, password) : null; + account.hasImportedNostrKey = true; accounts[id] = account; state.setState({ accounts }); await state.getState().saveToStorage(); diff --git a/src/extension/background-script/actions/nostr/setPrivateKey.ts b/src/extension/background-script/actions/nostr/setPrivateKey.ts index d22feb7b74..bed29ac6aa 100644 --- a/src/extension/background-script/actions/nostr/setPrivateKey.ts +++ b/src/extension/background-script/actions/nostr/setPrivateKey.ts @@ -1,4 +1,5 @@ -import { encryptData } from "~/common/lib/crypto"; +import { decryptData, encryptData } from "~/common/lib/crypto"; +import { deriveNostrPrivateKey } from "~/common/lib/mnemonic"; import type { MessagePrivateKeySet } from "~/types"; import state from "../../state"; @@ -20,6 +21,12 @@ const setPrivateKey = async (message: MessagePrivateKeySet) => { account.nostrPrivateKey = privateKey ? encryptData(privateKey, password) : null; + + // TODO: move deriveNostrPrivateKey to new Mnemonic object + account.hasImportedNostrKey = + !account.mnemonic || + deriveNostrPrivateKey(decryptData(account.mnemonic, password)) !== + privateKey; accounts[id] = account; state.setState({ accounts }); await state.getState().saveToStorage(); diff --git a/src/extension/content-script/onstart.ts b/src/extension/content-script/onstart.ts index 51d25f0336..045ffd5726 100644 --- a/src/extension/content-script/onstart.ts +++ b/src/extension/content-script/onstart.ts @@ -15,7 +15,7 @@ async function onstart() { injectScript(browser.runtime.getURL("js/inpageScriptWebLN.bundle.js")); // window.webbtc - if (account.hasSecretKey) { + if (account.hasMnemonic) { injectScript(browser.runtime.getURL("js/inpageScriptWebBTC.bundle.js")); } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index e1c5150f70..da50384cbb 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -457,7 +457,6 @@ "title": "Advanced Nostr Settings", "description": "Derive Nostr keys from your Secret Key or import your existing private key by pasting it in the \"Nostr Private Key\" field.", "imported_key_warning": "⚠️ You're currently using an imported or randomly generated Nostr key. Your Nostr private key cannot be restored by your Secret Key, so remember to back up your Nostr private key.", - "legacy_derived_key_warning": "⚠️ You're currently using a Nostr key derived from your account (legacy). Your Nostr private key cannot be restored by your Secret Key, so remember to back up your Nostr private key.", "can_restore": "✅ Nostr key derived from your secret key", "derive": "Derive Nostr keys from your Secret Key", "no_secret_key": "💡 You don't have a secret key yet. <0>Click here
to create your secret key and derive your nostr keys." diff --git a/src/types.ts b/src/types.ts index 363c0e9457..dbaf5ed354 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,6 +18,7 @@ export interface Account { name: string; nostrPrivateKey?: string | null; mnemonic?: string | null; + hasImportedNostrKey?: boolean; } export interface Accounts { From e64da1ba3db8385a91a6c4181f90238bc59f85fb Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 21 Jun 2023 21:02:02 +0700 Subject: [PATCH 083/118] chore: split backup page into generate and backup pages --- src/app/components/ContentBox/index.tsx | 9 ++ .../components/SecretKeyDescription/index.tsx | 39 ++++++ src/app/components/Tips/index.tsx | 2 +- src/app/router/Options/Options.tsx | 5 + .../Accounts/BackupSecretKey/index.tsx | 119 ++---------------- src/app/screens/Accounts/Detail/index.tsx | 2 +- .../Accounts/GenerateSecretKey/index.tsx | 114 +++++++++++++++++ .../Accounts/ImportSecretKey/index.tsx | 7 +- .../Accounts/NostrAdvancedSettings/index.tsx | 5 +- 9 files changed, 186 insertions(+), 116 deletions(-) create mode 100644 src/app/components/ContentBox/index.tsx create mode 100644 src/app/components/SecretKeyDescription/index.tsx create mode 100644 src/app/screens/Accounts/GenerateSecretKey/index.tsx diff --git a/src/app/components/ContentBox/index.tsx b/src/app/components/ContentBox/index.tsx new file mode 100644 index 0000000000..8425c7639a --- /dev/null +++ b/src/app/components/ContentBox/index.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +export function ContentBox({ children }: React.PropsWithChildren) { + return ( +
+ {children} +
+ ); +} diff --git a/src/app/components/SecretKeyDescription/index.tsx b/src/app/components/SecretKeyDescription/index.tsx new file mode 100644 index 0000000000..52eb004fbc --- /dev/null +++ b/src/app/components/SecretKeyDescription/index.tsx @@ -0,0 +1,39 @@ +import { useTranslation } from "react-i18next"; +import NostrIcon from "~/app/icons/NostrIcon"; + +function SecretKeyDescription() { + const { t } = useTranslation("translation", { + keyPrefix: "accounts.account_view.mnemonic", + }); + + return ( + <> +

+ {t("backup.description1")} +

+
+ } + title={t("backup.protocols.nostr")} + /> +
+ +

+ {t("backup.description2")} +

+ + ); +} + +export default SecretKeyDescription; + +type ProtocolListItemProps = { icon: React.ReactNode; title: string }; + +function ProtocolListItem({ icon, title }: ProtocolListItemProps) { + return ( +
+ {icon} + {title} +
+ ); +} diff --git a/src/app/components/Tips/index.tsx b/src/app/components/Tips/index.tsx index f7e20b5934..b4990b6c8f 100644 --- a/src/app/components/Tips/index.tsx +++ b/src/app/components/Tips/index.tsx @@ -40,7 +40,7 @@ export default function Tips() { border: "border-purple-500", arrow: "text-purple-500", backgroundIcon: , - link: `/accounts/${accountId}/secret-key/backup`, + link: `/accounts/${accountId}/secret-key/generate`, }, } as const), [accountId] diff --git a/src/app/router/Options/Options.tsx b/src/app/router/Options/Options.tsx index e708fd7fa0..6f2705d2c9 100644 --- a/src/app/router/Options/Options.tsx +++ b/src/app/router/Options/Options.tsx @@ -26,6 +26,7 @@ import Providers from "~/app/context/Providers"; import RequireAuth from "~/app/router/RequireAuth"; import { getConnectorRoutes, renderRoutes } from "~/app/router/connectorRoutes"; import BackupSecretKey from "~/app/screens/Accounts/BackupSecretKey"; +import GenerateSecretKey from "~/app/screens/Accounts/GenerateSecretKey"; import ImportSecretKey from "~/app/screens/Accounts/ImportSecretKey"; import NostrAdvancedSettings from "~/app/screens/Accounts/NostrAdvancedSettings"; import Discover from "~/app/screens/Discover"; @@ -93,6 +94,10 @@ function Options() { path=":id/secret-key/backup" element={} /> + } + /> } diff --git a/src/app/screens/Accounts/BackupSecretKey/index.tsx b/src/app/screens/Accounts/BackupSecretKey/index.tsx index 3455b71239..92026babbe 100644 --- a/src/app/screens/Accounts/BackupSecretKey/index.tsx +++ b/src/app/screens/Accounts/BackupSecretKey/index.tsx @@ -1,50 +1,29 @@ import Container from "@components/Container"; import Loading from "@components/Loading"; -import * as bip39 from "@scure/bip39"; -import { wordlist } from "@scure/bip39/wordlists/english"; -import React, { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; import { toast } from "react-toastify"; -import Alert from "~/app/components/Alert"; -import Button from "~/app/components/Button"; +import { ContentBox } from "~/app/components/ContentBox"; import MnemonicInputs from "~/app/components/MnemonicInputs"; -import Checkbox from "~/app/components/form/Checkbox"; -import { useAccount } from "~/app/context/AccountContext"; -import NostrIcon from "~/app/icons/NostrIcon"; -import { saveMnemonic } from "~/app/utils/saveMnemonic"; -import api from "~/common/lib/api"; +import SecretKeyDescription from "~/app/components/SecretKeyDescription"; import msg from "~/common/lib/msg"; function BackupSecretKey() { const [mnemonic, setMnemonic] = useState(); - const account = useAccount(); const { t } = useTranslation("translation", { keyPrefix: "accounts.account_view.mnemonic", }); - const [hasConfirmedBackup, setHasConfirmedBackup] = useState(false); - useState(false); - const [hasMnemonic, setHasMnemonic] = useState(false); - const [hasNostrPrivateKey, setHasNostrPrivateKey] = useState(false); const { id } = useParams(); const fetchData = useCallback(async () => { try { - const account = await api.getAccount(); - setHasNostrPrivateKey(account.nostrEnabled); - const accountMnemonic = (await msg.request("getMnemonic", { id, })) as string; - if (accountMnemonic) { - setMnemonic(accountMnemonic); - setHasMnemonic(true); - } else { - // generate a new mnemonic - setMnemonic(bip39.generateMnemonic(wordlist, 128)); - } + setMnemonic(accountMnemonic); } catch (e) { console.error(e); if (e instanceof Error) toast.error(`Error: ${e.message}`); @@ -55,102 +34,24 @@ function BackupSecretKey() { fetchData(); }, [fetchData]); - async function backupSecretKey() { - try { - if (!hasConfirmedBackup) { - throw new Error(t("backup.error_confirm")); - } - if (!account || !id) { - // type guard - throw new Error("No account available"); - } - if (!mnemonic) { - throw new Error("No mnemonic available"); - } - - await saveMnemonic(id, mnemonic); - toast.success(t("saved")); - history.back(); - } catch (e) { - if (e instanceof Error) toast.error(e.message); - } - } - - return !account || !mnemonic ? ( + return !mnemonic ? (
) : (
-
+

- {hasMnemonic ? t("backup.title") : t("generate.title")} + {t("backup.title")}

-

- {t("backup.description1")} -

-
- - } - title={t("backup.protocols.nostr")} - /> -
+ -

- {t("backup.description2")} -

- - <> - {!hasMnemonic && ( -
- { - setHasConfirmedBackup(event.target.checked); - }} - /> - -
- )} - -
- {!hasMnemonic && hasNostrPrivateKey && ( - {t("existing_nostr_key_notice")} - )} -
- {!hasMnemonic && ( -
-
- )} + +
); } export default BackupSecretKey; - -type ProtocolListItemProps = { icon: React.ReactNode; title: string }; - -function ProtocolListItem({ icon, title }: ProtocolListItemProps) { - return ( -
- {icon} - {title} -
- ); -} diff --git a/src/app/screens/Accounts/Detail/index.tsx b/src/app/screens/Accounts/Detail/index.tsx index 25be7b678c..3ee48f5159 100644 --- a/src/app/screens/Accounts/Detail/index.tsx +++ b/src/app/screens/Accounts/Detail/index.tsx @@ -376,7 +376,7 @@ function AccountDetail() {
- +
+ + + ); +} + +export default GenerateSecretKey; diff --git a/src/app/screens/Accounts/ImportSecretKey/index.tsx b/src/app/screens/Accounts/ImportSecretKey/index.tsx index b689c21d0d..f799660253 100644 --- a/src/app/screens/Accounts/ImportSecretKey/index.tsx +++ b/src/app/screens/Accounts/ImportSecretKey/index.tsx @@ -8,6 +8,7 @@ import { useParams } from "react-router-dom"; import { toast } from "react-toastify"; import Alert from "~/app/components/Alert"; import Button from "~/app/components/Button"; +import { ContentBox } from "~/app/components/ContentBox"; import MnemonicInputs from "~/app/components/MnemonicInputs"; import { useAccount } from "~/app/context/AccountContext"; import { saveMnemonic } from "~/app/utils/saveMnemonic"; @@ -89,11 +90,11 @@ function ImportSecretKey() { ) : (
-
+

{t("import.title")}

-

+

{t("import.description")}

@@ -101,7 +102,7 @@ function ImportSecretKey() { {hasNostrPrivateKey && ( {t("existing_nostr_key_notice")} )} -
+
- {currentPrivateKey ? ( + {mnemonic && currentPrivateKey ? ( hasImportedNostrKey ? ( {t("nostr.advanced_settings.imported_key_warning")} @@ -225,7 +226,7 @@ function NostrAdvancedSettings() { components={[ // eslint-disable-next-line react/jsx-key , From f17c907695b00e70be0fcfc3bbed7087d8543468 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 21 Jun 2023 21:08:20 +0700 Subject: [PATCH 084/118] fix: replace history back with navigate --- src/app/screens/Accounts/GenerateSecretKey/index.tsx | 6 ++++-- src/app/screens/Accounts/ImportSecretKey/index.tsx | 9 ++++++--- src/app/screens/Accounts/NostrAdvancedSettings/index.tsx | 9 ++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/app/screens/Accounts/GenerateSecretKey/index.tsx b/src/app/screens/Accounts/GenerateSecretKey/index.tsx index 2a732bb7d8..d8b8320530 100644 --- a/src/app/screens/Accounts/GenerateSecretKey/index.tsx +++ b/src/app/screens/Accounts/GenerateSecretKey/index.tsx @@ -4,7 +4,7 @@ import * as bip39 from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; import Alert from "~/app/components/Alert"; import Button from "~/app/components/Button"; @@ -17,6 +17,7 @@ import { saveMnemonic } from "~/app/utils/saveMnemonic"; import api from "~/common/lib/api"; function GenerateSecretKey() { + const navigate = useNavigate(); const [mnemonic, setMnemonic] = useState(); const account = useAccount(); const { t } = useTranslation("translation", { @@ -61,7 +62,8 @@ function GenerateSecretKey() { await saveMnemonic(id, mnemonic); toast.success(t("saved")); - history.back(); + // go to account settings + navigate(`/accounts/${id}`); } catch (e) { if (e instanceof Error) toast.error(e.message); } diff --git a/src/app/screens/Accounts/ImportSecretKey/index.tsx b/src/app/screens/Accounts/ImportSecretKey/index.tsx index f799660253..dbd522d226 100644 --- a/src/app/screens/Accounts/ImportSecretKey/index.tsx +++ b/src/app/screens/Accounts/ImportSecretKey/index.tsx @@ -4,7 +4,7 @@ import * as bip39 from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; import Alert from "~/app/components/Alert"; import Button from "~/app/components/Button"; @@ -19,6 +19,7 @@ function ImportSecretKey() { const [mnemonic, setMnemonic] = useState(""); const account = useAccount(); const { t: tCommon } = useTranslation("common"); + const navigate = useNavigate(); const { t } = useTranslation("translation", { keyPrefix: "accounts.account_view.mnemonic", }); @@ -53,7 +54,8 @@ function ImportSecretKey() { }, [fetchData]); function cancelImport() { - history.back(); + // go to account settings + navigate(`/accounts/${id}`); } async function importKey() { @@ -77,7 +79,8 @@ function ImportSecretKey() { await saveMnemonic(id, mnemonic); toast.success(t("saved")); - history.back(); + // go to account settings + navigate(`/accounts/${id}`); } catch (e) { if (e instanceof Error) toast.error(e.message); } diff --git a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx index b434ab5a0f..11308c4a54 100644 --- a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx +++ b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx @@ -6,7 +6,7 @@ import Container from "@components/Container"; import Loading from "@components/Loading"; import { FormEvent, useCallback, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; -import { Link, useParams } from "react-router-dom"; +import { Link, useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; import Alert from "~/app/components/Alert"; import Button from "~/app/components/Button"; @@ -25,6 +25,7 @@ function NostrAdvancedSettings() { const { t } = useTranslation("translation", { keyPrefix: "accounts.account_view", }); + const navigate = useNavigate(); // FIXME: use account hasMnemonic const [mnemonic, setMnemonic] = useState(""); const [currentPrivateKey, setCurrentPrivateKey] = useState(""); @@ -83,7 +84,8 @@ function NostrAdvancedSettings() { }, [currentPrivateKey, t]); function onCancel() { - history.back(); + // go to account settings + navigate(`/accounts/${id}`); } async function handleDeriveNostrKeyFromSecretKey() { @@ -132,7 +134,8 @@ function NostrAdvancedSettings() { toast.error(e.message); } } - history.back(); + // go to account settings + navigate(`/accounts/${id}`); } return !account ? ( From 7d261b96fdb0d978b09d23d085a2ae010e2e84ed Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 21 Jun 2023 21:22:54 +0700 Subject: [PATCH 085/118] chore: add nostr getPublicKey function --- .../actions/nostr/generatePrivateKey.ts | 4 +- .../actions/nostr/getPrivateKey.ts | 4 +- .../actions/nostr/getPublicKey.ts | 38 +++++++++++++++++++ .../actions/nostr/getPublicKeyOrPrompt.ts | 6 ++- .../background-script/actions/nostr/index.ts | 11 ++++-- .../actions/nostr/removePrivateKey.ts | 4 +- .../actions/nostr/setPrivateKey.ts | 4 +- src/extension/background-script/router.ts | 3 +- src/types.ts | 21 +++++----- 9 files changed, 70 insertions(+), 25 deletions(-) create mode 100644 src/extension/background-script/actions/nostr/getPublicKey.ts diff --git a/src/extension/background-script/actions/nostr/generatePrivateKey.ts b/src/extension/background-script/actions/nostr/generatePrivateKey.ts index a7aac8ec25..a3a69acbfb 100644 --- a/src/extension/background-script/actions/nostr/generatePrivateKey.ts +++ b/src/extension/background-script/actions/nostr/generatePrivateKey.ts @@ -1,11 +1,11 @@ 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 type { MessageNostrPrivateKeyGenerate } from "~/types"; import state from "../../state"; -const generatePrivateKey = async (message: MessagePrivateKeyGenerate) => { +const generatePrivateKey = async (message: MessageNostrPrivateKeyGenerate) => { const type = message?.args?.type; const privateKey = diff --git a/src/extension/background-script/actions/nostr/getPrivateKey.ts b/src/extension/background-script/actions/nostr/getPrivateKey.ts index 65596780de..61893512a9 100644 --- a/src/extension/background-script/actions/nostr/getPrivateKey.ts +++ b/src/extension/background-script/actions/nostr/getPrivateKey.ts @@ -1,9 +1,9 @@ import { decryptData } from "~/common/lib/crypto"; -import type { MessagePrivateKeyGet } from "~/types"; +import type { MessageNostrPrivateKeyGet } from "~/types"; import state from "../../state"; -const getPrivateKey = async (message: MessagePrivateKeyGet) => { +const getPrivateKey = async (message: MessageNostrPrivateKeyGet) => { const id = message?.args?.id; if (!id) { diff --git a/src/extension/background-script/actions/nostr/getPublicKey.ts b/src/extension/background-script/actions/nostr/getPublicKey.ts new file mode 100644 index 0000000000..72bec3a3d2 --- /dev/null +++ b/src/extension/background-script/actions/nostr/getPublicKey.ts @@ -0,0 +1,38 @@ +import { decryptData } from "~/common/lib/crypto"; +import Nostr from "~/extension/background-script/nostr"; +import { MessageNostrPublicKeyGet } from "~/types"; + +import state from "../../state"; + +const getPublicKey = async (message: MessageNostrPublicKeyGet) => { + const id = message?.args?.id; + + if (!id) { + return { + data: (await state.getState().getNostr()).getPublicKey(), + }; + } + + const accounts = state.getState().accounts; + if (Object.keys(accounts).includes(id)) { + const password = await state.getState().password(); + if (!password) { + return { + error: "Password is missing.", + }; + } + const account = accounts[id]; + if (!account.nostrPrivateKey) return { data: null }; + const privateKey = decryptData(account.nostrPrivateKey, password); + const publicKey = new Nostr(privateKey).getPublicKey(); + return { + data: publicKey, + }; + } + + return { + error: "Account does not exist.", + }; +}; + +export default getPublicKey; diff --git a/src/extension/background-script/actions/nostr/getPublicKeyOrPrompt.ts b/src/extension/background-script/actions/nostr/getPublicKeyOrPrompt.ts index 1bb2465349..5cf85b072e 100644 --- a/src/extension/background-script/actions/nostr/getPublicKeyOrPrompt.ts +++ b/src/extension/background-script/actions/nostr/getPublicKeyOrPrompt.ts @@ -1,11 +1,13 @@ import utils from "~/common/lib/utils"; -import type { MessagePublicKeyGet } from "~/types"; +import type { MessageNostrPublicKeyGetOrPrompt } from "~/types"; import { PermissionMethodNostr } from "~/types"; import state from "../../state"; import { addPermissionFor, hasPermissionFor } from "./helpers"; -const getPublicKeyOrPrompt = async (message: MessagePublicKeyGet) => { +const getPublicKeyOrPrompt = async ( + message: MessageNostrPublicKeyGetOrPrompt +) => { if (!("host" in message.origin)) { console.error("error", message.origin); return; diff --git a/src/extension/background-script/actions/nostr/index.ts b/src/extension/background-script/actions/nostr/index.ts index 7a0896fe6c..cf1e1d38f5 100644 --- a/src/extension/background-script/actions/nostr/index.ts +++ b/src/extension/background-script/actions/nostr/index.ts @@ -1,3 +1,5 @@ +import getPublicKey from "~/extension/background-script/actions/nostr/getPublicKey"; + import decryptOrPrompt from "./decryptOrPrompt"; import encryptOrPrompt from "./encryptOrPrompt"; import generatePrivateKey from "./generatePrivateKey"; @@ -10,14 +12,15 @@ import signEventOrPrompt from "./signEventOrPrompt"; import signSchnorrOrPrompt from "./signSchnorrOrPrompt"; export { + decryptOrPrompt, + encryptOrPrompt, generatePrivateKey, getPrivateKey, - removePrivateKey, - setPrivateKey, + getPublicKey, getPublicKeyOrPrompt, getRelays, + removePrivateKey, + setPrivateKey, signEventOrPrompt, signSchnorrOrPrompt, - encryptOrPrompt, - decryptOrPrompt, }; diff --git a/src/extension/background-script/actions/nostr/removePrivateKey.ts b/src/extension/background-script/actions/nostr/removePrivateKey.ts index 174a57137a..ff08095378 100644 --- a/src/extension/background-script/actions/nostr/removePrivateKey.ts +++ b/src/extension/background-script/actions/nostr/removePrivateKey.ts @@ -1,8 +1,8 @@ -import type { MessagePrivateKeyRemove } from "~/types"; +import type { MessageNostrPrivateKeyRemove } from "~/types"; import state from "../../state"; -const removePrivateKey = async (message: MessagePrivateKeyRemove) => { +const removePrivateKey = async (message: MessageNostrPrivateKeyRemove) => { const id = message.args?.id || state.getState().currentAccountId; const accounts = state.getState().accounts; diff --git a/src/extension/background-script/actions/nostr/setPrivateKey.ts b/src/extension/background-script/actions/nostr/setPrivateKey.ts index bed29ac6aa..c61f68328a 100644 --- a/src/extension/background-script/actions/nostr/setPrivateKey.ts +++ b/src/extension/background-script/actions/nostr/setPrivateKey.ts @@ -1,10 +1,10 @@ import { decryptData, encryptData } from "~/common/lib/crypto"; import { deriveNostrPrivateKey } from "~/common/lib/mnemonic"; -import type { MessagePrivateKeySet } from "~/types"; +import type { MessageNostrPrivateKeySet } from "~/types"; import state from "../../state"; -const setPrivateKey = async (message: MessagePrivateKeySet) => { +const setPrivateKey = async (message: MessageNostrPrivateKeySet) => { const id = message.args?.id || state.getState().currentAccountId; const password = await state.getState().password(); diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index f72a061996..27abc55b94 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -66,6 +66,7 @@ const routes = { nostr: { generatePrivateKey: nostr.generatePrivateKey, getPrivateKey: nostr.getPrivateKey, + getPublicKey: nostr.getPublicKey, removePrivateKey: nostr.removePrivateKey, setPrivateKey: nostr.setPrivateKey, }, @@ -122,4 +123,4 @@ const router = (path: FixMe) => { return route; }; -export { routes, router }; +export { router, routes }; diff --git a/src/types.ts b/src/types.ts index dbaf5ed354..3f10d66580 100644 --- a/src/types.ts +++ b/src/types.ts @@ -431,29 +431,31 @@ export interface MessageCurrencyRateGet extends MessageDefault { action: "getCurrencyRate"; } -// TODO: add Nostr Prefix -export interface MessagePublicKeyGet extends MessageDefault { +export interface MessageNostrPublicKeyGetOrPrompt extends MessageDefault { action: "getPublicKeyOrPrompt"; } -// TODO: add Nostr Prefix -export interface MessagePrivateKeyGet extends MessageDefault { +export interface MessageNostrPublicKeyGet extends MessageDefault { + args?: { + id?: Account["id"]; + }; + action: "getPublicKey"; +} +export interface MessageNostrPrivateKeyGet extends MessageDefault { args?: { id?: Account["id"]; }; action: "getPrivateKey"; } -// TODO: add Nostr Prefix -export interface MessagePrivateKeyGenerate extends MessageDefault { +export interface MessageNostrPrivateKeyGenerate extends MessageDefault { args?: { type?: "random"; }; action: "generatePrivateKey"; } -// TODO: add Nostr Prefix -export interface MessagePrivateKeySet extends MessageDefault { +export interface MessageNostrPrivateKeySet extends MessageDefault { args: { id?: Account["id"]; privateKey: string; @@ -461,8 +463,7 @@ export interface MessagePrivateKeySet extends MessageDefault { action: "setPrivateKey"; } -// TODO: add Nostr Prefix -export interface MessagePrivateKeyRemove extends MessageDefault { +export interface MessageNostrPrivateKeyRemove extends MessageDefault { args: { id?: Account["id"]; }; From e4a9c13ab8de6516b04fd474f87e3fa819598de3 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 21 Jun 2023 21:23:23 +0700 Subject: [PATCH 086/118] fix: do not get nostr private key in account detail screen --- src/app/screens/Accounts/Detail/index.tsx | 35 +++++++---------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/app/screens/Accounts/Detail/index.tsx b/src/app/screens/Accounts/Detail/index.tsx index 3ee48f5159..0dd8a7ec73 100644 --- a/src/app/screens/Accounts/Detail/index.tsx +++ b/src/app/screens/Accounts/Detail/index.tsx @@ -58,8 +58,6 @@ function AccountDetail() { const [accountName, setAccountName] = useState(""); const [hasMnemonic, setHasMnemonic] = useState(false); - // TODO: do not get private key here to just calculate public key! - const [currentPrivateKey, setCurrentPrivateKey] = useState(""); const [nostrPublicKey, setNostrPublicKey] = useState(""); const [hasImportedNostrKey, setHasImportedNostrKey] = useState(false); @@ -78,11 +76,17 @@ function AccountDetail() { setHasMnemonic(response.hasMnemonic); setHasImportedNostrKey(response.hasImportedNostrKey); - const priv = (await msg.request("nostr/getPrivateKey", { - id, - })) as string; - if (priv) { - setCurrentPrivateKey(priv); + if (response.nostrEnabled) { + const nostrPublicKeyHex = (await msg.request("nostr/getPublicKey", { + id, + })) as string; + if (nostrPublicKeyHex) { + const nostrPublicKeyNpub = nostr.hexToNip19( + nostrPublicKeyHex, + "npub" + ); + setNostrPublicKey(nostrPublicKeyNpub); + } } } } catch (e) { @@ -173,23 +177,6 @@ function AccountDetail() { } }, [fetchData, isLoadingSettings]); - useEffect(() => { - try { - setNostrPublicKey( - currentPrivateKey ? nostr.generatePublicKey(currentPrivateKey) : "" - ); - } catch (e) { - if (e instanceof Error) - toast.error( -

- {t("nostr.errors.failed_to_load")} -
- {e.message} -

- ); - } - }, [currentPrivateKey, t]); - return !account ? (
From 7b94714d432ed3562b7d6fbfa66f41fd52c8dcf2 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 21 Jun 2023 21:52:02 +0700 Subject: [PATCH 087/118] chore: move bitcoin network setting to account settings --- src/app/screens/Accounts/Detail/index.tsx | 39 ++++++++++++++++++- src/app/screens/Settings/index.tsx | 35 ----------------- src/common/lib/api.ts | 2 + src/common/settings.ts | 1 - .../actions/accounts/__tests__/get.test.ts | 3 ++ .../actions/accounts/edit.ts | 7 +++- .../background-script/actions/accounts/get.ts | 4 +- .../webbtc/__tests__/getAddress.test.ts | 4 +- .../actions/webbtc/getAddress.ts | 5 +-- .../events/__test__/notifications.test.ts | 1 - src/i18n/locales/en/translation.json | 22 +++++------ src/types.ts | 7 +++- 12 files changed, 70 insertions(+), 60 deletions(-) diff --git a/src/app/screens/Accounts/Detail/index.tsx b/src/app/screens/Accounts/Detail/index.tsx index 0dd8a7ec73..a11c6e04a1 100644 --- a/src/app/screens/Accounts/Detail/index.tsx +++ b/src/app/screens/Accounts/Detail/index.tsx @@ -24,13 +24,14 @@ import Avatar from "~/app/components/Avatar"; import Badge from "~/app/components/Badge"; import InputCopyButton from "~/app/components/InputCopyButton"; import MenuDivider from "~/app/components/Menu/MenuDivider"; +import Select from "~/app/components/form/Select"; import { useAccount } from "~/app/context/AccountContext"; import { useAccounts } from "~/app/context/AccountsContext"; import { useSettings } from "~/app/context/SettingsContext"; import api, { GetAccountRes } from "~/common/lib/api"; import msg from "~/common/lib/msg"; import nostr from "~/common/lib/nostr"; -import type { Account } from "~/types"; +import type { Account, BitcoinNetworkType } from "~/types"; type AccountAction = Pick; dayjs.extend(relativeTime); @@ -429,6 +430,42 @@ function AccountDetail() {
+ +
+
+

+ {t("bitcoin.network.title")} +

+

+ {t("bitcoin.network.subtitle")} +

+
+ +
+ +
+
diff --git a/src/app/screens/Settings/index.tsx b/src/app/screens/Settings/index.tsx index 1afaf43edf..44f3ffa4fa 100644 --- a/src/app/screens/Settings/index.tsx +++ b/src/app/screens/Settings/index.tsx @@ -368,41 +368,6 @@ function Settings() {
- -

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

-

- {t("bitcoin.hint")} -

-
- - {!isLoading && ( -
- -
- )} -
-
-
diff --git a/src/common/lib/api.ts b/src/common/lib/api.ts index 8edd236a8e..d78e676cb2 100644 --- a/src/common/lib/api.ts +++ b/src/common/lib/api.ts @@ -10,6 +10,7 @@ import type { AccountInfo, Accounts, Allowance, + BitcoinNetworkType, DbPayment, Invoice, LnurlAuthResponse, @@ -39,6 +40,7 @@ export interface GetAccountRes nostrEnabled: boolean; hasMnemonic: boolean; hasImportedNostrKey: boolean; + bitcoinNetwork: BitcoinNetworkType; } interface StatusRes { configured: boolean; diff --git a/src/common/settings.ts b/src/common/settings.ts index 4ea0e2faac..3c813e4640 100644 --- a/src/common/settings.ts +++ b/src/common/settings.ts @@ -17,5 +17,4 @@ export const DEFAULT_SETTINGS: SettingsStorage = { exchange: "alby", nostrEnabled: false, closedTips: [], - bitcoinNetwork: "bitcoin", }; diff --git a/src/extension/background-script/actions/accounts/__tests__/get.test.ts b/src/extension/background-script/actions/accounts/__tests__/get.test.ts index 22a5720826..622a9bc790 100644 --- a/src/extension/background-script/actions/accounts/__tests__/get.test.ts +++ b/src/extension/background-script/actions/accounts/__tests__/get.test.ts @@ -28,6 +28,7 @@ const mockState = { name: "Alby", nostrPrivateKey: "nostr-123-456", mnemonic: btcFixture.mnemnoic, + bitcoinNetwork: "regtest", }, "1e1e8ea6-493e-480b-9855-303d37506e97": { config: "config-123-456", @@ -60,6 +61,7 @@ describe("account info", () => { nostrEnabled: false, hasMnemonic: false, hasImportedNostrKey: true, + bitcoinNetwork: "bitcoin", }; expect(await getAccount(message)).toStrictEqual({ @@ -84,6 +86,7 @@ describe("account info", () => { nostrEnabled: true, hasMnemonic: true, hasImportedNostrKey: true, + bitcoinNetwork: "regtest", }; expect(await getAccount(message)).toStrictEqual({ diff --git a/src/extension/background-script/actions/accounts/edit.ts b/src/extension/background-script/actions/accounts/edit.ts index f8886feb02..fad7467761 100644 --- a/src/extension/background-script/actions/accounts/edit.ts +++ b/src/extension/background-script/actions/accounts/edit.ts @@ -6,7 +6,12 @@ const edit = async (message: MessageAccountEdit) => { const accountId = message.args.id; if (accountId in accounts) { - accounts[accountId].name = message.args.name; + if (message.args.name) { + accounts[accountId].name = message.args.name; + } + if (message.args.bitcoinNetwork) { + accounts[accountId].bitcoinNetwork = message.args.bitcoinNetwork; + } state.setState({ accounts }); // make sure we immediately persist the updated accounts diff --git a/src/extension/background-script/actions/accounts/get.ts b/src/extension/background-script/actions/accounts/get.ts index bfe044f4a0..8b1cb12042 100644 --- a/src/extension/background-script/actions/accounts/get.ts +++ b/src/extension/background-script/actions/accounts/get.ts @@ -1,3 +1,4 @@ +import { GetAccountRes } from "~/common/lib/api"; import state from "~/extension/background-script/state"; import type { MessageAccountGet } from "~/types"; @@ -14,7 +15,7 @@ const get = async (message: MessageAccountGet) => { if (!account) return; - const result = { + const result: GetAccountRes = { id: account.id, connector: account.connector, name: account.name, @@ -22,6 +23,7 @@ const get = async (message: MessageAccountGet) => { hasMnemonic: !!account.mnemonic, // Note: undefined (default for new accounts) it is also considered imported hasImportedNostrKey: account.hasImportedNostrKey !== false, + bitcoinNetwork: account.bitcoinNetwork || "bitcoin", }; return { diff --git a/src/extension/background-script/actions/webbtc/__tests__/getAddress.test.ts b/src/extension/background-script/actions/webbtc/__tests__/getAddress.test.ts index 6736ac62e4..cc030265b4 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/getAddress.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/getAddress.test.ts @@ -10,11 +10,9 @@ const mockState = { currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", getAccount: () => ({ mnemonic: btcFixture.mnemnoic, + bitcoinNetwork: "regtest", }), getConnector: jest.fn(), - settings: { - bitcoinNetwork: "regtest", - }, }; state.getState = jest.fn().mockReturnValue(mockState); diff --git a/src/extension/background-script/actions/webbtc/getAddress.ts b/src/extension/background-script/actions/webbtc/getAddress.ts index c017ce8cff..cfee8d8e7c 100644 --- a/src/extension/background-script/actions/webbtc/getAddress.ts +++ b/src/extension/background-script/actions/webbtc/getAddress.ts @@ -24,10 +24,9 @@ const getAddress = async (message: MessageGetAddress) => { throw new Error("No mnemonic set"); } const mnemonic = decryptData(account.mnemonic, password); - const settings = state.getState().settings; const derivationPath = - settings.bitcoinNetwork === "bitcoin" + (account.bitcoinNetwork || "bitcoin") === "bitcoin" ? BTC_TAPROOT_DERIVATION_PATH : BTC_TAPROOT_DERIVATION_PATH_REGTEST; @@ -36,7 +35,7 @@ const getAddress = async (message: MessageGetAddress) => { const address = getTaprootAddressFromPrivateKey( privateKey, - settings.bitcoinNetwork + account.bitcoinNetwork || "bitcoin" ); const data: BitcoinAddress = { diff --git a/src/extension/background-script/events/__test__/notifications.test.ts b/src/extension/background-script/events/__test__/notifications.test.ts index fa8a8d04ca..9f92db3416 100644 --- a/src/extension/background-script/events/__test__/notifications.test.ts +++ b/src/extension/background-script/events/__test__/notifications.test.ts @@ -31,7 +31,6 @@ const settings: SettingsStorage = { websiteEnhancements: true, nostrEnabled: false, closedTips: [], - bitcoinNetwork: "bitcoin", }; const mockState = { diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index da50384cbb..0a6080dd5b 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -449,6 +449,16 @@ "title": "Your Secret Key" } }, + "bitcoin": { + "network": { + "title": "Bitcoin Network", + "subtitle": "Choose network to derive addresses and decode transactions", + "options": { + "bitcoin": "Mainnet", + "regtest": "Regtest" + } + } + }, "nostr": { "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.", @@ -575,18 +585,6 @@ "system": "System" } }, - "bitcoin": { - "title": "Bitcoin", - "hint": "Manage your Bitcoin settings for WebBTC-enabled apps", - "network": { - "title": "Network", - "subtitle": "Choose network to derive addresses and decode transactions", - "options": { - "bitcoin": "Mainnet", - "regtest": "Regtest" - } - } - }, "show_fiat": { "title": "Sats to Fiat", "subtitle": "Always convert into selected currency from selected exchange" diff --git a/src/types.ts b/src/types.ts index 3f10d66580..2a19dcd545 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,6 +11,8 @@ import { Event } from "./extension/providers/nostr/types"; export type ConnectorType = keyof typeof connectors; +export type BitcoinNetworkType = "bitcoin" | "regtest"; + export interface Account { id: string; connector: ConnectorType; @@ -19,6 +21,7 @@ export interface Account { nostrPrivateKey?: string | null; mnemonic?: string | null; hasImportedNostrKey?: boolean; + bitcoinNetwork?: BitcoinNetworkType; } export interface Accounts { @@ -203,7 +206,8 @@ export interface MessageAccountAdd extends MessageDefault { export interface MessageAccountEdit extends MessageDefault { args: { id: Account["id"]; - name: Account["name"]; + name?: Account["name"]; + bitcoinNetwork?: BitcoinNetworkType; }; action: "editAccount"; } @@ -749,7 +753,6 @@ export interface SettingsStorage { exchange: SupportedExchanges; nostrEnabled: boolean; closedTips: TIPS[]; - bitcoinNetwork: "bitcoin" | "regtest"; } export interface Badge { From 46f7737ca7a2d449528a83373ccf96ab6f733a45 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 22 Jun 2023 13:41:48 +0700 Subject: [PATCH 088/118] chore: move saveMnemonic and saveNostrPrivateKey utility functions to background script --- .../Accounts/GenerateSecretKey/index.tsx | 16 +++-- .../Accounts/ImportSecretKey/index.tsx | 6 +- .../Accounts/NostrAdvancedSettings/index.tsx | 50 ++++++++------- src/app/utils/saveMnemonic.ts | 21 ------- src/app/utils/saveNostrPrivateKey.ts | 26 -------- src/common/lib/mnemonic.ts | 36 ----------- .../actions/accounts/select.ts | 1 + .../actions/mnemonic/setMnemonic.ts | 16 ++++- .../actions/nostr/generatePrivateKey.ts | 63 ++++++++----------- .../actions/nostr/removePrivateKey.ts | 6 +- .../actions/nostr/setPrivateKey.ts | 21 +++++-- .../webbtc/__tests__/getAddress.test.ts | 2 + .../actions/webbtc/getAddress.ts | 21 ++----- .../background-script/mnemonic/index.ts | 40 ++++++++++++ src/extension/background-script/state.ts | 24 ++++++- src/types.ts | 2 +- 16 files changed, 172 insertions(+), 179 deletions(-) delete mode 100644 src/app/utils/saveMnemonic.ts delete mode 100644 src/app/utils/saveNostrPrivateKey.ts delete mode 100644 src/common/lib/mnemonic.ts create mode 100644 src/extension/background-script/mnemonic/index.ts diff --git a/src/app/screens/Accounts/GenerateSecretKey/index.tsx b/src/app/screens/Accounts/GenerateSecretKey/index.tsx index d8b8320530..797d80c0a0 100644 --- a/src/app/screens/Accounts/GenerateSecretKey/index.tsx +++ b/src/app/screens/Accounts/GenerateSecretKey/index.tsx @@ -13,8 +13,8 @@ import MnemonicInputs from "~/app/components/MnemonicInputs"; import SecretKeyDescription from "~/app/components/SecretKeyDescription"; import Checkbox from "~/app/components/form/Checkbox"; import { useAccount } from "~/app/context/AccountContext"; -import { saveMnemonic } from "~/app/utils/saveMnemonic"; import api from "~/common/lib/api"; +import msg from "~/common/lib/msg"; function GenerateSecretKey() { const navigate = useNavigate(); @@ -47,7 +47,7 @@ function GenerateSecretKey() { fetchData(); }, [fetchData]); - async function backupSecretKey() { + async function saveGeneratedSecretKey() { try { if (!hasConfirmedBackup) { throw new Error(t("backup.error_confirm")); @@ -60,7 +60,11 @@ function GenerateSecretKey() { throw new Error("No mnemonic available"); } - await saveMnemonic(id, mnemonic); + await msg.request("setMnemonic", { + id, + mnemonic, + }); + toast.success(t("saved")); // go to account settings navigate(`/accounts/${id}`); @@ -106,7 +110,11 @@ function GenerateSecretKey() { )}
-
diff --git a/src/app/screens/Accounts/ImportSecretKey/index.tsx b/src/app/screens/Accounts/ImportSecretKey/index.tsx index dbd522d226..37d319a767 100644 --- a/src/app/screens/Accounts/ImportSecretKey/index.tsx +++ b/src/app/screens/Accounts/ImportSecretKey/index.tsx @@ -11,7 +11,6 @@ import Button from "~/app/components/Button"; import { ContentBox } from "~/app/components/ContentBox"; import MnemonicInputs from "~/app/components/MnemonicInputs"; import { useAccount } from "~/app/context/AccountContext"; -import { saveMnemonic } from "~/app/utils/saveMnemonic"; import api from "~/common/lib/api"; import msg from "~/common/lib/msg"; @@ -77,7 +76,10 @@ function ImportSecretKey() { throw new Error("Invalid mnemonic"); } - await saveMnemonic(id, mnemonic); + await msg.request("setMnemonic", { + id, + mnemonic, + }); toast.success(t("saved")); // go to account settings navigate(`/accounts/${id}`); diff --git a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx index 11308c4a54..e117d74ee7 100644 --- a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx +++ b/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx @@ -13,9 +13,7 @@ import Button from "~/app/components/Button"; import InputCopyButton from "~/app/components/InputCopyButton"; import TextField from "~/app/components/form/TextField"; import { useAccount } from "~/app/context/AccountContext"; -import { saveNostrPrivateKey } from "~/app/utils/saveNostrPrivateKey"; import { GetAccountRes } from "~/common/lib/api"; -import { deriveNostrPrivateKey } from "~/common/lib/mnemonic"; import msg from "~/common/lib/msg"; import { default as nostr, default as nostrlib } from "~/common/lib/nostr"; @@ -26,8 +24,7 @@ function NostrAdvancedSettings() { keyPrefix: "accounts.account_view", }); const navigate = useNavigate(); - // FIXME: use account hasMnemonic - const [mnemonic, setMnemonic] = useState(""); + const [hasMnemonic, setHasMnemonic] = useState(false); const [currentPrivateKey, setCurrentPrivateKey] = useState(""); const [nostrPrivateKey, setNostrPrivateKey] = useState(""); const [nostrPrivateKeyVisible, setNostrPrivateKeyVisible] = useState(false); @@ -43,18 +40,10 @@ function NostrAdvancedSettings() { if (priv) { setCurrentPrivateKey(priv); } - - // FIXME: do not get mnemonic here - const accountMnemonic = (await msg.request("getMnemonic", { - id, - })) as string; - if (accountMnemonic) { - setMnemonic(accountMnemonic); - } - const accountResponse = await msg.request("getAccount", { id, }); + setHasMnemonic(accountResponse.hasMnemonic); setHasImportedNostrKey(accountResponse.hasImportedNostrKey); } }, [id]); @@ -93,20 +82,19 @@ function NostrAdvancedSettings() { throw new Error("No id set"); } - if (!mnemonic) { + if (!hasMnemonic) { throw new Error("No mnemonic exists"); } - const nostrPrivateKey = await deriveNostrPrivateKey(mnemonic); - - await handleSaveNostrPrivateKey(nostrPrivateKey); + await handleSaveNostrPrivateKey(true); } - async function handleSaveNostrPrivateKey(nostrPrivateKey: string) { + // TODO: simplify this method (do not handle deriving, saving and removing in one) + async function handleSaveNostrPrivateKey(deriveFromMnemonic: boolean) { if (!id) { throw new Error("No id set"); } - if (nostrPrivateKey === currentPrivateKey) { + if (!deriveFromMnemonic && nostrPrivateKey === currentPrivateKey) { throw new Error("private key hasn't changed"); } @@ -120,10 +108,24 @@ function NostrAdvancedSettings() { } try { - saveNostrPrivateKey(id, nostrPrivateKey); + if (deriveFromMnemonic) { + await msg.request("nostr/generatePrivateKey", { + id, + }); + } else if (nostrPrivateKey) { + await msg.request("nostr/setPrivateKey", { + id, + privateKey: nostrPrivateKey, + }); + } else { + await msg.request("nostr/removePrivateKey", { + id, + }); + } + toast.success( t( - nostrPrivateKey + nostrPrivateKey || deriveFromMnemonic ? "nostr.private_key.success" : "nostr.private_key.successfully_removed" ) @@ -147,7 +149,7 @@ function NostrAdvancedSettings() { { e.preventDefault(); - handleSaveNostrPrivateKey(nostrPrivateKey); + handleSaveNostrPrivateKey(false); }} > @@ -161,7 +163,7 @@ function NostrAdvancedSettings() {

- {mnemonic && currentPrivateKey ? ( + {hasMnemonic && currentPrivateKey ? ( hasImportedNostrKey ? ( {t("nostr.advanced_settings.imported_key_warning")} @@ -215,7 +217,7 @@ function NostrAdvancedSettings() { /> {hasImportedNostrKey && (
- {mnemonic ? ( + {hasMnemonic ? (
-
+
-
+

{t("bitcoin.network.title")} From c718174eb27e163f026a9d074f117ac94f93039c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Fri, 23 Jun 2023 15:33:45 +0200 Subject: [PATCH 096/118] fix: improve warning copy --- src/i18n/locales/en/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index c67aca7fa4..8f86c39628 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -432,7 +432,7 @@ "error_confirm": "Please confirm that you have backed up your secret key.", "save": "Save Secret Key", "button": "Backup Secret Key", - "warning": "⚠️ Backup your Secret Key! Not backing it up might result in permanently loosing access to your Nostr identity or assets you manage with this key.", + "warning": "⚠️ Don't forget to backup your Secret Key! Not backing it up might result in permanently loosing access to your Nostr identity or assets you manage with this key.", "description1": "In addition to the Bitcoin Lightning Network, Alby allows you to generate keys and interact with other protocols such as:", "description2": "Your Secret Key is a set of 12 words that will allow you to access your keys to those protocols on a new device or in case you loose access to your account. Make sure to write it down on a piece of paper and keep it safe:", "protocols": { From ad61944e531eb5d380921b3d311cbe8c859a805c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Tue, 27 Jun 2023 11:06:12 +0200 Subject: [PATCH 097/118] fix: colors for alert --- src/app/components/Alert/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/Alert/index.tsx b/src/app/components/Alert/index.tsx index c7ee5bc7fc..cbf2f63309 100644 --- a/src/app/components/Alert/index.tsx +++ b/src/app/components/Alert/index.tsx @@ -11,9 +11,9 @@ export default function Alert({ type, children }: Props) { className={classNames( "rounded-md font-medium p-4", type == "warn" && - "text-orange-700 bg-orange-50 dark:text-orange-400 dark:bg-orange-900", + "text-orange-700 bg-orange-50 dark:text-orange-200 dark:bg-orange-900", type == "info" && - "text-blue-700 bg-blue-50 dark:text-blue-400 dark:bg-blue-900" + "text-blue-700 bg-blue-50 dark:text-blue-200 dark:bg-blue-900" )} >

{children}

From 8746d55b458ea8d7966ba982e6608d9db09e728f Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 28 Jun 2023 12:56:30 +0700 Subject: [PATCH 098/118] chore: remove unused types --- src/types.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/types.ts b/src/types.ts index fdad0e940e..59fb6ea753 100644 --- a/src/types.ts +++ b/src/types.ts @@ -156,10 +156,6 @@ export type NavigationState = { method: string; description: string; }; - derivationPath?: string; - index?: number; - num?: number; - change?: boolean; }; isPrompt?: true; // only passed via Prompt.tsx action: string; From 0620f848edf6c881013da0d3734926c69ca337fa Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 28 Jun 2023 12:56:46 +0700 Subject: [PATCH 099/118] chore: do not export nostr derivation path --- src/extension/background-script/mnemonic/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension/background-script/mnemonic/index.ts b/src/extension/background-script/mnemonic/index.ts index f68b3b3594..95d4f69e23 100644 --- a/src/extension/background-script/mnemonic/index.ts +++ b/src/extension/background-script/mnemonic/index.ts @@ -2,7 +2,7 @@ import * as secp256k1 from "@noble/secp256k1"; import { HDKey } from "@scure/bip32"; import * as bip39 from "@scure/bip39"; -export const NOSTR_DERIVATION_PATH = "m/44'/1237'/0'/0/0"; // NIP-06 +const NOSTR_DERIVATION_PATH = "m/44'/1237'/0'/0/0"; // NIP-06 class Mnemonic { readonly mnemonic: string; From efa060573853d329307e940e0b9b955a738b1a16 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 28 Jun 2023 14:36:10 +0700 Subject: [PATCH 100/118] fix: only show secret key import subsection if account does not have mnemonic --- src/app/screens/Accounts/Detail/index.tsx | 44 ++++++++++++----------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/app/screens/Accounts/Detail/index.tsx b/src/app/screens/Accounts/Detail/index.tsx index 3840434aa5..1d481891aa 100644 --- a/src/app/screens/Accounts/Detail/index.tsx +++ b/src/app/screens/Accounts/Detail/index.tsx @@ -377,27 +377,31 @@ function AccountDetail() {
- -
-
-

- {t("mnemonic.import.title")} -

-

- {t("mnemonic.import.description")} -

-
+ {!hasMnemonic && ( + <> + +
+
+

+ {t("mnemonic.import.title")} +

+

+ {t("mnemonic.import.description")} +

+
-
- -
-
+
+ +
+
+ + )}
From 0cad988a5a99892c0f36099be6fa0a6aa879ff03 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 28 Jun 2023 14:37:13 +0700 Subject: [PATCH 101/118] fix: get correct account, navigate to account settings if mnemonic already set --- .../Accounts/GenerateSecretKey/index.tsx | 41 ++++++++--------- .../Accounts/ImportSecretKey/index.tsx | 45 +++++++------------ src/common/lib/api.ts | 5 ++- 3 files changed, 39 insertions(+), 52 deletions(-) diff --git a/src/app/screens/Accounts/GenerateSecretKey/index.tsx b/src/app/screens/Accounts/GenerateSecretKey/index.tsx index f6de764b42..4af5e779db 100644 --- a/src/app/screens/Accounts/GenerateSecretKey/index.tsx +++ b/src/app/screens/Accounts/GenerateSecretKey/index.tsx @@ -1,6 +1,6 @@ import Container from "@components/Container"; import Loading from "@components/Loading"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; @@ -10,14 +10,12 @@ import { ContentBox } from "~/app/components/ContentBox"; import MnemonicInputs from "~/app/components/MnemonicInputs"; import SecretKeyDescription from "~/app/components/SecretKeyDescription"; import Checkbox from "~/app/components/form/Checkbox"; -import { useAccount } from "~/app/context/AccountContext"; import api from "~/common/lib/api"; import msg from "~/common/lib/msg"; function GenerateSecretKey() { const navigate = useNavigate(); const [mnemonic, setMnemonic] = useState(); - const account = useAccount(); const { t } = useTranslation("translation", { keyPrefix: "accounts.account_view.mnemonic", }); @@ -27,31 +25,30 @@ function GenerateSecretKey() { const { id } = useParams(); - const fetchData = useCallback(async () => { - try { - const account = await api.getAccount(); - setHasNostrPrivateKey(account.nostrEnabled); - const newMnemonic = (await msg.request("generateMnemonic")) as string; - setMnemonic(newMnemonic); - } catch (e) { - console.error(e); - if (e instanceof Error) toast.error(`Error: ${e.message}`); - } - }, []); - useEffect(() => { - fetchData(); - }, [fetchData]); + (async () => { + try { + const account = await api.getAccount(id); + setHasNostrPrivateKey(account.nostrEnabled); + if (account.hasMnemonic) { + // do not allow user to generate a mnemonic if they already have one for the current account + // go to account settings + navigate(`/accounts/${id}`); + } + const newMnemonic = (await msg.request("generateMnemonic")) as string; + setMnemonic(newMnemonic); + } catch (e) { + console.error(e); + if (e instanceof Error) toast.error(`Error: ${e.message}`); + } + })(); + }, [id, navigate]); async function saveGeneratedSecretKey() { try { if (!hasConfirmedBackup) { throw new Error(t("backup.error_confirm")); } - if (!account || !id) { - // type guard - throw new Error("No account available"); - } if (!mnemonic) { throw new Error("No mnemonic available"); } @@ -69,7 +66,7 @@ function GenerateSecretKey() { } } - return !account || !mnemonic ? ( + return !mnemonic ? (
diff --git a/src/app/screens/Accounts/ImportSecretKey/index.tsx b/src/app/screens/Accounts/ImportSecretKey/index.tsx index a67f4a459a..33d7cfa0c8 100644 --- a/src/app/screens/Accounts/ImportSecretKey/index.tsx +++ b/src/app/screens/Accounts/ImportSecretKey/index.tsx @@ -2,7 +2,7 @@ import Container from "@components/Container"; import Loading from "@components/Loading"; import * as bip39 from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; @@ -10,13 +10,11 @@ import Alert from "~/app/components/Alert"; import Button from "~/app/components/Button"; import { ContentBox } from "~/app/components/ContentBox"; import MnemonicInputs from "~/app/components/MnemonicInputs"; -import { useAccount } from "~/app/context/AccountContext"; import api from "~/common/lib/api"; import msg from "~/common/lib/msg"; function ImportSecretKey() { const [mnemonic, setMnemonic] = useState(""); - const account = useAccount(); const { t: tCommon } = useTranslation("common"); const navigate = useNavigate(); const { t } = useTranslation("translation", { @@ -24,27 +22,26 @@ function ImportSecretKey() { }); const [hasFetchedData, setHasFetchedData] = useState(false); - const [hasMnemonic, setHasMnemonic] = useState(false); const [hasNostrPrivateKey, setHasNostrPrivateKey] = useState(false); const { id } = useParams(); - const fetchData = useCallback(async () => { - try { - if (id) { - const account = await api.getAccount(); + useEffect(() => { + (async () => { + try { + const account = await api.getAccount(id); setHasNostrPrivateKey(account.nostrEnabled); - setHasMnemonic(account.hasMnemonic); + if (account.hasMnemonic) { + // do not allow user to import a mnemonic if they already have one for the current account + // go to account settings + navigate(`/accounts/${id}`); + } setHasFetchedData(true); + } catch (e) { + console.error(e); + if (e instanceof Error) toast.error(`Error: ${e.message}`); } - } catch (e) { - console.error(e); - if (e instanceof Error) toast.error(`Error: ${e.message}`); - } - }, [id]); - - useEffect(() => { - fetchData(); - }, [fetchData]); + })(); + }, [id, navigate]); function cancelImport() { // go to account settings @@ -53,16 +50,6 @@ function ImportSecretKey() { async function importKey() { try { - if (hasMnemonic) { - if (!window.confirm(t("import.confirm_overwrite"))) { - return; - } - } - if (!account || !id) { - // type guard - throw new Error("No account available"); - } - if ( mnemonic.split(" ").length !== 12 || !bip39.validateMnemonic(mnemonic, wordlist) @@ -82,7 +69,7 @@ function ImportSecretKey() { } } - return !account || !hasFetchedData ? ( + return !hasFetchedData ? (
diff --git a/src/common/lib/api.ts b/src/common/lib/api.ts index d78e676cb2..1be2cb7938 100644 --- a/src/common/lib/api.ts +++ b/src/common/lib/api.ts @@ -101,7 +101,10 @@ export const swrGetAccountInfo = async ( }); }; export const getAccounts = () => msg.request("getAccounts"); -export const getAccount = () => msg.request("getAccount"); +export const getAccount = (id?: string) => + msg.request("getAccount", { + id, + }); export const updateAllowance = () => msg.request("updateAllowance"); export const selectAccount = (id: string) => msg.request("selectAccount", { id }); From d5ae8ce1e61a2b70e010db16eded47d27a222af8 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 28 Jun 2023 14:39:43 +0700 Subject: [PATCH 102/118] chore: move mnemonic components into subfolder --- src/app/components/{ => mnemonic}/MnemonicInputs/index.tsx | 0 .../components/{ => mnemonic}/SecretKeyDescription/index.tsx | 0 src/app/screens/Accounts/BackupSecretKey/index.tsx | 4 ++-- src/app/screens/Accounts/GenerateSecretKey/index.tsx | 4 ++-- src/app/screens/Accounts/ImportSecretKey/index.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/app/components/{ => mnemonic}/MnemonicInputs/index.tsx (100%) rename src/app/components/{ => mnemonic}/SecretKeyDescription/index.tsx (100%) diff --git a/src/app/components/MnemonicInputs/index.tsx b/src/app/components/mnemonic/MnemonicInputs/index.tsx similarity index 100% rename from src/app/components/MnemonicInputs/index.tsx rename to src/app/components/mnemonic/MnemonicInputs/index.tsx diff --git a/src/app/components/SecretKeyDescription/index.tsx b/src/app/components/mnemonic/SecretKeyDescription/index.tsx similarity index 100% rename from src/app/components/SecretKeyDescription/index.tsx rename to src/app/components/mnemonic/SecretKeyDescription/index.tsx diff --git a/src/app/screens/Accounts/BackupSecretKey/index.tsx b/src/app/screens/Accounts/BackupSecretKey/index.tsx index 92026babbe..9a4069d0e9 100644 --- a/src/app/screens/Accounts/BackupSecretKey/index.tsx +++ b/src/app/screens/Accounts/BackupSecretKey/index.tsx @@ -5,8 +5,8 @@ import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; import { toast } from "react-toastify"; import { ContentBox } from "~/app/components/ContentBox"; -import MnemonicInputs from "~/app/components/MnemonicInputs"; -import SecretKeyDescription from "~/app/components/SecretKeyDescription"; +import MnemonicInputs from "~/app/components/mnemonic/MnemonicInputs"; +import SecretKeyDescription from "~/app/components/mnemonic/SecretKeyDescription"; import msg from "~/common/lib/msg"; function BackupSecretKey() { diff --git a/src/app/screens/Accounts/GenerateSecretKey/index.tsx b/src/app/screens/Accounts/GenerateSecretKey/index.tsx index 4af5e779db..7a3afc36aa 100644 --- a/src/app/screens/Accounts/GenerateSecretKey/index.tsx +++ b/src/app/screens/Accounts/GenerateSecretKey/index.tsx @@ -7,9 +7,9 @@ import { toast } from "react-toastify"; import Alert from "~/app/components/Alert"; import Button from "~/app/components/Button"; import { ContentBox } from "~/app/components/ContentBox"; -import MnemonicInputs from "~/app/components/MnemonicInputs"; -import SecretKeyDescription from "~/app/components/SecretKeyDescription"; import Checkbox from "~/app/components/form/Checkbox"; +import MnemonicInputs from "~/app/components/mnemonic/MnemonicInputs"; +import SecretKeyDescription from "~/app/components/mnemonic/SecretKeyDescription"; import api from "~/common/lib/api"; import msg from "~/common/lib/msg"; diff --git a/src/app/screens/Accounts/ImportSecretKey/index.tsx b/src/app/screens/Accounts/ImportSecretKey/index.tsx index 33d7cfa0c8..05d019753c 100644 --- a/src/app/screens/Accounts/ImportSecretKey/index.tsx +++ b/src/app/screens/Accounts/ImportSecretKey/index.tsx @@ -9,7 +9,7 @@ import { toast } from "react-toastify"; import Alert from "~/app/components/Alert"; import Button from "~/app/components/Button"; import { ContentBox } from "~/app/components/ContentBox"; -import MnemonicInputs from "~/app/components/MnemonicInputs"; +import MnemonicInputs from "~/app/components/mnemonic/MnemonicInputs"; import api from "~/common/lib/api"; import msg from "~/common/lib/msg"; From f3acf755009067b78fe7ba9219f808259e3ce3f4 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 28 Jun 2023 14:43:29 +0700 Subject: [PATCH 103/118] chore: rename nostr advanced settings to nostr settings --- src/app/router/Options/Options.tsx | 4 ++-- src/app/screens/Accounts/Detail/index.tsx | 6 +----- .../index.tsx | 18 ++++++++---------- src/i18n/locales/en/translation.json | 6 +++--- 4 files changed, 14 insertions(+), 20 deletions(-) rename src/app/screens/Accounts/{NostrAdvancedSettings => NostrSettings}/index.tsx (93%) diff --git a/src/app/router/Options/Options.tsx b/src/app/router/Options/Options.tsx index 6f2705d2c9..25e3006b77 100644 --- a/src/app/router/Options/Options.tsx +++ b/src/app/router/Options/Options.tsx @@ -28,7 +28,7 @@ import { getConnectorRoutes, renderRoutes } from "~/app/router/connectorRoutes"; import BackupSecretKey from "~/app/screens/Accounts/BackupSecretKey"; import GenerateSecretKey from "~/app/screens/Accounts/GenerateSecretKey"; import ImportSecretKey from "~/app/screens/Accounts/ImportSecretKey"; -import NostrAdvancedSettings from "~/app/screens/Accounts/NostrAdvancedSettings"; +import NostrSettings from "~/app/screens/Accounts/NostrSettings"; import Discover from "~/app/screens/Discover"; import AlbyWalletCreate from "~/app/screens/connectors/AlbyWallet/create"; import AlbyWalletLogin from "~/app/screens/connectors/AlbyWallet/login"; @@ -102,7 +102,7 @@ function Options() { path=":id/secret-key/import" element={} /> - } /> + } /> -
diff --git a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx b/src/app/screens/Accounts/NostrSettings/index.tsx similarity index 93% rename from src/app/screens/Accounts/NostrAdvancedSettings/index.tsx rename to src/app/screens/Accounts/NostrSettings/index.tsx index e117d74ee7..13e2a03623 100644 --- a/src/app/screens/Accounts/NostrAdvancedSettings/index.tsx +++ b/src/app/screens/Accounts/NostrSettings/index.tsx @@ -17,7 +17,7 @@ import { GetAccountRes } from "~/common/lib/api"; import msg from "~/common/lib/msg"; import { default as nostr, default as nostrlib } from "~/common/lib/nostr"; -function NostrAdvancedSettings() { +function NostrSettings() { const account = useAccount(); const { t: tCommon } = useTranslation("common"); const { t } = useTranslation("translation", { @@ -156,22 +156,20 @@ function NostrAdvancedSettings() {

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

- {t("nostr.advanced_settings.description")} + {t("nostr.settings.description")}

{hasMnemonic && currentPrivateKey ? ( hasImportedNostrKey ? ( - {t("nostr.advanced_settings.imported_key_warning")} + {t("nostr.settings.imported_key_warning")} ) : ( - - {t("nostr.advanced_settings.can_restore")} - + {t("nostr.settings.can_restore")} ) ) : null} @@ -220,13 +218,13 @@ function NostrAdvancedSettings() { {hasMnemonic ? (
- {hasMnemonic && currentPrivateKey ? ( + {hasMnemonic && + currentPrivateKey && + nostrPrivateKey === currentPrivateKey ? ( hasImportedNostrKey ? ( {t("nostr.settings.imported_key_warning")} @@ -199,7 +197,7 @@ function NostrSettings() { disabled endAdornment={} /> - {hasImportedNostrKey && ( + {hasImportedNostrKey && nostrPrivateKey === currentPrivateKey && (
{hasMnemonic ? (
- )} +
-
+
to create your secret key and derive your nostr keys." }, "private_key": {