From b1c76525d3fedca8fe74f46ec8fd076692056000 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 30 Jun 2023 13:33:51 +0700 Subject: [PATCH 01/22] re-add signPSBT changes --- package.json | 3 + src/app/router/Prompt/Prompt.tsx | 2 + .../screens/ConfirmSignPsbt/index.test.tsx | 118 ++++++++++++ src/app/screens/ConfirmSignPsbt/index.tsx | 173 ++++++++++++++++++ src/common/lib/psbt.ts | 76 ++++++++ .../actions/webbtc/__tests__/signPsbt.test.ts | 100 ++++++++++ .../actions/webbtc/getInfo.ts | 2 +- .../background-script/actions/webbtc/index.ts | 15 +- .../actions/webbtc/signPsbt.ts | 126 +++++++++++++ .../actions/webbtc/signPsbtWithPrompt.ts | 26 +++ src/extension/background-script/router.ts | 2 + src/extension/content-script/onendwebbtc.js | 1 + src/extension/providers/webbtc/index.ts | 8 + src/fixtures/btc.ts | 11 ++ src/types.ts | 7 + webpack.config.js | 5 + yarn.lock | 46 ++++- 17 files changed, 714 insertions(+), 7 deletions(-) create mode 100644 src/app/screens/ConfirmSignPsbt/index.test.tsx create mode 100644 src/app/screens/ConfirmSignPsbt/index.tsx create mode 100644 src/common/lib/psbt.ts create mode 100644 src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts create mode 100644 src/extension/background-script/actions/webbtc/signPsbt.ts create mode 100644 src/extension/background-script/actions/webbtc/signPsbtWithPrompt.ts diff --git a/package.json b/package.json index ef96434c9a..01f8861d4d 100644 --- a/package.json +++ b/package.json @@ -46,10 +46,12 @@ "@vespaiach/axios-fetch-adapter": "^0.3.0", "axios": "^0.27.2", "bech32": "^2.0.0", + "bitcoinjs-lib": "^6.1.0", "bolt11": "^1.4.1", "crypto-js": "^4.1.1", "dayjs": "^1.11.7", "dexie": "^3.2.3", + "ecpair": "^2.1.0", "elliptic": "^6.5.4", "html5-qrcode": "^2.3.8", "i18next": "^22.4.15", @@ -70,6 +72,7 @@ "react-toastify": "^9.1.3", "stream": "^0.0.2", "tailwindcss": "^3.3.2", + "tiny-secp256k1": "^2.2.1", "uuid": "^9.0.0", "webextension-polyfill": "^0.10.0", "zustand": "^3.7.2" diff --git a/src/app/router/Prompt/Prompt.tsx b/src/app/router/Prompt/Prompt.tsx index 536e19458a..6ce0f6a358 100644 --- a/src/app/router/Prompt/Prompt.tsx +++ b/src/app/router/Prompt/Prompt.tsx @@ -20,6 +20,7 @@ import { ToastContainer } from "react-toastify"; import Providers from "~/app/context/Providers"; import RequireAuth from "~/app/router/RequireAuth"; import ConfirmGetAddress from "~/app/screens/ConfirmGetAddress"; +import ConfirmSignPsbt from "~/app/screens/ConfirmSignPsbt"; import type { NavigationState, OriginData } from "~/types"; // Parse out the parameters from the querystring. @@ -107,6 +108,7 @@ function Prompt() { } /> } /> } /> + } /> } /> { + 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: () => ({ + bitcoinNetwork: "regtest", + }), + getConnector: jest.fn(), +}; + +state.getState = jest.fn().mockReturnValue(mockState); + +// mock get settings +msg.request = jest.fn().mockReturnValue({ + bitcoinNetwork: "regtest", +}); + +describe("ConfirmSignMessage", () => { + test("render", async () => { + await act(async () => { + render( + + + + ); + }); + + const user = userEvent.setup(); + + await act(async () => { + await user.click(screen.getByText("View addresses")); + }); + await act(async () => { + await user.click(screen.getByText("View PSBT hex")); + }); + + // TODO: update copy + expect( + await screen.findByText( + "This website asks you to sign a Partially Signed Bitcoin Transaction:" + ) + ).toBeInTheDocument(); + + expect( + await screen.findByText(btcFixture.regtestTaprootPsbt) + ).toBeInTheDocument(); + + // Check inputs + const inputsContainer = (await screen.getByText("Input") + .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(); + }); +}); diff --git a/src/app/screens/ConfirmSignPsbt/index.tsx b/src/app/screens/ConfirmSignPsbt/index.tsx new file mode 100644 index 0000000000..b64222ee76 --- /dev/null +++ b/src/app/screens/ConfirmSignPsbt/index.tsx @@ -0,0 +1,173 @@ +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 { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import Hyperlink from "~/app/components/Hyperlink"; +import Loading from "~/app/components/Loading"; +import ScreenHeader from "~/app/components/ScreenHeader"; +import { useNavigationState } from "~/app/hooks/useNavigationState"; +import { USER_REJECTED_ERROR } from "~/common/constants"; +import api from "~/common/lib/api"; +import msg from "~/common/lib/msg"; +import { Address, PsbtPreview, getPsbtPreview } from "~/common/lib/psbt"; +import type { OriginData } from "~/types"; + +function ConfirmSignPsbt() { + const navState = useNavigationState(); + const { t: tCommon } = useTranslation("common"); + const { t } = useTranslation("translation", { + keyPrefix: "confirm_sign_psbt", + }); + const navigate = useNavigate(); + + const psbt = navState.args?.psbt as string; + const origin = navState.origin as OriginData; + const [loading, setLoading] = useState(false); + const [successMessage, setSuccessMessage] = useState(""); + const [preview, setPreview] = useState(undefined); + const [showAddresses, setShowAddresses] = useState(false); + const [showHex, setShowHex] = useState(false); + + useEffect(() => { + (async () => { + const settings = await api.getSettings(); + setPreview(getPsbtPreview(psbt, settings.bitcoinNetwork)); + })(); + }, [origin, psbt]); + + async function confirm() { + try { + setLoading(true); + const response = await msg.request("signPsbt", { psbt }, { origin }); + msg.reply(response); + setSuccessMessage(tCommon("success")); + } catch (e) { + console.error(e); + if (e instanceof Error) toast.error(`${tCommon("error")}: ${e.message}`); + } finally { + setLoading(false); + } + } + + function reject(e: React.MouseEvent) { + e.preventDefault(); + msg.error(USER_REJECTED_ERROR); + } + + function close(e: React.MouseEvent) { + if (navState.isPrompt) { + window.close(); + } else { + e.preventDefault(); + navigate(-1); + } + } + + function toggleShowAddresses() { + setShowAddresses((current) => !current); + } + function toggleShowHex() { + setShowHex((current) => !current); + } + + if (!preview) { + return ; + } + + return ( +
+ + {!successMessage ? ( + +
+ +
+ {t("warning")} +
+
+

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

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

{t("input")}

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

{t("outputs")}

+
+ {preview.outputs.map((output) => ( + + ))} +
+
+ )} +
+ + {showHex && ( +
+ {psbt} +
+ )} +
+ +
+ ) : ( + + + + + )} +
+ ); +} + +function AddressPreview({ + address, + amount, + t, +}: Address & { + t: TFunction<"translation", "confirm_sign_psbt", "translation">; +}) { + return ( +
+

{address}

+

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

+
+ ); +} + +export default ConfirmSignPsbt; diff --git a/src/common/lib/psbt.ts b/src/common/lib/psbt.ts new file mode 100644 index 0000000000..b92366005c --- /dev/null +++ b/src/common/lib/psbt.ts @@ -0,0 +1,76 @@ +import * as btc from "@scure/btc-signer"; +import { Psbt, networks } from "bitcoinjs-lib"; + +export type Address = { amount: number; address: string }; + +export type PsbtPreview = { + inputs: Address[]; + outputs: Address[]; +}; + +export function getPsbtPreview( + psbt: string, + networkType?: keyof typeof networks +): PsbtPreview { + const network = networkType ? networks[networkType] : undefined; + + const unsignedPsbt = Psbt.fromHex(psbt, { + network, + }); + + const preview: PsbtPreview = { + inputs: [], + outputs: [], + }; + + for (let i = 0; i < unsignedPsbt.data.inputs.length; i++) { + if (i > 0) { + throw new Error("Multiple inputs currently unsupported"); + } + + const tapBip32Derivation = unsignedPsbt.data.inputs[i].tapBip32Derivation; + if (!tapBip32Derivation) { + throw new Error("No bip32Derivation in input " + i); + } + const address = btc.p2tr( + tapBip32Derivation[0].pubkey, + undefined, + network + ).address; + + if (!address) { + throw new Error("No address found in input " + i); + } + const witnessUtxo = unsignedPsbt.data.inputs[i].witnessUtxo; + if (!witnessUtxo) { + throw new Error("No witnessUtxo in input " + i); + } + + preview.inputs.push({ + amount: witnessUtxo.value, + address, + }); + } + for (let i = 0; i < unsignedPsbt.data.outputs.length; i++) { + const txOutput = unsignedPsbt.txOutputs[i]; + const output = unsignedPsbt.data.outputs[i]; + if (!output.tapBip32Derivation) { + throw new Error("No tapBip32Derivation in output"); + } + const address = btc.p2tr( + output.tapBip32Derivation[0].pubkey, + undefined, + network + ).address; + if (!address) { + throw new Error("No address found in output " + i); + } + + const previewOutput: Address = { + amount: txOutput.value, + address, + }; + preview.outputs.push(previewOutput); + } + return preview; +} diff --git a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts new file mode 100644 index 0000000000..58ecd040db --- /dev/null +++ b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts @@ -0,0 +1,100 @@ +import { hex } from "@scure/base"; +import * as btc from "@scure/btc-signer"; +import { getPsbtPreview } from "~/common/lib/psbt"; +import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; +import state from "~/extension/background-script/state"; +import { btcFixture } from "~/fixtures/btc"; +import type { MessageSignPsbt } from "~/types"; + +const passwordMock = jest.fn; + +const mockState = { + password: passwordMock, + currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", + getAccount: () => ({ + mnemonic: btcFixture.mnemnoic, + }), + getConnector: jest.fn(), + settings: { + bitcoinNetwork: "regtest", + }, +}; + +state.getState = jest.fn().mockReturnValue(mockState); + +jest.mock("~/common/lib/crypto", () => { + return { + decryptData: jest.fn((encrypted, _password) => { + return encrypted; + }), + }; +}); + +beforeEach(async () => { + // fill the DB first +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +async function sendPsbtMessage(psbt: string, derivationPath?: string) { + const message: MessageSignPsbt = { + application: "LBE", + prompt: true, + action: "signPsbt", + origin: { + internal: true, + }, + args: { + psbt, + }, + }; + + return await signPsbt(message); +} + +describe("signPsbt", () => { + test("1 input, taproot, regtest", async () => { + 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); + + const checkTx = btc.Transaction.fromRaw(hex.decode(result.data.signed)); + expect(checkTx.isFinal).toBe(true); + expect(result.data?.signed).toBe(btcFixture.regtestTaprootSignedPsbt); + }); +}); + +describe("signPsbt input validation", () => { + test("invalid psbt", async () => { + const result = await sendPsbtMessage("test"); + expect(result.error).not.toBe(null); + }); +}); + +describe("decode psbt", () => { + test("get taproot transaction preview", async () => { + const preview = getPsbtPreview(btcFixture.regtestTaprootPsbt, "regtest"); + 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); + }); +}); 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/index.ts b/src/extension/background-script/actions/webbtc/index.ts index 4f78ae22f0..c03795d9a7 100644 --- a/src/extension/background-script/actions/webbtc/index.ts +++ b/src/extension/background-script/actions/webbtc/index.ts @@ -1,5 +1,14 @@ -import getAddress from "./getAddress"; -import getAddressWithPrompt from "./getAddressWithPrompt"; +import getAddress from "~/extension/background-script/actions/webbtc/getAddress"; +import getAddressWithPrompt from "~/extension/background-script/actions/webbtc/getAddressWithPrompt"; +import signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; + import getInfo from "./getInfo"; +import signPsbtWithPrompt from "./signPsbtWithPrompt"; -export { getAddress, getAddressWithPrompt, getInfo }; +export { + getAddress, + getAddressWithPrompt, + getInfo, + signPsbt, + signPsbtWithPrompt, +}; 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..1c9efd6484 --- /dev/null +++ b/src/extension/background-script/actions/webbtc/signPsbt.ts @@ -0,0 +1,126 @@ +import * as secp256k1 from "@noble/secp256k1"; +import * as bitcoin from "bitcoinjs-lib"; +import ECPairFactory, { ECPairAPI } from "ecpair"; +import * as tinysecp from "tiny-secp256k1"; +import { decryptData } from "~/common/lib/crypto"; +import { + BTC_TAPROOT_DERIVATION_PATH, + BTC_TAPROOT_DERIVATION_PATH_REGTEST, + derivePrivateKey, +} from "~/common/lib/mnemonic"; +import state from "~/extension/background-script/state"; +import { MessageSignPsbt } from "~/types"; + +const signPsbt = async (message: MessageSignPsbt) => { + try { + // TODO: is this the correct way to decrypt the mnmenonic? + const password = await state.getState().password(); + if (!password) { + throw new Error("No password set"); + } + const account = await state.getState().getAccount(); + if (!account) { + throw new Error("No account selected"); + } + if (!account.mnemonic) { + throw new Error("No mnemonic set"); + } + const mnemonic = decryptData(account.mnemonic, password); + const settings = state.getState().settings; + + const derivationPath = + settings.bitcoinNetwork === "bitcoin" + ? BTC_TAPROOT_DERIVATION_PATH + : BTC_TAPROOT_DERIVATION_PATH_REGTEST; + + const privateKey = secp256k1.utils.hexToBytes( + derivePrivateKey(mnemonic, derivationPath) + ); + + const taprootPsbt = bitcoin.Psbt.fromHex(message.args.psbt, { + network: bitcoin.networks[settings.bitcoinNetwork], + }); + + // fix usages of window (unavailable in service worker) + globalThis.window ??= globalThis.window || {}; + if (!globalThis.window.crypto) { + globalThis.window.crypto = crypto; + } + + bitcoin.initEccLib(tinysecp); + const ECPair: ECPairAPI = ECPairFactory(tinysecp); + + const keyPair = tweakSigner( + ECPair, + ECPair.fromPrivateKey(Buffer.from(privateKey), { + network: bitcoin.networks[settings.bitcoinNetwork], + }), + { + network: bitcoin.networks[settings.bitcoinNetwork], + } + ); + + // 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 { + data: { + signed: signedTransaction, + }, + }; + } catch (e) { + console.error("signPsbt failed: ", e); + return { + error: "signPsbt failed: " + e, + }; + } +}; + +export default signPsbt; + +// Below code taken from https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/taproot.spec.ts#L636 +const toXOnly = (pubKey: Buffer) => + pubKey.length === 32 ? pubKey : pubKey.slice(1, 33); + +function tweakSigner( + ECPair: ECPairAPI, + signer: 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 = tinysecp.privateNegate(privateKey); + } + + const tweakedPrivateKey = tinysecp.privateAdd( + privateKey, + tapTweakHash(toXOnly(signer.publicKey), opts.tweakHash) + ); + if (!tweakedPrivateKey) { + throw new Error("Invalid tweaked private key!"); + } + + return ECPair.fromPrivateKey(Buffer.from(tweakedPrivateKey), { + network: opts.network, + }); +} + +function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer { + return bitcoin.crypto.taggedHash( + "TapTweak", + Buffer.concat(h ? [pubKey, h] : [pubKey]) + ); +} diff --git a/src/extension/background-script/actions/webbtc/signPsbtWithPrompt.ts b/src/extension/background-script/actions/webbtc/signPsbtWithPrompt.ts new file mode 100644 index 0000000000..0d0ccef65c --- /dev/null +++ b/src/extension/background-script/actions/webbtc/signPsbtWithPrompt.ts @@ -0,0 +1,26 @@ +import utils from "~/common/lib/utils"; +import { Message } from "~/types"; + +const signPsbtWithPrompt = async (message: Message) => { + const psbt = message.args.psbt; + if (typeof psbt !== "string") { + return { + error: "PSBT missing.", + }; + } + + try { + const response = await utils.openPrompt({ + ...message, + action: "confirmSignPsbt", + }); + return response; + } catch (e) { + console.error("signPsbt cancelled", e); + if (e instanceof Error) { + return { error: e.message }; + } + } +}; + +export default signPsbtWithPrompt; diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index 3a51420006..812c5b7564 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -59,6 +59,7 @@ const routes = { lnurl: lnurl, lnurlAuth: auth, getCurrencyRate: cache.getCurrencyRate, + signPsbt: webbtc.signPsbt, getAddress: webbtc.getAddress, setMnemonic: mnemonic.setMnemonic, getMnemonic: mnemonic.getMnemonic, @@ -77,6 +78,7 @@ const routes = { webbtc: { enable: allowances.enable, getInfo: webbtc.getInfo, + signPsbtWithPrompt: webbtc.signPsbtWithPrompt, getAddressWithPrompt: webbtc.getAddressWithPrompt, }, alby: { diff --git a/src/extension/content-script/onendwebbtc.js b/src/extension/content-script/onendwebbtc.js index ffec3824b2..9a60ad35b0 100644 --- a/src/extension/content-script/onendwebbtc.js +++ b/src/extension/content-script/onendwebbtc.js @@ -8,6 +8,7 @@ import shouldInject from "./shouldInject"; const webbtcCalls = [ "webbtc/enable", "webbtc/getInfo", + "webbtc/signPsbtWithPrompt", "webbtc/getAddressWithPrompt", ]; // calls that can be executed when `window.webbtc` is not enabled for the current content page diff --git a/src/extension/providers/webbtc/index.ts b/src/extension/providers/webbtc/index.ts index f3cf99e047..9bfaf376d1 100644 --- a/src/extension/providers/webbtc/index.ts +++ b/src/extension/providers/webbtc/index.ts @@ -37,6 +37,14 @@ export default class WebBTCProvider { return this.execute("getInfo"); } + signPsbt(psbt: string) { + if (!this.enabled) { + throw new Error("Provider must be enabled before calling signPsbt"); + } + + return this.execute("signPsbtWithPrompt", { psbt }); + } + sendTransaction(address: string, amount: string) { if (!this.enabled) { throw new Error( diff --git a/src/fixtures/btc.ts b/src/fixtures/btc.ts index c843532f74..5b6f46ce88 100644 --- a/src/fixtures/btc.ts +++ b/src/fixtures/btc.ts @@ -1,4 +1,15 @@ export const btcFixture = { + // generated in sparrow wallet using mock mnemonic below, + // native taproot derivation: m/86'/1'/0' - 1 input ("m/86'/1'/0'/0/0" - first receive address), 2 outputs, saved as binary PSBT file + // imported using `cat taproot.psbt | xxd -p -c 1000` + regtestTaprootPsbt: + "70736274ff0100890200000001b58806ecf5f7a2677dce4d357cc3ef14c59586db851f253e7d495b6506e7c8210100000000fdffffff02a54a4c0000000000225120d73acf6667b7850e6e00e21bb4525a2c5e2d88956a3d2c35b0bc06f4b0df01bc404b4c00000000002251202befa14431d4cb71889ea1df7a7eaa2f1d8b9107e60b01564e15dabe5c0dfd32340100004f01043587cf03e017d1bb8000000001835c0b51218376c61455428a9f47bfcce1f6ce8397ead7b00387e9d9ea568302cfbd7100311e0e85844c3738728314394eb8302a6b5070d692e41b14ba8180901073c5da0a5600008001000080000000800001007d020000000184d4669ffd8232e83b7bf70fd8425b913f83e8664ab7128f196b70c33afc8d9e0100000000fdffffff02dc556202000000001600147d221583ec7f1023a7188ce4e8d2836ff96aac1380969800000000002251203b82b2b2a9185315da6f80da5f06d0440d8a5e1457fa93387c2d919c86ec87862a01000001012b80969800000000002251203b82b2b2a9185315da6f80da5f06d0440d8a5e1457fa93387c2d919c86ec878601030400000000211655355ca83c973f1d97ce0e3843c85d78905af16b4dc531bc488e57212d230116190073c5da0a560000800100008000000080000000000000000001172055355ca83c973f1d97ce0e3843c85d78905af16b4dc531bc488e57212d230116002107b10ac97f676cf1f3ccdacb0b78171282bbe94a94df143201700dc59bcc15f368190073c5da0a5600008001000080000000800100000000000000010520b10ac97f676cf1f3ccdacb0b78171282bbe94a94df143201700dc59bcc15f3680021073058679f6d60b87ef921d98a2a9a1f1e0779dae27bedbd1cdb2f147a07835ac9190073c5da0a56000080010000800000008000000000010000000105203058679f6d60b87ef921d98a2a9a1f1e0779dae27bedbd1cdb2f147a07835ac900", + + // signed PSBT and verified by importing in sparrow and broadcasting transaction + // echo hex | xxd -r -p > taproot_signed.psbt + regtestTaprootSignedPsbt: + "02000000000101b58806ecf5f7a2677dce4d357cc3ef14c59586db851f253e7d495b6506e7c8210100000000fdffffff02a54a4c0000000000225120d73acf6667b7850e6e00e21bb4525a2c5e2d88956a3d2c35b0bc06f4b0df01bc404b4c00000000002251202befa14431d4cb71889ea1df7a7eaa2f1d8b9107e60b01564e15dabe5c0dfd32014091d48b7c4bb1dc7cb4d0da360dfd0ca35ea1e73ca6f1891c25a6a3bd90a6269eaa2ee97bca15969181981eb1abb1c9ab8574add9453355b00b521069dca7dc1634010000", + mnemnoic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", }; diff --git a/src/types.ts b/src/types.ts index 21c9c9cf12..dd10c2210e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -518,6 +518,13 @@ export interface MessageDecryptGet extends MessageDefault { action: "decrypt"; } +export interface MessageSignPsbt extends MessageDefault { + args: { + psbt: string; + }; + action: "signPsbt"; +} + export interface MessageGetAddress extends MessageDefault { // eslint-disable-next-line @typescript-eslint/ban-types args: {}; diff --git a/webpack.config.js b/webpack.config.js index 6d3681ce90..d02d402f33 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -61,6 +61,11 @@ var options = { }, mode: nodeEnv, + experiments: { + // TODO: remove along with tiny-secp256k1 + asyncWebAssembly: true, + }, + entry: { manifest: "./src/manifest.json", background: "./src/extension/background-script/index.ts", diff --git a/yarn.lock b/yarn.lock index 26f2d6f561..32e97643b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2565,6 +2565,11 @@ bip174@^2.0.1: resolved "https://registry.npmjs.org/bip174/-/bip174-2.0.1.tgz" integrity sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ== +bip174@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/bip174/-/bip174-2.1.0.tgz#cd3402581feaa5116f0f00a0eaee87a5843a2d30" + integrity sha512-lkc0XyiX9E9KiVAS1ZiOqK1xfiwvf4FXDDdkDq5crcDzOq+xGytY+14qCsqz7kCiy8rpN1CRNfacRhf9G3JNSA== + bitcoinjs-lib@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-6.0.1.tgz#4fa9438bb86a0449451ac58607e83d9b5a7732e6" @@ -2578,6 +2583,20 @@ bitcoinjs-lib@^6.0.0: varuint-bitcoin "^1.1.2" wif "^2.0.1" +bitcoinjs-lib@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-6.1.0.tgz#2e3123d63eab5e8e752fd7e2f237314f35ed738f" + integrity sha512-eupi1FBTJmPuAZdChnzTXLv2HBqFW2AICpzXZQLniP0V9FWWeeUQSMKES6sP8isy/xO0ijDexbgkdEyFVrsuJw== + dependencies: + bech32 "^2.0.0" + bip174 "^2.1.0" + bs58check "^2.1.2" + create-hash "^1.1.0" + ripemd160 "^2.0.2" + typeforce "^1.11.3" + varuint-bitcoin "^1.1.2" + wif "^2.0.1" + bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -3919,6 +3938,15 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +ecpair@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ecpair/-/ecpair-2.1.0.tgz#673f826b1d80d5eb091b8e2010c6b588e8d2cb45" + integrity sha512-cL/mh3MtJutFOvFc27GPZE2pWL3a3k4YvzUWEOvilnfZVlH3Jwgx/7d6tlD7/75tNk8TG2m+7Kgtz0SI1tWcqw== + dependencies: + randombytes "^2.1.0" + typeforce "^1.18.0" + wif "^2.0.6" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" @@ -8534,7 +8562,7 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -ripemd160@^2.0.1: +ripemd160@^2.0.1, ripemd160@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz" integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== @@ -9421,6 +9449,13 @@ thunky@^1.0.2: resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== +tiny-secp256k1@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tiny-secp256k1/-/tiny-secp256k1-2.2.1.tgz#a61d4791b7031aa08a9453178a131349c3e10f9b" + integrity sha512-/U4xfVqnVxJXN4YVsru0E6t5wVncu2uunB8+RVR40fYUxkKYUPS10f+ePQZgFBoE/Jbf9H1NBveupF2VmB58Ng== + dependencies: + uint8array-tools "0.0.7" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -9639,7 +9674,7 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typeforce@^1.11.3: +typeforce@^1.11.3, typeforce@^1.18.0: version "1.18.0" resolved "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz" integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g== @@ -9668,6 +9703,11 @@ typeson@^6.0.0, typeson@^6.1.0: resolved "https://registry.yarnpkg.com/typeson/-/typeson-6.1.0.tgz#5b2a53705a5f58ff4d6f82f965917cabd0d7448b" integrity sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA== +uint8array-tools@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/uint8array-tools/-/uint8array-tools-0.0.7.tgz#a7a2bb5d8836eae2fade68c771454e6a438b390d" + integrity sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ== + unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz" @@ -10128,7 +10168,7 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -wif@^2.0.1: +wif@^2.0.1, wif@^2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz" integrity sha1-CNP1IFbGZnkplyb63g1DKudLRwQ= From 6a959dda9269cef67beb674991f4991674ef1eac Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 5 Sep 2023 15:02:47 +0700 Subject: [PATCH 02/22] feat: move signpsbt to bitcoin object --- package.json | 3 +- .../actions/accounts/__tests__/get.test.ts | 2 +- .../actions/lnurl/__tests__/auth.test.ts | 4 +- .../webbtc/__tests__/getAddress.test.ts | 6 +- .../actions/webbtc/__tests__/signPsbt.test.ts | 10 +- .../actions/webbtc/signPsbt.ts | 108 +------------ .../background-script/bitcoin/index.ts | 143 +++++++++++------- .../background-script/bitcoin/networks.ts | 56 +++++++ .../background-script/liquid/index.ts | 7 +- src/fixtures/btc.ts | 2 +- webpack.config.js | 5 - yarn.lock | 45 +++--- 12 files changed, 187 insertions(+), 204 deletions(-) create mode 100644 src/extension/background-script/bitcoin/networks.ts diff --git a/package.json b/package.json index 0f9930036e..743dc7543e 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ }, "dependencies": { "@bitcoin-design/bitcoin-icons-react": "^0.1.9", + "@bitcoinerlab/secp256k1": "^1.0.5", "@getalby/sdk": "^2.2.3", "@headlessui/react": "^1.7.16", "@lightninglabs/lnc-web": "^0.2.4-alpha", @@ -51,9 +52,9 @@ "bitcoinjs-lib": "^6.1.0", "bolt11": "^1.4.1", "crypto-js": "^4.1.1", - "ecpair": "^2.1.0", "dayjs": "^1.11.9", "dexie": "^3.2.4", + "ecpair": "^2.1.0", "elliptic": "^6.5.4", "events": "^3.3.0", "html5-qrcode": "^2.3.8", 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/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/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 index 58ecd040db..d16a28ba73 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts @@ -2,6 +2,8 @@ import { hex } from "@scure/base"; import * as btc from "@scure/btc-signer"; import { getPsbtPreview } from "~/common/lib/psbt"; 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 { MessageSignPsbt } from "~/types"; @@ -12,12 +14,12 @@ const mockState = { password: passwordMock, currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", getAccount: () => ({ - mnemonic: btcFixture.mnemnoic, + mnemonic: btcFixture.mnemonic, + bitcoinNetwork: "regtest", }), + getMnemonic: () => new Mnemonic(btcFixture.mnemonic), + getBitcoin: () => new Bitcoin(new Mnemonic(btcFixture.mnemonic), "regtest"), getConnector: jest.fn(), - settings: { - bitcoinNetwork: "regtest", - }, }; state.getState = jest.fn().mockReturnValue(mockState); diff --git a/src/extension/background-script/actions/webbtc/signPsbt.ts b/src/extension/background-script/actions/webbtc/signPsbt.ts index 1c9efd6484..23823827d6 100644 --- a/src/extension/background-script/actions/webbtc/signPsbt.ts +++ b/src/extension/background-script/actions/webbtc/signPsbt.ts @@ -1,76 +1,11 @@ -import * as secp256k1 from "@noble/secp256k1"; -import * as bitcoin from "bitcoinjs-lib"; -import ECPairFactory, { ECPairAPI } from "ecpair"; -import * as tinysecp from "tiny-secp256k1"; -import { decryptData } from "~/common/lib/crypto"; -import { - BTC_TAPROOT_DERIVATION_PATH, - BTC_TAPROOT_DERIVATION_PATH_REGTEST, - derivePrivateKey, -} from "~/common/lib/mnemonic"; import state from "~/extension/background-script/state"; import { MessageSignPsbt } from "~/types"; const signPsbt = async (message: MessageSignPsbt) => { try { - // TODO: is this the correct way to decrypt the mnmenonic? - const password = await state.getState().password(); - if (!password) { - throw new Error("No password set"); - } - const account = await state.getState().getAccount(); - if (!account) { - throw new Error("No account selected"); - } - if (!account.mnemonic) { - throw new Error("No mnemonic set"); - } - const mnemonic = decryptData(account.mnemonic, password); - const settings = state.getState().settings; - - const derivationPath = - settings.bitcoinNetwork === "bitcoin" - ? BTC_TAPROOT_DERIVATION_PATH - : BTC_TAPROOT_DERIVATION_PATH_REGTEST; - - const privateKey = secp256k1.utils.hexToBytes( - derivePrivateKey(mnemonic, derivationPath) - ); - - const taprootPsbt = bitcoin.Psbt.fromHex(message.args.psbt, { - network: bitcoin.networks[settings.bitcoinNetwork], - }); - - // fix usages of window (unavailable in service worker) - globalThis.window ??= globalThis.window || {}; - if (!globalThis.window.crypto) { - globalThis.window.crypto = crypto; - } - - bitcoin.initEccLib(tinysecp); - const ECPair: ECPairAPI = ECPairFactory(tinysecp); - - const keyPair = tweakSigner( - ECPair, - ECPair.fromPrivateKey(Buffer.from(privateKey), { - network: bitcoin.networks[settings.bitcoinNetwork], - }), - { - network: bitcoin.networks[settings.bitcoinNetwork], - } - ); - - // 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(); + const bitcoin = await state.getState().getBitcoin(); + const signedTransaction = await bitcoin.signPsbt(message.args.psbt); return { data: { signed: signedTransaction, @@ -85,42 +20,3 @@ const signPsbt = async (message: MessageSignPsbt) => { }; export default signPsbt; - -// Below code taken from https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/taproot.spec.ts#L636 -const toXOnly = (pubKey: Buffer) => - pubKey.length === 32 ? pubKey : pubKey.slice(1, 33); - -function tweakSigner( - ECPair: ECPairAPI, - signer: 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 = tinysecp.privateNegate(privateKey); - } - - const tweakedPrivateKey = tinysecp.privateAdd( - privateKey, - tapTweakHash(toXOnly(signer.publicKey), opts.tweakHash) - ); - if (!tweakedPrivateKey) { - throw new Error("Invalid tweaked private key!"); - } - - return ECPair.fromPrivateKey(Buffer.from(tweakedPrivateKey), { - network: opts.network, - }); -} - -function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer { - return bitcoin.crypto.taggedHash( - "TapTweak", - Buffer.concat(h ? [pubKey, h] : [pubKey]) - ); -} diff --git a/src/extension/background-script/bitcoin/index.ts b/src/extension/background-script/bitcoin/index.ts index 1a649ec330..43bd337656 100644 --- a/src/extension/background-script/bitcoin/index.ts +++ b/src/extension/background-script/bitcoin/index.ts @@ -1,11 +1,19 @@ 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"; 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; @@ -16,6 +24,53 @@ class Bitcoin { this.networkType = networkType; this.network = networks[this.networkType]; } + + signPsbt(psbt: string) { + 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 taprootPsbt = bitcoin.Psbt.fromHex(psbt, { + network: this.network, + }); + + // // fix usages of window (unavailable in service worker) + // globalThis.window ??= globalThis.window || {}; + // if (!globalThis.window.crypto) { + // globalThis.window.crypto = crypto; + // } + + bitcoin.initEccLib(ecc); + 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 = @@ -45,59 +100,41 @@ class Bitcoin { 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); -interface Bip32 { - public: number; - private: number; +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); + } + + 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..2a06821f78 --- /dev/null +++ b/src/extension/background-script/bitcoin/networks.ts @@ -0,0 +1,56 @@ +// from https://github1s.com/bitcoinjs/bitcoinjs-lib +export interface Network { + messagePrefix: string; + bech32: string; + bip32: Bip32; + pubKeyHash: number; + scriptHash: number; + wif: number; +} + +interface Bip32 { + public: number; + private: number; +} + +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, +}; 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/fixtures/btc.ts b/src/fixtures/btc.ts index 5b6f46ce88..b1e407a222 100644 --- a/src/fixtures/btc.ts +++ b/src/fixtures/btc.ts @@ -10,6 +10,6 @@ export const btcFixture = { regtestTaprootSignedPsbt: "02000000000101b58806ecf5f7a2677dce4d357cc3ef14c59586db851f253e7d495b6506e7c8210100000000fdffffff02a54a4c0000000000225120d73acf6667b7850e6e00e21bb4525a2c5e2d88956a3d2c35b0bc06f4b0df01bc404b4c00000000002251202befa14431d4cb71889ea1df7a7eaa2f1d8b9107e60b01564e15dabe5c0dfd32014091d48b7c4bb1dc7cb4d0da360dfd0ca35ea1e73ca6f1891c25a6a3bd90a6269eaa2ee97bca15969181981eb1abb1c9ab8574add9453355b00b521069dca7dc1634010000", - mnemnoic: + mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", }; diff --git a/webpack.config.js b/webpack.config.js index e6cedfda0c..b7e4ad3f21 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -151,11 +151,6 @@ var options = { }, mode: nodeEnv, - experiments: { - // TODO: remove along with tiny-secp256k1 - asyncWebAssembly: true, - }, - entry: { manifest: "./src/manifest.json", background: "./src/extension/background-script/index.ts", diff --git a/yarn.lock b/yarn.lock index f5d5a64e34..1736a279a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -400,6 +400,14 @@ resolved "https://registry.yarnpkg.com/@bitcoin-design/bitcoin-icons-react/-/bitcoin-icons-react-0.1.9.tgz#25c18808f167e242cd15ba27c185d785f2728980" integrity sha512-nJvTD1+zG/ffHdMeGQ39vdsmEFo9WcCIP1RlR7ZpZoP2H+IgKwzwow8VSY6ebroLoCT7WWtUPJQSbgQwgWYrFg== +"@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" resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-17.7.1.tgz#f3ab35bd38d82fcd4ab03ec5a1e9db26d57fe1b0" @@ -1036,6 +1044,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" 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/secp256k1@^1.7.1": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -2700,11 +2713,6 @@ bip174-liquid@^1.0.3: resolved "https://registry.yarnpkg.com/bip174-liquid/-/bip174-liquid-1.0.3.tgz#5009444091da80277c45ee90c255d7919646f907" integrity sha512-e69sC0Cq2tBJuhG2+wieXv40DN13YBR/wbIjZp4Mqwpar5vQm8Ldqijdd6N33XG7LtfvQi/zKm5fSzdPY/9mgw== -bip174@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/bip174/-/bip174-2.0.1.tgz" - integrity sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ== - bip174@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/bip174/-/bip174-2.1.0.tgz#cd3402581feaa5116f0f00a0eaee87a5843a2d30" @@ -2717,20 +2725,21 @@ bip66@^1.1.0: dependencies: safe-buffer "^5.0.1" -bitcoinjs-lib@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-6.0.1.tgz#4fa9438bb86a0449451ac58607e83d9b5a7732e6" - integrity sha512-x/7D4jDj/MMkmO6t3p2CSDXTqpwZ/jRsRiJDmaiXabrR9XRo7jwby8HRn7EyK1h24rKFFI7vI0ay4czl6bDOZQ== +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.0.1" + 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.0.2: +bitcoinjs-lib@^6.1.0: version "6.1.3" resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-6.1.3.tgz#88aed5a8d052e9faa04c6402d3f0865441f928d7" integrity sha512-TYXs/Qf+GNk2nnsB9HrXWqzFuEgCg0Gx+v3UW3B8VuceFHXVvhT+7hRnTSvwkX0i8rz2rtujeU6gFaDcFqYFDw== @@ -9051,7 +9060,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== @@ -9967,13 +9976,6 @@ thunky@^1.0.2: resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -tiny-secp256k1@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tiny-secp256k1/-/tiny-secp256k1-2.2.1.tgz#a61d4791b7031aa08a9453178a131349c3e10f9b" - integrity sha512-/U4xfVqnVxJXN4YVsru0E6t5wVncu2uunB8+RVR40fYUxkKYUPS10f+ePQZgFBoE/Jbf9H1NBveupF2VmB58Ng== - dependencies: - uint8array-tools "0.0.7" - tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -10253,11 +10255,6 @@ typeson@^6.0.0, typeson@^6.1.0: resolved "https://registry.yarnpkg.com/typeson/-/typeson-6.1.0.tgz#5b2a53705a5f58ff4d6f82f965917cabd0d7448b" integrity sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA== -uint8array-tools@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/uint8array-tools/-/uint8array-tools-0.0.7.tgz#a7a2bb5d8836eae2fade68c771454e6a438b390d" - integrity sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ== - unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz" From 59560ff41a712ad56d8fd78c6eee72843ad37158 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 5 Sep 2023 16:12:55 +0700 Subject: [PATCH 03/22] feat: move getPsbtPreview to background script, update signPsbt dialog --- src/app/screens/ConfirmSignPsbt/index.tsx | 40 +++++----- src/common/lib/api.ts | 9 +++ src/common/lib/psbt.ts | 76 ------------------- .../actions/webbtc/getPsbtPreview.ts | 21 +++++ .../background-script/actions/webbtc/index.ts | 7 +- .../background-script/bitcoin/index.ts | 69 ++++++++++++++++- src/extension/background-script/router.ts | 3 + src/i18n/locales/en/translation.json | 11 +++ src/types.ts | 15 ++++ 9 files changed, 149 insertions(+), 102 deletions(-) delete mode 100644 src/common/lib/psbt.ts create mode 100644 src/extension/background-script/actions/webbtc/getPsbtPreview.ts diff --git a/src/app/screens/ConfirmSignPsbt/index.tsx b/src/app/screens/ConfirmSignPsbt/index.tsx index b64222ee76..21a9a145ff 100644 --- a/src/app/screens/ConfirmSignPsbt/index.tsx +++ b/src/app/screens/ConfirmSignPsbt/index.tsx @@ -14,14 +14,13 @@ 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 { Address, PsbtPreview, getPsbtPreview } from "~/common/lib/psbt"; -import type { OriginData } from "~/types"; +import type { OriginData, PsbtPreview } from "~/types"; function ConfirmSignPsbt() { const navState = useNavigationState(); const { t: tCommon } = useTranslation("common"); const { t } = useTranslation("translation", { - keyPrefix: "confirm_sign_psbt", + keyPrefix: "bitcoin.confirm_sign_psbt", }); const navigate = useNavigate(); @@ -35,8 +34,8 @@ function ConfirmSignPsbt() { useEffect(() => { (async () => { - const settings = await api.getSettings(); - setPreview(getPsbtPreview(psbt, settings.bitcoinNetwork)); + const preview = await api.bitcoin.getPsbtPreview(psbt); + setPreview(preview); })(); }, [origin, psbt]); @@ -90,39 +89,36 @@ function ConfirmSignPsbt() { image={origin.icon} url={origin.host} /> -
- {t("warning")} -

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

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

{t("input")}

- -
- )} - - {showAddresses && ( -
+ <> +

{t("inputs")}

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

{t("outputs")}

{preview.outputs.map((output) => ( ))}
-
+ + {showHex + ? t("hide_raw_transaction") + : t("view_raw_transaction")} + + )}
diff --git a/src/common/lib/api.ts b/src/common/lib/api.ts index 1af9c725ef..79c19becd4 100644 --- a/src/common/lib/api.ts +++ b/src/common/lib/api.ts @@ -27,6 +27,7 @@ import type { MessageLnurlAuth, MessageSettingsSet, NodeInfo, + PsbtPreview, PsetPreview, SettingsStorage, ValidateAccountResponse, @@ -235,6 +236,11 @@ const signPset = (pset: string): Promise => pset, }); +const getPsbtPreview = (psbt: string): Promise => + msg.request("webbtc/getPsbtPreview", { + psbt, + }); + export default { getAccount, getAccountInfo, @@ -278,4 +284,7 @@ export default { fetchAssetRegistry: fetchLiquidAssetRegistry, signPset: signPset, }, + bitcoin: { + getPsbtPreview, + }, }; diff --git a/src/common/lib/psbt.ts b/src/common/lib/psbt.ts deleted file mode 100644 index b92366005c..0000000000 --- a/src/common/lib/psbt.ts +++ /dev/null @@ -1,76 +0,0 @@ -import * as btc from "@scure/btc-signer"; -import { Psbt, networks } from "bitcoinjs-lib"; - -export type Address = { amount: number; address: string }; - -export type PsbtPreview = { - inputs: Address[]; - outputs: Address[]; -}; - -export function getPsbtPreview( - psbt: string, - networkType?: keyof typeof networks -): PsbtPreview { - const network = networkType ? networks[networkType] : undefined; - - const unsignedPsbt = Psbt.fromHex(psbt, { - network, - }); - - const preview: PsbtPreview = { - inputs: [], - outputs: [], - }; - - for (let i = 0; i < unsignedPsbt.data.inputs.length; i++) { - if (i > 0) { - throw new Error("Multiple inputs currently unsupported"); - } - - const tapBip32Derivation = unsignedPsbt.data.inputs[i].tapBip32Derivation; - if (!tapBip32Derivation) { - throw new Error("No bip32Derivation in input " + i); - } - const address = btc.p2tr( - tapBip32Derivation[0].pubkey, - undefined, - network - ).address; - - if (!address) { - throw new Error("No address found in input " + i); - } - const witnessUtxo = unsignedPsbt.data.inputs[i].witnessUtxo; - if (!witnessUtxo) { - throw new Error("No witnessUtxo in input " + i); - } - - preview.inputs.push({ - amount: witnessUtxo.value, - address, - }); - } - for (let i = 0; i < unsignedPsbt.data.outputs.length; i++) { - const txOutput = unsignedPsbt.txOutputs[i]; - const output = unsignedPsbt.data.outputs[i]; - if (!output.tapBip32Derivation) { - throw new Error("No tapBip32Derivation in output"); - } - const address = btc.p2tr( - output.tapBip32Derivation[0].pubkey, - undefined, - network - ).address; - if (!address) { - throw new Error("No address found in output " + i); - } - - const previewOutput: Address = { - amount: txOutput.value, - address, - }; - preview.outputs.push(previewOutput); - } - return preview; -} 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 927b389837..733d0b2053 100644 --- a/src/extension/background-script/actions/webbtc/index.ts +++ b/src/extension/background-script/actions/webbtc/index.ts @@ -1,14 +1,15 @@ 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 signPsbt from "~/extension/background-script/actions/webbtc/signPsbt"; - -import getInfo from "./getInfo"; -import signPsbtWithPrompt from "./signPsbtWithPrompt"; +import signPsbtWithPrompt from "~/extension/background-script/actions/webbtc/signPsbtWithPrompt"; export { getAddress, getAddressOrPrompt, getInfo, + getPsbtPreview, signPsbt, signPsbtWithPrompt, }; diff --git a/src/extension/background-script/bitcoin/index.ts b/src/extension/background-script/bitcoin/index.ts index 43bd337656..58fbb80f98 100644 --- a/src/extension/background-script/bitcoin/index.ts +++ b/src/extension/background-script/bitcoin/index.ts @@ -7,7 +7,12 @@ import { 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"; @@ -96,6 +101,68 @@ 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: [], + }; + + for (let i = 0; i < unsignedPsbt.data.inputs.length; i++) { + if (i > 0) { + throw new Error("Multiple inputs currently unsupported"); + } + + const tapBip32Derivation = unsignedPsbt.data.inputs[i].tapBip32Derivation; + if (!tapBip32Derivation) { + throw new Error("No bip32Derivation in input " + i); + } + const address = btc.p2tr( + tapBip32Derivation[0].pubkey, + undefined, + this.network + ).address; + + if (!address) { + throw new Error("No address found in input " + i); + } + const witnessUtxo = unsignedPsbt.data.inputs[i].witnessUtxo; + if (!witnessUtxo) { + throw new Error("No witnessUtxo in input " + i); + } + + preview.inputs.push({ + amount: witnessUtxo.value, + address, + }); + } + for (let i = 0; i < unsignedPsbt.data.outputs.length; i++) { + const txOutput = unsignedPsbt.txOutputs[i]; + const output = unsignedPsbt.data.outputs[i]; + if (!output.tapBip32Derivation) { + throw new Error("No tapBip32Derivation in output"); + } + const address = btc.p2tr( + output.tapBip32Derivation[0].pubkey, + undefined, + this.network + ).address; + if (!address) { + throw new Error("No address found in output " + i); + } + + const previewOutput: Address = { + amount: txOutput.value, + address, + }; + preview.outputs.push(previewOutput); + } + return preview; + } } export default Bitcoin; diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index 648c7b4f2f..0c6421aba3 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -81,6 +81,9 @@ const routes = { removePrivateKey: nostr.removePrivateKey, setPrivateKey: nostr.setPrivateKey, }, + webbtc: { + getPsbtPreview: webbtc.getPsbtPreview, + }, // Public calls that are accessible from the inpage script (through the content script) public: { diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index dba64dd0af..3cda82bc8e 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -818,6 +818,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", + "amount": "{{amount}} sats" + }, "allow": "Allow this website to:", "allow_sign": "Allow {{host}} to sign:", "block_and_ignore": "Block and ignore {{host}}", diff --git a/src/types.ts b/src/types.ts index fa274fb0a4..68086bda9a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -169,6 +169,7 @@ export type NavigationState = { sigHash?: string; description?: string; details?: string; + psbt?: string; requestPermission: { method: string; description: string; @@ -560,6 +561,13 @@ export interface MessageSignPsbt extends MessageDefault { action: "signPsbt"; } +export interface MessageGetPsbtPreview extends MessageDefault { + args: { + psbt: string; + }; + action: "getPsbtPreview"; +} + export interface MessageBalanceGet extends MessageDefault { action: "getBalance"; } @@ -943,3 +951,10 @@ export type EsploraAssetInfos = { }; export type EsploraAssetRegistry = Record; + +export type Address = { amount: number; address: string }; + +export type PsbtPreview = { + inputs: Address[]; + outputs: Address[]; +}; From dbd6ed17227a6a4f60287652d58ee8e9c01f3781 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 5 Sep 2023 16:30:44 +0700 Subject: [PATCH 04/22] fix: signPsbt tests --- src/app/screens/ConfirmSignPsbt/index.tsx | 4 +-- .../actions/webbtc/__tests__/signPsbt.test.ts | 31 ++++++++++++++++--- src/fixtures/btc.ts | 2 +- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/app/screens/ConfirmSignPsbt/index.tsx b/src/app/screens/ConfirmSignPsbt/index.tsx index 21a9a145ff..f6a8738c74 100644 --- a/src/app/screens/ConfirmSignPsbt/index.tsx +++ b/src/app/screens/ConfirmSignPsbt/index.tsx @@ -14,7 +14,7 @@ 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 { OriginData, PsbtPreview } from "~/types"; +import type { Address, OriginData, PsbtPreview } from "~/types"; function ConfirmSignPsbt() { const navState = useNavigationState(); @@ -154,7 +154,7 @@ function AddressPreview({ amount, t, }: Address & { - t: TFunction<"translation", "confirm_sign_psbt", "translation">; + t: TFunction<"translation", "bitcoin.confirm_sign_psbt", "translation">; }) { return (
diff --git a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts index d16a28ba73..08842eadc2 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts @@ -1,12 +1,16 @@ import { hex } from "@scure/base"; import * as btc from "@scure/btc-signer"; -import { getPsbtPreview } from "~/common/lib/psbt"; +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 { MessageSignPsbt } from "~/types"; +import type { + MessageGetPsbtPreview, + MessageSignPsbt, + PsbtPreview, +} from "~/types"; const passwordMock = jest.fn; @@ -40,7 +44,7 @@ afterEach(() => { jest.clearAllMocks(); }); -async function sendPsbtMessage(psbt: string, derivationPath?: string) { +async function sendPsbtMessage(psbt: string) { const message: MessageSignPsbt = { application: "LBE", prompt: true, @@ -56,6 +60,22 @@ async function sendPsbtMessage(psbt: string, derivationPath?: string) { 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 () => { const result = await sendPsbtMessage(btcFixture.regtestTaprootPsbt); @@ -82,7 +102,10 @@ describe("signPsbt input validation", () => { describe("decode psbt", () => { test("get taproot transaction preview", async () => { - const preview = getPsbtPreview(btcFixture.regtestTaprootPsbt, "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" diff --git a/src/fixtures/btc.ts b/src/fixtures/btc.ts index b1e407a222..5018bc3797 100644 --- a/src/fixtures/btc.ts +++ b/src/fixtures/btc.ts @@ -8,7 +8,7 @@ export const btcFixture = { // signed PSBT and verified by importing in sparrow and broadcasting transaction // echo hex | xxd -r -p > taproot_signed.psbt regtestTaprootSignedPsbt: - "02000000000101b58806ecf5f7a2677dce4d357cc3ef14c59586db851f253e7d495b6506e7c8210100000000fdffffff02a54a4c0000000000225120d73acf6667b7850e6e00e21bb4525a2c5e2d88956a3d2c35b0bc06f4b0df01bc404b4c00000000002251202befa14431d4cb71889ea1df7a7eaa2f1d8b9107e60b01564e15dabe5c0dfd32014091d48b7c4bb1dc7cb4d0da360dfd0ca35ea1e73ca6f1891c25a6a3bd90a6269eaa2ee97bca15969181981eb1abb1c9ab8574add9453355b00b521069dca7dc1634010000", + "02000000000101b58806ecf5f7a2677dce4d357cc3ef14c59586db851f253e7d495b6506e7c8210100000000fdffffff02a54a4c0000000000225120d73acf6667b7850e6e00e21bb4525a2c5e2d88956a3d2c35b0bc06f4b0df01bc404b4c00000000002251202befa14431d4cb71889ea1df7a7eaa2f1d8b9107e60b01564e15dabe5c0dfd320140dcb575e673464796c4484808517f3e59afd0948cad9eda4cfd137e4e80f2eced24108f6ec116d5e6860424be0132697f53e69a89d54413cdec68dfb51fc7203734010000", mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", From 3fdddcf3d44a6d0616316b871dc4119d3c3343a4 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 5 Sep 2023 16:59:07 +0700 Subject: [PATCH 05/22] chore: move signpsbt and getAddress into webbtc --- src/app/router/Prompt/Prompt.tsx | 7 +++++-- .../screens/{ => Bitcoin}/ConfirmSignPsbt/index.test.tsx | 0 src/app/screens/{ => Bitcoin}/ConfirmSignPsbt/index.tsx | 2 +- src/common/lib/api.ts | 5 +++++ .../background-script/actions/webbtc/signPsbtWithPrompt.ts | 2 +- src/extension/background-script/router.ts | 4 ++-- 6 files changed, 14 insertions(+), 6 deletions(-) rename src/app/screens/{ => Bitcoin}/ConfirmSignPsbt/index.test.tsx (100%) rename src/app/screens/{ => Bitcoin}/ConfirmSignPsbt/index.tsx (98%) diff --git a/src/app/router/Prompt/Prompt.tsx b/src/app/router/Prompt/Prompt.tsx index 507cf289c9..f84421bc94 100644 --- a/src/app/router/Prompt/Prompt.tsx +++ b/src/app/router/Prompt/Prompt.tsx @@ -23,7 +23,7 @@ import AlbyLogo from "~/app/components/AlbyLogo"; import Providers from "~/app/context/Providers"; import RequireAuth from "~/app/router/RequireAuth"; import BitcoinConfirmGetAddress from "~/app/screens/Bitcoin/ConfirmGetAddress"; -import ConfirmSignPsbt from "~/app/screens/ConfirmSignPsbt"; +import ConfirmSignPsbt from "~/app/screens/Bitcoin/ConfirmSignPsbt"; import Onboard from "~/app/screens/Onboard/Prompt"; import type { NavigationState, OriginData } from "~/types"; @@ -97,6 +97,10 @@ function Prompt() { path="public/webbtc/confirmGetAddress" element={} /> + } + /> } @@ -128,7 +132,6 @@ function Prompt() { } /> } /> } /> - } /> } /> } /> } /> diff --git a/src/app/screens/ConfirmSignPsbt/index.test.tsx b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx similarity index 100% rename from src/app/screens/ConfirmSignPsbt/index.test.tsx rename to src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx diff --git a/src/app/screens/ConfirmSignPsbt/index.tsx b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx similarity index 98% rename from src/app/screens/ConfirmSignPsbt/index.tsx rename to src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx index f6a8738c74..dd3043619f 100644 --- a/src/app/screens/ConfirmSignPsbt/index.tsx +++ b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx @@ -42,7 +42,7 @@ function ConfirmSignPsbt() { async function confirm() { try { setLoading(true); - const response = await msg.request("signPsbt", { psbt }, { origin }); + const response = await api.bitcoin.signPsbt(psbt); msg.reply(response); setSuccessMessage(tCommon("success")); } catch (e) { diff --git a/src/common/lib/api.ts b/src/common/lib/api.ts index 79c19becd4..89ec01d926 100644 --- a/src/common/lib/api.ts +++ b/src/common/lib/api.ts @@ -240,6 +240,10 @@ const getPsbtPreview = (psbt: string): Promise => msg.request("webbtc/getPsbtPreview", { psbt, }); +const signPsbt = (psbt: string): Promise => + msg.request("webbtc/signPsbt", { + psbt, + }); export default { getAccount, @@ -286,5 +290,6 @@ export default { }, bitcoin: { getPsbtPreview, + signPsbt, }, }; diff --git a/src/extension/background-script/actions/webbtc/signPsbtWithPrompt.ts b/src/extension/background-script/actions/webbtc/signPsbtWithPrompt.ts index 0d0ccef65c..9886009fc7 100644 --- a/src/extension/background-script/actions/webbtc/signPsbtWithPrompt.ts +++ b/src/extension/background-script/actions/webbtc/signPsbtWithPrompt.ts @@ -12,7 +12,7 @@ const signPsbtWithPrompt = async (message: Message) => { try { const response = await utils.openPrompt({ ...message, - action: "confirmSignPsbt", + action: "webbtc/confirmSignPsbt", }); return response; } catch (e) { diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index 0c6421aba3..0abaa3350d 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -62,8 +62,6 @@ const routes = { lnurl: lnurl, lnurlAuth: auth, getCurrencyRate: cache.getCurrencyRate, - signPsbt: webbtc.signPsbt, - getAddress: webbtc.getAddress, setMnemonic: mnemonic.setMnemonic, getMnemonic: mnemonic.getMnemonic, generateMnemonic: mnemonic.generateMnemonic, @@ -83,6 +81,8 @@ const routes = { }, webbtc: { getPsbtPreview: webbtc.getPsbtPreview, + signPsbt: webbtc.signPsbt, + getAddress: webbtc.getAddress, }, // Public calls that are accessible from the inpage script (through the content script) From 0f2a7d3177176236ab3bacfa084cd90fbc04f5c5 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 5 Sep 2023 17:36:21 +0700 Subject: [PATCH 06/22] fix: broken confirm sign psbt test --- .../Bitcoin/ConfirmSignPsbt/index.test.tsx | 40 +++++++++++-------- .../background-script/bitcoin/index.ts | 6 --- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx index 5613ff9b39..d18b18d3bc 100644 --- a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx +++ b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx @@ -6,6 +6,8 @@ import state from "~/extension/background-script/state"; import { btcFixture } from "~/fixtures/btc"; import type { OriginData } from "~/types"; +import Bitcoin from "~/extension/background-script/bitcoin"; +import Mnemonic from "~/extension/background-script/mnemonic"; import ConfirmSignPsbt from "./index"; const mockOrigin: OriginData = { @@ -44,19 +46,26 @@ 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); // mock get settings -msg.request = jest.fn().mockReturnValue({ - bitcoinNetwork: "regtest", -}); - -describe("ConfirmSignMessage", () => { +msg.request = jest + .fn() + .mockReturnValue( + new Bitcoin(new Mnemonic(btcFixture.mnemonic), "regtest").getPsbtPreview( + btcFixture.regtestTaprootPsbt + ) + ); + +describe("ConfirmSignPsbt", () => { test("render", async () => { await act(async () => { render( @@ -69,25 +78,17 @@ describe("ConfirmSignMessage", () => { const user = userEvent.setup(); await act(async () => { - await user.click(screen.getByText("View addresses")); - }); - await act(async () => { - await user.click(screen.getByText("View PSBT hex")); + await user.click(screen.getByText("View details")); }); - // TODO: update copy expect( await screen.findByText( - "This website asks you to sign a Partially Signed Bitcoin Transaction:" + "This website asks you to sign a Bitcoin Transaction:" ) ).toBeInTheDocument(); - expect( - await screen.findByText(btcFixture.regtestTaprootPsbt) - ).toBeInTheDocument(); - // Check inputs - const inputsContainer = (await screen.getByText("Input") + const inputsContainer = (await screen.getByText("Inputs") .parentElement) as HTMLElement; expect(inputsContainer).toBeInTheDocument(); const inputsRef = within(inputsContainer); @@ -114,5 +115,12 @@ describe("ConfirmSignMessage", () => { "bcrt1p90h6z3p36n9hrzy7580h5l429uwchyg8uc9sz4jwzhdtuhqdl5eqkcyx0f" ) ).toBeInTheDocument(); + + await act(async () => { + await user.click(screen.getByText("View raw transaction (Hex)")); + }); + expect( + await screen.findByText(btcFixture.regtestTaprootPsbt) + ).toBeInTheDocument(); }); }); diff --git a/src/extension/background-script/bitcoin/index.ts b/src/extension/background-script/bitcoin/index.ts index 58fbb80f98..c46ce19a8a 100644 --- a/src/extension/background-script/bitcoin/index.ts +++ b/src/extension/background-script/bitcoin/index.ts @@ -44,12 +44,6 @@ class Bitcoin { network: this.network, }); - // // fix usages of window (unavailable in service worker) - // globalThis.window ??= globalThis.window || {}; - // if (!globalThis.window.crypto) { - // globalThis.window.crypto = crypto; - // } - bitcoin.initEccLib(ecc); const ECPair: ECPairAPI = ECPairFactory(ecc); From cb9758ffd0e645e9558f591442a04b2bf40f675a Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 7 Sep 2023 14:59:56 +0700 Subject: [PATCH 07/22] chore: improve psbt preview transaction decoding --- .../screens/Bitcoin/ConfirmSignPsbt/index.tsx | 26 +++++++++++++---- .../actions/webbtc/__tests__/signPsbt.test.ts | 16 +++++++++++ .../background-script/bitcoin/index.ts | 28 ++++++++----------- src/fixtures/btc.ts | 4 +++ 4 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx index dd3043619f..be9649c37e 100644 --- a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx +++ b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx @@ -3,7 +3,7 @@ import Container from "@components/Container"; import PublisherCard from "@components/PublisherCard"; import SuccessMessage from "@components/SuccessMessage"; import { TFunction } from "i18next"; -import { useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; @@ -31,13 +31,22 @@ function ConfirmSignPsbt() { const [preview, setPreview] = useState(undefined); const [showAddresses, setShowAddresses] = useState(false); const [showHex, setShowHex] = useState(false); + const [error, setError] = useState(); useEffect(() => { (async () => { - const preview = await api.bitcoin.getPsbtPreview(psbt); - setPreview(preview); + try { + const preview = await api.bitcoin.getPsbtPreview(psbt); + setPreview(preview); + } catch (e) { + console.error(e); + const error = e as { message: string }; + const errorMessage = error.message || "Unknown error"; + setError(errorMessage); + toast.error(`${tCommon("error")}: ${errorMessage}`); + } })(); - }, [origin, psbt]); + }, [origin, psbt, tCommon]); async function confirm() { try { @@ -47,7 +56,10 @@ function ConfirmSignPsbt() { setSuccessMessage(tCommon("success")); } catch (e) { console.error(e); - if (e instanceof Error) toast.error(`${tCommon("error")}: ${e.message}`); + const error = e as { message: string }; + const errorMessage = error.message || "Unknown error"; + setError(errorMessage); + toast.error(`${tCommon("error")}: ${errorMessage}`); } finally { setLoading(false); } @@ -74,6 +86,10 @@ function ConfirmSignPsbt() { setShowHex((current) => !current); } + if (error) { + return

{error}

; + } + if (!preview) { return ; } diff --git a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts index 08842eadc2..510ae57e36 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts @@ -122,4 +122,20 @@ describe("decode psbt", () => { ); expect(preview.outputs[1].amount).toBe(5_000_000); }); + + test("get taproot transaction preview 2", async () => { + const previewResponse = await sendGetPsbtPreviewMessage( + btcFixture.testnetTaprootPsbt + ); + 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(5000); + expect(preview.outputs.length).toBe(1); + + expect(preview.outputs[0].address).toBe("UNKNOWN"); + expect(preview.outputs[0].amount).toBe(100); + }); }); diff --git a/src/extension/background-script/bitcoin/index.ts b/src/extension/background-script/bitcoin/index.ts index c46ce19a8a..e9652151c1 100644 --- a/src/extension/background-script/bitcoin/index.ts +++ b/src/extension/background-script/bitcoin/index.ts @@ -111,15 +111,13 @@ class Bitcoin { throw new Error("Multiple inputs currently unsupported"); } - const tapBip32Derivation = unsignedPsbt.data.inputs[i].tapBip32Derivation; - if (!tapBip32Derivation) { - throw new Error("No bip32Derivation in input " + i); + const pubkey: Buffer | undefined = + unsignedPsbt.data.inputs[i].tapInternalKey || + unsignedPsbt.data.inputs[i].tapBip32Derivation?.[0]?.pubkey; + if (!pubkey) { + throw new Error("No pubkey found in input " + i); } - const address = btc.p2tr( - tapBip32Derivation[0].pubkey, - undefined, - this.network - ).address; + const address = btc.p2tr(pubkey, undefined, this.network).address; if (!address) { throw new Error("No address found in input " + i); @@ -137,14 +135,12 @@ class Bitcoin { for (let i = 0; i < unsignedPsbt.data.outputs.length; i++) { const txOutput = unsignedPsbt.txOutputs[i]; const output = unsignedPsbt.data.outputs[i]; - if (!output.tapBip32Derivation) { - throw new Error("No tapBip32Derivation in output"); - } - const address = btc.p2tr( - output.tapBip32Derivation[0].pubkey, - undefined, - this.network - ).address; + + const pubkey = output.tapBip32Derivation?.[0].pubkey || txOutput.address; + + const address = pubkey + ? btc.p2tr(pubkey, undefined, this.network).address + : "UNKNOWN"; if (!address) { throw new Error("No address found in output " + i); } diff --git a/src/fixtures/btc.ts b/src/fixtures/btc.ts index 5018bc3797..bc7c11a68d 100644 --- a/src/fixtures/btc.ts +++ b/src/fixtures/btc.ts @@ -12,4 +12,8 @@ export const btcFixture = { mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + + // made with bitcoinjs-lib, only has one output + testnetTaprootPsbt: + "70736274ff01005e020000000135e316e6c54d3ef6b56394ef8fdb3addec3098b932820fbaf2818df0a1a6b3ca0100000000ffffffff016400000000000000225120da002fb251b6270e999364e434ed56219562b5a909eab26ae6797ca408f44223000000000001012b88130000000000002251203b82b2b2a9185315da6f80da5f06d0440d8a5e1457fa93387c2d919c86ec878601172055355ca83c973f1d97ce0e3843c85d78905af16b4dc531bc488e57212d2301160000", }; From 45e50038cb6a23bb05f8fd109b8518aaa9b7c0b0 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 8 Sep 2023 16:36:47 +0700 Subject: [PATCH 08/22] feat: add accountChanged to webbtc --- src/extension/content-script/webbtc.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/extension/content-script/webbtc.js b/src/extension/content-script/webbtc.js index 7152126118..b5d1d98a48 100644 --- a/src/extension/content-script/webbtc.js +++ b/src/extension/content-script/webbtc.js @@ -25,6 +25,16 @@ async function init() { return; } + browser.runtime.onMessage.addListener((request, sender, sendResponse) => { + // forward account changed messaged to inpage script + if (request.action === "accountChanged") { + 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) From 0a361e05a96058c9a685b4b95a42613897be319b Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 8 Sep 2023 16:39:15 +0700 Subject: [PATCH 09/22] fix: support multiple inputs in psbt preview --- .../actions/webbtc/__tests__/signPsbt.test.ts | 25 +++++--- .../background-script/bitcoin/index.ts | 27 ++++++--- .../background-script/bitcoin/networks.ts | 58 +------------------ src/fixtures/btc.ts | 9 ++- 4 files changed, 44 insertions(+), 75 deletions(-) diff --git a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts index 510ae57e36..9a223f3baf 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts @@ -19,10 +19,10 @@ const mockState = { currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", getAccount: () => ({ mnemonic: btcFixture.mnemonic, - bitcoinNetwork: "regtest", + bitcoinNetwork: "testnet", }), getMnemonic: () => new Mnemonic(btcFixture.mnemonic), - getBitcoin: () => new Bitcoin(new Mnemonic(btcFixture.mnemonic), "regtest"), + getBitcoin: () => new Bitcoin(new Mnemonic(btcFixture.mnemonic), "testnet"), getConnector: jest.fn(), }; @@ -125,17 +125,28 @@ describe("decode psbt", () => { test("get taproot transaction preview 2", async () => { const previewResponse = await sendGetPsbtPreviewMessage( - btcFixture.testnetTaprootPsbt + 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( - "bcrt1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqjeprhg" + "tb1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqlqt9zj" ); - expect(preview.inputs[0].amount).toBe(5000); - expect(preview.outputs.length).toBe(1); + expect(preview.inputs[0].amount).toBe(2700); + expect(preview.outputs.length).toBe(2); - expect(preview.outputs[0].address).toBe("UNKNOWN"); + // first address from mnemonic 2 + // FIXME: + 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( + "bcrt1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqjeprhg" + ); + expect(preview.outputs[1].amount).toBe(900); }); }); diff --git a/src/extension/background-script/bitcoin/index.ts b/src/extension/background-script/bitcoin/index.ts index e9652151c1..2f571fd279 100644 --- a/src/extension/background-script/bitcoin/index.ts +++ b/src/extension/background-script/bitcoin/index.ts @@ -107,17 +107,12 @@ class Bitcoin { }; for (let i = 0; i < unsignedPsbt.data.inputs.length; i++) { - if (i > 0) { - throw new Error("Multiple inputs currently unsupported"); - } - const pubkey: Buffer | undefined = unsignedPsbt.data.inputs[i].tapInternalKey || unsignedPsbt.data.inputs[i].tapBip32Derivation?.[0]?.pubkey; - if (!pubkey) { - throw new Error("No pubkey found in input " + i); - } - const address = btc.p2tr(pubkey, undefined, this.network).address; + const address = pubkey + ? btc.p2tr(pubkey, undefined, this.network).address + : "UNKNOWN"; if (!address) { throw new Error("No address found in input " + i); @@ -136,7 +131,21 @@ class Bitcoin { const txOutput = unsignedPsbt.txOutputs[i]; const output = unsignedPsbt.data.outputs[i]; - const pubkey = output.tapBip32Derivation?.[0].pubkey || txOutput.address; + const pubkey = + output.tapBip32Derivation?.[0].pubkey || + txOutput.address || + (txOutput.script && + (() => { + try { + return bitcoin.address.fromOutputScript( + txOutput.script, + this.network + ); + } catch (error) { + console.error(error); + } + return undefined; + })()); const address = pubkey ? btc.p2tr(pubkey, undefined, this.network).address diff --git a/src/extension/background-script/bitcoin/networks.ts b/src/extension/background-script/bitcoin/networks.ts index 2a06821f78..5962ad9154 100644 --- a/src/extension/background-script/bitcoin/networks.ts +++ b/src/extension/background-script/bitcoin/networks.ts @@ -1,56 +1,2 @@ -// from https://github1s.com/bitcoinjs/bitcoinjs-lib -export interface Network { - messagePrefix: string; - bech32: string; - bip32: Bip32; - pubKeyHash: number; - scriptHash: number; - wif: number; -} - -interface Bip32 { - public: number; - private: number; -} - -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, -}; +import { Network, networks } from "bitcoinjs-lib"; +export { Network, networks }; diff --git a/src/fixtures/btc.ts b/src/fixtures/btc.ts index bc7c11a68d..14163996e0 100644 --- a/src/fixtures/btc.ts +++ b/src/fixtures/btc.ts @@ -12,8 +12,11 @@ export const btcFixture = { 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, only has one output - testnetTaprootPsbt: - "70736274ff01005e020000000135e316e6c54d3ef6b56394ef8fdb3addec3098b932820fbaf2818df0a1a6b3ca0100000000ffffffff016400000000000000225120da002fb251b6270e999364e434ed56219562b5a909eab26ae6797ca408f44223000000000001012b88130000000000002251203b82b2b2a9185315da6f80da5f06d0440d8a5e1457fa93387c2d919c86ec878601172055355ca83c973f1d97ce0e3843c85d78905af16b4dc531bc488e57212d2301160000", + // made with bitcoinjs-lib + // sent from mnemonic to mnemonic2, with some change back to mnemonic (same address) + taprootPsbt2: + "70736274ff010089020000000101d245947b008821fdbf573087c061749fc1a6f74cee947767af917e1be324c10100000000ffffffff026400000000000000225120da002fb251b6270e999364e434ed56219562b5a909eab26ae6797ca408f4422340060000000000002251203b82b2b2a9185315da6f80da5f06d0440d8a5e1457fa93387c2d919c86ec8786000000000001012b8c0a0000000000002251203b82b2b2a9185315da6f80da5f06d0440d8a5e1457fa93387c2d919c86ec878601172055355ca83c973f1d97ce0e3843c85d78905af16b4dc531bc488e57212d230116000000", }; From 12bf0ea21548897086a654292907dfefe690a91c Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Sun, 10 Sep 2023 11:38:48 +0700 Subject: [PATCH 10/22] fix: signpsbt output address decoding, display fee --- .../Bitcoin/ConfirmSignPsbt/index.test.tsx | 7 ++++ .../screens/Bitcoin/ConfirmSignPsbt/index.tsx | 5 +++ .../actions/webbtc/__tests__/signPsbt.test.ts | 40 ++++++++++++------- .../background-script/bitcoin/index.ts | 25 ++++++------ src/i18n/locales/en/translation.json | 1 + src/types.ts | 1 + 6 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx index d18b18d3bc..43418ad286 100644 --- a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx +++ b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx @@ -116,6 +116,13 @@ describe("ConfirmSignPsbt", () => { ) ).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)")); }); diff --git a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx index edce3f4096..901eae8ace 100644 --- a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx +++ b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx @@ -129,6 +129,11 @@ function ConfirmSignPsbt() { ))}
+

{t("fee")}

+

+ {t("amount", { amount: preview.fee })} +

+ {showHex ? t("hide_raw_transaction") diff --git a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts index 9a223f3baf..8f61293b98 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts @@ -7,6 +7,7 @@ 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, @@ -14,19 +15,21 @@ import type { const passwordMock = jest.fn; -const mockState = { - password: passwordMock, - currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97", - getAccount: () => ({ - mnemonic: btcFixture.mnemonic, - bitcoinNetwork: "testnet", - }), - getMnemonic: () => new Mnemonic(btcFixture.mnemonic), - getBitcoin: () => new Bitcoin(new Mnemonic(btcFixture.mnemonic), "testnet"), - getConnector: 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); + state.getState = jest.fn().mockReturnValue(mockState); +} jest.mock("~/common/lib/crypto", () => { return { @@ -78,6 +81,7 @@ async function sendGetPsbtPreviewMessage(psbt: string) { 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"); @@ -95,6 +99,7 @@ describe("signPsbt", () => { describe("signPsbt input validation", () => { test("invalid psbt", async () => { + mockSettings("regtest"); const result = await sendPsbtMessage("test"); expect(result.error).not.toBe(null); }); @@ -102,6 +107,7 @@ describe("signPsbt input validation", () => { describe("decode psbt", () => { test("get taproot transaction preview", async () => { + mockSettings("regtest"); const previewResponse = await sendGetPsbtPreviewMessage( btcFixture.regtestTaprootPsbt ); @@ -121,9 +127,12 @@ describe("decode psbt", () => { "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 ); @@ -137,7 +146,6 @@ describe("decode psbt", () => { expect(preview.outputs.length).toBe(2); // first address from mnemonic 2 - // FIXME: expect(preview.outputs[0].address).toBe( "tb1pmgqzlvj3kcnsaxvnvnjrfm2kyx2k9ddfp84ty6hx0972gz85gg3slq3j59" ); @@ -145,8 +153,10 @@ describe("decode psbt", () => { // change sent back to original address expect(preview.outputs[1].address).toBe( - "bcrt1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqjeprhg" + "tb1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqlqt9zj" ); - expect(preview.outputs[1].amount).toBe(900); + expect(preview.outputs[1].amount).toBe(1600); + + expect(preview.fee).toBe(1000); }); }); diff --git a/src/extension/background-script/bitcoin/index.ts b/src/extension/background-script/bitcoin/index.ts index 2f571fd279..149b4301f1 100644 --- a/src/extension/background-script/bitcoin/index.ts +++ b/src/extension/background-script/bitcoin/index.ts @@ -28,6 +28,7 @@ class Bitcoin { this.mnemonic = mnemonic; this.networkType = networkType; this.network = networks[this.networkType]; + bitcoin.initEccLib(ecc); } signPsbt(psbt: string) { @@ -44,7 +45,6 @@ class Bitcoin { network: this.network, }); - bitcoin.initEccLib(ecc); const ECPair: ECPairAPI = ECPairFactory(ecc); const keyPair = tweakSigner( @@ -104,6 +104,7 @@ class Bitcoin { const preview: PsbtPreview = { inputs: [], outputs: [], + fee: 0, }; for (let i = 0; i < unsignedPsbt.data.inputs.length; i++) { @@ -129,10 +130,8 @@ class Bitcoin { } for (let i = 0; i < unsignedPsbt.data.outputs.length; i++) { const txOutput = unsignedPsbt.txOutputs[i]; - const output = unsignedPsbt.data.outputs[i]; - const pubkey = - output.tapBip32Derivation?.[0].pubkey || + const address = txOutput.address || (txOutput.script && (() => { @@ -145,14 +144,8 @@ class Bitcoin { console.error(error); } return undefined; - })()); - - const address = pubkey - ? btc.p2tr(pubkey, undefined, this.network).address - : "UNKNOWN"; - if (!address) { - throw new Error("No address found in output " + i); - } + })()) || + "UNKNOWN"; const previewOutput: Address = { amount: txOutput.value, @@ -160,6 +153,14 @@ class Bitcoin { }; 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; } } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index aed2b38c10..d9cf822dcb 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -827,6 +827,7 @@ "hide_raw_transaction": "Hide raw transaction (Hex)", "inputs": "Inputs", "outputs": "Outputs", + "fee": "Fee", "amount": "{{amount}} sats" }, "allow": "Allow this website to:", diff --git a/src/types.ts b/src/types.ts index 68086bda9a..7bb3e73436 100644 --- a/src/types.ts +++ b/src/types.ts @@ -957,4 +957,5 @@ export type Address = { amount: number; address: string }; export type PsbtPreview = { inputs: Address[]; outputs: Address[]; + fee: number; }; From 778c7389764c8e3f841c38b8a2ea2f6377eb9a0e Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 11 Sep 2023 15:00:41 +0700 Subject: [PATCH 11/22] chore: remove @scure/btc-signer --- package.json | 1 - .../actions/webbtc/__tests__/signPsbt.test.ts | 4 --- .../background-script/bitcoin/index.ts | 26 +++++++------- yarn.lock | 36 ++++--------------- 4 files changed, 20 insertions(+), 47 deletions(-) diff --git a/package.json b/package.json index 05179ddfc0..8e762a7cd2 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "@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", diff --git a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts index 8f61293b98..7a243ffa9c 100644 --- a/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts +++ b/src/extension/background-script/actions/webbtc/__tests__/signPsbt.test.ts @@ -1,5 +1,3 @@ -import { hex } from "@scure/base"; -import * as btc from "@scure/btc-signer"; 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"; @@ -91,8 +89,6 @@ describe("signPsbt", () => { expect(result.data?.signed).not.toBe(undefined); expect(result.error).toBe(undefined); - const checkTx = btc.Transaction.fromRaw(hex.decode(result.data.signed)); - expect(checkTx.isFinal).toBe(true); expect(result.data?.signed).toBe(btcFixture.regtestTaprootSignedPsbt); }); }); diff --git a/src/extension/background-script/bitcoin/index.ts b/src/extension/background-script/bitcoin/index.ts index 149b4301f1..89e2a95a59 100644 --- a/src/extension/background-script/bitcoin/index.ts +++ b/src/extension/background-script/bitcoin/index.ts @@ -1,5 +1,4 @@ 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 { @@ -80,11 +79,10 @@ class Bitcoin { const derivationPath = `${derivationPathWithoutIndex}/${index}`; const derivedKey = this.mnemonic.deriveKey(derivationPath); - const address = btc.getAddress( - "tr", - derivedKey.privateKey as Uint8Array, - this.network - ); + 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"); } @@ -111,12 +109,16 @@ class Bitcoin { const pubkey: Buffer | undefined = unsignedPsbt.data.inputs[i].tapInternalKey || unsignedPsbt.data.inputs[i].tapBip32Derivation?.[0]?.pubkey; - const address = pubkey - ? btc.p2tr(pubkey, undefined, this.network).address - : "UNKNOWN"; - if (!address) { - throw new Error("No address found in input " + i); + 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) { @@ -124,7 +126,7 @@ class Bitcoin { } preview.inputs.push({ - amount: witnessUtxo.value, + amount: unsignedPsbt.data.inputs[i].witnessUtxo?.value || 0, address, }); } diff --git a/yarn.lock b/yarn.lock index 346410a685..469ca2c4c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1027,18 +1027,6 @@ dependencies: "@noble/hashes" "1.3.1" -"@noble/curves@~0.8.3": - version "0.8.3" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-0.8.3.tgz#ad6d48baf2599cf1d58dcb734c14d5225c8996e0" - integrity sha512-OqaOf4RWDaCRuBKJLDURrgVxjLmneGsiCXGuzYB5y95YithZMA6w4uk34DHSm0rKMrrYiaeZj48/81EvaAScLQ== - dependencies: - "@noble/hashes" "1.3.0" - -"@noble/hashes@1.3.0", "@noble/hashes@^1.2.0": - 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/hashes@1.3.1", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" @@ -1049,6 +1037,11 @@ 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.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -1118,7 +1111,7 @@ resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.2.tgz#cba1cf0a04bc04cb66027c51fa600e9cbc388bc8" 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.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== @@ -1140,16 +1133,6 @@ "@noble/hashes" "~1.3.0" "@scure/base" "~1.1.0" -"@scure/btc-signer@^0.5.1": - version "0.5.1" - resolved "https://registry.yarnpkg.com/@scure/btc-signer/-/btc-signer-0.5.1.tgz#79e2e89a359c5ba9aa944ba5bd487a6ce1b2ad36" - 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.25.16": version "0.25.21" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.21.tgz#763b05a4b472c93a8db29b2c3e359d55b29ce272" @@ -7267,13 +7250,6 @@ methods@~1.1.2: resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= -micro-packed@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/micro-packed/-/micro-packed-0.3.2.tgz#3679188366c2283cb60a78366ed0416e5472b7cf" - integrity sha512-D1Bq0/lVOzdxhnX5vylCxZpdw5LylH7Vd81py0DfRsKUP36XYpwvy8ZIsECVo3UfnoROn8pdKqkOzL7Cd82sGA== - dependencies: - "@scure/base" "~1.1.1" - micromatch@4.0.5, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" From d00aba564cd3ce247cda84efefb9d2b0e2914b8e Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 9 Oct 2023 11:48:56 +0700 Subject: [PATCH 12/22] fix: only fire accountChanged event if provider is enabled --- src/extension/content-script/nostr.js | 2 +- src/extension/content-script/webbtc.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extension/content-script/nostr.js b/src/extension/content-script/nostr.js index 76fc586729..bb321c64eb 100644 --- a/src/extension/content-script/nostr.js +++ b/src/extension/content-script/nostr.js @@ -33,7 +33,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 diff --git a/src/extension/content-script/webbtc.js b/src/extension/content-script/webbtc.js index 370435c22f..bf16afa5d8 100644 --- a/src/extension/content-script/webbtc.js +++ b/src/extension/content-script/webbtc.js @@ -27,7 +27,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: "webbtc" }, window.location.origin From e59dcf9f9bb6bc9291298355b12b21eb4df52215 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 9 Oct 2023 11:54:29 +0700 Subject: [PATCH 13/22] feat: add accountChanged event for window.liquid --- src/extension/content-script/liquid.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/extension/content-script/liquid.js b/src/extension/content-script/liquid.js index 281431dd9d..aedb69094e 100644 --- a/src/extension/content-script/liquid.js +++ b/src/extension/content-script/liquid.js @@ -26,6 +26,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) From a1ad9b46bb4eb733096374047e372c7ac956eded Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 9 Oct 2023 12:18:07 +0700 Subject: [PATCH 14/22] fix: only error show toast when signPSBT fails --- src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx index 901eae8ace..3657cad47b 100644 --- a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx +++ b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx @@ -58,7 +58,6 @@ function ConfirmSignPsbt() { console.error(e); const error = e as { message: string }; const errorMessage = error.message || "Unknown error"; - setError(errorMessage); toast.error(`${tCommon("error")}: ${errorMessage}`); } finally { setLoading(false); From 78ae4fcf0ec2fc2e84abefa5e7738230255d3041 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 9 Oct 2023 12:33:31 +0700 Subject: [PATCH 15/22] fix: remove signPsbt error state and always use toasts --- src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx index 3657cad47b..0f0b697371 100644 --- a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx +++ b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx @@ -26,23 +26,22 @@ function ConfirmSignPsbt() { const psbt = navState.args?.psbt as string; const origin = navState.origin as OriginData; - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); const [successMessage, setSuccessMessage] = useState(""); const [preview, setPreview] = useState(undefined); const [showAddresses, setShowAddresses] = useState(false); const [showHex, setShowHex] = useState(false); - const [error, setError] = useState(); 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"; - setError(errorMessage); toast.error(`${tCommon("error")}: ${errorMessage}`); } })(); @@ -85,12 +84,12 @@ function ConfirmSignPsbt() { setShowHex((current) => !current); } - if (error) { - return

{error}

; - } - if (!preview) { - return ; + return ( +
+ +
+ ); } return ( From ed421bde101651d87b082ea13a87aa2d00e54347 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 9 Oct 2023 13:35:37 +0700 Subject: [PATCH 16/22] chore: remove unneeded dependency from package.json --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 18ba9e68fb..b6a9dcd822 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "crypto-js": "^4.1.1", "dayjs": "^1.11.9", "dexie": "^3.2.4", - "ecpair": "^2.1.0", "elliptic": "^6.5.4", "events": "^3.3.0", "html5-qrcode": "^2.3.8", From 2631656f94128271ed4909b15fc5caf37fd72d47 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 9 Oct 2023 13:39:52 +0700 Subject: [PATCH 17/22] chore: use getFormattedSats instead of new translation --- src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx | 7 +++++-- src/i18n/locales/en/translation.json | 3 +-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx index 0f0b697371..46879b6ef1 100644 --- a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx +++ b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx @@ -10,6 +10,7 @@ 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"; @@ -23,6 +24,7 @@ function ConfirmSignPsbt() { keyPrefix: "bitcoin.confirm_sign_psbt", }); const navigate = useNavigate(); + const { getFormattedSats } = useSettings(); const psbt = navState.args?.psbt as string; const origin = navState.origin as OriginData; @@ -129,7 +131,7 @@ function ConfirmSignPsbt() {

{t("fee")}

- {t("amount", { amount: preview.fee })} + {getFormattedSats(preview.fee)}

@@ -175,11 +177,12 @@ function AddressPreview({ }: Address & { t: TFunction<"translation", "bitcoin.confirm_sign_psbt", "translation">; }) { + const { getFormattedSats } = useSettings(); return (

{address}

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

); diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 0d5b187a12..fe3709a507 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -849,8 +849,7 @@ "hide_raw_transaction": "Hide raw transaction (Hex)", "inputs": "Inputs", "outputs": "Outputs", - "fee": "Fee", - "amount": "{{amount}} sats" + "fee": "Fee" }, "allow": "Allow this website to:", "allow_sign": "Allow {{host}} to sign:", From d61060b5d4bb0238e2f2b18a04b175b7200f5253 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 9 Oct 2023 13:43:05 +0700 Subject: [PATCH 18/22] fix: signpsbt allow sign translation --- src/i18n/locales/en/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index fe3709a507..aba5626df8 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -842,7 +842,7 @@ }, "confirm_sign_psbt": { "title": "Sign", - "allow_sign": "This website asks you to sign a Bitcoin Transaction:", + "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)", From 649795e1b1078fca6956152f0ed15f28cefff11e Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 9 Oct 2023 13:51:15 +0700 Subject: [PATCH 19/22] chore: make psbt and pset signing screens consistent with nostr --- .../screens/Bitcoin/ConfirmSignPsbt/index.tsx | 25 +++++++++++-------- src/app/screens/Liquid/ConfirmSignPset.tsx | 24 ++++++++++-------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx index 46879b6ef1..03355a37f8 100644 --- a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx +++ b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx @@ -109,14 +109,16 @@ function ConfirmSignPsbt() {

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

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

{t("inputs")}

{preview.inputs.map((input) => ( @@ -133,15 +135,16 @@ function ConfirmSignPsbt() {

{getFormattedSats(preview.fee)}

- +
+
{showHex ? t("hide_raw_transaction") : t("view_raw_transaction")} - - )} -
+
+ + )} {showHex && (
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 && (
From de7aeb9567b5a9a1477735478f6c12c3dab50870 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 9 Oct 2023 13:52:13 +0700 Subject: [PATCH 20/22] fix: casing in liquid transaction translation --- src/i18n/locales/en/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index aba5626df8..e069783546 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -827,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)", From 11360a8b5eb134341dd20c480cfb5c07355c8c75 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 9 Oct 2023 14:15:30 +0700 Subject: [PATCH 21/22] fix: update old comment --- .../Bitcoin/ConfirmSignPsbt/index.test.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx index 43418ad286..8afcf54d4a 100644 --- a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx +++ b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx @@ -6,6 +6,7 @@ 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"; @@ -56,7 +57,17 @@ const mockState = { state.getState = jest.fn().mockReturnValue(mockState); -// mock get settings +jest.mock("~/app/context/SettingsContext", () => ({ + useSettings: () => ({ + getFormattedSats: (amount: number) => + getFormattedSats({ + amount, + locale: "en", + }), + }), +})); + +// mock getPsbtPreview request msg.request = jest .fn() .mockReturnValue( @@ -83,7 +94,7 @@ describe("ConfirmSignPsbt", () => { expect( await screen.findByText( - "This website asks you to sign a Bitcoin Transaction:" + "This website asks you to sign a bitcoin transaction" ) ).toBeInTheDocument(); From 48372099b91a51bc3968b2f7938d28835d9a735c Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Mon, 9 Oct 2023 21:58:46 +0700 Subject: [PATCH 22/22] fix: liquid casing in sign transaction translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: René Aaron <100827540+reneaaron@users.noreply.github.com> --- src/i18n/locales/en/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index e069783546..7516cb5523 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -827,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)",