diff --git a/dapp/package.json b/dapp/package.json index 372d30a62..62b096de5 100644 --- a/dapp/package.json +++ b/dapp/package.json @@ -24,7 +24,7 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", - "@orangekit/react": "1.0.0-beta.32-dev.0", + "@orangekit/react": "1.0.0-beta.33-dev.3", "@orangekit/sign-in-with-wallet": "1.0.0-beta.6", "@reduxjs/toolkit": "^2.2.0", "@rehooks/local-storage": "^2.4.5", diff --git a/dapp/src/hooks/index.ts b/dapp/src/hooks/index.ts index 96f4c0205..bfd3f635d 100644 --- a/dapp/src/hooks/index.ts +++ b/dapp/src/hooks/index.ts @@ -34,3 +34,5 @@ export { default as useReferral } from "./useReferral" export { default as useMats } from "./useMats" export { default as useIsEmbed } from "./useIsEmbed" export { default as useSignMessageAndCreateSession } from "./useSignMessageAndCreateSession" +export { default as useTriggerConnectWalletModal } from "./useTriggerConnectWalletModal" +export { default as useLastUsedBtcAddress } from "./useLastUsedBtcAddress" diff --git a/dapp/src/hooks/orangeKit/useBitcoinProvider.ts b/dapp/src/hooks/orangeKit/useBitcoinProvider.ts index 5b6cdaeaa..41e9bce37 100644 --- a/dapp/src/hooks/orangeKit/useBitcoinProvider.ts +++ b/dapp/src/hooks/orangeKit/useBitcoinProvider.ts @@ -8,7 +8,8 @@ export function useBitcoinProvider(): UseBitcoinProviderReturn { const connector = useConnector() return useMemo(() => { - if (!connector) return undefined + if (!connector || typeof connector.getBitcoinProvider !== "function") + return undefined return connector.getBitcoinProvider() }, [connector]) diff --git a/dapp/src/hooks/useLastUsedBtcAddress.ts b/dapp/src/hooks/useLastUsedBtcAddress.ts new file mode 100644 index 000000000..cbe0b787a --- /dev/null +++ b/dapp/src/hooks/useLastUsedBtcAddress.ts @@ -0,0 +1,28 @@ +import { useCallback } from "react" +import useLocalStorage from "./useLocalStorage" + +export const LAST_USED_BTC_ADDRESS_KEY = "lastUsedBtcAddress" + +export default function useLastUsedBtcAddress() { + const [address, setAddress] = useLocalStorage( + LAST_USED_BTC_ADDRESS_KEY, + undefined, + ) + + const setAddressInLocalStorage = useCallback( + (btcAddress: string) => { + setAddress(btcAddress) + }, + [setAddress], + ) + + const removeAddressFromLocalStorage = useCallback(() => { + setAddress(undefined) + }, [setAddress]) + + return { + address, + setAddressInLocalStorage, + removeAddressFromLocalStorage, + } +} diff --git a/dapp/src/hooks/useLocalStorage.ts b/dapp/src/hooks/useLocalStorage.ts index 4eed2d95f..ed42703b5 100644 --- a/dapp/src/hooks/useLocalStorage.ts +++ b/dapp/src/hooks/useLocalStorage.ts @@ -1,5 +1,13 @@ import { useLocalStorage as useRehooksLocalStorage } from "@rehooks/local-storage" +export const getLocalStorageItem = (key: string): string | undefined => { + const value = localStorage.getItem(key) + if (value === "undefined" || value === "null" || value === null) + return undefined + + return value +} + export default function useLocalStorage(key: string, defaultValue: T) { return useRehooksLocalStorage(key, defaultValue) } diff --git a/dapp/src/hooks/useResetWalletState.ts b/dapp/src/hooks/useResetWalletState.ts index 8d4c1bb2f..28d72030d 100644 --- a/dapp/src/hooks/useResetWalletState.ts +++ b/dapp/src/hooks/useResetWalletState.ts @@ -1,4 +1,4 @@ -import { useQueryClient } from "@tanstack/react-query" +import { QueryFilters, useQueryClient } from "@tanstack/react-query" import { useCallback } from "react" import { resetState } from "#/store/wallet" import { queryKeysFactory } from "#/constants" @@ -12,8 +12,10 @@ export default function useResetWalletState() { const dispatch = useAppDispatch() const resetQueries = useCallback(async () => { - queryClient.removeQueries({ queryKey: userKeys.all }) - await queryClient.resetQueries({ queryKey: userKeys.all }) + const filters: QueryFilters = { queryKey: userKeys.all, exact: true } + + queryClient.removeQueries(filters) + await queryClient.resetQueries(filters) }, [queryClient]) return useCallback(() => { diff --git a/dapp/src/hooks/useSignMessageAndCreateSession.ts b/dapp/src/hooks/useSignMessageAndCreateSession.ts index 8e67d9b76..ab1a81193 100644 --- a/dapp/src/hooks/useSignMessageAndCreateSession.ts +++ b/dapp/src/hooks/useSignMessageAndCreateSession.ts @@ -1,9 +1,13 @@ import { OrangeKitConnector } from "#/types" // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { acreApi, orangeKit } from "#/utils" +import { acreApi, getExpirationDate, orangeKit } from "#/utils" import { generateNonce } from "@orangekit/sign-in-with-wallet" import { useCallback } from "react" import { useSignMessage } from "wagmi" +import { ACRE_SESSION_EXPIRATION_TIME } from "#/constants" +import useLocalStorage from "./useLocalStorage" + +const initialSession = { address: "", sessionId: 0 } function useSignMessageAndCreateSession() { const { @@ -12,6 +16,16 @@ function useSignMessageAndCreateSession() { reset: resetMessageStatus, } = useSignMessage() + // TODO: Temporary solution to mock the session mechanism for Ledger Live App + // integration. To fully support the session mechanism exposed by Acre API + // backend we need sign message with "zero" address not fresh address. This is + // will be supported in future versions of Ledger Bitcoin App. + const [session, setSession] = useLocalStorage<{ + address: string + sessionId: number + }>("acre.session", initialSession) + const { address: sessionAddress, sessionId } = session + const signMessageAndCreateSession = useCallback( async (connectedConnector: OrangeKitConnector, btcAddress: string) => { // const session = await acreApi.getSession() @@ -35,6 +49,14 @@ function useSignMessageAndCreateSession() { // if (!("nonce" in session)) { // throw new Error("Session nonce not available") // } + if ( + new Date(sessionId).getTime() > Date.now() && + btcAddress === sessionAddress + ) { + // The session is valid no need to sign message. + return + } + const nonce = generateNonce() const message = orangeKit.createSignInWithWalletMessage(btcAddress, nonce) @@ -44,13 +66,18 @@ function useSignMessageAndCreateSession() { connector: orangeKit.typeConversionToConnector(connectedConnector), }) + const newSessionId = getExpirationDate( + ACRE_SESSION_EXPIRATION_TIME, + ).getTime() + setSession({ address: btcAddress, sessionId: newSessionId }) + // const publicKey = await connectedConnector // .getBitcoinProvider() // .getPublicKey() // await acreApi.createSession(message, signedMessage, publicKey) }, - [signMessageAsync], + [signMessageAsync, sessionAddress, sessionId, setSession], ) return { diff --git a/dapp/src/hooks/useTriggerConnectWalletModal.ts b/dapp/src/hooks/useTriggerConnectWalletModal.ts new file mode 100644 index 000000000..644638ef0 --- /dev/null +++ b/dapp/src/hooks/useTriggerConnectWalletModal.ts @@ -0,0 +1,22 @@ +import { useEffect, useRef } from "react" +import { MODAL_TYPES } from "#/types" +import { useIsSignedMessage } from "./store/useIsSignedMessage" +import { useModal } from "./useModal" +import useIsEmbed from "./useIsEmbed" + +export default function useTriggerConnectWalletModal() { + const isSignedMessage = useIsSignedMessage() + const { isEmbed } = useIsEmbed() + const { openModal, closeModal } = useModal() + const isMounted = useRef(false) + + useEffect(() => { + if (!isMounted.current && isEmbed && !isSignedMessage) { + isMounted.current = true + openModal(MODAL_TYPES.CONNECT_WALLET, { + withCloseButton: false, + closeOnEsc: false, + }) + } + }, [closeModal, isEmbed, isSignedMessage, openModal]) +} diff --git a/dapp/src/hooks/useWallet.ts b/dapp/src/hooks/useWallet.ts index 5d6c1da7e..108c9baa3 100644 --- a/dapp/src/hooks/useWallet.ts +++ b/dapp/src/hooks/useWallet.ts @@ -11,6 +11,7 @@ import { useConnector } from "./orangeKit/useConnector" import { useBitcoinProvider } from "./orangeKit/useBitcoinProvider" import useBitcoinBalance from "./orangeKit/useBitcoinBalance" import useResetWalletState from "./useResetWalletState" +import useLastUsedBtcAddress from "./useLastUsedBtcAddress" const { typeConversionToConnector, typeConversionToOrangeKitConnector } = orangeKit @@ -39,6 +40,8 @@ export function useWallet(): UseWalletReturn { const provider = useBitcoinProvider() const { data: balance } = useBitcoinBalance() const resetWalletState = useResetWalletState() + const { setAddressInLocalStorage, removeAddressFromLocalStorage } = + useLastUsedBtcAddress() const [address, setAddress] = useState(undefined) @@ -82,8 +85,10 @@ export function useWallet(): UseWalletReturn { const onDisconnect = useCallback(() => { disconnect() + setAddress(undefined) resetWalletState() - }, [disconnect, resetWalletState]) + removeAddressFromLocalStorage() + }, [disconnect, removeAddressFromLocalStorage, resetWalletState]) useEffect(() => { const fetchBitcoinAddress = async () => { @@ -91,13 +96,14 @@ export function useWallet(): UseWalletReturn { const btcAddress = await connector.getBitcoinAddress() setAddress(btcAddress) + setAddressInLocalStorage(btcAddress) } else { setAddress(undefined) } } logPromiseFailure(fetchBitcoinAddress()) - }, [connector, provider]) + }, [connector, provider, setAddressInLocalStorage]) return useMemo( () => ({ diff --git a/dapp/src/pages/DashboardPage/index.tsx b/dapp/src/pages/DashboardPage/index.tsx index 811c9f212..cd083af23 100644 --- a/dapp/src/pages/DashboardPage/index.tsx +++ b/dapp/src/pages/DashboardPage/index.tsx @@ -1,5 +1,5 @@ import React from "react" -import { useMobileMode } from "#/hooks" +import { useMobileMode, useTriggerConnectWalletModal } from "#/hooks" import MobileModeBanner from "#/components/MobileModeBanner" import { featureFlags } from "#/constants" import DashboardCard from "./DashboardCard" @@ -13,6 +13,8 @@ import UsefulLinks from "./UsefulLinks" export default function DashboardPage() { const isMobileMode = useMobileMode() + useTriggerConnectWalletModal() + return isMobileMode ? ( ) : ( diff --git a/dapp/src/wagmiConfig.ts b/dapp/src/wagmiConfig.ts index 302ec30b5..beef7ae17 100644 --- a/dapp/src/wagmiConfig.ts +++ b/dapp/src/wagmiConfig.ts @@ -9,6 +9,8 @@ import { import { env } from "./constants" import { router } from "./utils" import { SEARCH_PARAMS_NAMES } from "./router/path" +import { LAST_USED_BTC_ADDRESS_KEY } from "./hooks/useLastUsedBtcAddress" +import { getLocalStorageItem } from "./hooks/useLocalStorage" const isTestnet = env.USE_TESTNET const CHAIN_ID = isTestnet ? sepolia.id : mainnet.id @@ -28,8 +30,12 @@ const transports = chains.reduce( const orangeKitUnisatConnector = getOrangeKitUnisatConnector(connectorConfig) const orangeKitOKXConnector = getOrangeKitOKXConnector(connectorConfig) const orangeKitXverseConnector = getOrangeKitXverseConnector(connectorConfig) -const orangeKitLedgerLiveConnector = - getOrangeKitLedgerLiveConnector(connectorConfig) +const orangeKitLedgerLiveConnector = getOrangeKitLedgerLiveConnector({ + ...connectorConfig, + options: { + tryConnectToAddress: getLocalStorageItem(LAST_USED_BTC_ADDRESS_KEY), + }, +}) const embedConnectors = [orangeKitLedgerLiveConnector()] const defaultConnectors = [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8d626b92..50c1c016e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,8 +36,8 @@ importers: specifier: ^11.11.0 version: 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.3)(react@18.3.1) '@orangekit/react': - specifier: 1.0.0-beta.32-dev.0 - version: 1.0.0-beta.32-dev.0(@tanstack/react-query@5.45.0)(@types/react@18.3.3)(react-dom@18.3.1)(react-native@0.74.2)(typescript@5.4.5) + specifier: 1.0.0-beta.33-dev.3 + version: 1.0.0-beta.33-dev.3(@tanstack/react-query@5.45.0)(@types/react@18.3.3)(react-dom@18.3.1)(react-native@0.74.2)(typescript@5.4.5) '@orangekit/sign-in-with-wallet': specifier: 1.0.0-beta.6 version: 1.0.0-beta.6(bech32@2.0.0)(ethers@6.13.0) @@ -1908,6 +1908,13 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true + /@bitcoin-js/tiny-secp256k1-asmjs@2.2.3: + resolution: {integrity: sha512-arFPdEZi9RIiaG76OZswTnAU0KfuiLwGw2VNfD66LKhzlbfOnX1o1WI/GI3qm9UbjG/0QOzZu/KmTNvL79x/DQ==} + engines: {node: '>=14.0.0'} + dependencies: + uint8array-tools: 0.0.7 + dev: false + /@bitcoinerlab/secp256k1@1.1.1: resolution: {integrity: sha512-uhjW51WfVLpnHN7+G0saDcM/k9IqcyTbZ+bDgLF3AX8V/a3KXSE9vn7UPBrcdU72tp0J4YPR7BHp2m7MLAZ/1Q==} dependencies: @@ -5771,13 +5778,12 @@ packages: - ethers dev: false - /@orangekit/react@1.0.0-beta.32-dev.0(@tanstack/react-query@5.45.0)(@types/react@18.3.3)(react-dom@18.3.1)(react-native@0.74.2)(typescript@5.4.5): - resolution: {integrity: sha512-3yllsWIjqEmgOEYIHiazv4sYDsb3KD1t9Ts8tOG0/By67e2r2yXDQ+ahq7gIJqVWj9qEeo6HMiUSq6ttZL90YA==} + /@orangekit/react@1.0.0-beta.33-dev.3(@tanstack/react-query@5.45.0)(@types/react@18.3.3)(react-dom@18.3.1)(react-native@0.74.2)(typescript@5.4.5): + resolution: {integrity: sha512-oZ6kIF9rgoXqfxCN5akJ/wxp0wedc8X8l+pLcz+wqY5HGf/RTOqSMkH/lFLx38AFGVJGueMgwNz1nllFYfFsiA==} dependencies: '@ledgerhq/wallet-api-client': 1.5.10 - '@orangekit/sdk': 1.0.0-beta.16 + '@orangekit/sdk': 1.0.0-beta.18-dev.0(typescript@5.4.5) '@rainbow-me/rainbowkit': 2.0.2(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)(viem@2.8.16)(wagmi@2.5.12) - '@swan-bitcoin/xpub-lib': 0.1.5 ethers: 6.12.1 react: 18.3.1 sats-connect: 2.8.0(typescript@5.4.5) @@ -5832,6 +5838,27 @@ packages: - utf-8-validate dev: false + /@orangekit/sdk@1.0.0-beta.18-dev.0(typescript@5.4.5): + resolution: {integrity: sha512-WGpBkoM0UIp++j6F6ql8vRUtyooZ3yV2hV5dtH5QtINHYPTSbo8dCBvDCPsSokrn1ev2bT1BTPpP+PXVGQuOzQ==} + dependencies: + '@bitcoin-js/tiny-secp256k1-asmjs': 2.2.3 + '@gelatonetwork/relay-sdk': 5.5.6 + '@noble/curves': 1.4.0 + '@noble/hashes': 1.4.0 + '@orangekit/contracts': 1.0.0-beta.3(ethers@6.12.1) + '@safe-global/protocol-kit': 3.1.1 + bip32: 5.0.0-rc.0(typescript@5.4.5) + bitcoinjs-lib: 6.1.5 + ethers: 6.12.1 + transitivePeerDependencies: + - bufferutil + - debug + - encoding + - supports-color + - typescript + - utf-8-validate + dev: false + /@orangekit/sign-in-with-wallet-parser@1.0.0-beta.6: resolution: {integrity: sha512-Yi6ohSJV4/Ovrq5c7jD+kPE8pZxLhWtFbZjKRwUW8JL60P/tcyT5o0etul0reqcY2iBlIo5aoC2Hh0noRGl86w==} dependencies: @@ -9620,6 +9647,10 @@ packages: resolution: {integrity: sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==} dev: false + /base-x@5.0.0: + resolution: {integrity: sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==} + dev: false + /base58-js@1.0.5: resolution: {integrity: sha512-LkkAPP8Zu+c0SVNRTRVDyMfKVORThX+rCViget00xdgLRrKkClCTz1T7cIrpr69ShwV5XJuuoZvMvJ43yURwkA==} engines: {node: '>= 8'} @@ -9736,6 +9767,19 @@ packages: wif: 2.0.6 dev: false + /bip32@5.0.0-rc.0(typescript@5.4.5): + resolution: {integrity: sha512-5hVFGrdCnF8GB1Lj2eEo4PRE7+jp+3xBLnfNjydivOkMvKmUKeJ9GG8uOy8prmWl3Oh154uzgfudR1FRkNBudA==} + engines: {node: '>=18.0.0'} + dependencies: + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.7 + uint8array-tools: 0.0.8 + valibot: 0.37.0(typescript@5.4.5) + wif: 5.0.0 + transitivePeerDependencies: + - typescript + dev: false + /bip39@3.0.2: resolution: {integrity: sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ==} dependencies: @@ -10028,6 +10072,12 @@ packages: base-x: 4.0.0 dev: false + /bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + dependencies: + base-x: 5.0.0 + dev: false + /bs58check@2.1.2: resolution: {integrity: sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==} dependencies: @@ -10042,6 +10092,13 @@ packages: bs58: 5.0.0 dev: false + /bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + dependencies: + '@noble/hashes': 1.4.0 + bs58: 6.0.0 + dev: false + /bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} dependencies: @@ -20279,6 +20336,16 @@ packages: dev: true optional: true + /uint8array-tools@0.0.7: + resolution: {integrity: sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==} + engines: {node: '>=14.0.0'} + dev: false + + /uint8array-tools@0.0.8: + resolution: {integrity: sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==} + engines: {node: '>=14.0.0'} + dev: false + /uint8arrays@3.1.0: resolution: {integrity: sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog==} dependencies: @@ -20656,6 +20723,17 @@ packages: resolution: {integrity: sha512-ZpFWuI+bs5+PP66q4zVFn4e4t/s5jmMw5iPBZmGUoi8iQqXyU9YY/BLCAyk62Z/bNS8qdUNBEyx52952qdqW3w==} dev: false + /valibot@0.37.0(typescript@5.4.5): + resolution: {integrity: sha512-FQz52I8RXgFgOHym3XHYSREbNtkgSjF9prvMFH1nBsRyfL6SfCzoT1GuSDTlbsuPubM7/6Kbw0ZMQb8A+V+VsQ==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + typescript: 5.4.5 + dev: false + /valid-url@1.0.9: resolution: {integrity: sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==} dev: false @@ -22281,6 +22359,12 @@ packages: bs58check: 2.1.2 dev: false + /wif@5.0.0: + resolution: {integrity: sha512-iFzrC/9ne740qFbNjTZ2FciSRJlHIXoxqk/Y5EnE08QOXu1WjJyCCswwDTYbohAOEnlCtLaAAQBhyaLRFh2hMA==} + dependencies: + bs58check: 4.0.0 + dev: false + /word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'}