diff --git a/dapp/.env b/dapp/.env index 750d2de79..9b9a93186 100644 --- a/dapp/.env +++ b/dapp/.env @@ -11,6 +11,7 @@ VITE_REFERRAL=0 # TODO: Set this env variable in CI. VITE_TBTC_API_ENDPOINT="" +VITE_ACRE_API_ENDPOINT="http://localhost:8788/api/v1/" # API KEYS VITE_GELATO_RELAY_API_KEY="htaJCy_XHj8WsE3w53WBMurfySDtjLP_TrNPPa6IPIc_" # this key should not be used on production diff --git a/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx b/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx index 6f8d5fe0d..a323181eb 100644 --- a/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx +++ b/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx @@ -3,6 +3,7 @@ import { CONNECTION_ERRORS, ONE_SEC_IN_MILLISECONDS } from "#/constants" import { useAppDispatch, useModal, + useSignMessageAndCreateSession, useWallet, useWalletConnectionError, } from "#/hooks" @@ -20,7 +21,6 @@ import { ImageProps, VStack, } from "@chakra-ui/react" -import { useSignMessage } from "wagmi" import { IconArrowNarrowRight } from "@tabler/icons-react" import { AnimatePresence, Variants, motion } from "framer-motion" import ArrivingSoonTooltip from "../ArrivingSoonTooltip" @@ -61,7 +61,8 @@ export default function ConnectWalletButton({ onDisconnect, status: connectionStatus, } = useWallet() - const { signMessageAsync, status: signMessageStatus } = useSignMessage() + const { signMessageStatus, signMessageAndCreateSession } = + useSignMessageAndCreateSession() const { closeModal } = useModal() const dispatch = useAppDispatch() @@ -82,22 +83,18 @@ export default function ConnectWalletButton({ } }, [closeModal, dispatch, onSuccess]) - const handleSignMessage = useCallback( + const handleSignMessageAndCreateSession = useCallback( async (connectedConnector: OrangeKitConnector, btcAddress: string) => { - const message = orangeKit.createSignInWithWalletMessage(btcAddress) - const signedMessage = await signMessageAsync({ - message, - connector: orangeKit.typeConversionToConnector(connectedConnector), - }) - try { - await orangeKit.verifySignInWithWalletMessage(message, signedMessage) + await signMessageAndCreateSession(connectedConnector, btcAddress) + onSuccessSignMessage() } catch (error) { + console.error("Failed to sign siww message", error) setConnectionError(CONNECTION_ERRORS.INVALID_SIWW_SIGNATURE) } }, - [signMessageAsync, onSuccessSignMessage, setConnectionError], + [signMessageAndCreateSession, onSuccessSignMessage, setConnectionError], ) const onSuccessConnection = useCallback( @@ -106,9 +103,9 @@ export default function ConnectWalletButton({ if (!btcAddress) return - await handleSignMessage(connector, btcAddress) + await handleSignMessageAndCreateSession(connector, btcAddress) }, - [connector, handleSignMessage], + [connector, handleSignMessageAndCreateSession], ) const handleConnection = useCallback(() => { @@ -241,7 +238,9 @@ export default function ConnectWalletButton({ size="lg" variant="outline" onClick={() => - logPromiseFailure(handleSignMessage(connector, address)) + logPromiseFailure( + handleSignMessageAndCreateSession(connector, address), + ) } > Resume and try again diff --git a/dapp/src/constants/env.ts b/dapp/src/constants/env.ts index 57945f2f1..d2b31b236 100644 --- a/dapp/src/constants/env.ts +++ b/dapp/src/constants/env.ts @@ -22,6 +22,8 @@ const NETWORK_TYPE = USE_TESTNET ? "testnet" : "mainnet" const LATEST_COMMIT_HASH = import.meta.env.VITE_LATEST_COMMIT_HASH +const ACRE_API_ENDPOINT = import.meta.env.VITE_ACRE_API_ENDPOINT + export default { PROD, USE_TESTNET, @@ -35,4 +37,5 @@ export default { MEZO_PORTAL_API_KEY, NETWORK_TYPE, LATEST_COMMIT_HASH, + ACRE_API_ENDPOINT, } diff --git a/dapp/src/constants/time.ts b/dapp/src/constants/time.ts index 0620f3abd..cffccbe3d 100644 --- a/dapp/src/constants/time.ts +++ b/dapp/src/constants/time.ts @@ -10,3 +10,7 @@ export const REFETCH_INTERVAL_IN_MILLISECONDS = ONE_SEC_IN_MILLISECONDS * ONE_MINUTE_IN_SECONDS * 5 export const DATE_FORMAT_LOCALE_TAG = "us-US" + +// 7 days +export const ACRE_SESSION_EXPIRATION_TIME = + ONE_WEEK_IN_SECONDS * ONE_SEC_IN_MILLISECONDS diff --git a/dapp/src/hooks/index.ts b/dapp/src/hooks/index.ts index 47ce5dd48..4aad5a775 100644 --- a/dapp/src/hooks/index.ts +++ b/dapp/src/hooks/index.ts @@ -32,3 +32,4 @@ export { default as useLocalStorage } from "./useLocalStorage" export { default as useDetectReferral } from "./useDetectReferral" export { default as useReferral } from "./useReferral" export { default as useMats } from "./useMats" +export { default as useSignMessageAndCreateSession } from "./useSignMessageAndCreateSession" diff --git a/dapp/src/hooks/useSignMessageAndCreateSession.ts b/dapp/src/hooks/useSignMessageAndCreateSession.ts new file mode 100644 index 000000000..a59639595 --- /dev/null +++ b/dapp/src/hooks/useSignMessageAndCreateSession.ts @@ -0,0 +1,57 @@ +import { OrangeKitConnector } from "#/types" +import { acreApi, orangeKit } from "#/utils" +import { useCallback } from "react" +import { useSignMessage } from "wagmi" + +function useSignMessageAndCreateSession() { + const { signMessageAsync, status: signMessageStatus } = useSignMessage() + + const signMessageAndCreateSession = useCallback( + async (connectedConnector: OrangeKitConnector, btcAddress: string) => { + let session = await acreApi.getSession() + const hasSessionAddress = "address" in session + + const isSessionAddressEqual = hasSessionAddress + ? (session as { address: string }).address === btcAddress + : false + + if (hasSessionAddress && isSessionAddressEqual) { + return + } + + if (hasSessionAddress && !isSessionAddressEqual) { + // Delete session. + await acreApi.deleteSession() + // Ask for nonce to create new session. + session = await acreApi.getSession() + } + + if (!("nonce" in session)) { + throw new Error("Session nonce not available") + } + + const message = orangeKit.createSignInWithWalletMessage( + btcAddress, + session.nonce, + ) + + const signedMessage = await signMessageAsync({ + message, + connector: orangeKit.typeConversionToConnector(connectedConnector), + }) + + const publicKey = await connectedConnector + .getBitcoinProvider() + .getPublicKey() + + await acreApi.createSession(message, signedMessage, publicKey) + }, + [signMessageAsync], + ) + + return { + signMessageAndCreateSession, + signMessageStatus, + } +} +export default useSignMessageAndCreateSession diff --git a/dapp/src/utils/acre-api.ts b/dapp/src/utils/acre-api.ts new file mode 100644 index 000000000..8210f1d62 --- /dev/null +++ b/dapp/src/utils/acre-api.ts @@ -0,0 +1,41 @@ +import { env } from "#/constants" +import axiosStatic from "axios" + +const axios = axiosStatic.create({ + baseURL: env.ACRE_API_ENDPOINT, + withCredentials: true, +}) + +async function getSession() { + const response = await axios.get<{ nonce: string } | { address: string }>( + "session", + ) + + return response.data +} + +async function createSession( + message: string, + signature: string, + publicKey: string, +) { + const response = await axios.post<{ success: boolean }>("session", { + message, + signature, + publicKey, + }) + + if (!response.data.success) { + throw new Error("Failed to create Acre session") + } +} + +async function deleteSession() { + await axios.delete("session") +} + +export default { + createSession, + getSession, + deleteSession, +} diff --git a/dapp/src/utils/index.ts b/dapp/src/utils/index.ts index 5cf970f2b..ba68a6887 100644 --- a/dapp/src/utils/index.ts +++ b/dapp/src/utils/index.ts @@ -17,3 +17,4 @@ export { default as userAgent } from "./userAgent" export { default as referralProgram } from "./referralProgram" export { default as mezoPortalAPI } from "./mezoPortalApi" export { default as router } from "./router" +export { default as acreApi } from "./acre-api" diff --git a/dapp/src/utils/orangeKit.ts b/dapp/src/utils/orangeKit.ts index 20ee9f52b..27b8197cc 100644 --- a/dapp/src/utils/orangeKit.ts +++ b/dapp/src/utils/orangeKit.ts @@ -1,4 +1,8 @@ -import { CONNECTION_ERRORS, wallets } from "#/constants" +import { + ACRE_SESSION_EXPIRATION_TIME, + CONNECTION_ERRORS, + wallets, +} from "#/constants" import { ConnectionErrorData, OrangeKitError, @@ -10,6 +14,7 @@ import { } from "@orangekit/react" import { Connector } from "wagmi" import { SignInWithWalletMessage } from "@orangekit/sign-in-with-wallet" +import { getExpirationDate } from "./time" const getWalletInfo = (connector: OrangeKitConnector) => { switch (connector.id) { @@ -37,7 +42,7 @@ const isConnectedStatus = (status: string) => status === "connected" const isOrangeKitConnector = (connector?: Connector) => connector?.type === "orangekit" -const createSignInWithWalletMessage = (address: string) => { +const createSignInWithWalletMessage = (address: string, nonce: string) => { const { host: domain, origin: uri } = window.location const message = new SignInWithWalletMessage({ @@ -47,6 +52,10 @@ const createSignInWithWalletMessage = (address: string) => { issuedAt: new Date().toISOString(), version: "1", networkFamily: "bitcoin", + expirationTime: getExpirationDate( + ACRE_SESSION_EXPIRATION_TIME, + ).toISOString(), + nonce, }) return message.prepareMessage() diff --git a/dapp/src/utils/time.ts b/dapp/src/utils/time.ts index bd40d351d..494d73ddc 100644 --- a/dapp/src/utils/time.ts +++ b/dapp/src/utils/time.ts @@ -84,13 +84,14 @@ export const displayBlockTimestamp = (blockTimestamp: number) => { return getRelativeTime(blockTimestamp) } +export const getExpirationDate = (duration: number, startDate?: Date) => { + const date = startDate ?? new Date() + return new Date(date.getTime() + duration) +} + /** * Returns the expiration timestamp from the start date considering the specified duration. * If the startDate is not passed, the function will take the current time as the start date. */ -export const getExpirationTimestamp = (duration: number, startDate?: Date) => { - const date = startDate ?? new Date() - const expirationDate = new Date(date.getTime() + duration) - - return dateToUnixTimestamp(expirationDate) -} +export const getExpirationTimestamp = (duration: number, startDate?: Date) => + dateToUnixTimestamp(getExpirationDate(duration, startDate)) diff --git a/dapp/src/vite-env.d.ts b/dapp/src/vite-env.d.ts index 556cb5767..9108fde70 100644 --- a/dapp/src/vite-env.d.ts +++ b/dapp/src/vite-env.d.ts @@ -16,6 +16,7 @@ interface ImportMetaEnv { readonly VITE_SUBGRAPH_API_KEY: string readonly VITE_MEZO_PORTAL_API_KEY: string readonly VITE_LATEST_COMMIT_HASH: string + readonly VITE_ACRE_API_ENDPOINT: string } interface ImportMeta {