Skip to content

Commit

Permalink
Support only for P2PKH and P2WPKH addresses in dApp (#256)
Browse files Browse the repository at this point in the history
Closes #239

The user can have several BTC addresses of different types (segwit,
native segwit, legacy). Not all of these addresses we support to execute
stake. The user should use the legacy or native segwit address. This PR
adds support for invalid types of addresses.

### What has been done

- Added a validation function to the SDK.
- Tooltip when the user is connected using not supported address.
  • Loading branch information
kkosiorowska authored Mar 14, 2024
2 parents 6d5e5af + 2a90f32 commit 5dcfc76
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 42 deletions.
92 changes: 51 additions & 41 deletions dapp/src/components/Header/ConnectWallet.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from "react"
import { Button, HStack, Icon } from "@chakra-ui/react"
import { Account } from "@ledgerhq/wallet-api-client"
import { Button, HStack, Icon, Tooltip } from "@chakra-ui/react"
import {
useRequestBitcoinAccount,
useRequestEthereumAccount,
Expand All @@ -9,42 +8,43 @@ import {
import { CurrencyBalance } from "#/components/shared/CurrencyBalance"
import { TextMd } from "#/components/shared/Typography"
import { Bitcoin, EthereumIcon } from "#/assets/icons"
import { truncateAddress, logPromiseFailure } from "#/utils"
import { Account } from "@ledgerhq/wallet-api-client"
import { CURRENCY_ID_BITCOIN } from "#/constants"
import {
isSupportedBTCAddressType,
logPromiseFailure,
truncateAddress,
} from "#/utils"

export type ConnectButtonsProps = {
leftIcon: typeof Icon
account: Account | undefined
requestAccount: () => Promise<void>
}
const getCustomDataByAccount = (
account?: Account,
): { text: string; colorScheme?: string } => {
if (!account) return { text: "Not connected", colorScheme: "error" }

function ConnectButton({
leftIcon,
account,
requestAccount,
}: ConnectButtonsProps) {
const colorScheme = !account ? "error" : undefined
const { address, currency } = account

const handleClick = () => {
logPromiseFailure(requestAccount())
}
if (currency === CURRENCY_ID_BITCOIN && !isSupportedBTCAddressType(address))
return { text: "Not supported", colorScheme: "error" }

return (
<Button
variant="card"
colorScheme={colorScheme}
leftIcon={<Icon as={leftIcon} boxSize={6} />}
onClick={handleClick}
>
{account ? truncateAddress(account.address) : "Not connected"}
</Button>
)
return { text: truncateAddress(address) }
}

export default function ConnectWallet() {
const { requestAccount: requestBitcoinAccount } = useRequestBitcoinAccount()
const { requestAccount: requestEthereumAccount } = useRequestEthereumAccount()
const { btcAccount, ethAccount } = useWalletContext()

const customDataBtcAccount = getCustomDataByAccount(btcAccount)
const customDataEthAccount = getCustomDataByAccount(ethAccount)

const handleConnectBitcoinAccount = () => {
logPromiseFailure(requestBitcoinAccount())
}

const handleConnectEthereumAccount = () => {
logPromiseFailure(requestEthereumAccount())
}

return (
<HStack spacing={4}>
<HStack display={{ base: "none", md: "flex" }}>
Expand All @@ -54,20 +54,30 @@ export default function ConnectWallet() {
amount={btcAccount?.balance.toString()}
/>
</HStack>
<ConnectButton
leftIcon={Bitcoin}
account={btcAccount}
requestAccount={async () => {
await requestBitcoinAccount()
}}
/>
<ConnectButton
leftIcon={EthereumIcon}
account={ethAccount}
requestAccount={async () => {
await requestEthereumAccount()
}}
/>
<Tooltip
label="Currently, we support only Legacy or Native SegWit addresses. Please try connecting another address."
placement="top"
isDisabled={
!(btcAccount && !isSupportedBTCAddressType(btcAccount.address))
}
>
<Button
variant="card"
colorScheme={customDataBtcAccount.colorScheme}
leftIcon={<Icon as={Bitcoin} boxSize={6} />}
onClick={handleConnectBitcoinAccount}
>
{customDataBtcAccount.text}
</Button>
</Tooltip>
<Button
variant="card"
colorScheme={customDataEthAccount.colorScheme}
leftIcon={<Icon as={EthereumIcon} boxSize={6} />}
onClick={handleConnectEthereumAccount}
>
{customDataEthAccount.text}
</Button>
</HStack>
)
}
2 changes: 1 addition & 1 deletion dapp/src/components/shared/ActivityBar/ActivityCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function ActivityCard({ activity, onRemove }: ActivityCardType) {
symbolFontWeight="medium"
/>
{isCompleted ? (
<Tooltip label="Remove" placement="top" paddingX={3} paddingY={2}>
<Tooltip label="Remove" placement="top">
<CloseButton
size="sm"
onClick={onClose}
Expand Down
2 changes: 2 additions & 0 deletions dapp/src/theme/Tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ export const tooltipTheme: ComponentSingleStyleConfig = {
borderRadius: "md",
bg: "grey.700",
[$arrowBg.variable]: "colors.grey.700",
py: 2,
px: 3,
},
}
7 changes: 7 additions & 0 deletions dapp/src/utils/address.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
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)}`
}

// tBTC v2 deposit process supports only 2PKH or P2WPKH Bitcoin address
export const isSupportedBTCAddressType = (address: string): boolean =>
isPublicKeyHashTypeAddress(address, BITCOIN_NETWORK)
32 changes: 32 additions & 0 deletions sdk/src/lib/bitcoin/address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
BitcoinAddressConverter,
BitcoinNetwork,
BitcoinScriptUtils,
} from "@keep-network/tbtc-v2.ts"

/**
* 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,
network: BitcoinNetwork,
): boolean => {
try {
const outputScript = BitcoinAddressConverter.addressToOutputScript(
address,
network,
)

return (
BitcoinScriptUtils.isP2PKHScript(outputScript) ||
BitcoinScriptUtils.isP2WPKHScript(outputScript)
)
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
return false
}
}
1 change: 1 addition & 0 deletions sdk/src/lib/bitcoin/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./transaction"
export * from "./network"
export * from "./address"
92 changes: 92 additions & 0 deletions sdk/test/lib/bitcoin/address.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {
BitcoinAddressConverter,
BitcoinScriptUtils,
} 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",
}))

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)
})
},
)
})

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<boolean>
let spyOnIsP2WPKHScript: jest.SpyInstance<boolean>
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()
})
},
)
})
})
84 changes: 84 additions & 0 deletions sdk/test/lib/bitcoin/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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: "P2TR",
network: BitcoinNetwork.Testnet,
address: "tb1pwrq754496svp5dkxht4chuezy4zt6fwf2l8lpv0gexudeggqzuesvd985f",
scriptPubKey: Hex.from(
"512070c1ea56a5d4181a36c6baeb8bf3222544bd25c957cff0b1e8c9b8dca1001733",
),
},
// Mainnet addresses.
{
type: "P2PKH",
network: BitcoinNetwork.Mainnet,
address: "12higDjoCCNXSA95xZMWUdPvXNmkAduhWv",
scriptPubKey: Hex.from(
"76a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac",
),
},
{
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",
),
},
{
type: "P2TR",
network: BitcoinNetwork.Mainnet,
address: "bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k",
scriptPubKey: Hex.from(
"5120339ce7e165e67d93adb3fef88a6d4beed33f01fa876f05a225242b82a631abc0",
),
},
]

0 comments on commit 5dcfc76

Please sign in to comment.