From 7c4f3c8d972d3653088b757b9b873854338792ac Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Tue, 20 Feb 2024 15:54:02 +0100 Subject: [PATCH 01/12] Show information about the supported BTC account type --- dapp/src/components/Header/ConnectButton.tsx | 51 ++++++++++++++++++++ dapp/src/components/Header/ConnectWallet.tsx | 34 +------------ dapp/src/constants/chains.ts | 7 ++- dapp/src/utils/address.ts | 6 +++ sdk/src/lib/bitcoin/index.ts | 1 + sdk/src/lib/bitcoin/network.ts | 2 + sdk/src/lib/utils/bitcoin.ts | 22 +++++++++ sdk/src/lib/utils/index.ts | 1 + 8 files changed, 91 insertions(+), 33 deletions(-) create mode 100644 dapp/src/components/Header/ConnectButton.tsx create mode 100644 sdk/src/lib/bitcoin/network.ts create mode 100644 sdk/src/lib/utils/bitcoin.ts diff --git a/dapp/src/components/Header/ConnectButton.tsx b/dapp/src/components/Header/ConnectButton.tsx new file mode 100644 index 000000000..7c0d66c89 --- /dev/null +++ b/dapp/src/components/Header/ConnectButton.tsx @@ -0,0 +1,51 @@ +import React from "react" +import { Button, Icon } from "@chakra-ui/react" +import { Account } from "@ledgerhq/wallet-api-client" +import { + truncateAddress, + asyncWrapper, + isSupportedBTCAddressType, +} from "#/utils" +import { CURRENCY_ID_BITCOIN } from "#/constants" + +const getCustomDataByAccount = ( + account?: Account, +): { text: string; colorScheme?: string } => { + if (!account) return { text: "Not connected", colorScheme: "error" } + + const { address, currency } = account + + if (currency === CURRENCY_ID_BITCOIN && !isSupportedBTCAddressType(address)) + return { text: "Not supported", colorScheme: "error" } + + return { text: truncateAddress(address) } +} + +type ConnectButtonsProps = { + leftIcon: typeof Icon + account: Account | undefined + requestAccount: () => Promise +} + +export function ConnectButton({ + leftIcon, + account, + requestAccount, +}: ConnectButtonsProps) { + const { colorScheme, text } = getCustomDataByAccount(account) + + const handleClick = () => { + asyncWrapper(requestAccount()) + } + + return ( + + ) +} diff --git a/dapp/src/components/Header/ConnectWallet.tsx b/dapp/src/components/Header/ConnectWallet.tsx index 038a069ff..6233788f0 100644 --- a/dapp/src/components/Header/ConnectWallet.tsx +++ b/dapp/src/components/Header/ConnectWallet.tsx @@ -1,6 +1,5 @@ import React from "react" -import { Button, HStack, Icon } from "@chakra-ui/react" -import { Account } from "@ledgerhq/wallet-api-client" +import { HStack } from "@chakra-ui/react" import { useRequestBitcoinAccount, useRequestEthereumAccount, @@ -9,36 +8,7 @@ import { import { CurrencyBalance } from "#/components/shared/CurrencyBalance" import { TextMd } from "#/components/shared/Typography" import { Bitcoin, EthereumIcon } from "#/assets/icons" -import { truncateAddress, asyncWrapper } from "#/utils" - -export type ConnectButtonsProps = { - leftIcon: typeof Icon - account: Account | undefined - requestAccount: () => Promise -} - -function ConnectButton({ - leftIcon, - account, - requestAccount, -}: ConnectButtonsProps) { - const colorScheme = !account ? "error" : undefined - - const handleClick = () => { - asyncWrapper(requestAccount()) - } - - return ( - - ) -} +import { ConnectButton } from "./ConnectButton" export default function ConnectWallet() { const { requestAccount: requestBitcoinAccount } = useRequestBitcoinAccount() diff --git a/dapp/src/constants/chains.ts b/dapp/src/constants/chains.ts index fc9396426..150d7575f 100644 --- a/dapp/src/constants/chains.ts +++ b/dapp/src/constants/chains.ts @@ -1,5 +1,5 @@ import { Chain } from "#/types" -import { EthereumNetwork } from "@acre-btc/sdk" +import { EthereumNetwork, BitcoinNetwork } from "@acre-btc/sdk" export const BLOCK_EXPLORER: Record = { ethereum: { title: "Etherscan", url: "https://etherscan.io" }, @@ -8,3 +8,8 @@ export const BLOCK_EXPLORER: Record = { export const ETHEREUM_NETWORK: EthereumNetwork = import.meta.env.VITE_USE_TESTNET === "true" ? "sepolia" : "mainnet" + +export const BITCOIN_NETWORK: BitcoinNetwork = + import.meta.env.VITE_USE_TESTNET === "true" + ? BitcoinNetwork.Testnet + : BitcoinNetwork.Mainnet diff --git a/dapp/src/utils/address.ts b/dapp/src/utils/address.ts index 7ed3caa1a..6ead9b228 100644 --- a/dapp/src/utils/address.ts +++ b/dapp/src/utils/address.ts @@ -1,3 +1,9 @@ +import { BITCOIN_NETWORK } from "#/constants" +import { isPublicKeyHashTypeAddress } from "@acre-btc/sdk" + export function truncateAddress(address: string): string { return `${address.slice(0, 6)}…${address.slice(-5)}` } + +export const isSupportedBTCAddressType = (address: string): boolean => + isPublicKeyHashTypeAddress(address, BITCOIN_NETWORK) diff --git a/sdk/src/lib/bitcoin/index.ts b/sdk/src/lib/bitcoin/index.ts index 2e48b1561..2652d91ad 100644 --- a/sdk/src/lib/bitcoin/index.ts +++ b/sdk/src/lib/bitcoin/index.ts @@ -1 +1,2 @@ export * from "./transaction" +export * from "./network" diff --git a/sdk/src/lib/bitcoin/network.ts b/sdk/src/lib/bitcoin/network.ts new file mode 100644 index 000000000..bfd491bbb --- /dev/null +++ b/sdk/src/lib/bitcoin/network.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export { BitcoinNetwork } from "@keep-network/tbtc-v2.ts" diff --git a/sdk/src/lib/utils/bitcoin.ts b/sdk/src/lib/utils/bitcoin.ts new file mode 100644 index 000000000..4851c1e54 --- /dev/null +++ b/sdk/src/lib/utils/bitcoin.ts @@ -0,0 +1,22 @@ +import { + BitcoinAddressConverter, + BitcoinNetwork, + BitcoinScriptUtils, +} from "@keep-network/tbtc-v2.ts" + +// P2PKH, P2WPKH, P2SH, or P2WSH +// eslint-disable-next-line import/prefer-default-export +export const isPublicKeyHashTypeAddress = ( + address: string, + network: BitcoinNetwork, +): boolean => { + const outputScript = BitcoinAddressConverter.addressToOutputScript( + address, + network, + ) + + return ( + BitcoinScriptUtils.isP2PKHScript(outputScript) || + BitcoinScriptUtils.isP2WPKHScript(outputScript) + ) +} diff --git a/sdk/src/lib/utils/index.ts b/sdk/src/lib/utils/index.ts index db8dfd8ea..9fe962dbe 100644 --- a/sdk/src/lib/utils/index.ts +++ b/sdk/src/lib/utils/index.ts @@ -1,2 +1,3 @@ export * from "./hex" export * from "./ethereum-signer" +export * from "./bitcoin" From d232104e459ba4bc27caa65de6edf2aafcac4d2f Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Thu, 22 Feb 2024 08:30:29 +0100 Subject: [PATCH 02/12] Add docs for `isPublicKeyHashTypeAddress` --- sdk/src/lib/utils/bitcoin.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sdk/src/lib/utils/bitcoin.ts b/sdk/src/lib/utils/bitcoin.ts index 4851c1e54..02c4fecdf 100644 --- a/sdk/src/lib/utils/bitcoin.ts +++ b/sdk/src/lib/utils/bitcoin.ts @@ -4,7 +4,11 @@ import { BitcoinScriptUtils, } from "@keep-network/tbtc-v2.ts" -// P2PKH, P2WPKH, P2SH, or P2WSH +/** + * Checks if the address is of type P2PKH or P2WPKH + * @param address The address to be checked. + * @param network The network for which the check will be done. + */ // eslint-disable-next-line import/prefer-default-export export const isPublicKeyHashTypeAddress = ( address: string, From 06097198a9a338412e6fd264a01a3ce33508ceee Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Thu, 22 Feb 2024 09:35:20 +0100 Subject: [PATCH 03/12] Add unit tests for `isPublicKeyHashTypeAddress` --- .../{utils/bitcoin.ts => bitcoin/address.ts} | 0 sdk/src/lib/bitcoin/index.ts | 1 + sdk/src/lib/utils/index.ts | 1 - sdk/test/lib/bitcoin/address.test.ts | 32 +++++++++++++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) rename sdk/src/lib/{utils/bitcoin.ts => bitcoin/address.ts} (100%) create mode 100644 sdk/test/lib/bitcoin/address.test.ts diff --git a/sdk/src/lib/utils/bitcoin.ts b/sdk/src/lib/bitcoin/address.ts similarity index 100% rename from sdk/src/lib/utils/bitcoin.ts rename to sdk/src/lib/bitcoin/address.ts diff --git a/sdk/src/lib/bitcoin/index.ts b/sdk/src/lib/bitcoin/index.ts index 2652d91ad..b19a9cff0 100644 --- a/sdk/src/lib/bitcoin/index.ts +++ b/sdk/src/lib/bitcoin/index.ts @@ -1,2 +1,3 @@ export * from "./transaction" export * from "./network" +export * from "./address" diff --git a/sdk/src/lib/utils/index.ts b/sdk/src/lib/utils/index.ts index 9fe962dbe..db8dfd8ea 100644 --- a/sdk/src/lib/utils/index.ts +++ b/sdk/src/lib/utils/index.ts @@ -1,3 +1,2 @@ export * from "./hex" export * from "./ethereum-signer" -export * from "./bitcoin" diff --git a/sdk/test/lib/bitcoin/address.test.ts b/sdk/test/lib/bitcoin/address.test.ts new file mode 100644 index 000000000..8b844a2ae --- /dev/null +++ b/sdk/test/lib/bitcoin/address.test.ts @@ -0,0 +1,32 @@ +import { isPublicKeyHashTypeAddress, BitcoinNetwork } from "../../../src" + +const NATIVE_SEGWIT = "tb1qcurf0mwx8h3ttfp9la5dfa5lzncpfvkl0dscjd" +const LEGACY = "mneUnWAggR9kBj8fvfqdTe84y5v5BJSBFX" +const SEGWIT = "2NDzfWSP81zBXCHb2YNMt5i6XPzo4L3pm1n" + +describe("isPublicKeyHashTypeAddress", () => { + describe("when address is of type P2PKH or P2WPKH", () => { + it("should return true", () => { + const result = isPublicKeyHashTypeAddress( + NATIVE_SEGWIT, + BitcoinNetwork.Testnet, + ) + + expect(result).toBeTruthy() + }) + + it("should return true", () => { + const result = isPublicKeyHashTypeAddress(LEGACY, BitcoinNetwork.Testnet) + + expect(result).toBeTruthy() + }) + }) + + describe("when address is of type P2SH", () => { + it("should return false", () => { + const result = isPublicKeyHashTypeAddress(SEGWIT, BitcoinNetwork.Testnet) + + expect(result).toBeFalsy() + }) + }) +}) From af42a80cb4c7a5a18fbd1a6239b8a526571f15d0 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Mon, 26 Feb 2024 14:03:56 +0100 Subject: [PATCH 04/12] Add a doc for `isSupportedBTCAddressType` --- dapp/src/utils/address.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/dapp/src/utils/address.ts b/dapp/src/utils/address.ts index 6ead9b228..addc7b18b 100644 --- a/dapp/src/utils/address.ts +++ b/dapp/src/utils/address.ts @@ -5,5 +5,6 @@ export function truncateAddress(address: string): string { return `${address.slice(0, 6)}…${address.slice(-5)}` } +// tBTC v2 deposit process supports only 2PKH or P2WPKH Bitcoin address export const isSupportedBTCAddressType = (address: string): boolean => isPublicKeyHashTypeAddress(address, BITCOIN_NETWORK) From 27aa0d01167a1108bb55381f84ac0600ca2441b4 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Tue, 5 Mar 2024 11:50:45 +0100 Subject: [PATCH 05/12] Handling type verification for taproot addresses Ledger has support for taproot addresses is not supported as BTC recovery address in tBTC. When we pass such an address to the `addressToOutputScript` function we get an error. Taproot is not supported on the protocol level anyway so let's handle this check from the SDK side. If an error is catching, let's simply return false. --- sdk/src/lib/bitcoin/address.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/sdk/src/lib/bitcoin/address.ts b/sdk/src/lib/bitcoin/address.ts index 02c4fecdf..1ca65ac8f 100644 --- a/sdk/src/lib/bitcoin/address.ts +++ b/sdk/src/lib/bitcoin/address.ts @@ -14,13 +14,17 @@ export const isPublicKeyHashTypeAddress = ( address: string, network: BitcoinNetwork, ): boolean => { - const outputScript = BitcoinAddressConverter.addressToOutputScript( - address, - network, - ) + try { + const outputScript = BitcoinAddressConverter.addressToOutputScript( + address, + network, + ) - return ( - BitcoinScriptUtils.isP2PKHScript(outputScript) || - BitcoinScriptUtils.isP2WPKHScript(outputScript) - ) + return ( + BitcoinScriptUtils.isP2PKHScript(outputScript) || + BitcoinScriptUtils.isP2WPKHScript(outputScript) + ) + } catch (error) { + return false + } } From a939ccc50b75705af7a3352788cee932948af0a2 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Tue, 5 Mar 2024 11:59:32 +0100 Subject: [PATCH 06/12] Fix typo for SDK docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rafał Czajkowski <57687279+r-czajkowski@users.noreply.github.com> --- sdk/src/lib/bitcoin/address.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/lib/bitcoin/address.ts b/sdk/src/lib/bitcoin/address.ts index 1ca65ac8f..06ce8b00b 100644 --- a/sdk/src/lib/bitcoin/address.ts +++ b/sdk/src/lib/bitcoin/address.ts @@ -5,7 +5,7 @@ import { } from "@keep-network/tbtc-v2.ts" /** - * Checks if the address is of type P2PKH or P2WPKH + * Checks if the address is of type P2PKH or P2WPKH. * @param address The address to be checked. * @param network The network for which the check will be done. */ From 84e96959e2efd211812760745c1695002a52d4ae Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Tue, 5 Mar 2024 12:12:55 +0100 Subject: [PATCH 07/12] Test refactoring for `isPublicKeyHashTypeAddress` --- sdk/test/lib/bitcoin/address.test.ts | 57 +++++++++++------------ sdk/test/lib/bitcoin/data.ts | 68 ++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 28 deletions(-) create mode 100644 sdk/test/lib/bitcoin/data.ts diff --git a/sdk/test/lib/bitcoin/address.test.ts b/sdk/test/lib/bitcoin/address.test.ts index 8b844a2ae..06a58098d 100644 --- a/sdk/test/lib/bitcoin/address.test.ts +++ b/sdk/test/lib/bitcoin/address.test.ts @@ -1,32 +1,33 @@ -import { isPublicKeyHashTypeAddress, BitcoinNetwork } from "../../../src" - -const NATIVE_SEGWIT = "tb1qcurf0mwx8h3ttfp9la5dfa5lzncpfvkl0dscjd" -const LEGACY = "mneUnWAggR9kBj8fvfqdTe84y5v5BJSBFX" -const SEGWIT = "2NDzfWSP81zBXCHb2YNMt5i6XPzo4L3pm1n" +import { BitcoinAddressConverter } from "@keep-network/tbtc-v2.ts" +import { isPublicKeyHashTypeAddress } from "../../../src" +import { btcAddresses } from "./data" describe("isPublicKeyHashTypeAddress", () => { - describe("when address is of type P2PKH or P2WPKH", () => { - it("should return true", () => { - const result = isPublicKeyHashTypeAddress( - NATIVE_SEGWIT, - BitcoinNetwork.Testnet, + const btcAddressesWithExpectedResult = btcAddresses.map((address) => ({ + ...address, + expectedResult: address.type === "P2PKH" || address.type === "P2WPKH", + })) + describe.each(btcAddressesWithExpectedResult)( + "when it is $type $network address", + ({ expectedResult, network, address, scriptPubKey }) => { + const spyOnAddressToOutputScript = jest.spyOn( + BitcoinAddressConverter, + "addressToOutputScript", ) - - expect(result).toBeTruthy() - }) - - it("should return true", () => { - const result = isPublicKeyHashTypeAddress(LEGACY, BitcoinNetwork.Testnet) - - expect(result).toBeTruthy() - }) - }) - - describe("when address is of type P2SH", () => { - it("should return false", () => { - const result = isPublicKeyHashTypeAddress(SEGWIT, BitcoinNetwork.Testnet) - - expect(result).toBeFalsy() - }) - }) + let result: boolean + beforeAll(() => { + result = isPublicKeyHashTypeAddress(address, network) + }) + it("should convert address to output script", () => { + expect(spyOnAddressToOutputScript).toHaveBeenCalledWith( + address, + network, + ) + expect(spyOnAddressToOutputScript).toHaveReturnedWith(scriptPubKey) + }) + it(`should return ${expectedResult}`, () => { + expect(result).toBe(expectedResult) + }) + }, + ) }) diff --git a/sdk/test/lib/bitcoin/data.ts b/sdk/test/lib/bitcoin/data.ts new file mode 100644 index 000000000..e182a455a --- /dev/null +++ b/sdk/test/lib/bitcoin/data.ts @@ -0,0 +1,68 @@ +import { BitcoinNetwork, Hex } from "../../../src" + +// eslint-disable-next-line import/prefer-default-export +export const btcAddresses: { + type: string + network: BitcoinNetwork + address: string + scriptPubKey: Hex +}[] = [ + // Testnet addresses. + { + type: "P2PKH", + network: BitcoinNetwork.Testnet, + address: "mjc2zGWypwpNyDi4ZxGbBNnUA84bfgiwYc", + scriptPubKey: Hex.from( + "76a9142cd680318747b720d67bf4246eb7403b476adb3488ac", + ), + }, + { + type: "P2WPKH", + network: BitcoinNetwork.Testnet, + address: "tb1qumuaw3exkxdhtut0u85latkqfz4ylgwstkdzsx", + scriptPubKey: Hex.from("0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0"), + }, + { + type: "P2SH", + network: BitcoinNetwork.Testnet, + address: "2MsM67NLa71fHvTUBqNENW15P68nHB2vVXb", + scriptPubKey: Hex.from("a914011beb6fb8499e075a57027fb0a58384f2d3f78487"), + }, + { + type: "P2WSH", + network: BitcoinNetwork.Testnet, + address: "tb1qau95mxzh2249aa3y8exx76ltc2sq0e7kw8hj04936rdcmnynhswqqz02vv", + scriptPubKey: Hex.from( + "0020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c", + ), + }, + { + type: "P2PKH", + network: BitcoinNetwork.Mainnet, + address: "12higDjoCCNXSA95xZMWUdPvXNmkAduhWv", + scriptPubKey: Hex.from( + "76a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac", + ), + }, + // Mainnet addresses. + { + type: "P2WPKH", + network: BitcoinNetwork.Mainnet, + address: "bc1q34aq5drpuwy3wgl9lhup9892qp6svr8ldzyy7c", + scriptPubKey: Hex.from("00148d7a0a3461e3891723e5fdf8129caa0075060cff"), + }, + { + type: "P2SH", + network: BitcoinNetwork.Mainnet, + address: "342ftSRCvFHfCeFFBuz4xwbeqnDw6BGUey", + scriptPubKey: Hex.from("a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87"), + }, + { + type: "P2WSH", + network: BitcoinNetwork.Mainnet, + address: "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak", + scriptPubKey: Hex.from( + "0020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", + ), + }, +] From cc20ff80d41d28a64278f84ea2d165b5f2bc79e8 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Tue, 5 Mar 2024 17:33:49 +0100 Subject: [PATCH 08/12] Add test cases for P2TR type addresses --- sdk/test/lib/bitcoin/address.test.ts | 36 +++++++++++++++++++++------- sdk/test/lib/bitcoin/data.ts | 18 +++++++++++++- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/sdk/test/lib/bitcoin/address.test.ts b/sdk/test/lib/bitcoin/address.test.ts index 06a58098d..fa071c408 100644 --- a/sdk/test/lib/bitcoin/address.test.ts +++ b/sdk/test/lib/bitcoin/address.test.ts @@ -2,29 +2,49 @@ import { BitcoinAddressConverter } from "@keep-network/tbtc-v2.ts" import { isPublicKeyHashTypeAddress } from "../../../src" import { btcAddresses } from "./data" +// tBTC v2 deposit process supports: +// - Deposit supports: P2PKH or P2WPKH (as recovery address) +// - Redemption supports: P2PKH, P2WPKH, P2SH, and P2WSH (as redeemer address) +const isSupportedByTBTC = (type: string): boolean => + type === "P2PKH" || type === "P2WPKH" || type === "P2SH" || type === "P2WSH" + describe("isPublicKeyHashTypeAddress", () => { const btcAddressesWithExpectedResult = btcAddresses.map((address) => ({ ...address, expectedResult: address.type === "P2PKH" || address.type === "P2WPKH", + // Should not convert addresses that are not supported by tBTC v2 + shouldConvertAddress: isSupportedByTBTC(address.type), })) + describe.each(btcAddressesWithExpectedResult)( "when it is $type $network address", - ({ expectedResult, network, address, scriptPubKey }) => { + ({ + expectedResult, + network, + address, + scriptPubKey, + shouldConvertAddress, + }) => { const spyOnAddressToOutputScript = jest.spyOn( BitcoinAddressConverter, "addressToOutputScript", ) let result: boolean + beforeAll(() => { result = isPublicKeyHashTypeAddress(address, network) }) - it("should convert address to output script", () => { - expect(spyOnAddressToOutputScript).toHaveBeenCalledWith( - address, - network, - ) - expect(spyOnAddressToOutputScript).toHaveReturnedWith(scriptPubKey) - }) + + if (shouldConvertAddress) { + it("should convert address to output script", () => { + expect(spyOnAddressToOutputScript).toHaveBeenCalledWith( + address, + network, + ) + expect(spyOnAddressToOutputScript).toHaveReturnedWith(scriptPubKey) + }) + } + it(`should return ${expectedResult}`, () => { expect(result).toBe(expectedResult) }) diff --git a/sdk/test/lib/bitcoin/data.ts b/sdk/test/lib/bitcoin/data.ts index e182a455a..4a37fb37b 100644 --- a/sdk/test/lib/bitcoin/data.ts +++ b/sdk/test/lib/bitcoin/data.ts @@ -36,6 +36,15 @@ export const btcAddresses: { "0020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c", ), }, + { + type: "P2TR", + network: BitcoinNetwork.Testnet, + address: "tb1pwrq754496svp5dkxht4chuezy4zt6fwf2l8lpv0gexudeggqzuesvd985f", + scriptPubKey: Hex.from( + "512070c1ea56a5d4181a36c6baeb8bf3222544bd25c957cff0b1e8c9b8dca1001733", + ), + }, + // Mainnet addresses. { type: "P2PKH", network: BitcoinNetwork.Mainnet, @@ -44,7 +53,6 @@ export const btcAddresses: { "76a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac", ), }, - // Mainnet addresses. { type: "P2WPKH", network: BitcoinNetwork.Mainnet, @@ -65,4 +73,12 @@ export const btcAddresses: { "0020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", ), }, + { + type: "P2TR", + network: BitcoinNetwork.Mainnet, + address: "bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k", + scriptPubKey: Hex.from( + "5120339ce7e165e67d93adb3fef88a6d4beed33f01fa876f05a225242b82a631abc0", + ), + }, ] From 27ac2ae82ee302e7ce5e68e0ee9f6636636d0ef3 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Fri, 8 Mar 2024 10:35:49 +0100 Subject: [PATCH 09/12] Add a tooltip for not supported Bitcoin address --- dapp/src/components/Header/ConnectButton.tsx | 51 ------------- dapp/src/components/Header/ConnectWallet.tsx | 72 ++++++++++++++----- .../shared/ActivityBar/ActivityCard.tsx | 2 +- dapp/src/theme/Tooltip.ts | 2 + 4 files changed, 59 insertions(+), 68 deletions(-) delete mode 100644 dapp/src/components/Header/ConnectButton.tsx diff --git a/dapp/src/components/Header/ConnectButton.tsx b/dapp/src/components/Header/ConnectButton.tsx deleted file mode 100644 index 1b94e9c9b..000000000 --- a/dapp/src/components/Header/ConnectButton.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from "react" -import { Button, Icon } from "@chakra-ui/react" -import { Account } from "@ledgerhq/wallet-api-client" -import { - truncateAddress, - logPromiseFailure, - isSupportedBTCAddressType, -} from "#/utils" -import { CURRENCY_ID_BITCOIN } from "#/constants" - -const getCustomDataByAccount = ( - account?: Account, -): { text: string; colorScheme?: string } => { - if (!account) return { text: "Not connected", colorScheme: "error" } - - const { address, currency } = account - - if (currency === CURRENCY_ID_BITCOIN && !isSupportedBTCAddressType(address)) - return { text: "Not supported", colorScheme: "error" } - - return { text: truncateAddress(address) } -} - -type ConnectButtonsProps = { - leftIcon: typeof Icon - account: Account | undefined - requestAccount: () => Promise -} - -export function ConnectButton({ - leftIcon, - account, - requestAccount, -}: ConnectButtonsProps) { - const { colorScheme, text } = getCustomDataByAccount(account) - - const handleClick = () => { - logPromiseFailure(requestAccount()) - } - - return ( - - ) -} diff --git a/dapp/src/components/Header/ConnectWallet.tsx b/dapp/src/components/Header/ConnectWallet.tsx index 6233788f0..b546c270c 100644 --- a/dapp/src/components/Header/ConnectWallet.tsx +++ b/dapp/src/components/Header/ConnectWallet.tsx @@ -1,5 +1,5 @@ import React from "react" -import { HStack } from "@chakra-ui/react" +import { Button, HStack, Icon, Tooltip } from "@chakra-ui/react" import { useRequestBitcoinAccount, useRequestEthereumAccount, @@ -8,13 +8,43 @@ import { import { CurrencyBalance } from "#/components/shared/CurrencyBalance" import { TextMd } from "#/components/shared/Typography" import { Bitcoin, EthereumIcon } from "#/assets/icons" -import { ConnectButton } from "./ConnectButton" +import { Account } from "@ledgerhq/wallet-api-client" +import { CURRENCY_ID_BITCOIN } from "#/constants" +import { + isSupportedBTCAddressType, + logPromiseFailure, + truncateAddress, +} from "#/utils" + +const getCustomDataByAccount = ( + account?: Account, +): { text: string; colorScheme?: string } => { + if (!account) return { text: "Not connected", colorScheme: "error" } + + const { address, currency } = account + + if (currency === CURRENCY_ID_BITCOIN && !isSupportedBTCAddressType(address)) + return { text: "Not supported", colorScheme: "error" } + + return { text: truncateAddress(address) } +} export default function ConnectWallet() { const { requestAccount: requestBitcoinAccount } = useRequestBitcoinAccount() const { requestAccount: requestEthereumAccount } = useRequestEthereumAccount() const { btcAccount, ethAccount } = useWalletContext() + const dataBtcAccount = getCustomDataByAccount(btcAccount) + const dataEthAccount = getCustomDataByAccount(ethAccount) + + const handleConnectBitcoinAccount = () => { + logPromiseFailure(requestBitcoinAccount()) + } + + const handleConnectEthereumAccount = () => { + logPromiseFailure(requestEthereumAccount()) + } + return ( @@ -24,20 +54,30 @@ export default function ConnectWallet() { amount={btcAccount?.balance.toString()} /> - { - await requestBitcoinAccount() - }} - /> - { - await requestEthereumAccount() - }} - /> + + + + ) } diff --git a/dapp/src/components/shared/ActivityBar/ActivityCard.tsx b/dapp/src/components/shared/ActivityBar/ActivityCard.tsx index 96e8cc8ba..89fb48faf 100644 --- a/dapp/src/components/shared/ActivityBar/ActivityCard.tsx +++ b/dapp/src/components/shared/ActivityBar/ActivityCard.tsx @@ -50,7 +50,7 @@ function ActivityCard({ activity, onRemove }: ActivityCardType) { symbolFontWeight="medium" /> {isCompleted ? ( - + Date: Fri, 8 Mar 2024 11:06:41 +0100 Subject: [PATCH 10/12] Show error in console for `isPublicKeyHashTypeAddress` --- sdk/src/lib/bitcoin/address.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/src/lib/bitcoin/address.ts b/sdk/src/lib/bitcoin/address.ts index 06ce8b00b..884e210e0 100644 --- a/sdk/src/lib/bitcoin/address.ts +++ b/sdk/src/lib/bitcoin/address.ts @@ -25,6 +25,8 @@ export const isPublicKeyHashTypeAddress = ( BitcoinScriptUtils.isP2WPKHScript(outputScript) ) } catch (error) { + // eslint-disable-next-line no-console + console.error(error) return false } } From 4f312c80d526a92c03bad0a9f3deadb9c7f84bd9 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Fri, 8 Mar 2024 11:09:04 +0100 Subject: [PATCH 11/12] Rename from `data` to `customData` --- dapp/src/components/Header/ConnectWallet.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dapp/src/components/Header/ConnectWallet.tsx b/dapp/src/components/Header/ConnectWallet.tsx index b546c270c..26de7fbcc 100644 --- a/dapp/src/components/Header/ConnectWallet.tsx +++ b/dapp/src/components/Header/ConnectWallet.tsx @@ -34,8 +34,8 @@ export default function ConnectWallet() { const { requestAccount: requestEthereumAccount } = useRequestEthereumAccount() const { btcAccount, ethAccount } = useWalletContext() - const dataBtcAccount = getCustomDataByAccount(btcAccount) - const dataEthAccount = getCustomDataByAccount(ethAccount) + const customDataBtcAccount = getCustomDataByAccount(btcAccount) + const customDataEthAccount = getCustomDataByAccount(ethAccount) const handleConnectBitcoinAccount = () => { logPromiseFailure(requestBitcoinAccount()) @@ -63,20 +63,20 @@ export default function ConnectWallet() { > ) From 1bf72c0a279ae9106bab2ed4bdb48bb88ae46071 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Thu, 14 Mar 2024 13:01:03 +0100 Subject: [PATCH 12/12] Split tests for `isPublicKeyHashTypeAddress` into two scenarios: - supported address - not supported by tBTC network --- sdk/test/lib/bitcoin/address.test.ts | 97 +++++++++++++++++++--------- 1 file changed, 68 insertions(+), 29 deletions(-) diff --git a/sdk/test/lib/bitcoin/address.test.ts b/sdk/test/lib/bitcoin/address.test.ts index fa071c408..c21e7841b 100644 --- a/sdk/test/lib/bitcoin/address.test.ts +++ b/sdk/test/lib/bitcoin/address.test.ts @@ -1,4 +1,7 @@ -import { BitcoinAddressConverter } from "@keep-network/tbtc-v2.ts" +import { + BitcoinAddressConverter, + BitcoinScriptUtils, +} from "@keep-network/tbtc-v2.ts" import { isPublicKeyHashTypeAddress } from "../../../src" import { btcAddresses } from "./data" @@ -12,42 +15,78 @@ describe("isPublicKeyHashTypeAddress", () => { const btcAddressesWithExpectedResult = btcAddresses.map((address) => ({ ...address, expectedResult: address.type === "P2PKH" || address.type === "P2WPKH", - // Should not convert addresses that are not supported by tBTC v2 - shouldConvertAddress: isSupportedByTBTC(address.type), })) - describe.each(btcAddressesWithExpectedResult)( - "when it is $type $network address", - ({ - expectedResult, - network, - address, - scriptPubKey, - shouldConvertAddress, - }) => { - const spyOnAddressToOutputScript = jest.spyOn( - BitcoinAddressConverter, - "addressToOutputScript", - ) - let result: boolean - - beforeAll(() => { - result = isPublicKeyHashTypeAddress(address, network) - }) - - if (shouldConvertAddress) { + describe("when an address is supported by tBTC network", () => { + const supportedAddresses = btcAddressesWithExpectedResult.filter( + ({ type }) => isSupportedByTBTC(type), + ) + + describe.each(supportedAddresses)( + "when it is $type $network address", + ({ expectedResult, network, address, scriptPubKey }) => { + const spyOnAddressToOutputScript = jest.spyOn( + BitcoinAddressConverter, + "addressToOutputScript", + ) + let result: boolean + + beforeAll(() => { + result = isPublicKeyHashTypeAddress(address, network) + }) + it("should convert address to output script", () => { expect(spyOnAddressToOutputScript).toHaveBeenCalledWith( address, network, ) + expect(spyOnAddressToOutputScript).toHaveReturnedWith(scriptPubKey) }) - } - it(`should return ${expectedResult}`, () => { - expect(result).toBe(expectedResult) - }) - }, - ) + it(`should return ${expectedResult}`, () => { + expect(result).toBe(expectedResult) + }) + }, + ) + }) + + describe("when an address is not supported by tBTC network", () => { + const notSupportedAddresses = btcAddressesWithExpectedResult.filter( + ({ type }) => !isSupportedByTBTC(type), + ) + + describe.each(notSupportedAddresses)( + "when it is $type $network address", + ({ network, address }) => { + const spyOnAddressToOutputScript = jest.spyOn( + BitcoinAddressConverter, + "addressToOutputScript", + ) + let spyOnIsP2PKHScript: jest.SpyInstance + let spyOnIsP2WPKHScript: jest.SpyInstance + let result: boolean + + beforeAll(() => { + spyOnIsP2PKHScript = jest.spyOn(BitcoinScriptUtils, "isP2PKHScript") + spyOnIsP2WPKHScript = jest.spyOn(BitcoinScriptUtils, "isP2WPKHScript") + + result = isPublicKeyHashTypeAddress(address, network) + }) + + it("should not be able to convert address to output script", () => { + expect(spyOnAddressToOutputScript).toThrow() + }) + + it("should not check if an address is P2PKH or P2WPKH", () => { + expect(spyOnIsP2PKHScript).not.toHaveBeenCalled() + expect(spyOnIsP2WPKHScript).not.toHaveBeenCalled() + }) + + it("should return false", () => { + expect(result).toBeFalsy() + }) + }, + ) + }) })