diff --git a/create-packages.sh b/create-packages.sh index b3d48057a2..82a8da6bc2 100755 --- a/create-packages.sh +++ b/create-packages.sh @@ -2,6 +2,39 @@ # Extract version from package.json VERSION=$(node -pe "require('./package.json').version") +NODE_ENV=production + +if [ -z ${ALBY_OAUTH_CLIENT_ID_CHROME+x} ]; +then + echo "OAuth client id for Chrome:" + read ALBY_OAUTH_CLIENT_ID_CHROME +fi + +if [ -z ${ALBY_OAUTH_CLIENT_SECRET_CHROME+x} ]; +then + echo "OAuth client secret for Chrome:" + read ALBY_OAUTH_CLIENT_SECRET_CHROME +fi + +if [ -z ${ALBY_OAUTH_CLIENT_ID_FIREFOX+x} ]; +then + echo "OAuth client id for Firefox:" + read ALBY_OAUTH_CLIENT_ID_FIREFOX +fi + +if [ -z ${ALBY_OAUTH_CLIENT_SECRET_FIREFOX+x} ]; +then + echo "OAuth client secret for Firefox:" + read ALBY_OAUTH_CLIENT_SECRET_FIREFOX +fi + +if [ -z ${ALBY_API_URL+x} ]; +then + ALBY_API_URL="https://api.getalby.com" +fi + +echo "Creating the build for v$VERSION" +yarn build echo "Creating zip packages for v$VERSION" cd dist/production @@ -38,4 +71,4 @@ echo "Created alby-opera-v$VERSION.crx (SHA512: $SHA)" echo "done!" -cd ../../../ \ No newline at end of file +cd ../../../ diff --git a/package.json b/package.json index f8c43e519a..37a571c8ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lightning-browser-extension", - "version": "3.2.1", + "version": "3.3.0", "description": "Lightning browser extension", "private": true, "repository": "https://github.com/bumi/lightning-browser-extension.git", @@ -21,7 +21,7 @@ "build:firefox": "NODE_ENV=production TARGET_BROWSER=firefox webpack", "build:opera": "NODE_ENV=production TARGET_BROWSER=opera webpack", "build": "yarn build:chrome && yarn build:firefox && yarn build:opera", - "package": "yarn build && ./create-packages.sh", + "package": "./create-packages.sh", "lint": "yarn lint:js && yarn tsc:compile && yarn format:fix", "lint:js": "eslint src --ext .js,.jsx,.ts,.tsx --max-warnings 0", "lint:js:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix", @@ -35,7 +35,8 @@ "prepare": "husky install" }, "dependencies": { - "@bitcoin-design/bitcoin-icons-react": "^0.1.9", + "@bitcoin-design/bitcoin-icons-react": "^0.1.10", + "@bitcoinerlab/secp256k1": "^1.0.5", "@getalby/sdk": "^2.2.3", "@headlessui/react": "^1.7.16", "@lightninglabs/lnc-web": "^0.2.4-alpha", @@ -43,11 +44,11 @@ "@noble/secp256k1": "^2.0.0", "@scure/bip32": "^1.3.1", "@scure/bip39": "^1.2.1", - "@scure/btc-signer": "^0.5.1", "@tailwindcss/forms": "^0.5.4", "@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.9", diff --git a/src/app/components/Badge/index.tsx b/src/app/components/Badge/index.tsx index be8125b1cd..2fe880828a 100644 --- a/src/app/components/Badge/index.tsx +++ b/src/app/components/Badge/index.tsx @@ -1,22 +1,22 @@ import { useTranslation } from "react-i18next"; +import { classNames } from "~/app/utils"; type Props = { label: "budget" | "auth" | "imported"; - color: string; - textColor: string; - small?: boolean; + className: string; }; -export default function Badge({ label, color, textColor, small }: Props) { +export default function Badge({ label, className }: Props) { const { t: tComponents } = useTranslation("components", { keyPrefix: "badge", }); return ( {tComponents(`label.${label}`)} diff --git a/src/app/components/BadgesList/index.tsx b/src/app/components/BadgesList/index.tsx index f91d696bb2..524121449d 100644 --- a/src/app/components/BadgesList/index.tsx +++ b/src/app/components/BadgesList/index.tsx @@ -10,29 +10,20 @@ export default function BadgesList({ allowance }: Props) { if (allowance.remainingBudget > 0) { badges.push({ label: "budget", - color: "green-bitcoin", - textColor: "white", + className: "bg-blue-500 text-white", }); } if (allowance.lnurlAuth) { badges.push({ label: "auth", - color: "green-bitcoin", - textColor: "white", + className: "bg-green-bitcoin text-white", }); } return ( <> {badges?.map((b) => { - return ( - - ); + return ; })} ); diff --git a/src/app/components/LNURLAuth/index.tsx b/src/app/components/LNURLAuth/index.tsx new file mode 100644 index 0000000000..cb11ef5eca --- /dev/null +++ b/src/app/components/LNURLAuth/index.tsx @@ -0,0 +1,140 @@ +import Button from "@components/Button"; +import ConfirmOrCancel from "@components/ConfirmOrCancel"; +import Container from "@components/Container"; +import ContentMessage from "@components/ContentMessage"; +import PublisherCard from "@components/PublisherCard"; +import ResultCard from "@components/ResultCard"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +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 type { LNURLAuthServiceResponse } from "~/types"; + +function LNURLAuthComponent() { + const { t } = useTranslation("translation", { keyPrefix: "lnurlauth" }); + const { t: tCommon } = useTranslation("common"); + + const navigate = useNavigate(); + const navState = useNavigationState(); + + const details = navState.args?.lnurlDetails as LNURLAuthServiceResponse; + const origin = navState.origin; + + const [successMessage, setSuccessMessage] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + const [loading, setLoading] = useState(false); + + async function confirm() { + try { + setLoading(true); + const response = await api.lnurlAuth({ + origin, + lnurlDetails: details, + }); + + if (navState.isPrompt && origin?.host) { + const allowance = await api.getAllowance(origin.host); + + if (allowance.lnurlAuth === false) { + await msg.request("updateAllowance", { + id: allowance.id, + lnurlAuth: true, + }); + } + } + + if (response.success) { + setSuccessMessage( + t("success", { name: origin ? origin.name : details.domain }) + ); + // ATTENTION: if this LNURL is called through `webln.lnurl` then we immediately return and return the response. This closes the window which means the user will NOT see the above successAction. + // We assume this is OK when it is called through webln. + if (navState.isPrompt) { + msg.reply(response); + } + } else { + setErrorMessage(t("errors.status")); + } + } catch (e) { + console.error(e); + if (e instanceof Error) { + setErrorMessage(`Error: ${e.message}`); + } + } finally { + setLoading(false); + } + } + + function reject(e: React.MouseEvent) { + e.preventDefault(); + if (navState.isPrompt) { + msg.error(USER_REJECTED_ERROR); + } else { + navigate(-1); + } + } + + function close(e: React.MouseEvent) { + // will never be reached via prompt + e.preventDefault(); + navigate(-1); + } + + return ( +
+ + {!successMessage ? ( + <> + +
+ {origin ? ( + + ) : ( + + )} + + + + {errorMessage && ( +

{errorMessage}

+ )} +
+ +
+ + ) : ( + + +
+
+
+ )} +
+ ); +} + +export default LNURLAuthComponent; diff --git a/src/app/components/PaymentSummary/index.tsx b/src/app/components/PaymentSummary/index.tsx index 641816c6ea..15ae87942b 100644 --- a/src/app/components/PaymentSummary/index.tsx +++ b/src/app/components/PaymentSummary/index.tsx @@ -41,7 +41,7 @@ const PaymentSummary: FC = ({
{tCommon("description")}
-
+
{description}
diff --git a/src/app/components/QRCode/index.tsx b/src/app/components/QRCode/index.tsx index faf5719506..ff0447617e 100644 --- a/src/app/components/QRCode/index.tsx +++ b/src/app/components/QRCode/index.tsx @@ -5,9 +5,19 @@ export type Props = { value: string; size?: number; className?: string; + + // set the level to Q if there are overlays + // Q will improve error correction (so we can add overlays covering up to 25% of the QR) + // at the price of decreased information density (meaning the QR codes "pixels" have to be + // smaller to encode the same information). + // While that isn't that much of a problem for lightning addresses (because they are usually quite short), + // for invoices that contain larger amount of data those QR codes can get "harder" to read. + // (meaning you have to aim your phone very precisely and have to wait longer for the reader + // to recognize the QR code) + level?: "Q" | undefined; }; -export default function QRCode({ value, size, className }: Props) { +export default function QRCode({ value, size, level, className }: Props) { const theme = useTheme(); const fgColor = theme === "dark" ? "#FFFFFF" : "#000000"; const bgColor = theme === "dark" ? "#000000" : "#FFFFFF"; @@ -16,10 +26,10 @@ export default function QRCode({ value, size, className }: Props) { ); } diff --git a/src/app/components/TransactionsTable/index.tsx b/src/app/components/TransactionsTable/index.tsx index ab155106d3..af143e6a92 100644 --- a/src/app/components/TransactionsTable/index.tsx +++ b/src/app/components/TransactionsTable/index.tsx @@ -6,8 +6,6 @@ import Button from "~/app/components/Button"; import { useSettings } from "~/app/context/SettingsContext"; import { Transaction } from "~/types"; -import Badge from "../Badge"; - export type Props = { transactions: Transaction[] | null | undefined; loading?: boolean; @@ -60,18 +58,6 @@ export default function TransactionsTable({ {tx.date}

- {tx.badges && ( -
- {tx.badges.map((badge) => ( - - ))} -
- )}

diff --git a/src/app/components/onboard/index.tsx b/src/app/components/onboard/index.tsx index 152b26f410..d5dfb14dc3 100644 --- a/src/app/components/onboard/index.tsx +++ b/src/app/components/onboard/index.tsx @@ -1,5 +1,6 @@ import { ClockIcon, + InfoCircleIcon, TwoKeysIcon, } from "@bitcoin-design/bitcoin-icons-react/outline"; import { useTranslation } from "react-i18next"; @@ -9,7 +10,6 @@ import PublisherCard from "~/app/components/PublisherCard"; import ScreenHeader from "~/app/components/ScreenHeader"; import { useAccount } from "~/app/context/AccountContext"; import { useNavigationState } from "~/app/hooks/useNavigationState"; -import InfoCircleIcon from "~/app/icons/InfoCircleIcon"; import { NO_KEYS_ERROR, USER_REJECTED_ERROR } from "~/common/constants"; import api from "~/common/lib/api"; import msg from "~/common/lib/msg"; diff --git a/src/app/icons/InfoCircleIcon.tsx b/src/app/icons/InfoCircleIcon.tsx deleted file mode 100644 index 2c99d4b941..0000000000 --- a/src/app/icons/InfoCircleIcon.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { SVGProps } from "react"; - -export default function InfoCircleIcon(props: SVGProps) { - return ( - - - - - - ); -} diff --git a/src/app/router/Prompt/Prompt.tsx b/src/app/router/Prompt/Prompt.tsx index 36ffae360c..a063ca49b5 100644 --- a/src/app/router/Prompt/Prompt.tsx +++ b/src/app/router/Prompt/Prompt.tsx @@ -22,6 +22,8 @@ import Toaster from "~/app/components/Toast/Toaster"; import Providers from "~/app/context/Providers"; import RequireAuth from "~/app/router/RequireAuth"; import BitcoinConfirmGetAddress from "~/app/screens/Bitcoin/ConfirmGetAddress"; +import ConfirmSignPsbt from "~/app/screens/Bitcoin/ConfirmSignPsbt"; +import ConfirmPaymentAsync from "~/app/screens/ConfirmPaymentAsync"; import AlbyEnable from "~/app/screens/Enable/AlbyEnable"; import LiquidEnable from "~/app/screens/Enable/LiquidEnable"; import NostrEnable from "~/app/screens/Enable/NostrEnable"; @@ -109,6 +111,10 @@ function Prompt() { path="public/webbtc/confirmGetAddress" element={} /> + } + /> } @@ -137,6 +143,10 @@ function Prompt() { } /> } /> } /> + } + /> } /> } /> } /> diff --git a/src/app/screens/Accounts/Detail/index.tsx b/src/app/screens/Accounts/Detail/index.tsx index 78ded234f9..54ad2b3a40 100644 --- a/src/app/screens/Accounts/Detail/index.tsx +++ b/src/app/screens/Accounts/Detail/index.tsx @@ -201,6 +201,7 @@ function AccountDetail() { onChange={(event) => { setAccountName(event.target.value); }} + required />

@@ -403,8 +404,7 @@ function AccountDetail() {
)} diff --git a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx new file mode 100644 index 0000000000..8afcf54d4a --- /dev/null +++ b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx @@ -0,0 +1,144 @@ +import { act, render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +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 { getFormattedSats } from "~/common/utils/currencyConvert"; +import Bitcoin from "~/extension/background-script/bitcoin"; +import Mnemonic from "~/extension/background-script/mnemonic"; +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: btcFixture.regtestTaprootPsbt, + }, + })), + }; +}); + +const passwordMock = jest.fn; + +const mockState = { + password: passwordMock, + currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", + getAccount: () => ({ + mnemonic: btcFixture.mnemonic, + bitcoinNetwork: "regtest", + }), + getMnemonic: () => new Mnemonic(btcFixture.mnemonic), + getBitcoin: () => new Bitcoin(new Mnemonic(btcFixture.mnemonic), "regtest"), + getConnector: jest.fn(), +}; + +state.getState = jest.fn().mockReturnValue(mockState); + +jest.mock("~/app/context/SettingsContext", () => ({ + useSettings: () => ({ + getFormattedSats: (amount: number) => + getFormattedSats({ + amount, + locale: "en", + }), + }), +})); + +// mock getPsbtPreview request +msg.request = jest + .fn() + .mockReturnValue( + new Bitcoin(new Mnemonic(btcFixture.mnemonic), "regtest").getPsbtPreview( + btcFixture.regtestTaprootPsbt + ) + ); + +describe("ConfirmSignPsbt", () => { + test("render", async () => { + await act(async () => { + render( + + + + ); + }); + + const user = userEvent.setup(); + + await act(async () => { + await user.click(screen.getByText("View details")); + }); + + expect( + await screen.findByText( + "This website asks you to sign a bitcoin transaction" + ) + ).toBeInTheDocument(); + + // Check inputs + const inputsContainer = (await screen.getByText("Inputs") + .parentElement) as HTMLElement; + expect(inputsContainer).toBeInTheDocument(); + const inputsRef = within(inputsContainer); + expect( + await inputsRef.findByText( + "bcrt1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqjeprhg" + ) + ).toBeInTheDocument(); + + // Check outputs + const outputsContainer = screen.getByText("Outputs") + .parentElement as HTMLElement; + expect(outputsContainer).toBeInTheDocument(); + + const outputsRef = within(outputsContainer); + expect( + await outputsRef.findByText( + "bcrt1p6uav7en8k7zsumsqugdmg5j6930zmzy4dg7jcddshsr0fvxlqx7qnc7l22" + ) + ).toBeInTheDocument(); + + expect( + await outputsRef.findByText( + "bcrt1p90h6z3p36n9hrzy7580h5l429uwchyg8uc9sz4jwzhdtuhqdl5eqkcyx0f" + ) + ).toBeInTheDocument(); + + // Check fee + const feeContainer = screen.getByText("Fee").parentElement as HTMLElement; + expect(feeContainer).toBeInTheDocument(); + + const feeRef = within(feeContainer); + expect(await feeRef.findByText("155 sats")).toBeInTheDocument(); + + await act(async () => { + await user.click(screen.getByText("View raw transaction (Hex)")); + }); + expect( + await screen.findByText(btcFixture.regtestTaprootPsbt) + ).toBeInTheDocument(); + }); +}); diff --git a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx new file mode 100644 index 0000000000..03355a37f8 --- /dev/null +++ b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx @@ -0,0 +1,194 @@ +import ConfirmOrCancel from "@components/ConfirmOrCancel"; +import Container from "@components/Container"; +import PublisherCard from "@components/PublisherCard"; +import SuccessMessage from "@components/SuccessMessage"; +import { TFunction } from "i18next"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import Hyperlink from "~/app/components/Hyperlink"; +import Loading from "~/app/components/Loading"; +import ScreenHeader from "~/app/components/ScreenHeader"; +import toast from "~/app/components/Toast"; +import { useSettings } from "~/app/context/SettingsContext"; +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 type { Address, OriginData, PsbtPreview } from "~/types"; + +function ConfirmSignPsbt() { + const navState = useNavigationState(); + const { t: tCommon } = useTranslation("common"); + const { t } = useTranslation("translation", { + keyPrefix: "bitcoin.confirm_sign_psbt", + }); + const navigate = useNavigate(); + const { getFormattedSats } = useSettings(); + + const psbt = navState.args?.psbt as string; + const origin = navState.origin as OriginData; + const [loading, setLoading] = useState(true); + const [successMessage, setSuccessMessage] = useState(""); + const [preview, setPreview] = useState(undefined); + const [showAddresses, setShowAddresses] = useState(false); + const [showHex, setShowHex] = useState(false); + + useEffect(() => { + (async () => { + try { + const preview = await api.bitcoin.getPsbtPreview(psbt); + setPreview(preview); + setLoading(false); + } catch (e) { + console.error(e); + const error = e as { message: string }; + const errorMessage = error.message || "Unknown error"; + toast.error(`${tCommon("error")}: ${errorMessage}`); + } + })(); + }, [origin, psbt, tCommon]); + + async function confirm() { + try { + setLoading(true); + const response = await api.bitcoin.signPsbt(psbt); + msg.reply(response); + setSuccessMessage(tCommon("success")); + } catch (e) { + console.error(e); + const error = e as { message: string }; + const errorMessage = error.message || "Unknown error"; + toast.error(`${tCommon("error")}: ${errorMessage}`); + } 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); + } + } + + function toggleShowAddresses() { + setShowAddresses((current) => !current); + } + function toggleShowHex() { + setShowHex((current) => !current); + } + + if (!preview) { + return ( +
+ +
+ ); + } + + return ( +
+ + {!successMessage ? ( + +
+ +
+

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

+
+
+ + {showAddresses ? t("hide_details") : t("view_details")} + +
+ + {showAddresses && ( + <> +
+

{t("inputs")}

+
+ {preview.inputs.map((input) => ( + + ))} +
+

{t("outputs")}

+
+ {preview.outputs.map((output) => ( + + ))} +
+

{t("fee")}

+

+ {getFormattedSats(preview.fee)} +

+
+
+ + {showHex + ? t("hide_raw_transaction") + : t("view_raw_transaction")} + +
+ + )} + + {showHex && ( +
+ {psbt} +
+ )} +
+ +
+ ) : ( + + + + + )} +
+ ); +} + +function AddressPreview({ + address, + amount, + t, +}: Address & { + t: TFunction<"translation", "bitcoin.confirm_sign_psbt", "translation">; +}) { + const { getFormattedSats } = useSettings(); + return ( +
+

{address}

+

+ {getFormattedSats(amount)} +

+
+ ); +} + +export default ConfirmSignPsbt; diff --git a/src/app/screens/ConfirmPayment/index.tsx b/src/app/screens/ConfirmPayment/index.tsx index 4138f41c76..90321990ad 100644 --- a/src/app/screens/ConfirmPayment/index.tsx +++ b/src/app/screens/ConfirmPayment/index.tsx @@ -15,6 +15,7 @@ import { useAccount } from "~/app/context/AccountContext"; import { useSettings } from "~/app/context/SettingsContext"; 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"; function ConfirmPayment() { @@ -76,16 +77,9 @@ function ConfirmPayment() { try { setLoading(true); - // TODO: move to api - const response = await msg.request( - "sendPayment", - { paymentRequest: paymentRequest }, - { - origin: navState.origin, - } - ); - if (response.error) { - throw new Error(response.error as string); + const response = await api.sendPayment(paymentRequest, navState.origin); + if ("error" in response) { + throw new Error(response.error); } auth.fetchAccountInfo(); // Update balance. @@ -156,7 +150,7 @@ function ConfirmPayment() {
diff --git a/src/app/screens/ConfirmPaymentAsync/index.test.tsx b/src/app/screens/ConfirmPaymentAsync/index.test.tsx new file mode 100644 index 0000000000..dd525f0fed --- /dev/null +++ b/src/app/screens/ConfirmPaymentAsync/index.test.tsx @@ -0,0 +1,80 @@ +import { act, render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { settingsFixture as mockSettings } from "~/../tests/fixtures/settings"; +import type { OriginData } from "~/types"; + +import ConfirmPaymentAsync 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, +}; + +const paymentRequest = + "lnbc250n1p3qzycupp58uc2wa29470f98wrxmy4xwuqt8cywjygf5t2cp0s376y7nwdyq3sdqhf35kw6r5de5kueeqg3jk6mccqzpgxqyz5vqsp5wfdmwtv5rmru00ajsnn3f8lzpxa4snug2tmqvc8zj8semr4kjjts9qyyssq83h74pte8nrkqs8sr2hscv5zcdmhwunwnd6xr3mskeayh96pu7ksswa6p7trknlpp6t3js4k6uytxutv5ecgcwaxz7fj4zfy5khjcjcpf66muy"; + +let parameters = {}; + +jest.mock("~/app/hooks/useNavigationState", () => { + return { + useNavigationState: jest.fn(() => parameters), + }; +}); + +let mockGetFiatValue = jest.fn(); +let mockSettingsTmp = { ...mockSettings }; + +jest.mock("~/app/context/SettingsContext", () => ({ + useSettings: () => ({ + settings: mockSettingsTmp, + isLoading: false, + updateSetting: jest.fn(), + getFormattedFiat: mockGetFiatValue, + getFormattedNumber: jest.fn(), + getFormattedSats: jest.fn(() => "25 sats"), + }), +})); + +describe("ConfirmPaymentAsync", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test("prompt: renders with fiat", async () => { + parameters = { + origin: mockOrigin, + args: { + paymentRequest, + }, + }; + + mockSettingsTmp = { ...mockSettings }; + mockGetFiatValue = jest.fn(() => Promise.resolve("$0.01")); + + await act(async () => { + render( + + + + ); + }); + + expect(await screen.findByText("Amount")).toBeInTheDocument(); + expect(await screen.findByText("Description")).toBeInTheDocument(); + expect(await screen.findByText("(~$0.01)")).toBeInTheDocument(); + }); +}); diff --git a/src/app/screens/ConfirmPaymentAsync/index.tsx b/src/app/screens/ConfirmPaymentAsync/index.tsx new file mode 100644 index 0000000000..26a12cd2f7 --- /dev/null +++ b/src/app/screens/ConfirmPaymentAsync/index.tsx @@ -0,0 +1,140 @@ +import ConfirmOrCancel from "@components/ConfirmOrCancel"; +import Container from "@components/Container"; +import PaymentSummary from "@components/PaymentSummary"; +import PublisherCard from "@components/PublisherCard"; +import lightningPayReq from "bolt11"; +import { useEffect, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import Alert from "~/app/components/Alert"; +import Hyperlink from "~/app/components/Hyperlink"; +import ScreenHeader from "~/app/components/ScreenHeader"; +import toast from "~/app/components/Toast"; +import { useSettings } from "~/app/context/SettingsContext"; +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"; + +function ConfirmPaymentAsync() { + const { + isLoading: isLoadingSettings, + settings, + getFormattedFiat, + } = useSettings(); + + const showFiat = !isLoadingSettings && settings.showFiat; + + const { t } = useTranslation("translation", { + keyPrefix: "confirm_payment_async", + }); + + const navState = useNavigationState(); + const paymentRequest = navState.args?.paymentRequest as string; + const invoice = lightningPayReq.decode(paymentRequest); + + const navigate = useNavigate(); + + const [fiatAmount, setFiatAmount] = useState(""); + + useEffect(() => { + (async () => { + if (showFiat && invoice.satoshis) { + const res = await getFormattedFiat(invoice.satoshis); + setFiatAmount(res); + } + })(); + }, [invoice.satoshis, showFiat, getFormattedFiat]); + + const [loading, setLoading] = useState(false); + + async function confirm() { + try { + setLoading(true); + const response = await api.sendPaymentAsync( + paymentRequest, + navState.origin + ); + + if ("error" in response) { + throw new Error(response.error); + } + + msg.reply(response); + } catch (e) { + console.error(e); + if (e instanceof Error) toast.error(`Error: ${e.message}`); + } finally { + setLoading(false); + } + } + + function reject(e: React.MouseEvent) { + e.preventDefault(); + if (navState.isPrompt) { + msg.error(USER_REJECTED_ERROR); + } else { + navigate(-1); + } + } + + function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + confirm(); + } + + return ( +
+ +
+ +
+ {navState.origin && ( + + )} +
+
+ +
+
+
+ + , + ]} + /> + +
+
+
+ +
+
+
+
+ ); +} + +export default ConfirmPaymentAsync; diff --git a/src/app/screens/Enable/WebbtcEnable.tsx b/src/app/screens/Enable/WebbtcEnable.tsx index 99fc52d15e..fe325dcf3c 100644 --- a/src/app/screens/Enable/WebbtcEnable.tsx +++ b/src/app/screens/Enable/WebbtcEnable.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import LiquidEnableComponent from "~/app/components/Enable/LiquidEnable"; +import WebbtcEnableComponent from "~/app/components/Enable/WebbtcEnable"; import Onboard from "~/app/components/onboard"; import { useAccount } from "~/app/context/AccountContext"; import api from "~/common/lib/api"; @@ -34,7 +34,7 @@ export default function WebbtcEnable(props: Props) { return ( <> {hasMnemonic ? ( - + ) : ( )} diff --git a/src/app/screens/LNURLAuth/index.tsx b/src/app/screens/LNURLAuth/index.tsx index 6d9e71b406..c09def68a3 100644 --- a/src/app/screens/LNURLAuth/index.tsx +++ b/src/app/screens/LNURLAuth/index.tsx @@ -1,140 +1,38 @@ -import Button from "@components/Button"; -import ConfirmOrCancel from "@components/ConfirmOrCancel"; -import Container from "@components/Container"; -import ContentMessage from "@components/ContentMessage"; -import PublisherCard from "@components/PublisherCard"; -import ResultCard from "@components/ResultCard"; -import React, { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; -import ScreenHeader from "~/app/components/ScreenHeader"; -import { useNavigationState } from "~/app/hooks/useNavigationState"; -import { USER_REJECTED_ERROR } from "~/common/constants"; +import { useEffect, useState } from "react"; +import LNURLAuthComponent from "~/app/components/LNURLAuth"; +import Onboard from "~/app/components/onboard"; +import { useAccount } from "~/app/context/AccountContext"; +import { isAlbyOAuthAccount } from "~/app/utils"; import api from "~/common/lib/api"; -import msg from "~/common/lib/msg"; -import type { LNURLAuthServiceResponse } from "~/types"; -function LNURLAuth() { - const { t } = useTranslation("translation", { keyPrefix: "lnurlauth" }); - const { t: tCommon } = useTranslation("common"); - - const navigate = useNavigate(); - const navState = useNavigationState(); - - const details = navState.args?.lnurlDetails as LNURLAuthServiceResponse; - const origin = navState.origin; - - const [successMessage, setSuccessMessage] = useState(""); - const [errorMessage, setErrorMessage] = useState(""); - const [loading, setLoading] = useState(false); - - async function confirm() { - try { - setLoading(true); - const response = await api.lnurlAuth({ - origin, - lnurlDetails: details, - }); - - if (navState.isPrompt && origin?.host) { - const allowance = await api.getAllowance(origin.host); - - if (allowance.lnurlAuth === false) { - await msg.request("updateAllowance", { - id: allowance.id, - lnurlAuth: true, - }); +export default function LNURLAuth() { + const { account } = useAccount(); + const [hasMnemonic, setHasMnemonic] = useState(false); + const [albyOAuthAccount, setAlbyOAuthAccount] = useState(false); + + useEffect(() => { + async function fetchAccountInfo() { + try { + const fetchedAccount = await api.getAccount(); + const isOAuthAccount = isAlbyOAuthAccount(fetchedAccount.connectorType); + setAlbyOAuthAccount(isOAuthAccount); + + if (fetchedAccount.hasMnemonic) { + setHasMnemonic(true); + } else { + setHasMnemonic(false); } + } catch (e) { + console.error(e); } - - if (response.success) { - setSuccessMessage( - t("success", { name: origin ? origin.name : details.domain }) - ); - // ATTENTION: if this LNURL is called through `webln.lnurl` then we immediately return and return the response. This closes the window which means the user will NOT see the above successAction. - // We assume this is OK when it is called through webln. - if (navState.isPrompt) { - msg.reply(response); - } - } else { - setErrorMessage(t("errors.status")); - } - } catch (e) { - console.error(e); - if (e instanceof Error) { - setErrorMessage(`Error: ${e.message}`); - } - } finally { - setLoading(false); - } - } - - function reject(e: React.MouseEvent) { - e.preventDefault(); - if (navState.isPrompt) { - msg.error(USER_REJECTED_ERROR); - } else { - navigate(-1); } - } - function close(e: React.MouseEvent) { - // will never be reached via prompt - e.preventDefault(); - navigate(-1); - } + fetchAccountInfo(); + }, [account]); return ( -
- - {!successMessage ? ( - <> - -
- {origin ? ( - - ) : ( - - )} - - - - {errorMessage && ( -

{errorMessage}

- )} -
- -
- - ) : ( - - -
-
-
- )} -
+ <> + {albyOAuthAccount && !hasMnemonic ? : } + ); } - -export default LNURLAuth; diff --git a/src/app/screens/Liquid/ConfirmSignPset.tsx b/src/app/screens/Liquid/ConfirmSignPset.tsx index 119458a732..862ed58f94 100644 --- a/src/app/screens/Liquid/ConfirmSignPset.tsx +++ b/src/app/screens/Liquid/ConfirmSignPset.tsx @@ -117,14 +117,16 @@ function ConfirmSignPset() {
-
- - {showDetails ? t("hide_details") : t("view_details")} - -
+
+
+ + {showDetails ? t("hide_details") : t("view_details")} + +
- {showDetails && ( - <> + {showDetails && ( + <> +

{t("inputs")}

@@ -154,15 +156,17 @@ function ConfirmSignPset() { ))}
+
+
{showRawTransaction ? t("hide_raw_transaction") : t("view_raw_transaction")} - - )} -
+ + + )} {showRawTransaction && (
diff --git a/src/app/screens/Receive/index.tsx b/src/app/screens/Receive/index.tsx index a2966598d4..9558692df9 100644 --- a/src/app/screens/Receive/index.tsx +++ b/src/app/screens/Receive/index.tsx @@ -76,14 +76,10 @@ function Receive() { ) : ( <> - )} diff --git a/src/app/screens/ReceiveInvoice/index.tsx b/src/app/screens/ReceiveInvoice/index.tsx index e0dfe387eb..02242e8e2c 100644 --- a/src/app/screens/ReceiveInvoice/index.tsx +++ b/src/app/screens/ReceiveInvoice/index.tsx @@ -1,6 +1,5 @@ import { CaretLeftIcon, - CheckIcon, CopyIcon, } from "@bitcoin-design/bitcoin-icons-react/outline"; import Button from "@components/Button"; @@ -15,6 +14,7 @@ import Confetti from "react-confetti"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import QRCode from "~/app/components/QRCode"; +import ResultCard from "~/app/components/ResultCard"; import toast from "~/app/components/Toast"; import { useAccount } from "~/app/context/AccountContext"; import { useSettings } from "~/app/context/SettingsContext"; @@ -46,9 +46,6 @@ function ReceiveInvoice() { paymentRequest: string; rHash: string; } | null>(); - const [copyInvoiceLabel, setCopyInvoiceLabel] = useState( - tCommon("actions.copy_invoice") as string - ); const [paid, setPaid] = useState(false); const [pollingForPayment, setPollingForPayment] = useState(false); @@ -142,44 +139,22 @@ function ReceiveInvoice() { if (!invoice) return null; return ( <> -
- - {paid && ( -
-
-
- -
-

{t("success")}

-
-
- )} -
- {paid && ( -
-
- )} {!paid && ( <> -
+
+
+ +
+
+
-
{pollingForPayment && (
@@ -210,15 +184,30 @@ function ReceiveInvoice() { )} {paid && ( - { - confetti && confetti.reset(); - }} - style={{ pointerEvents: "none" }} - /> + <> + +
+
+ { + confetti && confetti.reset(); + }} + style={{ pointerEvents: "none" }} + /> + )} ); @@ -239,11 +228,13 @@ function ReceiveInvoice() { {t("title")} {invoice ? ( - {renderInvoice()} + + {renderInvoice()} + ) : ( -
-
-
+
+ +
diff --git a/src/common/lib/api.ts b/src/common/lib/api.ts index 3211403022..36101807fb 100644 --- a/src/common/lib/api.ts +++ b/src/common/lib/api.ts @@ -9,6 +9,8 @@ import { ConnectPeerResponse, MakeInvoiceArgs, MakeInvoiceResponse, + SendPaymentAsyncResponse, + SendPaymentResponse, } from "~/extension/background-script/connectors/connector.interface"; import type { Account, @@ -27,6 +29,8 @@ import type { MessageLnurlAuth, MessageSettingsSet, NodeInfo, + OriginData, + PsbtPreview, PsetPreview, SettingsStorage, ValidateAccountResponse, @@ -173,6 +177,29 @@ export const lnurlAuth = ( ): Promise => msg.request("lnurlAuth", options); +export const sendPayment = ( + paymentRequest: string, + origin: OriginData | undefined +) => + msg.request( + "sendPayment", + { paymentRequest }, + { + origin, + } + ); +export const sendPaymentAsync = ( + paymentRequest: string, + origin: OriginData | undefined +) => + msg.request( + "sendPaymentAsync", + { paymentRequest }, + { + origin, + } + ); + export const getCurrencyRate = async () => msg.request<{ rate: number }>("getCurrencyRate"); @@ -236,6 +263,15 @@ const signPset = (pset: string): Promise => pset, }); +const getPsbtPreview = (psbt: string): Promise => + msg.request("webbtc/getPsbtPreview", { + psbt, + }); +const signPsbt = (psbt: string): Promise => + msg.request("webbtc/signPsbt", { + psbt, + }); + export default { getAccount, getAccountInfo, @@ -262,6 +298,8 @@ export default { getInvoices, lnurlAuth, getCurrencyRate, + sendPayment, + sendPaymentAsync, nostr: { getPrivateKey: getNostrPrivateKey, getPublicKey: getNostrPublicKey, @@ -279,4 +317,8 @@ export default { fetchAssetRegistry: fetchLiquidAssetRegistry, signPset: signPset, }, + bitcoin: { + getPsbtPreview, + signPsbt, + }, }; 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 bf0ddd4f8b..6022ed3c94 100644 --- a/src/extension/background-script/actions/accounts/__tests__/get.test.ts +++ b/src/extension/background-script/actions/accounts/__tests__/get.test.ts @@ -27,7 +27,7 @@ const mockState = { id: "8b7f1dc6-ab87-4c6c-bca5-19fa8632731e", name: "Alby", nostrPrivateKey: "nostr-123-456", - mnemonic: btcFixture.mnemnoic, + mnemonic: btcFixture.mnemonic, bitcoinNetwork: "regtest", useMnemonicForLnurlAuth: true, }, diff --git a/src/extension/background-script/actions/alby/enable.ts b/src/extension/background-script/actions/alby/enable.ts index ceb6665d07..b47c7b26e1 100644 --- a/src/extension/background-script/actions/alby/enable.ts +++ b/src/extension/background-script/actions/alby/enable.ts @@ -16,7 +16,9 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => { .equalsIgnoreCase(host) .first(); - if (isUnlocked && allowance && allowance.enabled) { + const enabledFor = new Set(allowance?.enabledFor); + + if (isUnlocked && allowance && allowance.enabled && enabledFor.has("alby")) { return { data: { enabled: true }, }; @@ -38,8 +40,12 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => { if (!allowance.id) { return { data: { error: "id is missing" } }; } + + enabledFor.add("alby"); + await db.allowances.update(allowance.id, { enabled: true, + enabledFor, name: message.origin.name, imageURL: message.origin.icon, }); @@ -49,6 +55,7 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => { name: message.origin.name, imageURL: message.origin.icon, enabled: true, + enabledFor: ["alby"], lastPaymentAt: 0, totalBudget: 0, remainingBudget: 0, diff --git a/src/extension/background-script/actions/alby/index.ts b/src/extension/background-script/actions/alby/index.ts index e9478e1569..ecef3392b1 100644 --- a/src/extension/background-script/actions/alby/index.ts +++ b/src/extension/background-script/actions/alby/index.ts @@ -1,2 +1,3 @@ import enable from "./enable"; -export { enable }; +import isEnabled from "./isEnabled"; +export { enable, isEnabled }; diff --git a/src/extension/background-script/actions/alby/isEnabled.ts b/src/extension/background-script/actions/alby/isEnabled.ts new file mode 100644 index 0000000000..ed3aae85d7 --- /dev/null +++ b/src/extension/background-script/actions/alby/isEnabled.ts @@ -0,0 +1,30 @@ +import { getHostFromSender } from "~/common/utils/helpers"; +import db from "~/extension/background-script/db"; +import type { MessageAllowanceEnable, Sender } from "~/types"; + +import state from "../../state"; + +const isEnabled = async (message: MessageAllowanceEnable, sender: Sender) => { + const host = getHostFromSender(sender); + if (!host) return; + + const isUnlocked = await state.getState().isUnlocked(); + const allowance = await db.allowances + .where("host") + .equalsIgnoreCase(host) + .first(); + + const enabledFor = new Set(allowance?.enabledFor); + + if (isUnlocked && allowance && allowance.enabled && enabledFor.has("alby")) { + return { + data: { isEnabled: true }, + }; + } else { + return { + data: { isEnabled: false }, + }; + } +}; + +export default isEnabled; diff --git a/src/extension/background-script/actions/allowances/__tests__/delete.test.ts b/src/extension/background-script/actions/allowances/__tests__/delete.test.ts index 11c0a924a7..e7804e1884 100644 --- a/src/extension/background-script/actions/allowances/__tests__/delete.test.ts +++ b/src/extension/background-script/actions/allowances/__tests__/delete.test.ts @@ -78,6 +78,7 @@ describe("delete allowance", () => { id: 1, imageURL: "https://pro.kollider.xyz/favicon.ico", lastPaymentAt: 0, + enabledFor: ["webln"], lnurlAuth: true, name: "pro kollider", remainingBudget: 500, diff --git a/src/extension/background-script/actions/liquid/enable.ts b/src/extension/background-script/actions/liquid/enable.ts index b560416bbf..331ccc3319 100644 --- a/src/extension/background-script/actions/liquid/enable.ts +++ b/src/extension/background-script/actions/liquid/enable.ts @@ -17,7 +17,15 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => { .equalsIgnoreCase(host) .first(); - if (isUnlocked && allowance && allowance.enabled && account?.mnemonic) { + const enabledFor = new Set(allowance?.enabledFor); + + if ( + isUnlocked && + allowance && + allowance.enabled && + account?.mnemonic && + enabledFor.has("liquid") + ) { return { data: { enabled: true }, }; @@ -39,8 +47,12 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => { if (!allowance.id) { return { data: { error: "id is missing" } }; } + + enabledFor.add("liquid"); + await db.allowances.update(allowance.id, { enabled: true, + enabledFor, name: message.origin.name, imageURL: message.origin.icon, }); @@ -50,6 +62,7 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => { name: message.origin.name, imageURL: message.origin.icon, enabled: true, + enabledFor: ["liquid"], lastPaymentAt: 0, totalBudget: 0, remainingBudget: 0, diff --git a/src/extension/background-script/actions/liquid/index.ts b/src/extension/background-script/actions/liquid/index.ts index e1e48d8bd0..71a2f937a6 100644 --- a/src/extension/background-script/actions/liquid/index.ts +++ b/src/extension/background-script/actions/liquid/index.ts @@ -2,6 +2,7 @@ import enable from "./enable"; import fetchAssetRegistry from "./fetchAssetRegistry"; import getAddressOrPrompt from "./getAddressOrPrompt"; import getPsetPreview from "./getPsetPreview"; +import isEnabled from "./isEnabled"; import signPset from "./signPset"; import signPsetWithPrompt from "./signPsetWithPrompt"; @@ -10,6 +11,7 @@ export { fetchAssetRegistry, getAddressOrPrompt, getPsetPreview, + isEnabled, signPset, signPsetWithPrompt, }; diff --git a/src/extension/background-script/actions/liquid/isEnabled.ts b/src/extension/background-script/actions/liquid/isEnabled.ts new file mode 100644 index 0000000000..bfb351f5a6 --- /dev/null +++ b/src/extension/background-script/actions/liquid/isEnabled.ts @@ -0,0 +1,35 @@ +import { getHostFromSender } from "~/common/utils/helpers"; +import db from "~/extension/background-script/db"; +import type { MessageAllowanceEnable, Sender } from "~/types"; + +import state from "../../state"; + +const isEnabled = async (message: MessageAllowanceEnable, sender: Sender) => { + const host = getHostFromSender(sender); + if (!host) return; + + const isUnlocked = await state.getState().isUnlocked(); + const allowance = await db.allowances + .where("host") + .equalsIgnoreCase(host) + .first(); + + const enabledFor = new Set(allowance?.enabledFor); + + if ( + isUnlocked && + allowance && + allowance.enabled && + enabledFor.has("liquid") + ) { + return { + data: { isEnabled: true }, + }; + } else { + return { + data: { isEnabled: false }, + }; + } +}; + +export default isEnabled; diff --git a/src/extension/background-script/actions/ln/index.ts b/src/extension/background-script/actions/ln/index.ts index 5f26c3fe3e..ad3b3f7eb1 100644 --- a/src/extension/background-script/actions/ln/index.ts +++ b/src/extension/background-script/actions/ln/index.ts @@ -6,6 +6,7 @@ import keysend from "./keysend"; import makeInvoice from "./makeInvoice"; import request from "./request"; import sendPayment from "./sendPayment"; +import sendPaymentAsync from "./sendPaymentAsync"; import signMessage from "./signMessage"; export { @@ -15,7 +16,8 @@ export { invoices, keysend, makeInvoice, + request, sendPayment, + sendPaymentAsync, signMessage, - request, }; diff --git a/src/extension/background-script/actions/ln/sendPaymentAsync.ts b/src/extension/background-script/actions/ln/sendPaymentAsync.ts new file mode 100644 index 0000000000..77592c5cbb --- /dev/null +++ b/src/extension/background-script/actions/ln/sendPaymentAsync.ts @@ -0,0 +1,34 @@ +import state from "~/extension/background-script/state"; +import { MessageSendPayment } from "~/types"; + +export default async function sendPayment(message: MessageSendPayment) { + const accountId = await state.getState().currentAccountId; + if (!accountId) { + return { + error: "Select an account.", + }; + } + + const { paymentRequest } = message.args; + if (typeof paymentRequest !== "string") { + return { + error: "Payment request missing.", + }; + } + + const connector = await state.getState().getConnector(); + + // NOTE: currently there is no way to know if the initial payment + // succeeds or not. The payment might not work at all or the http request might time out + // before the HODL invoice is paid or times out itself. + // any errors thrown by sendPayment will not be caught. + // NOTE: it is the receiver's responsibility to check if they have received the payment or not + // and update the UI or re-prompt the user if they haven't received a payment. + connector.sendPayment({ + paymentRequest, + }); + + return { + data: {}, + }; +} diff --git a/src/extension/background-script/actions/lnurl/__tests__/auth.test.ts b/src/extension/background-script/actions/lnurl/__tests__/auth.test.ts index 8832b92524..c8db39b2a8 100644 --- a/src/extension/background-script/actions/lnurl/__tests__/auth.test.ts +++ b/src/extension/background-script/actions/lnurl/__tests__/auth.test.ts @@ -39,11 +39,11 @@ describe("auth with mnemonic", () => { password: passwordMock, currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", getAccount: () => ({ - mnemonic: btcFixture.mnemnoic, + mnemonic: btcFixture.mnemonic, bitcoinNetwork: "regtest", useMnemonicForLnurlAuth: true, }), - getMnemonic: () => new Mnemonic(btcFixture.mnemnoic), + getMnemonic: () => new Mnemonic(btcFixture.mnemonic), getConnector: jest.fn(), }; diff --git a/src/extension/background-script/actions/lnurl/authOrPrompt.ts b/src/extension/background-script/actions/lnurl/authOrPrompt.ts index 4f2fb2d6a0..1a897f9165 100644 --- a/src/extension/background-script/actions/lnurl/authOrPrompt.ts +++ b/src/extension/background-script/actions/lnurl/authOrPrompt.ts @@ -33,12 +33,9 @@ async function authOrPrompt( // we have the check the unlock status manually. The account can still be locked // If it is locked we must show a prompt to unlock const isUnlocked = await state.getState().isUnlocked(); + const account = await state.getState().getAccount(); - // check if there is a publisher and lnurlAuth is enabled, - // otherwise we we prompt the user - if (isUnlocked && allowance && allowance.enabled && allowance.lnurlAuth) { - return await authFunction({ lnurlDetails, origin: message.origin }); - } else { + async function authPrompt() { try { const promptMessage = { ...message, @@ -49,12 +46,28 @@ async function authOrPrompt( }, }; - return await utils.openPrompt(promptMessage); + const response = await utils.openPrompt(promptMessage); + return response; } catch (e) { // user rejected return { error: e instanceof Error ? e.message : e }; } } + + // check if there is a publisher and lnurlAuth is enabled, + // otherwise we we prompt the user + + if ( + isUnlocked && + allowance && + allowance.enabled && + allowance.lnurlAuth && + (!account?.useMnemonicForLnurlAuth || account?.mnemonic) + ) { + return await authFunction({ lnurlDetails, origin: message.origin }); + } + + return await authPrompt(); } export default authOrPrompt; diff --git a/src/extension/background-script/actions/lnurl/index.ts b/src/extension/background-script/actions/lnurl/index.ts index f199e035b9..1c0588b2be 100644 --- a/src/extension/background-script/actions/lnurl/index.ts +++ b/src/extension/background-script/actions/lnurl/index.ts @@ -39,4 +39,4 @@ async function lnurl(message: MessageWebLnLnurl, sender: Sender) { } export default lnurl; -export { authOrPrompt, payWithPrompt, withdrawWithPrompt, auth }; +export { auth, authOrPrompt, payWithPrompt, withdrawWithPrompt }; diff --git a/src/extension/background-script/actions/nostr/enable.ts b/src/extension/background-script/actions/nostr/enable.ts index d8c9fa8d98..4879b154f5 100644 --- a/src/extension/background-script/actions/nostr/enable.ts +++ b/src/extension/background-script/actions/nostr/enable.ts @@ -18,11 +18,14 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => { .equalsIgnoreCase(host) .first(); + const enabledFor = new Set(allowance?.enabledFor); + if ( isUnlocked && allowance && allowance.enabled && - account?.nostrPrivateKey + account?.nostrPrivateKey && + enabledFor.has("nostr") ) { return { data: { enabled: true }, @@ -45,8 +48,12 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => { if (!allowance.id) { return { data: { error: "id is missing" } }; } + + enabledFor.add("nostr"); + await db.allowances.update(allowance.id, { enabled: true, + enabledFor, name: message.origin.name, imageURL: message.origin.icon, }); @@ -55,6 +62,7 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => { host: host, name: message.origin.name, imageURL: message.origin.icon, + enabledFor: ["nostr"], enabled: true, lastPaymentAt: 0, totalBudget: 0, diff --git a/src/extension/background-script/actions/nostr/index.ts b/src/extension/background-script/actions/nostr/index.ts index 54ae6b8c47..32fb85eee4 100644 --- a/src/extension/background-script/actions/nostr/index.ts +++ b/src/extension/background-script/actions/nostr/index.ts @@ -7,6 +7,7 @@ import generatePrivateKey from "./generatePrivateKey"; import getPrivateKey from "./getPrivateKey"; import getPublicKeyOrPrompt from "./getPublicKeyOrPrompt"; import getRelays from "./getRelays"; +import isEnabled from "./isEnabled"; import removePrivateKey from "./removePrivateKey"; import setPrivateKey from "./setPrivateKey"; import signEventOrPrompt from "./signEventOrPrompt"; @@ -21,6 +22,7 @@ export { getPublicKey, getPublicKeyOrPrompt, getRelays, + isEnabled, removePrivateKey, setPrivateKey, signEventOrPrompt, diff --git a/src/extension/background-script/actions/nostr/isEnabled.ts b/src/extension/background-script/actions/nostr/isEnabled.ts new file mode 100644 index 0000000000..0d9986a19a --- /dev/null +++ b/src/extension/background-script/actions/nostr/isEnabled.ts @@ -0,0 +1,30 @@ +import { getHostFromSender } from "~/common/utils/helpers"; +import db from "~/extension/background-script/db"; +import type { MessageAllowanceEnable, Sender } from "~/types"; + +import state from "../../state"; + +const isEnabled = async (message: MessageAllowanceEnable, sender: Sender) => { + const host = getHostFromSender(sender); + if (!host) return; + + const isUnlocked = await state.getState().isUnlocked(); + const allowance = await db.allowances + .where("host") + .equalsIgnoreCase(host) + .first(); + + const enabledFor = new Set(allowance?.enabledFor); + + if (isUnlocked && allowance && allowance.enabled && enabledFor.has("nostr")) { + return { + data: { isEnabled: true }, + }; + } else { + return { + data: { isEnabled: false }, + }; + } +}; + +export default isEnabled; diff --git a/src/extension/background-script/actions/onboard/index.ts b/src/extension/background-script/actions/onboard/index.ts deleted file mode 100644 index fc53c5af82..0000000000 --- a/src/extension/background-script/actions/onboard/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import prompt from "./prompt"; - -export { prompt }; diff --git a/src/extension/background-script/actions/onboard/prompt.ts b/src/extension/background-script/actions/onboard/prompt.ts deleted file mode 100644 index 90d41f95ef..0000000000 --- a/src/extension/background-script/actions/onboard/prompt.ts +++ /dev/null @@ -1,19 +0,0 @@ -import utils from "~/common/lib/utils"; -import { getHostFromSender } from "~/common/utils/helpers"; -import type { MessageAllowanceEnable, Sender } from "~/types"; - -const prompt = async (message: MessageAllowanceEnable, sender: Sender) => { - const host = getHostFromSender(sender); - if (!host) return; - - try { - await utils.openPrompt(message); - } catch (e) { - console.error(e); - if (e instanceof Error) { - return { error: e.message }; - } - } -}; - -export default prompt; 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 055eded865..58bfdd6f7b 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/getAddress.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/getAddress.test.ts @@ -11,11 +11,11 @@ const mockState = { password: passwordMock, currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", getAccount: () => ({ - mnemonic: btcFixture.mnemnoic, + mnemonic: btcFixture.mnemonic, bitcoinNetwork: "regtest", }), - getMnemonic: () => new Mnemonic(btcFixture.mnemnoic), - getBitcoin: () => new Bitcoin(new Mnemonic(btcFixture.mnemnoic), "regtest"), + getMnemonic: () => new Mnemonic(btcFixture.mnemonic), + getBitcoin: () => new Bitcoin(new Mnemonic(btcFixture.mnemonic), "regtest"), getConnector: jest.fn(), }; 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..7a243ffa9c --- /dev/null +++ b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts @@ -0,0 +1,158 @@ +import getPsbtPreview from "~/extension/background-script/actions/webbtc/getPsbtPreview"; +import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; +import Bitcoin from "~/extension/background-script/bitcoin"; +import Mnemonic from "~/extension/background-script/mnemonic"; +import state from "~/extension/background-script/state"; +import { btcFixture } from "~/fixtures/btc"; +import type { + BitcoinNetworkType, + MessageGetPsbtPreview, + MessageSignPsbt, + PsbtPreview, +} from "~/types"; + +const passwordMock = jest.fn; + +function mockSettings(network: BitcoinNetworkType) { + const mockState = { + password: passwordMock, + currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", + getAccount: () => ({ + mnemonic: btcFixture.mnemonic, + bitcoinNetwork: network, + }), + getMnemonic: () => new Mnemonic(btcFixture.mnemonic), + getBitcoin: () => new Bitcoin(new Mnemonic(btcFixture.mnemonic), network), + getConnector: jest.fn(), + }; + + 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 sendPsbtMessage(psbt: string) { + const message: MessageSignPsbt = { + application: "LBE", + prompt: true, + action: "signPsbt", + origin: { + internal: true, + }, + args: { + psbt, + }, + }; + + return await signPsbt(message); +} + +async function sendGetPsbtPreviewMessage(psbt: string) { + const message: MessageGetPsbtPreview = { + application: "LBE", + prompt: true, + action: "getPsbtPreview", + origin: { + internal: true, + }, + args: { + psbt, + }, + }; + + return await getPsbtPreview(message); +} + +describe("signPsbt", () => { + test("1 input, taproot, regtest", async () => { + mockSettings("regtest"); + const result = await sendPsbtMessage(btcFixture.regtestTaprootPsbt); + if (!result.data) { + throw new Error("Result should have data"); + } + + expect(result.data).not.toBe(undefined); + expect(result.data?.signed).not.toBe(undefined); + expect(result.error).toBe(undefined); + + expect(result.data?.signed).toBe(btcFixture.regtestTaprootSignedPsbt); + }); +}); + +describe("signPsbt input validation", () => { + test("invalid psbt", async () => { + mockSettings("regtest"); + const result = await sendPsbtMessage("test"); + expect(result.error).not.toBe(null); + }); +}); + +describe("decode psbt", () => { + test("get taproot transaction preview", async () => { + mockSettings("regtest"); + const previewResponse = await sendGetPsbtPreviewMessage( + btcFixture.regtestTaprootPsbt + ); + const preview = previewResponse.data as PsbtPreview; + expect(preview.inputs.length).toBe(1); + expect(preview.inputs[0].address).toBe( + "bcrt1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqjeprhg" + ); + expect(preview.inputs[0].amount).toBe(10_000_000); + expect(preview.outputs.length).toBe(2); + + expect(preview.outputs[0].address).toBe( + "bcrt1p6uav7en8k7zsumsqugdmg5j6930zmzy4dg7jcddshsr0fvxlqx7qnc7l22" + ); + expect(preview.outputs[0].amount).toBe(4_999_845); + expect(preview.outputs[1].address).toBe( + "bcrt1p90h6z3p36n9hrzy7580h5l429uwchyg8uc9sz4jwzhdtuhqdl5eqkcyx0f" + ); + expect(preview.outputs[1].amount).toBe(5_000_000); + + expect(preview.fee).toBe(155); + }); + + test("get taproot transaction preview 2", async () => { + mockSettings("testnet"); + const previewResponse = await sendGetPsbtPreviewMessage( + btcFixture.taprootPsbt2 + ); + const preview = previewResponse.data as PsbtPreview; + expect(preview.inputs.length).toBe(1); + // first address from mnemonic 1 + expect(preview.inputs[0].address).toBe( + "tb1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqlqt9zj" + ); + expect(preview.inputs[0].amount).toBe(2700); + expect(preview.outputs.length).toBe(2); + + // first address from mnemonic 2 + expect(preview.outputs[0].address).toBe( + "tb1pmgqzlvj3kcnsaxvnvnjrfm2kyx2k9ddfp84ty6hx0972gz85gg3slq3j59" + ); + expect(preview.outputs[0].amount).toBe(100); + + // change sent back to original address + expect(preview.outputs[1].address).toBe( + "tb1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqlqt9zj" + ); + expect(preview.outputs[1].amount).toBe(1600); + + expect(preview.fee).toBe(1000); + }); +}); diff --git a/src/extension/background-script/actions/webbtc/enable.ts b/src/extension/background-script/actions/webbtc/enable.ts index b560416bbf..26f0d008be 100644 --- a/src/extension/background-script/actions/webbtc/enable.ts +++ b/src/extension/background-script/actions/webbtc/enable.ts @@ -17,7 +17,15 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => { .equalsIgnoreCase(host) .first(); - if (isUnlocked && allowance && allowance.enabled && account?.mnemonic) { + const enabledFor = new Set(allowance?.enabledFor); + + if ( + isUnlocked && + allowance && + allowance.enabled && + account?.mnemonic && + enabledFor.has("webbtc") + ) { return { data: { enabled: true }, }; @@ -39,8 +47,12 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => { if (!allowance.id) { return { data: { error: "id is missing" } }; } + + enabledFor.add("webbtc"); + await db.allowances.update(allowance.id, { enabled: true, + enabledFor, name: message.origin.name, imageURL: message.origin.icon, }); @@ -50,6 +62,7 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => { name: message.origin.name, imageURL: message.origin.icon, enabled: true, + enabledFor: ["webbtc"], lastPaymentAt: 0, totalBudget: 0, remainingBudget: 0, diff --git a/src/extension/background-script/actions/webbtc/getInfo.ts b/src/extension/background-script/actions/webbtc/getInfo.ts index 9043e35101..720a1fef32 100644 --- a/src/extension/background-script/actions/webbtc/getInfo.ts +++ b/src/extension/background-script/actions/webbtc/getInfo.ts @@ -1,7 +1,7 @@ import { MessageGetInfo } from "~/types"; const getInfo = async (message: MessageGetInfo) => { - const supportedMethods = ["getInfo", "getAddress"]; + const supportedMethods = ["getInfo", "signPsbt", "getAddress"]; return { data: { diff --git a/src/extension/background-script/actions/webbtc/getPsbtPreview.ts b/src/extension/background-script/actions/webbtc/getPsbtPreview.ts new file mode 100644 index 0000000000..6ae11784b2 --- /dev/null +++ b/src/extension/background-script/actions/webbtc/getPsbtPreview.ts @@ -0,0 +1,21 @@ +import state from "~/extension/background-script/state"; +import { MessageGetPsbtPreview } from "~/types"; + +const getPsbtPreview = async (message: MessageGetPsbtPreview) => { + try { + const bitcoin = await state.getState().getBitcoin(); + + const data = bitcoin.getPsbtPreview(message.args.psbt); + + return { + data, + }; + } catch (e) { + console.error("getPsbtPreview failed: ", e); + return { + error: "getPsbtPreview failed: " + e, + }; + } +}; + +export default getPsbtPreview; diff --git a/src/extension/background-script/actions/webbtc/index.ts b/src/extension/background-script/actions/webbtc/index.ts index 18779f9280..6ddef47736 100644 --- a/src/extension/background-script/actions/webbtc/index.ts +++ b/src/extension/background-script/actions/webbtc/index.ts @@ -1,6 +1,19 @@ -import enable from "./enable"; -import getAddress from "./getAddress"; -import getAddressOrPrompt from "./getAddressOrPrompt"; -import getInfo from "./getInfo"; +import enable from "~/extension/background-script/actions/webbtc/enable"; +import getAddress from "~/extension/background-script/actions/webbtc/getAddress"; +import getAddressOrPrompt from "~/extension/background-script/actions/webbtc/getAddressOrPrompt"; +import getInfo from "~/extension/background-script/actions/webbtc/getInfo"; +import getPsbtPreview from "~/extension/background-script/actions/webbtc/getPsbtPreview"; +import isEnabled from "~/extension/background-script/actions/webbtc/isEnabled"; +import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; +import signPsbtWithPrompt from "~/extension/background-script/actions/webbtc/signPsbtWithPrompt"; -export { enable, getAddress, getAddressOrPrompt, getInfo }; +export { + enable, + getAddress, + getAddressOrPrompt, + getInfo, + getPsbtPreview, + isEnabled, + signPsbt, + signPsbtWithPrompt, +}; diff --git a/src/extension/background-script/actions/webbtc/isEnabled.ts b/src/extension/background-script/actions/webbtc/isEnabled.ts new file mode 100644 index 0000000000..d19355ca66 --- /dev/null +++ b/src/extension/background-script/actions/webbtc/isEnabled.ts @@ -0,0 +1,35 @@ +import { getHostFromSender } from "~/common/utils/helpers"; +import db from "~/extension/background-script/db"; +import type { MessageAllowanceEnable, Sender } from "~/types"; + +import state from "../../state"; + +const isEnabled = async (message: MessageAllowanceEnable, sender: Sender) => { + const host = getHostFromSender(sender); + if (!host) return; + + const isUnlocked = await state.getState().isUnlocked(); + const allowance = await db.allowances + .where("host") + .equalsIgnoreCase(host) + .first(); + + const enabledFor = new Set(allowance?.enabledFor); + + if ( + isUnlocked && + allowance && + allowance.enabled && + enabledFor.has("webbtc") + ) { + return { + data: { isEnabled: true }, + }; + } else { + return { + data: { isEnabled: false }, + }; + } +}; + +export default isEnabled; 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..23823827d6 --- /dev/null +++ b/src/extension/background-script/actions/webbtc/signPsbt.ts @@ -0,0 +1,22 @@ +import state from "~/extension/background-script/state"; +import { MessageSignPsbt } from "~/types"; + +const signPsbt = async (message: MessageSignPsbt) => { + try { + const bitcoin = await state.getState().getBitcoin(); + + const signedTransaction = await bitcoin.signPsbt(message.args.psbt); + return { + data: { + signed: signedTransaction, + }, + }; + } catch (e) { + console.error("signPsbt failed: ", e); + return { + error: "signPsbt failed: " + e, + }; + } +}; + +export default signPsbt; 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..9886009fc7 --- /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: "webbtc/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/actions/webln/enable.ts b/src/extension/background-script/actions/webln/enable.ts index ceb6665d07..f1be38ee93 100644 --- a/src/extension/background-script/actions/webln/enable.ts +++ b/src/extension/background-script/actions/webln/enable.ts @@ -16,7 +16,9 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => { .equalsIgnoreCase(host) .first(); - if (isUnlocked && allowance && allowance.enabled) { + const enabledFor = new Set(allowance?.enabledFor); + + if (isUnlocked && allowance && allowance.enabled && enabledFor.has("webln")) { return { data: { enabled: true }, }; @@ -38,8 +40,12 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => { if (!allowance.id) { return { data: { error: "id is missing" } }; } + + enabledFor.add("webln"); + await db.allowances.update(allowance.id, { enabled: true, + enabledFor, name: message.origin.name, imageURL: message.origin.icon, }); @@ -49,6 +55,7 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => { name: message.origin.name, imageURL: message.origin.icon, enabled: true, + enabledFor: ["webln"], lastPaymentAt: 0, totalBudget: 0, remainingBudget: 0, diff --git a/src/extension/background-script/actions/webln/index.ts b/src/extension/background-script/actions/webln/index.ts index faa8513fec..a869ba7139 100644 --- a/src/extension/background-script/actions/webln/index.ts +++ b/src/extension/background-script/actions/webln/index.ts @@ -1,17 +1,21 @@ import enable from "./enable"; import getBalanceOrPrompt from "./getBalanceOrPrompt"; +import isEnabled from "./isEnabled"; import keysendOrPrompt from "./keysendOrPrompt"; import lnurl from "./lnurl"; import makeInvoiceOrPrompt from "./makeInvoiceOrPrompt"; +import { sendPaymentAsyncWithPrompt } from "./sendPaymentAsyncWithPrompt"; import { sendPaymentOrPrompt } from "./sendPaymentOrPrompt"; import signMessageOrPrompt from "./signMessageOrPrompt"; export { enable, getBalanceOrPrompt, + isEnabled, keysendOrPrompt, lnurl, makeInvoiceOrPrompt, + sendPaymentAsyncWithPrompt, sendPaymentOrPrompt, signMessageOrPrompt, }; diff --git a/src/extension/background-script/actions/webln/isEnabled.ts b/src/extension/background-script/actions/webln/isEnabled.ts new file mode 100644 index 0000000000..d72319a43c --- /dev/null +++ b/src/extension/background-script/actions/webln/isEnabled.ts @@ -0,0 +1,30 @@ +import { getHostFromSender } from "~/common/utils/helpers"; +import db from "~/extension/background-script/db"; +import type { MessageAllowanceEnable, Sender } from "~/types"; + +import state from "../../state"; + +const isEnabled = async (message: MessageAllowanceEnable, sender: Sender) => { + const host = getHostFromSender(sender); + if (!host) return; + + const isUnlocked = await state.getState().isUnlocked(); + const allowance = await db.allowances + .where("host") + .equalsIgnoreCase(host) + .first(); + + const enabledFor = new Set(allowance?.enabledFor); + + if (isUnlocked && allowance && allowance.enabled && enabledFor.has("webln")) { + return { + data: { isEnabled: true }, + }; + } else { + return { + data: { isEnabled: false }, + }; + } +}; + +export default isEnabled; diff --git a/src/extension/background-script/actions/webln/sendPaymentAsyncWithPrompt.ts b/src/extension/background-script/actions/webln/sendPaymentAsyncWithPrompt.ts new file mode 100644 index 0000000000..830422888a --- /dev/null +++ b/src/extension/background-script/actions/webln/sendPaymentAsyncWithPrompt.ts @@ -0,0 +1,21 @@ +import utils from "~/common/lib/utils"; +import { MessageSendPayment } from "~/types"; + +// Async payments cannot be budgeted for (they do not get saved to the extension DB) +// so always require a prompt. +async function sendPaymentAsyncWithPrompt(message: MessageSendPayment) { + try { + const response = await utils.openPrompt({ + ...message, + action: "confirmPaymentAsync", + }); + return response; + } catch (e) { + console.error("Payment cancelled", e); + if (e instanceof Error) { + return { error: e.message }; + } + } +} + +export { sendPaymentAsyncWithPrompt }; diff --git a/src/extension/background-script/actions/webln/sendPaymentOrPrompt.ts b/src/extension/background-script/actions/webln/sendPaymentOrPrompt.ts index 2333db4474..48a22752e6 100644 --- a/src/extension/background-script/actions/webln/sendPaymentOrPrompt.ts +++ b/src/extension/background-script/actions/webln/sendPaymentOrPrompt.ts @@ -61,9 +61,4 @@ async function payWithPrompt(message: Message) { } } -export { - sendPaymentOrPrompt, - payWithPrompt, - checkAllowance, - sendPaymentWithAllowance, -}; +export { sendPaymentOrPrompt, payWithPrompt, checkAllowance }; diff --git a/src/extension/background-script/bitcoin/index.ts b/src/extension/background-script/bitcoin/index.ts index 1a649ec330..89e2a95a59 100644 --- a/src/extension/background-script/bitcoin/index.ts +++ b/src/extension/background-script/bitcoin/index.ts @@ -1,11 +1,23 @@ import * as secp256k1 from "@noble/secp256k1"; -import * as btc from "@scure/btc-signer"; +import * as bitcoin from "bitcoinjs-lib"; +import ECPairFactory, { ECPairAPI } from "ecpair"; +import { + Network, + networks, +} from "~/extension/background-script/bitcoin/networks"; import Mnemonic from "~/extension/background-script/mnemonic"; -import { BitcoinAddress, BitcoinNetworkType } from "~/types"; +import { + Address, + BitcoinAddress, + BitcoinNetworkType, + PsbtPreview, +} from "~/types"; const BTC_TAPROOT_DERIVATION_PATH = "m/86'/0'/0'/0"; const BTC_TAPROOT_DERIVATION_PATH_REGTEST = "m/86'/1'/0'/0"; +import * as ecc from "@bitcoinerlab/secp256k1"; + class Bitcoin { readonly networkType: BitcoinNetworkType; readonly mnemonic: Mnemonic; @@ -15,8 +27,10 @@ class Bitcoin { this.mnemonic = mnemonic; this.networkType = networkType; this.network = networks[this.networkType]; + bitcoin.initEccLib(ecc); } - getTaprootAddress(): BitcoinAddress { + + signPsbt(psbt: string) { const index = 0; const derivationPathWithoutIndex = this.networkType === "bitcoin" @@ -26,11 +40,49 @@ class Bitcoin { const derivationPath = `${derivationPathWithoutIndex}/${index}`; const derivedKey = this.mnemonic.deriveKey(derivationPath); - const address = btc.getAddress( - "tr", - derivedKey.privateKey as Uint8Array, - this.network + const taprootPsbt = bitcoin.Psbt.fromHex(psbt, { + network: this.network, + }); + + const ECPair: ECPairAPI = ECPairFactory(ecc); + + const keyPair = tweakSigner( + ECPair, + ECPair.fromPrivateKey(Buffer.from(derivedKey.privateKey as Uint8Array), { + network: this.network, + }), + { + network: this.network, + } ); + + // Step 1: Sign the Taproot PSBT inputs + taprootPsbt.data.inputs.forEach((input, index) => { + taprootPsbt.signTaprootInput(index, keyPair); + }); + + // Step 2: Finalize the Taproot PSBT + taprootPsbt.finalizeAllInputs(); + + // Step 3: Get the finalized transaction + const signedTransaction = taprootPsbt.extractTransaction().toHex(); + + return signedTransaction; + } + getTaprootAddress(): BitcoinAddress { + const index = 0; + const derivationPathWithoutIndex = + this.networkType === "bitcoin" + ? BTC_TAPROOT_DERIVATION_PATH + : BTC_TAPROOT_DERIVATION_PATH_REGTEST; + + const derivationPath = `${derivationPathWithoutIndex}/${index}`; + const derivedKey = this.mnemonic.deriveKey(derivationPath); + + const { address } = bitcoin.payments.p2tr({ + internalPubkey: toXOnly(Buffer.from(derivedKey.publicKey as Uint8Array)), + network: this.network, + }); if (!address) { throw new Error("No taproot address found from private key"); } @@ -41,63 +93,117 @@ class Bitcoin { publicKey: secp256k1.etc.bytesToHex(derivedKey.publicKey as Uint8Array), }; } + + getPsbtPreview(psbt: string): PsbtPreview { + const unsignedPsbt = bitcoin.Psbt.fromHex(psbt, { + network: this.network, + }); + + const preview: PsbtPreview = { + inputs: [], + outputs: [], + fee: 0, + }; + + for (let i = 0; i < unsignedPsbt.data.inputs.length; i++) { + const pubkey: Buffer | undefined = + unsignedPsbt.data.inputs[i].tapInternalKey || + unsignedPsbt.data.inputs[i].tapBip32Derivation?.[0]?.pubkey; + + let address = "UNKNOWN"; + if (pubkey) { + const pubkeyAddress = bitcoin.payments.p2tr({ + internalPubkey: pubkey, + network: this.network, + }).address; + if (pubkeyAddress) { + address = pubkeyAddress; + } + } + const witnessUtxo = unsignedPsbt.data.inputs[i].witnessUtxo; + if (!witnessUtxo) { + throw new Error("No witnessUtxo in input " + i); + } + + preview.inputs.push({ + amount: unsignedPsbt.data.inputs[i].witnessUtxo?.value || 0, + address, + }); + } + for (let i = 0; i < unsignedPsbt.data.outputs.length; i++) { + const txOutput = unsignedPsbt.txOutputs[i]; + + const address = + txOutput.address || + (txOutput.script && + (() => { + try { + return bitcoin.address.fromOutputScript( + txOutput.script, + this.network + ); + } catch (error) { + console.error(error); + } + return undefined; + })()) || + "UNKNOWN"; + + const previewOutput: Address = { + amount: txOutput.value, + address, + }; + preview.outputs.push(previewOutput); + } + + for (const input of preview.inputs) { + preview.fee += input.amount; + } + for (const output of preview.outputs) { + preview.fee -= output.amount; + } + + return preview; + } } export default Bitcoin; -// from https://github1s.com/bitcoinjs/bitcoinjs-lib -interface Network { - messagePrefix: string; - bech32: string; - bip32: Bip32; - pubKeyHash: number; - scriptHash: number; - wif: number; -} +// 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: 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; + if (!privateKey) { + throw new Error("Private key is required for tweaking signer!"); + } + if (signer.publicKey[0] === 3) { + privateKey = ecc.privateNegate(privateKey); + } -interface Bip32 { - public: number; - private: number; + const tweakedPrivateKey = ecc.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, + }); } -const bitcoin: Network = { - messagePrefix: "\x18Bitcoin Signed Message:\n", - bech32: "bc", - bip32: { - public: 0x0488b21e, - private: 0x0488ade4, - }, - pubKeyHash: 0x00, - scriptHash: 0x05, - wif: 0x80, -}; - -export const testnet: Network = { - messagePrefix: "\x18Bitcoin Signed Message:\n", - bech32: "tb", - bip32: { - public: 0x043587cf, - private: 0x04358394, - }, - pubKeyHash: 0x6f, - scriptHash: 0xc4, - wif: 0xef, -}; - -const regtest: Network = { - messagePrefix: "\x18Bitcoin Signed Message:\n", - bech32: "bcrt", - bip32: { - public: 0x043587cf, - private: 0x04358394, - }, - pubKeyHash: 0x6f, - scriptHash: 0xc4, - wif: 0xef, -}; - -export const networks = { - bitcoin, - testnet, - regtest, -}; +function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer { + return bitcoin.crypto.taggedHash( + "TapTweak", + Buffer.concat(h ? [pubKey, h] : [pubKey]) + ); +} diff --git a/src/extension/background-script/bitcoin/networks.ts b/src/extension/background-script/bitcoin/networks.ts new file mode 100644 index 0000000000..5962ad9154 --- /dev/null +++ b/src/extension/background-script/bitcoin/networks.ts @@ -0,0 +1,2 @@ +import { Network, networks } from "bitcoinjs-lib"; +export { Network, networks }; diff --git a/src/extension/background-script/connectors/alby.ts b/src/extension/background-script/connectors/alby.ts index 260feebfcb..3df2a38fd7 100644 --- a/src/extension/background-script/connectors/alby.ts +++ b/src/extension/background-script/connectors/alby.ts @@ -69,7 +69,14 @@ export default class Alby implements Connector { } get supportedMethods() { - return ["getInfo", "keysend", "makeInvoice", "sendPayment", "getBalance"]; + return [ + "getInfo", + "keysend", + "makeInvoice", + "sendPayment", + "sendPaymentAsync", + "getBalance", + ]; } // not yet implemented diff --git a/src/extension/background-script/connectors/citadel.ts b/src/extension/background-script/connectors/citadel.ts index 24e1a90234..1cde1135d9 100644 --- a/src/extension/background-script/connectors/citadel.ts +++ b/src/extension/background-script/connectors/citadel.ts @@ -56,6 +56,7 @@ class CitadelConnector implements Connector { return [ "makeInvoice", "sendPayment", + "sendPaymentAsync", "signMessage", "getInfo", "getBalance", diff --git a/src/extension/background-script/connectors/commando.ts b/src/extension/background-script/connectors/commando.ts index 6ea545af53..9d7d71d6e0 100644 --- a/src/extension/background-script/connectors/commando.ts +++ b/src/extension/background-script/connectors/commando.ts @@ -148,6 +148,7 @@ export default class Commando implements Connector { "keysend", "makeInvoice", "sendPayment", + "sendPaymentAsync", "signMessage", "getBalance", ...flattenRequestMethods(supportedMethods), diff --git a/src/extension/background-script/connectors/connector.interface.ts b/src/extension/background-script/connectors/connector.interface.ts index 03700cef9b..cd08e97ef7 100644 --- a/src/extension/background-script/connectors/connector.interface.ts +++ b/src/extension/background-script/connectors/connector.interface.ts @@ -69,6 +69,11 @@ export type SendPaymentResponse = { }; }; +export type SendPaymentAsyncResponse = { + // eslint-disable-next-line @typescript-eslint/ban-types + data: {}; +}; + export interface SendPaymentArgs { paymentRequest: string; } diff --git a/src/extension/background-script/connectors/eclair.ts b/src/extension/background-script/connectors/eclair.ts index 4f73c267ff..73a569e34d 100644 --- a/src/extension/background-script/connectors/eclair.ts +++ b/src/extension/background-script/connectors/eclair.ts @@ -47,6 +47,7 @@ class Eclair implements Connector { "keysend", "makeInvoice", "sendPayment", + "sendPaymentAsync", "signMessage", "getBalance", ]; diff --git a/src/extension/background-script/connectors/galoy.ts b/src/extension/background-script/connectors/galoy.ts index 9690cfdd80..cd68376f04 100644 --- a/src/extension/background-script/connectors/galoy.ts +++ b/src/extension/background-script/connectors/galoy.ts @@ -47,6 +47,7 @@ class Galoy implements Connector { "getInfo", "makeInvoice", "sendPayment", + "sendPaymentAsync", "signMessage", "getBalance", ]; diff --git a/src/extension/background-script/connectors/lnbits.ts b/src/extension/background-script/connectors/lnbits.ts index 31939584c4..4d2de70e9f 100644 --- a/src/extension/background-script/connectors/lnbits.ts +++ b/src/extension/background-script/connectors/lnbits.ts @@ -50,6 +50,7 @@ class LnBits implements Connector { "getInfo", "makeInvoice", "sendPayment", + "sendPaymentAsync", "signMessage", "getBalance", ]; diff --git a/src/extension/background-script/connectors/lnc.ts b/src/extension/background-script/connectors/lnc.ts index 69789de0f6..062aef2f5b 100644 --- a/src/extension/background-script/connectors/lnc.ts +++ b/src/extension/background-script/connectors/lnc.ts @@ -194,6 +194,7 @@ class Lnc implements Connector { "keysend", "makeInvoice", "sendPayment", + "sendPaymentAsync", "signMessage", "getBalance", ...flattenRequestMethods(Object.keys(methods)), diff --git a/src/extension/background-script/connectors/lnd.ts b/src/extension/background-script/connectors/lnd.ts index a488a12951..7bf6968151 100644 --- a/src/extension/background-script/connectors/lnd.ts +++ b/src/extension/background-script/connectors/lnd.ts @@ -167,6 +167,7 @@ class Lnd implements Connector { "keysend", "makeInvoice", "sendPayment", + "sendPaymentAsync", "signMessage", "getBalance", ...flattenRequestMethods(Object.keys(methods)), diff --git a/src/extension/background-script/connectors/lndhub.ts b/src/extension/background-script/connectors/lndhub.ts index 0d0e7473bd..871b899f8b 100644 --- a/src/extension/background-script/connectors/lndhub.ts +++ b/src/extension/background-script/connectors/lndhub.ts @@ -71,6 +71,7 @@ export default class LndHub implements Connector { "keysend", "makeInvoice", "sendPayment", + "sendPaymentAsync", "signMessage", "getBalance", ]; diff --git a/src/extension/background-script/db.ts b/src/extension/background-script/db.ts index 28c841d490..96358d3178 100644 --- a/src/extension/background-script/db.ts +++ b/src/extension/background-script/db.ts @@ -51,6 +51,11 @@ export class DB extends Dexie { payments: "++id,accountId,allowanceId,host,location,name,description,totalAmount,totalFees,preimage,paymentRequest,paymentHash,destination,createdAt", }); + this.version(6).stores({ + allowances: + "++id,&host,name,imageURL,tag,enabled,*enabledFor,totalBudget,remainingBudget,lastPaymentAt,lnurlAuth,createdAt", + }); + this.on("ready", this.loadFromStorage.bind(this)); this.allowances = this.table("allowances"); this.payments = this.table("payments"); diff --git a/src/extension/background-script/liquid/index.ts b/src/extension/background-script/liquid/index.ts index c436c5f16f..a0ec6a6fd5 100644 --- a/src/extension/background-script/liquid/index.ts +++ b/src/extension/background-script/liquid/index.ts @@ -20,7 +20,6 @@ import { getPsetPreview } from "~/extension/background-script/liquid/pset"; import Mnemonic from "~/extension/background-script/mnemonic"; import { LiquidNetworkType, PsetPreview } from "~/types"; -import * as tinysecp from "../liquid/secp256k1"; import * as tinysecp256k1Adapter from "./secp256k1"; const LIQUID_DERIVATION_PATH = "m/84'/1776'/0'/0/0"; @@ -174,7 +173,7 @@ class Liquid { signer.addSignature( inIndex, partialSig, - Pset.SchnorrSigValidator(tinysecp) + Pset.SchnorrSigValidator(tinysecp256k1Adapter) ); continue; @@ -221,7 +220,7 @@ class Liquid { signer.addSignature( inIndex, partialSig, - Pset.SchnorrSigValidator(tinysecp) + Pset.SchnorrSigValidator(tinysecp256k1Adapter) ); } } @@ -241,7 +240,7 @@ class Liquid { } private deriveLiquidMasterBlindingKey(): string { - return SLIP77Factory(tinysecp) + return SLIP77Factory(tinysecp256k1Adapter) .fromSeed(Buffer.from(bip39.mnemonicToSeedSync(this.mnemonic.mnemonic))) .masterKey.toString("hex"); } diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index 1cd1b50b4e..643b98f94d 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -8,7 +8,6 @@ import * as ln from "./actions/ln"; import lnurl, { auth } from "./actions/lnurl"; import * as mnemonic from "./actions/mnemonic"; import * as nostr from "./actions/nostr"; -import * as onboard from "./actions/onboard"; import * as payments from "./actions/payments"; import * as permissions from "./actions/permissions"; import * as settings from "./actions/settings"; @@ -33,6 +32,7 @@ const routes = { getInfo: ln.getInfo, getInvoices: ln.invoices, sendPayment: ln.sendPayment, + sendPaymentAsync: ln.sendPaymentAsync, keysend: ln.keysend, checkPayment: ln.checkPayment, signMessage: ln.signMessage, @@ -63,7 +63,6 @@ const routes = { lnurl: lnurl, lnurlAuth: auth, getCurrencyRate: cache.getCurrencyRate, - getAddress: webbtc.getAddress, setMnemonic: mnemonic.setMnemonic, getMnemonic: mnemonic.getMnemonic, generateMnemonic: mnemonic.generateMnemonic, @@ -81,23 +80,32 @@ const routes = { removePrivateKey: nostr.removePrivateKey, setPrivateKey: nostr.setPrivateKey, }, + webbtc: { + getPsbtPreview: webbtc.getPsbtPreview, + signPsbt: webbtc.signPsbt, + getAddress: webbtc.getAddress, + }, // Public calls that are accessible from the inpage script (through the content script) public: { webbtc: { + isEnabled: webbtc.isEnabled, enable: webbtc.enable, getInfo: webbtc.getInfo, + signPsbtWithPrompt: webbtc.signPsbtWithPrompt, getAddressOrPrompt: webbtc.getAddressOrPrompt, }, alby: { + isEnabled: alby.isEnabled, enable: alby.enable, addAccount: accounts.promptAdd, }, webln: { - onboard: onboard.prompt, enable: webln.enable, + isEnabled: webln.isEnabled, getInfo: ln.getInfo, sendPaymentOrPrompt: webln.sendPaymentOrPrompt, + sendPaymentAsyncWithPrompt: webln.sendPaymentAsyncWithPrompt, keysendOrPrompt: webln.keysendOrPrompt, signMessageOrPrompt: webln.signMessageOrPrompt, lnurl: webln.lnurl, @@ -106,11 +114,13 @@ const routes = { request: ln.request, }, liquid: { + isEnabled: liquid.isEnabled, enable: liquid.enable, getAddressOrPrompt: liquid.getAddressOrPrompt, signPsetWithPrompt: liquid.signPsetWithPrompt, }, nostr: { + isEnabled: nostr.isEnabled, enable: nostr.enable, getPublicKeyOrPrompt: nostr.getPublicKeyOrPrompt, signEventOrPrompt: nostr.signEventOrPrompt, diff --git a/src/extension/content-script/alby.js b/src/extension/content-script/alby.js index 09302ab21d..d8e8e2ea44 100644 --- a/src/extension/content-script/alby.js +++ b/src/extension/content-script/alby.js @@ -5,9 +5,9 @@ import shouldInject from "./shouldInject"; // Alby calls that can be executed from the AlbyProvider. // Update when new calls are added -const albyCalls = ["alby/enable", "alby/addAccount"]; +const albyCalls = ["alby/enable", "alby/addAccount", "alby/isEnabled"]; // calls that can be executed when alby is not enabled for the current content page -const disabledCalls = ["alby/enable"]; +const disabledCalls = ["alby/enable", "alby/isEnabled"]; let isEnabled = false; // store if alby is enabled for this content page let isRejected = false; // store if the alby enable call failed. if so we do not prompt again @@ -70,6 +70,9 @@ async function init() { isRejected = true; } } + if (ev.data.action === "alby/isEnabled") { + isEnabled = response.data?.isEnabled; + } postMessage(ev, response); }; return browser.runtime diff --git a/src/extension/content-script/liquid.js b/src/extension/content-script/liquid.js index 0d1f5e147a..5fa6ca70da 100644 --- a/src/extension/content-script/liquid.js +++ b/src/extension/content-script/liquid.js @@ -9,9 +9,12 @@ const liquidCalls = [ "liquid/getAddressOrPrompt", "liquid/signPsetWithPrompt", "liquid/enable", + "liquid/isEnabled", + "liquid/on", + "liquid/off", ]; // calls that can be executed when liquid is not enabled for the current content page -const disabledCalls = ["liquid/enable"]; +const disabledCalls = ["liquid/enable", "liquid/isEnabled"]; let isEnabled = false; // store if liquid is enabled for this content page let isRejected = false; // store if the liquid enable call failed. if so we do not prompt again @@ -24,6 +27,16 @@ async function init() { return; } + browser.runtime.onMessage.addListener((request, sender, sendResponse) => { + // forward account changed messaged to inpage script + if (request.action === "accountChanged" && isEnabled) { + window.postMessage( + { action: "accountChanged", scope: "liquid" }, + window.location.origin + ); + } + }); + // message listener to listen to inpage liquid 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 liquid available to the page) @@ -76,6 +89,10 @@ async function init() { } } + if (ev.data.action === `${SCOPE}/isEnabled`) { + isEnabled = response.data?.isEnabled; + } + postMessage(ev, response); }; diff --git a/src/extension/content-script/nostr.js b/src/extension/content-script/nostr.js index 92ff55266c..e3d8a287d2 100644 --- a/src/extension/content-script/nostr.js +++ b/src/extension/content-script/nostr.js @@ -16,9 +16,10 @@ const nostrCalls = [ "nostr/on", "nostr/off", "nostr/emit", + "nostr/isEnabled", ]; // calls that can be executed when nostr is not enabled for the current content page -const disabledCalls = ["nostr/enable"]; +const disabledCalls = ["nostr/enable", "nostr/isEnabled"]; 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 @@ -31,7 +32,7 @@ async function init() { browser.runtime.onMessage.addListener((request, sender, sendResponse) => { // forward account changed messaged to inpage script - if (request.action === "accountChanged") { + if (request.action === "accountChanged" && isEnabled) { window.postMessage( { action: "accountChanged", scope: "nostr" }, window.location.origin @@ -92,6 +93,9 @@ async function init() { isRejected = true; } } + if (ev.data.action === "nostr/isEnabled") { + isEnabled = response.data?.isEnabled; + } postMessage(ev, response); }; diff --git a/src/extension/content-script/webbtc.js b/src/extension/content-script/webbtc.js index db2ff09d87..3646c10287 100644 --- a/src/extension/content-script/webbtc.js +++ b/src/extension/content-script/webbtc.js @@ -7,10 +7,14 @@ import shouldInject from "./shouldInject"; const webbtcCalls = [ "webbtc/enable", "webbtc/getInfo", + "webbtc/signPsbtWithPrompt", "webbtc/getAddressOrPrompt", + "webbtc/isEnabled", + "webbtc/on", + "webbtc/off", ]; // calls that can be executed when `window.webbtc` is not enabled for the current content page -const disabledCalls = ["webbtc/enable"]; +const disabledCalls = ["webbtc/enable", "webbtc/isEnabled"]; 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 @@ -23,6 +27,16 @@ async function init() { return; } + browser.runtime.onMessage.addListener((request, sender, sendResponse) => { + // forward account changed messaged to inpage script + if (request.action === "accountChanged" && isEnabled) { + window.postMessage( + { action: "accountChanged", scope: "webbtc" }, + window.location.origin + ); + } + }); + // 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) @@ -75,6 +89,10 @@ async function init() { } } + if (ev.data.action === `${SCOPE}/isEnabled`) { + isEnabled = response.data?.isEnabled; + } + postMessage(ev, response); }; diff --git a/src/extension/content-script/webln.js b/src/extension/content-script/webln.js index 21b51e9f99..8f38c31b20 100644 --- a/src/extension/content-script/webln.js +++ b/src/extension/content-script/webln.js @@ -11,6 +11,7 @@ const weblnCalls = [ "webln/getInfo", "webln/lnurl", "webln/sendPaymentOrPrompt", + "webln/sendPaymentAsyncWithPrompt", "webln/keysendOrPrompt", "webln/makeInvoice", "webln/signMessageOrPrompt", @@ -19,9 +20,10 @@ const weblnCalls = [ "webln/on", "webln/emit", "webln/off", + "webln/isEnabled", ]; // calls that can be executed when webln is not enabled for the current content page -const disabledCalls = ["webln/enable"]; +const disabledCalls = ["webln/enable", "webln/isEnabled"]; let isEnabled = false; // store if webln is enabled for this content page let isRejected = false; // store if the webln enable call failed. if so we do not prompt again @@ -101,6 +103,10 @@ async function init() { isRejected = true; } } + + if (ev.data.action === "webln/isEnabled") { + isEnabled = response.data?.isEnabled; + } postMessage(ev, response); }; return browser.runtime diff --git a/src/extension/providers/providerBase.ts b/src/extension/providers/providerBase.ts index 58d9087266..0b3188ad45 100644 --- a/src/extension/providers/providerBase.ts +++ b/src/extension/providers/providerBase.ts @@ -2,30 +2,31 @@ import { EventEmitter } from "events"; import { postMessage } from "~/extension/providers/postMessage"; export default class ProviderBase { - enabled: boolean; + private _isEnabled: boolean; private _eventEmitter: EventEmitter; private _scope: string; constructor(scope: string) { - this.enabled = false; + this._scope = scope; + this._isEnabled = false; this._eventEmitter = new EventEmitter(); this._scope = scope; } protected _checkEnabled(methodName: string): void { - if (!this.enabled) { + if (!this._isEnabled) { throw new Error(`Provider must be enabled before calling ${methodName}`); } } async enable(): Promise { - if (this.enabled) { + if (this._isEnabled) { return; } const result = await this.execute("enable"); if (typeof result.enabled === "boolean") { - this.enabled = result.enabled; + this._isEnabled = result.enabled; } } @@ -43,6 +44,17 @@ export default class ProviderBase { this._eventEmitter.emit(...args); } + async isEnabled(): Promise { + if (this._isEnabled) { + return true; + } + const result = await this.execute("isEnabled"); + if (typeof result.isEnabled === "boolean") { + this._isEnabled = result.isEnabled; + } + return this._isEnabled; + } + // NOTE: new call `action`s must be specified also in the content script execute( action: string, diff --git a/src/extension/providers/webbtc/index.ts b/src/extension/providers/webbtc/index.ts index 3bd8e38a7f..5df462a1d9 100644 --- a/src/extension/providers/webbtc/index.ts +++ b/src/extension/providers/webbtc/index.ts @@ -7,13 +7,8 @@ declare global { } export default class WebBTCProvider extends ProviderBase { - isEnabled: boolean; - executing: boolean; - constructor() { super("webbtc"); - this.isEnabled = false; - this.executing = false; } getInfo() { @@ -21,6 +16,12 @@ export default class WebBTCProvider extends ProviderBase { return this.execute("getInfo"); } + signPsbt(psbt: string) { + this._checkEnabled("signPsbt"); + + return this.execute("signPsbtWithPrompt", { psbt }); + } + sendTransaction(address: string, amount: string) { this._checkEnabled("sendTransaction"); throw new Error("Alby does not support `sendTransaction`"); diff --git a/src/extension/providers/webln/index.ts b/src/extension/providers/webln/index.ts index c0b9b8dd73..2ed7815354 100644 --- a/src/extension/providers/webln/index.ts +++ b/src/extension/providers/webln/index.ts @@ -39,6 +39,10 @@ export default class WebLNProvider extends ProviderBase { this._checkEnabled("sendPayment"); return this.execute("sendPaymentOrPrompt", { paymentRequest }); } + sendPaymentAsync(paymentRequest: string) { + this._checkEnabled("sendPaymentAsync"); + return this.execute("sendPaymentAsyncWithPrompt", { paymentRequest }); + } keysend(args: KeysendArgs) { this._checkEnabled("keysend"); diff --git a/src/fixtures/allowances.ts b/src/fixtures/allowances.ts index 61fef45e2e..4574e0b978 100644 --- a/src/fixtures/allowances.ts +++ b/src/fixtures/allowances.ts @@ -7,6 +7,7 @@ export const allowanceFixture: DbAllowance[] = [ id: 1, imageURL: "https://pro.kollider.xyz/favicon.ico", lastPaymentAt: 0, + enabledFor: ["webln"], lnurlAuth: true, name: "pro kollider", remainingBudget: 500, diff --git a/src/fixtures/btc.ts b/src/fixtures/btc.ts index c843532f74..14163996e0 100644 --- a/src/fixtures/btc.ts +++ b/src/fixtures/btc.ts @@ -1,4 +1,22 @@ export const btcFixture = { - mnemnoic: + // 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: + "02000000000101b58806ecf5f7a2677dce4d357cc3ef14c59586db851f253e7d495b6506e7c8210100000000fdffffff02a54a4c0000000000225120d73acf6667b7850e6e00e21bb4525a2c5e2d88956a3d2c35b0bc06f4b0df01bc404b4c00000000002251202befa14431d4cb71889ea1df7a7eaa2f1d8b9107e60b01564e15dabe5c0dfd320140dcb575e673464796c4484808517f3e59afd0948cad9eda4cfd137e4e80f2eced24108f6ec116d5e6860424be0132697f53e69a89d54413cdec68dfb51fc7203734010000", + + mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + mnemonic2: + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon cactus", + + // made with bitcoinjs-lib + // sent from mnemonic to mnemonic2, with some change back to mnemonic (same address) + taprootPsbt2: + "70736274ff010089020000000101d245947b008821fdbf573087c061749fc1a6f74cee947767af917e1be324c10100000000ffffffff026400000000000000225120da002fb251b6270e999364e434ed56219562b5a909eab26ae6797ca408f4422340060000000000002251203b82b2b2a9185315da6f80da5f06d0440d8a5e1457fa93387c2d919c86ec8786000000000001012b8c0a0000000000002251203b82b2b2a9185315da6f80da5f06d0440d8a5e1457fa93387c2d919c86ec878601172055355ca83c973f1d97ce0e3843c85d78905af16b4dc531bc488e57212d230116000000", }; diff --git a/src/i18n/locales/cs/translation.json b/src/i18n/locales/cs/translation.json index 205d360753..7486600627 100644 --- a/src/i18n/locales/cs/translation.json +++ b/src/i18n/locales/cs/translation.json @@ -252,7 +252,7 @@ }, "title": "Připojte Lightning peněženku", "lnc": { - "title": "LND s LNC", + "title": "Lightning terminál (LNC)", "page": { "title": "Připojit k vašemu LND uzlu", "description": "Vytvořte nové sezení v terminálu (litd) pro získání nové párovací fráze a vložte ji zde" @@ -266,14 +266,28 @@ "description": "Přihlášení k vašemu účtu Kollider", "title": "Kollider", "errors": { - "connection_failed": "Připojení se nezdařilo. Jste si jisti, že jste zadali údaje k účtu správně?" + "connection_failed": "Připojení se nezdařilo. Jste si jisti, že jste zadali údaje k účtu správně?", + "user_already_exists": "Uživatelské jméno již existuje", + "registration_limit_exceeded": "Limit registrací byl překročen, zkuste to znovu později." }, "username": { "label": "Vložte vaše uživatelské jméno ke Kollider" }, "currency": { "label": "Zvolte měnu vašeho účtu" + }, + "choose_path": { + "create_new": "Přihlásit se", + "title": "Připojení k peněžence Kollider", + "description": "Přihlašte se nebo si vytvořte nový Kollider účet a propojte ho s Alby." + }, + "warning": "⚠️ Prosím ujistěte se, že máte své přihlašovací údaje uložené bezpečně ve svém správci hesel. Bez těchto údajů nepůjde váš účet obnovit.", + "create": { + "title": "Vytvořte si svůj Kollider účet a propojte ho s Alby" } + }, + "umbrel_lightning_node": { + "title": "Lightning uzel" } }, "home": { @@ -284,12 +298,19 @@ "recent_transactions": "Nedávné transakce", "allowance_view": { "recent_transactions": "Nedávné transakce", - "no_transactions": "Prozatím žádné transakce na <0>{{name}}." + "no_transactions": "Prozatím žádné transakce na <0>{{name}}.", + "sats": "satů", + "total_spent": "Cekově utraceno", + "total_payments": "Platby celkem", + "permissions": "Oprávnění" }, "default_view": { "recent_transactions": "Nedávné transakce", "is_blocked_hint": "Alby je v současnosti zakázaná na {host}}", - "block_removed": "Povolen {{host}}. Prosím znovu načtěte webovou stránku." + "block_removed": "Povolen {{host}}. Prosím znovu načtěte webovou stránku.", + "no_outgoing_transactions": "Zatím z tohoto účtu neproběhla žádná odchozí transakce.", + "all_transactions_link": "Zobrazit všechny transakce", + "no_incoming_transactions": "Zatím na tento účet neproběhla žádná příchozí transakce." } }, "accounts": { @@ -330,6 +351,11 @@ "title": "Nostr", "public_key": { "label": "Veřejný klíč" + }, + "settings": { + "label": "Nastavení Nostru", + "remove": "Odstranit současné klíče", + "title": "Nastavení Nostru" } }, "actions": { @@ -349,6 +375,45 @@ "name": { "title": "Jméno", "placeholder": "Jméno účtu" + }, + "mnemonic": { + "new": { + "title": "Co je to Master klíč?" + }, + "generate": { + "button": "Vygenerovat Master klíč", + "confirm": "Zazálohoval jsem svou záložní (recovery) frázi k mému Master klíči na soukromém bezpečném místě.", + "title": "Vygenerování nového Master klíče", + "error_confirm": "Prosím potvrďte, že jste zazálohovali svou záložní frázi." + }, + "inputs": { + "title": "Záložní fráze" + }, + "saved": "Master klíč byl zašifrován a úspěšně uložen.", + "lnurl": { + "use_mnemonic": "Používat Master klíč pro přihlašování do aplikací podporujících autentizaci přes Lightning (LNURL Auth)", + "title": "Přihlášení přes Lightning" + }, + "backup": { + "protocols": { + "nostr": "Nostr" + }, + "save": "Uložit Master klíč", + "button": "Zobrazit záložní frázi" + }, + "title": "🔑 Správa klíčů", + "import": { + "button": "Importovat Master klíč", + "title": "Import Master klíče" + } + }, + "network": { + "options": { + "testnet": "Testnet", + "bitcoin": "Mainnet", + "regtest": "Regtest" + }, + "title": "Síť" } } }, @@ -608,7 +673,8 @@ }, "alby": { "title": "Alby", - "description": "Zaregistrujte se, nebo se přihlašte ke svému existujícímu Alby účtu a začněte využívat lightning platby." + "description": "Zaregistrujte se, nebo se přihlašte ke svému existujícímu Alby účtu a začněte využívat lightning platby.", + "connect": "Připojit se s Alby" } }, "alby": { @@ -652,6 +718,27 @@ "title": "🕹️ Vyzkoušejte demo Alby" } } + }, + "distributions": { + "title": "Připojit k {{name}}", + "raspiblitz": { + "name": "Raspiblitz" + }, + "mynode": { + "name": "myNode" + }, + "btcpay": { + "name": "BTCPay" + }, + "start9": { + "name": "Start9" + }, + "umbrel": { + "name": "Umbrel" + }, + "citadel": { + "name": "Citadel" + } } }, "common": { diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index 4b8ca2d244..66aa0f0fab 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -959,7 +959,7 @@ "request1": "Anfrage zum Lesen deines öffentlichen Nostr-Schlüssels" }, "webbtc_enable": { - "request2": "Rechnungen und Liquidinformationen anfordern", + "request2": "Rechnungen und Webbtc-Informationen anfordern", "title": "Mit WebBTC verbinden" }, "alby_enable": { diff --git a/src/i18n/locales/el/translation.json b/src/i18n/locales/el/translation.json index 7fb27bf4db..e409546aaf 100644 --- a/src/i18n/locales/el/translation.json +++ b/src/i18n/locales/el/translation.json @@ -37,6 +37,9 @@ "other": { "title": "Άλλα πορτοφόλια", "connect": "Σύνδεση" + }, + "alby": { + "title": "Λογαριασμός Alby" } }, "alby": { @@ -44,13 +47,24 @@ "set_password": { "choose_password": { "label": "Κωδικός" + }, + "errors": { + "mismatched_password": "Οι κωδικοί δεν ταιριάζουν.", + "enter_password": "Παρακαλώ εισάγετε έναν κωδικό.", + "confirm_password": "Παρακαλώ επιβεβαιώστε τον κωδικός σας." + }, + "confirm_password": { + "label": "Επιβεβαίωση κωδικού πρόσβασης" } } } }, "choose_connector": { "lndhub_bluewallet": { - "title": "Bluewallet" + "title": "Bluewallet", + "page": { + "title": "Σύνδεση σε BlueWallet" + } }, "lnbits": { "title": "LNbits" @@ -59,7 +73,17 @@ "title": "LNDHub" }, "citadel": { - "title": "Citadel" + "title": "Citadel", + "url": { + "label": "Citadel URL", + "placeholder": "http://citadel.local" + }, + "page": { + "title": "Σύνδεση σε κόμβο <0>Citadel" + }, + "password": { + "label": "Κωδικός Citadel" + } }, "start9": { "title": "Start9" @@ -73,7 +97,11 @@ "eclair": { "title": "Eclair", "url": { - "placeholder": "http://localhost:8080" + "placeholder": "http://localhost:8080", + "label": "Eclair URL" + }, + "password": { + "label": "Κωδικός Eclair" } }, "kollider": { @@ -85,6 +113,14 @@ "port": { "label": "Θύρα" } + }, + "title": "Σύνδεση με πορτοφόλι Lightning", + "description": "Σύνδεση σε εξωτερικό πορτοφόλι lightning ή σε κόμβο", + "umbrel": { + "page": { + "title": "Σύνδεση σε κόμβο <0>Umbrel" + }, + "title": "Umbrel" } }, "distributions": { diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 0a0f178cd5..fb1b1da9b9 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -530,7 +530,6 @@ }, "webln_enable": { "title": "Connect to WebLN", - "request2": "Request invoices and lightning information" }, "alby_enable": { @@ -548,7 +547,7 @@ }, "webbtc_enable": { "title": "Connect to WebBTC", - "request2": "Request invoices and liquid information" + "request2": "Request invoices and Webbtc information" }, "unlock": { "unlock_to_continue": "Unlock to continue", @@ -828,7 +827,7 @@ }, "confirm_sign_pset": { "title": "Sign", - "allow_sign": "This website asks you to sign a Liquid Transaction:", + "allow_sign": "This website asks you to sign a Liquid transaction", "view_details": "View details", "hide_details": "Hide details", "view_raw_transaction": "View raw transaction (Base64)", @@ -841,6 +840,17 @@ "confirm_get_address": { "title": "Get Address" }, + "confirm_sign_psbt": { + "title": "Sign", + "allow_sign": "This website asks you to sign a bitcoin transaction", + "view_details": "View details", + "hide_details": "Hide details", + "view_raw_transaction": "View raw transaction (Hex)", + "hide_raw_transaction": "Hide raw transaction (Hex)", + "inputs": "Inputs", + "outputs": "Outputs", + "fee": "Fee" + }, "allow": "Allow this website to:", "allow_sign": "Allow {{host}} to sign:", "block_and_ignore": "Block and ignore {{host}}", @@ -890,6 +900,13 @@ "time_estimate": "ℹ️ Transactions usually arrive within 10-30 minutes.", "service_unavailable": "Service temporary not available, please try again in a few minutes." }, + "confirm_payment_async": { + "title": "Approve Payment", + "description": "This is a HOLD invoice. <0>Learn more »", + "actions": { + "pay_now": "Pay now" + } + }, "confirm_payment": { "title": "Approve Payment", "success": "Payment of {{amount}} successful!", diff --git a/src/i18n/locales/pt_BR/translation.json b/src/i18n/locales/pt_BR/translation.json index c1e2958b4b..d64ea8b21a 100644 --- a/src/i18n/locales/pt_BR/translation.json +++ b/src/i18n/locales/pt_BR/translation.json @@ -465,7 +465,7 @@ }, "subtitle": "Escolha a rede para gerar endereços e decodificar transações bitcoin e liquid" }, - "no_mnemonic_hint": "💡 Você ainda não tem uma chave mestra. <0>Clique aqui
para criar sua chave mestra e desbloquear essas configurações." + "no_mnemonic_hint": "💡 Você ainda não tem uma chave mestra. <0>Clique aqui
para gerar sua chave mestra e desbloquear essas configurações." } }, "unlock": { @@ -608,12 +608,12 @@ "actions": { "create_invoice": "Gerar fatura", "redeem": { - "description": "Receba bitcoin usando um código LNURL", - "title": "Resgatar" + "description": "Receba bitcoin de forma instantânea usando um código LNURL", + "title": "Vale-satoshi" }, "invoice": { "title": "Fatura relâmpago", - "description": "Solicite pagamentos instantâneos e de valores específicos" + "description": "Receba pagamentos bitcoin de valores específicos e de forma instantânea" }, "bitcoin_address": { "description": "Receba via endereço bitcoin usando um serviço de trocas", @@ -740,7 +740,7 @@ "confirm_request_permission": { "title": "Aprovar solicitação", "allow": "Permitir este site executar:", - "always_allow": "Lembrar minha escolha e não perguntar novamente" + "always_allow": "Lembrar e não perguntar novamente" }, "discover": { "title": "Explore o ecossistema ⚡️", @@ -766,7 +766,7 @@ "title": "Sua carteira Alby está pronta", "description": "Algumas dicas para você começar a usar a Alby 🐝", "mnemonic": { - "description": "Planejando usar apps Nostr?\nClique aqui para gerar suas chaves.", + "description": "Planejando usar apps Nostr?\nClique aqui para configurar suas chaves.", "title": "Nostr" } } @@ -843,7 +843,7 @@ "instructions2": "Seu endereço bitcoin está na página <0>Receber." }, "lnurlredeem": { - "title": "Resgatar bitcoin", + "title": "Resgatar vale-satoshi", "input": { "placeholder": "LNURL...", "label": "Código LNURL" @@ -867,7 +867,7 @@ "outputs": "Saídas", "amount": "{{amount}} {{ticker}}", "title": "Assinar", - "allow_sign": "Este site solicita que você assine uma transação Liquid:", + "allow_sign": "Este site solicita que você assine uma transação liquid", "inputs": "Entradas" }, "bitcoin": { @@ -877,7 +877,18 @@ "allow": "Permitir este site:", "allow_sign": "Permitir {{host}} assinar:", "block_and_ignore": "Bloquear e ignorar {{host}}", - "block_added": "{{host}} adicionado na lista de bloqueios, recarregue o site." + "block_added": "{{host}} adicionado na lista de bloqueios, recarregue o site.", + "confirm_sign_psbt": { + "title": "Assinar", + "hide_raw_transaction": "Ocultar transação bruta (Hex)", + "view_raw_transaction": "Ver transação bruta (Hex)", + "inputs": "Entradas", + "fee": "Taxa", + "hide_details": "Ocultar detalhes", + "allow_sign": "Este site solicita que você assine uma transação de bitcoin", + "view_details": "Ver detalhes", + "outputs": "Saídas" + } }, "liquid": { "title": "Liquid", @@ -907,19 +918,48 @@ "provider": { "label": "Serviço provedor de trocas" }, - "time_estimate": "ℹ️ As transações geralmente são recebidas dentro de 10 a 30 minutos." + "time_estimate": "ℹ️ As transações geralmente são recebidas no prazo de 10 a 30 minutos.", + "title": "Enviar para um endereço bitcoin", + "recipient": { + "label": "Endereço" + } }, "onboard": { "request2": "Você ainda não configurou suas chaves", "request3": "A configuração é rápida e fácil", "title": "Configure suas chaves", "actions": { - "start_setup": "Iniciar configuração" - } + "start_setup": "Configurar" + }, + "request1": "Interagir com este site exige configurar uma chave" }, "nostr_enable": { "request2": "Assinar eventos usando sua chave privada Nostr", - "request1": "Solicitar a leitura de sua chave pública Nostr" + "request1": "Solicitar a leitura de sua chave pública Nostr", + "title": "Conectar no Nostr" + }, + "webln_enable": { + "title": "Conectar no WebLN", + "request2": "Solicitar faturas e informações da rede relâmpago" + }, + "webbtc_enable": { + "title": "Conectar no WebBTC", + "request2": "Solicitar faturas e informações Webbtc" + }, + "liquid_enable": { + "title": "Conectar na Liquid", + "request2": "Solicitar faturas e informações da liquid" + }, + "alby_enable": { + "title": "Conectar na Alby", + "request2": "Solicitar faturas e informações da alby" + }, + "confirm_payment_async": { + "title": "Aprovar pagamento", + "description": "Esta é uma fatura do tipo HOLD. <0>Saiba mais »", + "actions": { + "pay_now": "Pagar agora" + } } }, "common": { @@ -956,7 +996,7 @@ "copy_invoice": "Copiar código", "back": "Voltar", "log_in": "Fazer login", - "remember": "Lembrar minha escolha e não perguntar novamente", + "remember": "Lembrar e não perguntar novamente", "receive_again": "Receber outro pagamento", "transactions": "Transações", "more": "Mais", @@ -966,7 +1006,8 @@ "download": "Baixar", "copy_clipboard": "Copiar para área de transferência", "disconnect": "Desconectar", - "copied_to_clipboard": "Copiado para a área de transferência" + "copied_to_clipboard": "Copiado para a área de transferência", + "review": "Revisar" }, "errors": { "connection_failed": "Falha na conexão", @@ -977,7 +1018,7 @@ "message": "Mensagem", "help": "Ajuda", "response": "Resposta", - "success_message": "{{amount}}{{fiatAmount}} estão a caminho de {{destination}}", + "success_message": "{{amount}}{{fiatAmount}} enviados para {{destination}}", "advanced": "Avançado", "discover": "Explorar", "confirm_password": "Confirmar senha", @@ -1024,7 +1065,7 @@ "screen_reader": "Opções limite de gastos" }, "enable_login": { - "title": "Login relâmpago", + "title": "Login", "subtitle": "O login é realizado de forma automática, sem necessidade de confirmação." }, "edit_permissions": "Permissões" @@ -1093,7 +1134,7 @@ }, "badge": { "label": { - "auth": "LOGIN RELÂMPAGO", + "auth": "LOGIN", "imported": "IMPORTADA", "budget": "LIMITE DE GASTOS" } diff --git a/src/i18n/locales/sl/translation.json b/src/i18n/locales/sl/translation.json index e1c13197df..b170e75d94 100644 --- a/src/i18n/locales/sl/translation.json +++ b/src/i18n/locales/sl/translation.json @@ -165,6 +165,9 @@ }, "privKey": { "label": "Lokalni zasebni ključ (samodejno generiran)" + }, + "errors": { + "connection_failed": "Neuspešna povezava. Je tvoje Core Lightning vozlišče povezano in uporablja commando vtičnik?" } }, "lnc": { @@ -240,7 +243,8 @@ "umbrel": { "title": "Umbrel", "page": { - "title": "Poveži <0>Umbrel vozlišče" + "title": "Poveži <0>Umbrel vozlišče", + "instructions": "1. V tvoji nadzorni plošči Lightning vozliška pojdi na <0>Poveži denarnico<1/>2. Izberi <0>REST (Tor) ali <0>REST (Local Network) način<1/>3. Skopiraj <0>lndconnect URL in ga prilepi spodaj" }, "rest_url": { "label": "lndconnect REST URL", @@ -260,6 +264,12 @@ "invalid_uri": "Nepravilni LNDHub URI", "connection_failed": "Povezava ni bila uspešna. Je tvoj LNDHub URI pravilen?" } + }, + "blink": { + "page": { + "title": "Poveži z <0>Blink denarnico" + }, + "title": "Blink denarnica" } }, "welcome": { @@ -340,7 +350,12 @@ "recent_transactions": "Pretekle transakcije", "allowance_view": { "recent_transactions": "Pretekle transakcije", - "no_transactions": "NA <0>{{name}} še ni transakcij." + "no_transactions": "NA <0>{{name}} še ni transakcij.", + "sats": "sats", + "budget_spent": "Porabljen proračun", + "total_spent": "Porabljeno skupaj", + "total_payments": "Skupaj plačila", + "permissions": "Pravice" }, "default_view": { "recent_transactions": "Pretekle transakcije", @@ -360,7 +375,8 @@ "screen_reader": "Izvozi podrobnosti računa", "your_ln_address": "Tvoj Lightning naslov:", "waiting": "čakanje na LndHub podatke...", - "export_uri": "LNDHub URI poverilnice" + "export_uri": "LNDHub URI poverilnice", + "scan_qr": "Uvozi to denarnico v Zeus ali BlueWallet s skeniranjem QR kode." }, "name": { "title": "Prikazno ime", @@ -371,7 +387,10 @@ "saved": "Glavni ključ kriptiran in uspešno shranjen.", "generate": { "title": "Generiraj nov Glavni ključ", - "button": "Generiraj Glavni ključ" + "button": "Generiraj Glavni ključ", + "confirm": "Naredil sem varnostno kopijo fraze in shranil na varno mesto.", + "description": "Glavni ključ ti omogoča interakcijo z različnimi protokoli, kot so: Nostr, Bitcoin omrežje ali LNURL-Auth.", + "error_confirm": "Prosim potrdi, da si naredil varnostno kopijo obnovitvene fraze." }, "backup": { "title": "Varnostno kopiraj obnovitveno frazo", @@ -395,7 +414,11 @@ "title": "Obnovitvena fraza" }, "lnurl": { - "title": "Prijava z Lightning" + "title": "Prijava z Lightning", + "use_mnemonic": "Uporabi Glavni ključ za prijavo v Lightning omogočene aplikacije (LNURL Auth)" + }, + "new": { + "title": "Kaj je Glavni ključ?" } }, "network": { @@ -410,7 +433,9 @@ "settings": { "label": "Nostr nastavitve", "remove": "Odstrani trenutne ključe", - "title": "Nostr nastavitve" + "title": "Nostr nastavitve", + "can_restore": "✅ Nostr ključ, izpeljan iz vašega glavnega ključa", + "derive": "Izpelji iz glavnega ključa" }, "setup": { "title": "Konfiguriraj Nostr ključe", @@ -421,12 +446,17 @@ "label": "Uporabi Nostr zasebni ključ" }, "recovery_phrase": { - "label": "Uporabi obnovitveno frazo" - } + "label": "Uporabi obnovitveno frazo", + "description": "Za uvoz ključev Nostr uporabite frazo za obnovitev glavnega ključa" + }, + "description": "Uporabite obstoječi zasebni ključ Nostr ali ga izpeljite iz svojega glavnega ključa", + "title": "Kako želite uvoziti svoj račun Nostr?" }, "new": { - "label": "Ustvari nov Nostr račun" - } + "label": "Ustvari nov Nostr račun", + "description": "Ustvarite glavni ključ s parom novih ključev Nostr." + }, + "new_to_nostr": "Ste novi pri Nostru? <0>Preverite več" }, "title": "Nostr", "public_key": { @@ -436,20 +466,26 @@ "screen_reader": "Generiraj nov Nostr ključ za svoj račun", "title": "Generiraj nov Nostr ključ", "actions": { - "random_keys": "Generiraj naključni ključ" + "random_keys": "Generiraj naključni ključ", + "derived_keys": "Izpelji ključ iz računa" } }, "private_key": { - "label": "Zasebni ključ Nostr" + "label": "Zasebni ključ Nostr", + "title": "Upravljajte svoj zasebni ključ Nostr" }, "actions": { "generate": "Generiraj nov ključ" + }, + "errors": { + "failed_to_load": "Nalaganje ključa Nostr ni uspelo. Je to veljaven ključ Nostr?" } }, "title1": "Nastavitve računa", "remove": { "title": "Odstrani ta račun", - "error": "Vnešeno ime računa se ne ujema." + "error": "Vnešeno ime računa se ne ujema.", + "subtitle": "Odstrani vse odobritve, podatke o plačilih in ključe, povezane s tem računom." }, "remove_secretkey": { "subtitle": "Odstrani Glavni ključ iz tega računa.", @@ -484,7 +520,153 @@ "browser_notifications": { "title": "Obvestila brskalnika" }, - "title": "Nastavitve" + "title": "Nastavitve", + "personal_data": { + "title": "Osebni podatki" + }, + "camera_access": { + "allow": "Omogoči dostop do kamere", + "subtitle": "Za skeniranje QR kod", + "title": "Dostop do kamere", + "granted": "Dovoljenje omogočeno" + }, + "lnurl_auth": { + "legacy_lnurl_auth": { + "title": "Starerjša verzija podpisovanja za LNDhub in LNbits" + }, + "title": "LNURL-Auth", + "legacy_lnurl_auth_202207": { + "title": "Stara verzija LNURL-Auth" + } + }, + "currency": { + "title": "Valuta", + "subtitle": "Zneske dodatno prikaži v tej valuti" + }, + "exchange": { + "subtitle": "Vir menjalnih tečajev bitcoinov", + "title": "Vir tečaja" + }, + "show_fiat": { + "title": "Sats v Fiat", + "subtitle": "Vedno pretvori v izbrano valuto iz izbrane menjalnice" + }, + "language": { + "title": "Jezik", + "subtitle": "Prevodi še niso 100% dokončani. <0>Pomagajte nam prevesti Alby v vaš jezik!" + }, + "theme": { + "title": "Tema", + "options": { + "system": "Sistem", + "dark": "Temna", + "light": "Svetla" + }, + "subtitle": "Uporabi Alby v temnem ali svetlem načinu" + }, + "website_enhancements": { + "subtitle": "Izboljšave napitnin za Twitter, YouTube itd.", + "title": "Izboljšave spletne strani" + } + }, + "webln_enable": { + "title": "Poveži se z WebLN", + "request2": "Zahtevaj račune in Lightning informacije" + }, + "make_invoice": { + "title": "Ustvari račun", + "amount": { + "label": "Znesek (Satoshi)" + }, + "errors": { + "amount_too_small": "Znesek je manjši on minimalne vrednosti", + "amount_too_big": "Znesek je višji od maksimalne vrednosti" + }, + "memo": { + "label": "Beležka" + } + }, + "on_chain": { + "go": "Pojdi na svoj račun Alby →", + "title": "Prejmi preko bitcoin naslova", + "instructions1": "Če želiš prejemati bitcoin prek naslova bitcoin, se prijavi v svoj <0>Alby račun na getalby.com", + "instructions2": "Svoj bitcoin naslov najdete na strani <0>Prejmi." + }, + "confirm_sign_pset": { + "allow_sign": "To spletno mesto zahteva, da podpišeš Liquid transakcijo", + "hide_details": "Skrij podrobnosti", + "view_details": "Poglej podrobnosti", + "title": "Podpiši" + }, + "publishers": { + "description": "Spletna mesta, kjer si že uporabljal Alby", + "discover": "Okdrij spletna mesta", + "publisher": { + "allowance": { + "used_budget": "porabljeni satoshiji", + "title": "Odobritev" + } + }, + "no_info": "Videti je, da Albyja še nisi uporabljal na nobenem spletnem mestu.", + "title": "Tvoja ⚡ spletna stran" + }, + "discover": { + "tips": { + "demo": { + "title": "Alby demo", + "description": "Odkrijte Albyjeve lastnosti\nna naši predstavitveni spletni strani" + }, + "mnemonic": { + "title": "Nostr" + }, + "top_up_wallet": { + "description": "Potrebuješ polnitev?\nSkupaj zmoremo!", + "title": "Kupi Bitcoin" + }, + "description": "Nekaj nasvetov, da začneš brneti🐝", + "title": "Tvoja denarnica Alby je pripravljena" + }, + "list": { + "gaming": "Igre", + "trading": "Trgovanje", + "nostr": "Nostr", + "shopping": "Nakupovanje", + "nodeManagement": "Upravljanje vozlišča", + "showcases": "Primeri", + "miscellaneous": "Razno", + "entertainment": "Zabava" + }, + "description": "Spletna mesta in spletne aplikacije, kjer lahko uporabljaš Alby", + "title": "Razišči ekosistem Lightning ⚡️" + }, + "receive": { + "payment": { + "status": "Preveri status plačila" + } + }, + "confirm_sign_message": { + "title": "Podpiši", + "content": "To spletno mesto te prosi, da podpišeš:" + }, + "liquid_enable": { + "request2": "Pridobi račune in podatke o liquid", + "title": "Poveži se z Liquid" + }, + "nostr_enable": { + "title": "Povežite se z Nostr", + "request2": "Podpiše dogodke s tvojim zasebnim ključem Nostr", + "request1": "Zahteva dostop do javnega ključa Nostr" + }, + "webbtc_enable": { + "request2": "Pridobi račune in podatke o Webbtc", + "title": "Poveži se z WebBTC" + }, + "alby_enable": { + "title": "Poveži se z Albyjem", + "request2": "Zahtevajte račune in podatke o albi" + }, + "scan_qrcode": { + "title": "Čakam na skeniranje" } }, "permissions": { diff --git a/src/i18n/locales/sv/translation.json b/src/i18n/locales/sv/translation.json index ad9e2ab144..ae6214b1a5 100644 --- a/src/i18n/locales/sv/translation.json +++ b/src/i18n/locales/sv/translation.json @@ -303,7 +303,12 @@ "recent_transactions": "Senaste Transaktioner", "allowance_view": { "recent_transactions": "Senaste Transaktioner", - "no_transactions": "Inga transaktioner för <0>{{name}} ännu." + "no_transactions": "Inga transaktioner för <0>{{name}} ännu.", + "sats": "sats", + "budget_spent": "Budget förbrukad", + "total_spent": "Totalt spenderat", + "total_payments": "Totala betalningar", + "permissions": "Behörigheter" }, "default_view": { "recent_transactions": "Senaste Transaktioner", @@ -339,7 +344,8 @@ "label": "Nostr Privat Nyckel", "subtitle": "Klistra in din nostr privata nyckel eller skapa en ny. <0>Läs mer »", "success": "Nostr privat nyckel krypterad och sparad.", - "failed_to_remove": "Det angivna kontonamnet matchade inte, din gamla Nostr privata nyckel har återställts." + "failed_to_remove": "Det angivna kontonamnet matchade inte, din gamla Nostr privata nyckel har återställts.", + "warning": "Vänligen ange namnet på kontot för att bekräfta raderingen av din nostr privata nyckel:\n\n{{ name }}" }, "public_key": { "label": "Nostr Publik Nyckel" @@ -386,7 +392,8 @@ "remove": { "title": "Ta bort detta konto", "subtitle": "Tar bort alla traktamenten, betalningsdata och nycklar som är kopplade till detta konto.", - "error": "Det angivna kontonamnet matchade inte." + "error": "Det angivna kontonamnet matchade inte.", + "confirm": "Ange namnet på kontot för att bekräfta raderingen av ditt konto.\n\n{{ name }}\n\n ⚠️ Alla associerade nycklar (Master Key, Nostr, etc) kommer att raderas om du fortsätter. Se till att du har säkerhetskopierat dem, det finns inget annat sätt att återställa dem." }, "title1": "Konto inställningar", "title2": "Konto", @@ -430,7 +437,8 @@ "items": { "recovery_phrase": "Du kan alltid komma åt din huvudnyckel genom att använda dess återställningsfras.", "words": "Återställningsfrasen består av 12 ord som fungerar som ett lösenord, men det kan inte ändras eller återställas om det tappas bort.", - "storage": "Se till att skriva ner det någonstans säkert och privat!" + "storage": "Se till att skriva ner det någonstans säkert och privat!", + "keys": "Master Key låter dig interagera med olika protokoll som: Nostr, Liquid eller baslager av Bitcoin." } }, "import": { @@ -445,12 +453,16 @@ "lnurl": { "title": "Logga in med Lightning", "use_mnemonic": "Använd Master Key för att logga in på lightning appar (LNURL Auth)" + }, + "new": { + "title": "Vad är en Master Key?" } }, "remove_secretkey": { "title": "Ta bort huvudnyckeln", "subtitle": "Tar bort huvudnyckeln från det här kontot.", - "success": "Masternyckeln har tagits bort." + "success": "Masternyckeln har tagits bort.", + "confirm": "Ange namnet på kontot för att bekräfta raderingen av din huvudnyckel.\n\n{{ name }}" }, "no_mnemonic_hint": "💡 Du har ingen huvudnyckel än. <0>Klicka här
för att skapa din huvudnyckel och låsa upp dessa inställningar.", "network": { @@ -630,7 +642,19 @@ "receive": { "title": "Tag emot", "actions": { - "create_invoice": "Skapa Faktura" + "create_invoice": "Skapa Faktura", + "redeem": { + "description": "Ta ut en bitcoin-kupong direkt via en LNURL-kod", + "title": "Lösa in" + }, + "invoice": { + "title": "Lightning faktura", + "description": "Begär omedelbara och specifika belopp bitcoinbetalningar" + }, + "bitcoin_address": { + "description": "Ta emot via bitcoin-adress med en bytestjänst", + "title": "Bitcoin adress" + } }, "amount": { "label": "Belopp", @@ -878,7 +902,18 @@ "allow": "Tillåt den här webbplatsen att:", "allow_sign": "Tillåt {{host}} att signera:", "block_and_ignore": "Blockera och ignorera {{host}}", - "block_added": "Lade till {{host}} till blockeringslistan, ladda om webbplatsen." + "block_added": "Lade till {{host}} till blockeringslistan, ladda om webbplatsen.", + "confirm_sign_psbt": { + "title": "Signera", + "hide_raw_transaction": "Göm råtransaktion (hexadecimal)", + "view_raw_transaction": "Visa råtransaktion (hexadecimal)", + "inputs": "Ingångar", + "fee": "Avgift", + "hide_details": "Göm detaljer", + "allow_sign": "Den här webbplatsen ber dig att underteckna en bitcoin-transaktion", + "view_details": "Visa detaljer", + "outputs": "Utgångar" + } }, "liquid": { "title": "Liquid", @@ -913,6 +948,43 @@ }, "view_on_explorer": "Visa på mempool explorer", "time_estimate": "ℹ️ Transaktioner kommer vanligtvis inom 10-30 minuter." + }, + "webln_enable": { + "title": "Anslut till WebLN", + "request2": "Begär fakturor och lightning information" + }, + "onboard": { + "request2": "Du har inte konfigurerat dina nycklar än", + "request1": "Denna webbplats kräver nyckelinteraktion", + "request3": "Installationen är snabb och enkel", + "title": "Ställ in dina nycklar", + "actions": { + "start_setup": "Starta installationen" + } + }, + "liquid_enable": { + "request2": "Begär fakturor och likvida uppgifter", + "title": "Anslut till Liquid" + }, + "nostr_enable": { + "title": "Anslut till Nostr", + "request2": "Signera händelser med din privata Nostr-nyckel", + "request1": "Begär att läsa din publika Nostr-nyckel" + }, + "webbtc_enable": { + "request2": "Begär fakturor och Webbtc-information", + "title": "Anslut till WebBTC" + }, + "alby_enable": { + "title": "Anslut till Alby", + "request2": "Begär fakturor och albyinformation" + }, + "confirm_payment_async": { + "title": "Godkänn betalning", + "description": "Detta är en HOLD-faktura. <0>Läs mer »", + "actions": { + "pay_now": "Betala nu" + } } }, "common": { @@ -960,7 +1032,9 @@ "copy_clipboard": "Kopiera till urklipp", "paste": "Klistra in", "paste_clipboard": "Klistra in från urklipp", - "review": "Recension" + "review": "Recension", + "copied_to_clipboard": "Kopierade till urklipp", + "disconnect": "Koppla ifrån" }, "errors": { "connection_failed": "Anslutning misslyckades", @@ -995,21 +1069,28 @@ "citadel": "Citadel" }, "or": "eller", - "pasted": "Klistrat in!" + "pasted": "Klistrat in!", + "enable": { + "block_added": "Lade till {{host}} till blockeringslistan, ladda om webbplatsen.", + "request1": "Begär godkännande för transaktioner", + "allow": "Tillåt den här webbplatsen att:", + "block_and_ignore": "Blockera och ignorera {{host}}" + } }, "components": { "allowance_menu": { - "confirm_delete": "Är du säker på att du vill radera webbplatsen?", - "hint": "Detta nollställer nuvarande budget", + "confirm_delete": "Är du säker på att du vill koppla bort den här webbplatsen?", + "hint": "Om du ändrar detta återställs den nuvarande budgeten", "new_budget": { - "label": "Ny budget" + "label": "Betalningsbudget med ett klick", + "link_label": "Ställ in en budget för betalningar med ett klick" }, "enable_login": { "title": "Aktivera webbplatsinloggning", "subtitle": "Logga in automatiskt utan bekräftelse när webbplatsen begär det." }, "edit_allowance": { - "title": "Redigera Inställningar", + "title": "Webbplatsinställningar", "screen_reader": "Alternativ för Beloppsgräns" }, "edit_permissions": "Redigera behörigheter" @@ -1073,12 +1154,15 @@ } }, "publishers_table": { - "payments": "betalningar" + "payments": "betalningar", + "total": "totalt", + "budget": "budget" }, "badge": { "label": { "auth": "LOGGA IN", - "imported": "IMPORTERAD" + "imported": "IMPORTERAD", + "budget": "BUDGET" } }, "companion_download_info": { diff --git a/src/manifest.json b/src/manifest.json index f730c39b4f..0c6d9c5e06 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -113,6 +113,18 @@ ], "default_title": "Alby - Bitcoin Lightning Wallet" }, + "commands": { + "__chrome___execute_action": { + "suggested_key": { + "default": "Alt+Shift+A" + } + }, + "__firefox___execute_browser_action": { + "suggested_key": { + "default": "Alt+Shift+A" + } + } + }, "options_ui": { "page": "options.html", "open_in_tab": true, diff --git a/src/types.ts b/src/types.ts index 8e1d5e8da5..1bdb51b022 100644 --- a/src/types.ts +++ b/src/types.ts @@ -170,6 +170,7 @@ export type NavigationState = { sigHash?: string; description?: string; details?: string; + psbt?: string; requestPermission: { method: string; description: string; @@ -554,6 +555,20 @@ export interface MessageDecryptGet extends MessageDefault { action: "decrypt"; } +export interface MessageSignPsbt extends MessageDefault { + args: { + psbt: string; + }; + action: "signPsbt"; +} + +export interface MessageGetPsbtPreview extends MessageDefault { + args: { + psbt: string; + }; + action: "getPsbtPreview"; +} + export interface MessageBalanceGet extends MessageDefault { action: "getBalance"; } @@ -694,7 +709,6 @@ export interface RequestInvoiceArgs { export type Transaction = { amount?: string; boostagram?: Invoice["boostagram"]; - badges?: Badge[]; createdAt?: string; currency?: string; date: string; @@ -806,6 +820,7 @@ export interface Blocklist extends DbBlocklist {} export interface DbAllowance { createdAt: string; + enabledFor?: string[]; enabled: boolean; host: string; id?: number; @@ -844,8 +859,7 @@ export interface SettingsStorage { export interface Badge { label: "budget" | "auth" | "imported"; - color: string; - textColor: string; + className: string; } export interface Publisher @@ -937,3 +951,11 @@ export type EsploraAssetInfos = { }; export type EsploraAssetRegistry = Record; + +export type Address = { amount: number; address: string }; + +export type PsbtPreview = { + inputs: Address[]; + outputs: Address[]; + fee: number; +}; diff --git a/yarn.lock b/yarn.lock index 7abc149e11..2e969676a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -354,10 +354,18 @@ resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@bitcoin-design/bitcoin-icons-react@^0.1.9": - version "0.1.9" - resolved "https://registry.npmjs.org/@bitcoin-design/bitcoin-icons-react/-/bitcoin-icons-react-0.1.9.tgz" - integrity sha512-nJvTD1+zG/ffHdMeGQ39vdsmEFo9WcCIP1RlR7ZpZoP2H+IgKwzwow8VSY6ebroLoCT7WWtUPJQSbgQwgWYrFg== +"@bitcoin-design/bitcoin-icons-react@^0.1.10": + version "0.1.10" + resolved "https://registry.yarnpkg.com/@bitcoin-design/bitcoin-icons-react/-/bitcoin-icons-react-0.1.10.tgz#11acf1d386c1094d3eff673ca236345860762f64" + integrity sha512-f7GSutKHa4EK4LWI/phnGCJsN8fzFbVAVQ4F1MYxiza34LVmXmbgHUmdP/BR8ZeQSIbZLt19inpJZDBtQvYe4Q== + +"@bitcoinerlab/secp256k1@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@bitcoinerlab/secp256k1/-/secp256k1-1.0.5.tgz#4643ba73619c24c7c455cc63c6338c69c2cf187c" + integrity sha512-8gT+ukTCFN2rTxn4hD9Jq3k+UJwcprgYjfK/SQUSLgznXoIgsBnlPuARMkyyuEjycQK9VvnPiejKdszVTflh+w== + dependencies: + "@noble/hashes" "^1.1.5" + "@noble/secp256k1" "^1.7.1" "@commitlint/cli@^17.7.1": version "17.7.1" @@ -957,23 +965,21 @@ dependencies: "@noble/hashes" "1.3.1" -"@noble/curves@~0.8.3": - version "0.8.3" - resolved "https://registry.npmjs.org/@noble/curves/-/curves-0.8.3.tgz" - integrity sha512-OqaOf4RWDaCRuBKJLDURrgVxjLmneGsiCXGuzYB5y95YithZMA6w4uk34DHSm0rKMrrYiaeZj48/81EvaAScLQ== - dependencies: - "@noble/hashes" "1.3.0" - -"@noble/hashes@1.3.0": - version "1.3.0" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz" - integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg== - -"@noble/hashes@1.3.1", "@noble/hashes@^1.2.0", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": +"@noble/hashes@1.3.1", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": version "1.3.1" resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz" integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== +"@noble/hashes@^1.1.5": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + +"@noble/hashes@^1.2.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== + "@noble/secp256k1@^1.7.1": version "1.7.1" resolved "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz" @@ -1040,7 +1046,7 @@ resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.7.2.tgz" integrity sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A== -"@scure/base@1.1.1", "@scure/base@~1.1.0", "@scure/base@~1.1.1": +"@scure/base@1.1.1", "@scure/base@~1.1.0": version "1.1.1" resolved "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== @@ -1062,16 +1068,6 @@ "@noble/hashes" "~1.3.0" "@scure/base" "~1.1.0" -"@scure/btc-signer@^0.5.1": - version "0.5.1" - resolved "https://registry.npmjs.org/@scure/btc-signer/-/btc-signer-0.5.1.tgz" - 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.27.8": version "0.27.8" resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" @@ -2584,6 +2580,20 @@ bip66@^1.1.0: safe-buffer "^5.0.1" bitcoinjs-lib@^6.0.0, bitcoinjs-lib@^6.0.2: + 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" + +bitcoinjs-lib@^6.1.0: version "6.1.3" resolved "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.3.tgz" integrity sha512-TYXs/Qf+GNk2nnsB9HrXWqzFuEgCg0Gx+v3UW3B8VuceFHXVvhT+7hRnTSvwkX0i8rz2rtujeU6gFaDcFqYFDw== @@ -2715,9 +2725,9 @@ bs58@^5.0.0: dependencies: base-x "^4.0.0" -bs58check@<3.0.0, bs58check@^2.0.0: +bs58check@<3.0.0, bs58check@^2.0.0, bs58check@^2.1.2: version "2.1.2" - resolved "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA== dependencies: bs58 "^4.0.0" @@ -6708,13 +6718,6 @@ methods@~1.1.2: resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" integrity "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" -micro-packed@~0.3.2: - version "0.3.2" - resolved "https://registry.npmjs.org/micro-packed/-/micro-packed-0.3.2.tgz" - integrity sha512-D1Bq0/lVOzdxhnX5vylCxZpdw5LylH7Vd81py0DfRsKUP36XYpwvy8ZIsECVo3UfnoROn8pdKqkOzL7Cd82sGA== - dependencies: - "@scure/base" "~1.1.1" - micromatch@4.0.5, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" @@ -8326,7 +8329,7 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -ripemd160@^2.0.0, ripemd160@^2.0.1: +ripemd160@^2.0.0, 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== @@ -9889,10 +9892,10 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -wif@^2.0.6: +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= sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==" + resolved "https://registry.yarnpkg.com/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704" + integrity sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ== dependencies: bs58check "<3.0.0"