Skip to content

Commit

Permalink
Bitcoin native experience (#371)
Browse files Browse the repository at this point in the history
Closes: #351

This PR integrates with the OrangeKit SDK to predict the depositor owner
Ethereum address based on user's Bitcoin address. We want to transition
to Bitcoin native experience in the SDK as well so we should operate on
the bitcoin addresses only and hide the Ethereum integration under the
hood. In the future, we will update the other function parameters to
accept Bitcoin addresses instead of Ethereum.

Here we also introduce the `BitcoinProvider` interface which defines the
`getAddress` method. We should rely on the `BitcoinProvider`
implementation to get the Bitcoin address because different wallets use
different strategies. For example, in Ledger Live the address is
"renewed" each time funds are received in order to allow some privacy.
In this case, relying only on the Bitcoin address, the user would create
deposits for different Ethereum addresses each time the Bitcoin address
was renewed.

Therefore, we implement the `BitcoinProvider` for Ledger Live Wallet API
that it awalys returns the same Bitcoin address based on the extended
public key (`xpub`). The Ledger Live Wallet API increments the `index`
in the derivation path each time funds are received(`m/x'/0'/0'/0/0`;
`m/x'/0'/0'/0/1`; `m/x'/0'/0'/0/2` ...), so to get the same address we
need to derive a single address from a given extended public key - an
address under the `m/x'/0'/0'/0/0` path. Thanks to this we can create a
deposit for the same Ethereum address even if Ledger Live renewed the
Bitcoin address.
  • Loading branch information
nkuba authored May 29, 2024
2 parents ae598d1 + 60c1231 commit 3c68902
Show file tree
Hide file tree
Showing 43 changed files with 1,077 additions and 551 deletions.
3 changes: 3 additions & 0 deletions dapp/.env
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ VITE_REFERRAL=123
VITE_DEFENDER_RELAYER_WEBHOOK_URL="https://api.defender.openzeppelin.com/actions/a0d6d2e2-ce9c-4619-aa2b-6c874fe97af7/runs/webhook/b1f17c89-8230-46e3-866f-a3213887974c/Sbddsy54cJ6sPg2bLPyuHJ"

VITE_ACRE_SUBGRAPH_URL="https://api.studio.thegraph.com/query/73600/acre/version/latest"

# TODO: Set this env variable in CI.
VITE_TBTC_API_ENDPOINT=""
3 changes: 2 additions & 1 deletion dapp/ledger-manifest-development.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"account.list",
"message.sign",
"transaction.sign",
"transaction.signAndBroadcast"
"transaction.signAndBroadcast",
"bitcoin.getXPub"
],
"domains": ["http://*"],
"type": "walletApp"
Expand Down
20 changes: 11 additions & 9 deletions dapp/src/acre-react/contexts/AcreSdkContext.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React, { useCallback, useMemo, useState } from "react"
import { LedgerLiveEthereumSigner } from "#/web3"
import { Acre, EthereumNetwork } from "@acre-btc/sdk"
import { Acre } from "@acre-btc/sdk"
import { BitcoinProvider } from "@acre-btc/sdk/dist/src/lib/bitcoin/providers"
import { BITCOIN_NETWORK } from "#/constants"

const TBTC_API_ENDPOINT = import.meta.env.VITE_TBTC_API_ENDPOINT
const ETH_RPC_URL = import.meta.env.VITE_ETH_HOSTNAME_HTTP

type AcreSdkContextValue = {
acre?: Acre
init: (ethereumAddress: string, network: EthereumNetwork) => Promise<void>
init: (bitcoinProvider: BitcoinProvider) => Promise<void>
isInitialized: boolean
}

Expand All @@ -21,14 +23,14 @@ export function AcreSdkProvider({ children }: { children: React.ReactNode }) {
const [isInitialized, setIsInitialized] = useState<boolean>(false)

const init = useCallback<AcreSdkContextValue["init"]>(
async (ethereumAddress: string, network: EthereumNetwork) => {
if (!ethereumAddress) throw new Error("Ethereum address not defined")

const sdk = await Acre.initializeEthereum(
await LedgerLiveEthereumSigner.fromAddress(ethereumAddress),
network,
async (bitcoinProvider: BitcoinProvider) => {
const sdk: Acre = await Acre.initialize(
BITCOIN_NETWORK,
bitcoinProvider,
TBTC_API_ENDPOINT,
ETH_RPC_URL,
)

setAcre(sdk)
setIsInitialized(true)
},
Expand Down
20 changes: 5 additions & 15 deletions dapp/src/acre-react/hooks/useStakeFlow.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { useCallback, useState } from "react"
import {
StakeInitialization,
EthereumAddress,
DepositReceipt,
} from "@acre-btc/sdk"
import { StakeInitialization, DepositReceipt } from "@acre-btc/sdk"
import { useAcreContext } from "./useAcreContext"

export type UseStakeFlowReturn = {
initStake: (
bitcoinRecoveryAddress: string,
ethereumAddress: string,
referral: number,
bitcoinRecoveryAddress?: string,
) => Promise<void>
btcAddress?: string
depositReceipt?: DepositReceipt
Expand All @@ -30,17 +25,12 @@ export function useStakeFlow(): UseStakeFlowReturn {
>(undefined)

const initStake = useCallback(
async (
bitcoinRecoveryAddress: string,
ethereumAddress: string,
referral: number,
) => {
async (referral: number, bitcoinRecoveryAddress?: string) => {
if (!acre || !isInitialized) throw new Error("Acre SDK not defined")

const initializedStakeFlow = await acre.staking.initializeStake(
bitcoinRecoveryAddress,
EthereumAddress.from(ethereumAddress),
referral,
bitcoinRecoveryAddress,
)

const btcDepositAddress = await initializedStakeFlow.getBitcoinAddress()
Expand All @@ -58,7 +48,7 @@ export function useStakeFlow(): UseStakeFlowReturn {
const signMessage = useCallback(async () => {
if (!stakeFlow) throw new Error("Initialize stake first")

await stakeFlow.signMessage()
await Promise.resolve(stakeFlow.signMessage())
}, [stakeFlow])

const stake = useCallback(async () => {
Expand Down
12 changes: 3 additions & 9 deletions dapp/src/components/TransactionModal/ActionFormModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { ReactNode, useCallback, useState } from "react"
import { Box, ModalBody, ModalCloseButton, ModalHeader } from "@chakra-ui/react"
import { useAppDispatch, useStakeFlowContext, useWalletContext } from "#/hooks"
import { useAppDispatch, useStakeFlowContext } from "#/hooks"
import { ACTION_FLOW_TYPES, ActionFlowType, BaseFormProps } from "#/types"
import { TokenAmountFormValues } from "#/components/shared/TokenAmountForm/TokenAmountFormBase"
import { logPromiseFailure } from "#/utils"
Expand All @@ -26,7 +26,6 @@ const FORM_DATA: Record<
}

function ActionFormModal({ type }: { type: ActionFlowType }) {
const { btcAccount, ethAccount } = useWalletContext()
const { initStake } = useStakeFlowContext()
const dispatch = useAppDispatch()

Expand All @@ -35,13 +34,8 @@ function ActionFormModal({ type }: { type: ActionFlowType }) {
const { heading, renderComponent } = FORM_DATA[type]

const handleInitStake = useCallback(async () => {
const btcAddress = btcAccount?.address
const ethAddress = ethAccount?.address

if (btcAddress && ethAddress) {
await initStake(btcAddress, ethAddress)
}
}, [btcAccount?.address, ethAccount?.address, initStake])
await initStake()
}, [initStake])

const handleSubmitForm = useCallback(
async (values: TokenAmountFormValues) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export default function DepositBTCModal() {
const response = await depositTelemetry(
depositReceipt,
btcAddress,
ethAccount.address,
ethAccount,
)

if (response.verificationStatus === "valid") {
Expand Down
15 changes: 2 additions & 13 deletions dapp/src/components/TransactionModal/ModalContentWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import {
useActionFlowTxHash,
useActionFlowType,
useRequestBitcoinAccount,
useRequestEthereumAccount,
useWalletContext,
} from "#/hooks"
import { BitcoinIcon, EthereumIcon } from "#/assets/icons"
import { BitcoinIcon } from "#/assets/icons"
import { PROCESS_STATUSES } from "#/types"
import { isSupportedBTCAddressType } from "#/utils"
import ActionFormModal from "./ActionFormModal"
Expand All @@ -22,9 +21,8 @@ export default function ModalContentWrapper({
}: {
children: React.ReactNode
}) {
const { btcAccount, ethAccount } = useWalletContext()
const { btcAccount } = useWalletContext()
const { requestAccount: requestBitcoinAccount } = useRequestBitcoinAccount()
const { requestAccount: requestEthereumAccount } = useRequestEthereumAccount()
const status = useActionFlowStatus()
const type = useActionFlowType()
const tokenAmount = useActionFlowTokenAmount()
Expand All @@ -39,15 +37,6 @@ export default function ModalContentWrapper({
/>
)

if (!ethAccount)
return (
<MissingAccountModal
currency="ethereum"
icon={EthereumIcon}
requestAccount={requestEthereumAccount}
/>
)

if (!tokenAmount) return <ActionFormModal type={type} />

if (status === PROCESS_STATUSES.LOADING) return <LoadingModal />
Expand Down
16 changes: 5 additions & 11 deletions dapp/src/contexts/StakeFlowContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import {
import { REFERRAL } from "#/constants"

type StakeFlowContextValue = Omit<UseStakeFlowReturn, "initStake"> & {
initStake: (
bitcoinRecoveryAddress: string,
ethereumAddress: string,
) => Promise<void>
initStake: () => Promise<void>
}

export const StakeFlowContext = React.createContext<StakeFlowContextValue>({
Expand All @@ -29,14 +26,11 @@ export function StakeFlowProvider({ children }: { children: React.ReactNode }) {
stake,
} = useStakeFlow()

const initStake = useCallback(
async (bitcoinRecoveryAddress: string, ethereumAddress: string) => {
if (!acre) throw new Error("Acre SDK not defined")
const initStake = useCallback(async () => {
if (!acre) throw new Error("Acre SDK not defined")

await acreInitStake(bitcoinRecoveryAddress, ethereumAddress, REFERRAL)
},
[acreInitStake, acre],
)
await acreInitStake(REFERRAL)
}, [acreInitStake, acre])

const context = useMemo(
() => ({
Expand Down
6 changes: 3 additions & 3 deletions dapp/src/contexts/WalletContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import React, { createContext, useEffect, useMemo, useState } from "react"
type WalletContextValue = {
btcAccount: Account | undefined
setBtcAccount: React.Dispatch<React.SetStateAction<Account | undefined>>
ethAccount: Account | undefined
setEthAccount: React.Dispatch<React.SetStateAction<Account | undefined>>
ethAccount: string | undefined
setEthAccount: React.Dispatch<React.SetStateAction<string | undefined>>
isConnected: boolean
}

Expand All @@ -23,7 +23,7 @@ export function WalletContextProvider({
children: React.ReactNode
}): React.ReactElement {
const [btcAccount, setBtcAccount] = useState<Account | undefined>(undefined)
const [ethAccount, setEthAccount] = useState<Account | undefined>(undefined)
const [ethAccount, setEthAccount] = useState<string | undefined>(undefined)
const [isConnected, setIsConnected] = useState<boolean>(false)

useEffect(() => {
Expand Down
1 change: 0 additions & 1 deletion dapp/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export * from "./toasts"
export * from "./sdk"
export * from "./useDetectThemeMode"
export * from "./useRequestBitcoinAccount"
export * from "./useRequestEthereumAccount"
export * from "./useWalletContext"
export * from "./useSidebar"
export * from "./useDocsDrawer"
Expand Down
2 changes: 1 addition & 1 deletion dapp/src/hooks/sdk/useFetchBTCBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function useFetchBTCBalance() {
const getBtcBalance = async () => {
if (!isInitialized || !ethAccount || !acre) return

const chainIdentifier = EthereumAddress.from(ethAccount.address)
const chainIdentifier = EthereumAddress.from(ethAccount)
const sharesBalance = await acre.staking.sharesBalance(chainIdentifier)
const estimatedBitcoinBalance =
await acre.staking.estimatedBitcoinBalance(chainIdentifier)
Expand Down
19 changes: 12 additions & 7 deletions dapp/src/hooks/sdk/useInitializeAcreSdk.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { useEffect } from "react"
import { ETHEREUM_NETWORK } from "#/constants"
import { BITCOIN_NETWORK } from "#/constants"
import { logPromiseFailure } from "#/utils"
import { useAcreContext } from "#/acre-react/hooks"
import { LedgerLiveWalletApiBitcoinProvider } from "@acre-btc/sdk/dist/src/lib/bitcoin/providers"
import { useWalletContext } from "../useWalletContext"

export function useInitializeAcreSdk() {
const { ethAccount } = useWalletContext()
const { btcAccount } = useWalletContext()
const { init } = useAcreContext()

useEffect(() => {
if (!ethAccount?.address) return
if (!btcAccount?.id) return

const initSDK = async (ethAddress: string) => {
await init(ethAddress, ETHEREUM_NETWORK)
const initSDK = async (bitcoinAccountId: string) => {
const bitcoinProvider = await LedgerLiveWalletApiBitcoinProvider.init(
bitcoinAccountId,
BITCOIN_NETWORK,
)
await init(bitcoinProvider)
}
logPromiseFailure(initSDK(ethAccount.address))
}, [ethAccount?.address, init])
logPromiseFailure(initSDK(btcAccount.id))
}, [btcAccount?.id, init])
}
1 change: 0 additions & 1 deletion dapp/src/hooks/toasts/useInitGlobalToasts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useShowWalletErrorToast } from "./useShowWalletErrorToast"

export function useInitGlobalToasts() {
useShowWalletErrorToast("ethereum")
useShowWalletErrorToast("bitcoin")
}
8 changes: 2 additions & 6 deletions dapp/src/hooks/toasts/useShowWalletErrorToast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,17 @@ import { useToast } from "./useToast"
import { useWallet } from "../useWallet"
import { useTimeout } from "../useTimeout"

const { BITCOIN_WALLET_ERROR, ETHEREUM_WALLET_ERROR } = TOAST_IDS
const { BITCOIN_WALLET_ERROR } = TOAST_IDS

const WALLET_ERROR_TOAST_ID = {
bitcoin: {
id: BITCOIN_WALLET_ERROR,
Component: TOASTS[BITCOIN_WALLET_ERROR],
},
ethereum: {
id: ETHEREUM_WALLET_ERROR,
Component: TOASTS[ETHEREUM_WALLET_ERROR],
},
}

export function useShowWalletErrorToast(
type: "bitcoin" | "ethereum",
type: "bitcoin",
delay = ONE_SEC_IN_MILLISECONDS,
) {
const {
Expand Down
26 changes: 0 additions & 26 deletions dapp/src/hooks/useRequestEthereumAccount.ts

This file was deleted.

19 changes: 13 additions & 6 deletions dapp/src/hooks/useWallet.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { useMemo } from "react"
import { ZeroAddress } from "ethers"
import { useWalletContext } from "./useWalletContext"
import { useRequestBitcoinAccount } from "./useRequestBitcoinAccount"
import { useRequestEthereumAccount } from "./useRequestEthereumAccount"

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

return useMemo(
() => ({
bitcoin: { account: btcAccount, requestAccount: requestBitcoinAccount },
ethereum: { account: ethAccount, requestAccount: requestEthereumAccount },
bitcoin: {
account: btcAccount,
requestAccount: async () => {
await requestBitcoinAccount()
// TODO: Temporary solution - we do not need the eth account and we
// want to create the Acre SDK w/o passing the Ethereum Account.
setEthAccount(ZeroAddress)
},
},
ethereum: { account: ethAccount },
}),
[btcAccount, requestBitcoinAccount, ethAccount, requestEthereumAccount],
[btcAccount, requestBitcoinAccount, ethAccount, setEthAccount],
)
}
2 changes: 0 additions & 2 deletions dapp/src/types/toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {

export const TOAST_IDS = {
BITCOIN_WALLET_ERROR: "bitcoin-wallet-error",
ETHEREUM_WALLET_ERROR: "ethereum-wallet-error",
SIGNING_ERROR: "signing-error",
DEPOSIT_TRANSACTION_ERROR: "deposit-transaction-error",
} as const
Expand All @@ -17,7 +16,6 @@ export type ToastID = (typeof TOAST_IDS)[keyof typeof TOAST_IDS]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const TOASTS: Record<ToastID, (props: any) => ReactNode> = {
[TOAST_IDS.BITCOIN_WALLET_ERROR]: WalletErrorToast,
[TOAST_IDS.ETHEREUM_WALLET_ERROR]: WalletErrorToast,
[TOAST_IDS.SIGNING_ERROR]: SigningMessageErrorToast,
[TOAST_IDS.DEPOSIT_TRANSACTION_ERROR]: DepositTransactionErrorToast,
}
1 change: 1 addition & 0 deletions dapp/src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ interface ImportMetaEnv {
readonly VITE_REFERRAL: number
readonly VITE_TBTC_API_ENDPOINT: string
readonly VITE_ACRE_SUBGRAPH_URL: string
readonly VITE_TBTC_API_ENDPOINT: string
}

interface ImportMeta {
Expand Down
1 change: 0 additions & 1 deletion dapp/src/web3/index.ts

This file was deleted.

Loading

0 comments on commit 3c68902

Please sign in to comment.