diff --git a/context/HashConnectProvider.tsx b/context/HashConnectProvider.tsx index ce47e8c..659b680 100644 --- a/context/HashConnectProvider.tsx +++ b/context/HashConnectProvider.tsx @@ -267,22 +267,16 @@ export default function HashConnectProvider({ if (debug) console.info("===============Saving to localstorage::============="); const { metadata, ...restData } = data; + const walletType = extensionType || connectedAccountType || "hashpack"; const saveObj: SaveData = { ...saveData, pairedWalletData: metadata, pairedAccounts: restData.accountIds, - walletExtensionType: extensionType || connectedAccountType, + walletExtensionType: walletType, ...restData, }; - // console.log(saveObj, "saveObj"); - // await setSaveData((prevSaveData) => { - // prevSaveData.pairedWalletData = metadata; - // console.log("restData", { ...prevSaveData, ...restData }); - // return { ...prevSaveData, ...restData }; - // }); - // console.log("saveData", saveData); setSaveData(saveObj); - // setAccountId(restData.accountIds[0]); + setConnectedAccountType(walletType); StorageService.saveData(saveObj); if ( saveObj !== undefined && @@ -451,6 +445,11 @@ export default function HashConnectProvider({ }; const getAccounts = async (accountId: string) => { + // Set status to connected first, even if balance query fails + if (accountId && connectedAccountType) { + setStatus(WalletStatus.WALLET_CONNECTED); + } + //Create the account info query try { if (connectedAccountType === "hashpack") { @@ -463,12 +462,11 @@ export default function HashConnectProvider({ const balance = await bladeService.getBalance(); setAccountBalance(balance); } - if (connectedAccountType !== "") { - setStatus("WALLET_CONNECTED"); - } } catch (error: any) { setNetworkError(true); console.log(error.message); + // Status is already set to WALLET_CONNECTED above, so wallet connection is still recognized + // even if balance query fails } }; @@ -487,10 +485,17 @@ export default function HashConnectProvider({ }; const signTransaction = async (transactionString: string) => { - // console.log("transactionString", transactionString); - const transaction = Buffer.from(transactionString, "base64"); + if (!transactionString || transactionString.trim() === "") { + throw new Error("Transaction string is empty"); + } + + if (!selectedAccount) { + throw new Error("No account selected. Please connect your wallet."); + } - // console.log("transaction", transaction.buffer); + // Convert base64 string to Uint8Array + const transactionBuffer = Buffer.from(transactionString.trim(), "base64"); + const transaction = new Uint8Array(transactionBuffer); const response: MessageTypes.TransactionResponse = await sendTransaction( transaction, @@ -498,20 +503,17 @@ export default function HashConnectProvider({ true ); - // console.log("response", response); if (response.success && response.signedTransaction) { - // console.log("signedTransaction", signedTransaction); - const signedTransaction = Buffer.from( response.signedTransaction ).toString("base64"); - // console.log(encodedSignature); - // const output: signedTransactionParams = { - // userId: selectedAccount, - // signature: encodedSignature, - // }; - // console.log("output", output); return signedTransaction; + } else if (response.error) { + throw new Error(`Transaction signing failed: ${response.error}`); + } else if (!response.success) { + throw new Error( + "Transaction signing was cancelled or rejected. Please check your wallet." + ); } return null; @@ -681,17 +683,40 @@ export default function HashConnectProvider({ return_trans: boolean = false ) => { const topic = saveData.topic; + + if (!topic) { + throw new Error( + "HashConnect is not properly initialized. Topic is missing. Please reconnect your wallet." + ); + } + + if (!saveData.privateKey) { + throw new Error( + "HashConnect is not properly initialized. Private key is missing. Please reconnect your wallet." + ); + } + + // Ensure HashConnect is initialized with private key for encryption + // This is required before sending transactions + try { + await hashConnect.init(metadata ?? APP_CONFIG, saveData.privateKey); + } catch (error: any) { + // If already initialized, that's fine - continue + if (!error.message?.includes("already initialized")) { + console.warn("HashConnect init warning:", error.message); + } + } + const transaction: MessageTypes.Transaction = { topic: topic, byteArray: trans, - metadata: { accountToSign: acctToSign, returnTransaction: return_trans, }, }; - const response = await hashConnect.sendTransaction(topic, transaction); + const response = await hashConnect.sendTransaction(topic, transaction); return response; }; diff --git a/hooks/useDAppConnector.tsx b/hooks/useDAppConnector.tsx new file mode 100644 index 0000000..d6444ac --- /dev/null +++ b/hooks/useDAppConnector.tsx @@ -0,0 +1,185 @@ +import { useState, useEffect, useCallback } from "react"; +import { LedgerId } from "@hashgraph/sdk"; +import { config } from "../config/config"; + +type WalletStatus = + | "INITIALIZING" + | "WALLET_NOT_CONNECTED" + | "WALLET_CONNECTED"; + +interface UseDAppConnectorReturn { + connect: () => Promise; + signTransaction: (transactionString: string) => Promise; + status: WalletStatus; + accountId: string | null; + isConnecting: boolean; + error: string | null; +} + +const PROJECT_ID = "f891b15efe53e71351537c2c62cd024d"; + +export const useDAppConnector = (): UseDAppConnectorReturn => { + const [status, setStatus] = useState("INITIALIZING"); + const [accountId, setAccountId] = useState(null); + const [isConnecting, setIsConnecting] = useState(false); + const [error, setError] = useState(null); + const [dAppConnector, setDAppConnector] = useState(null); + const [signer, setSigner] = useState(null); + + useEffect(() => { + if (typeof window === "undefined") return; + + let mounted = true; + let connectionInterval: NodeJS.Timeout; + + const initializeDAppConnector = async () => { + try { + setStatus("INITIALIZING"); + + // Dynamic import for client-side only + const { + DAppConnector, + HederaChainId, + HederaJsonRpcMethod, + HederaSessionEvent, + } = await import("@hashgraph/hedera-wallet-connect"); + + const networkName = config.network.name; + const ledgerId = + networkName === "mainnet" ? LedgerId.MAINNET : LedgerId.TESTNET; + const chainId = + networkName === "mainnet" + ? HederaChainId.Mainnet + : HederaChainId.Testnet; + + const metadata = { + name: "Stader HBAR staking", + description: + "Liquid staking with Stader. Stake HBAR with Stader to earn rewards.", + url: window.location.origin, + icons: ["https://hedera.staderlabs.com/static/stader_logo.svg"], + }; + + const connector = new DAppConnector( + metadata, + ledgerId, + PROJECT_ID, + Object.values(HederaJsonRpcMethod), + [HederaSessionEvent.AccountsChanged, HederaSessionEvent.ChainChanged], + [chainId] + ); + + await connector.init({ logger: "error" }); + + const checkConnection = () => { + if (connector.signers && connector.signers.length > 0) { + const firstSigner = connector.signers[0]; + const accId = firstSigner.getAccountId()?.toString() || null; + + if (mounted && accId) { + setAccountId(accId); + setSigner(firstSigner); + setStatus("WALLET_CONNECTED"); + setError(null); + } + } else if (mounted) { + setAccountId(null); + setSigner(null); + setStatus("WALLET_NOT_CONNECTED"); + } + }; + + checkConnection(); + + connectionInterval = setInterval(() => { + if (mounted) checkConnection(); + }, 1000); + + if (mounted) { + setDAppConnector(connector); + } + } catch (err) { + console.error("Failed to initialize DAppConnector:", err); + if (mounted) { + setError("Failed to initialize wallet connector"); + setStatus("WALLET_NOT_CONNECTED"); + } + } + }; + + initializeDAppConnector(); + + return () => { + mounted = false; + if (connectionInterval) { + clearInterval(connectionInterval); + } + }; + }, []); + + const connect = useCallback(async () => { + if (!dAppConnector) { + throw new Error( + "DAppConnector is not initialized. Please refresh the page." + ); + } + + setIsConnecting(true); + setError(null); + + try { + await dAppConnector.openModal(); + } catch (err) { + console.error("Failed to connect:", err); + setError("Failed to connect wallet"); + } finally { + setTimeout(() => setIsConnecting(false), 2000); + } + }, [dAppConnector]); + + const signTransaction = useCallback( + async (transactionString: string): Promise => { + if (!signer || !accountId || !dAppConnector) { + const errorMsg = + "Wallet not connected. Please connect your wallet first."; + setError(errorMsg); + throw new Error(errorMsg); + } + + try { + const { HederaJsonRpcMethod } = await import( + "@hashgraph/hedera-wallet-connect" + ); + + const network = dAppConnector.ledgerId?.toString() || "testnet"; + const signerAccountId = `hedera:${network}:${accountId}`; + + const result = await signer.request({ + method: HederaJsonRpcMethod.SignTransaction, + params: { + signerAccountId: signerAccountId, + transactionBytes: transactionString, + }, + }); + + return result.signedTransaction || result; + } catch (err) { + const errorMsg = + err instanceof Error ? err.message : "Failed to sign transaction"; + console.error("Transaction signing failed:", errorMsg, err); + setError(errorMsg); + throw err; + } + }, + [signer, accountId, dAppConnector] + ); + + return { + connect, + signTransaction, + status, + accountId, + isConnecting, + error, + }; +}; diff --git a/next.config.js b/next.config.js index c08ef37..b27b444 100644 --- a/next.config.js +++ b/next.config.js @@ -93,122 +93,64 @@ const securityHeaders = [ }, ]; -// const sentryWebpackPluginOptions = { -// // Additional config options for the Sentry Webpack plugin. Keep in mind that -// // the following options are set automatically, and overriding them is not -// // recommended: -// // release, url, org, project, authToken, configFile, stripPrefix, -// // urlPrefix, include, ignore - -// silent: true, // Suppresses all logs -// authToken: process.env.SENTRY_AUTH_TOKEN, -// // For all available options, see: -// // https://github.com/getsentry/sentry-webpack-plugin#options. -// }; - -// module.exports = withSentryConfig( -// withTM( -// withImages( -// { -// poweredByHeader: false, - -// productionBrowserSourceMaps: true, -// webpack: (config) => { -// return { -// ...config, -// resolve: { -// ...config.resolve, -// extensions: getSupportedExtensions(config.resolve.extensions), -// }, -// }; -// }, -// sassOptions: { -// includePaths: [path.join(__dirname, "styles")], - -// prependData: `@import "./styles/_mixins.scss";`, -// }, -// compiler: { -// // ssr and displayName are configured by default -// styledComponents: true, -// }, -// async redirects() { -// return [ -// { -// source: "/", -// destination: "/lt-pools", -// permanent: true, -// }, -// ]; -// }, -// async headers() { -// return [ -// { -// // Apply these headers to all routes. -// source: "/:path*", -// headers: securityHeaders, -// }, -// ]; -// }, -// }, -// sentryWebpackPluginOptions -// ) -// ) -// ); -// ... existing code up to line 94 ... - const sentryWebpackPluginOptions = { - silent: true, + // Additional config options for the Sentry Webpack plugin. Keep in mind that + // the following options are set automatically, and overriding them is not + // recommended: + // release, url, org, project, authToken, configFile, stripPrefix, + // urlPrefix, include, ignore + + silent: true, // Suppresses all logs authToken: process.env.SENTRY_AUTH_TOKEN, - // Skip Sentry release creation if no valid token - dryRun: !process.env.SENTRY_AUTH_TOKEN, + // For all available options, see: + // https://github.com/getsentry/sentry-webpack-plugin#options. }; -// Create base config without Sentry -const baseConfig = withTM( - withImages({ - poweredByHeader: false, - - productionBrowserSourceMaps: true, - webpack: (config) => { - return { - ...config, - resolve: { - ...config.resolve, - extensions: getSupportedExtensions(config.resolve.extensions), +module.exports = withSentryConfig( + withTM( + withImages( + { + poweredByHeader: false, + + productionBrowserSourceMaps: true, + webpack: (config) => { + return { + ...config, + resolve: { + ...config.resolve, + extensions: getSupportedExtensions(config.resolve.extensions), + }, + }; }, - }; - }, - sassOptions: { - includePaths: [path.join(__dirname, "styles")], + sassOptions: { + includePaths: [path.join(__dirname, "styles")], - prependData: `@import "./styles/_mixins.scss";`, - }, - compiler: { - // ssr and displayName are configured by default - styledComponents: true, - }, - async redirects() { - return [ - { - source: "/", - destination: "/lt-pools", - permanent: true, + prependData: `@import "./styles/_mixins.scss";`, }, - ]; - }, - async headers() { - return [ - { - // Apply these headers to all routes. - source: "/:path*", - headers: securityHeaders, + compiler: { + // ssr and displayName are configured by default + styledComponents: true, }, - ]; - }, - }) -); - -// Only apply Sentry if auth token is present -module.exports = process.env.SENTRY_AUTH_TOKEN - ? withSentryConfig(baseConfig, sentryWebpackPluginOptions) - : baseConfig; \ No newline at end of file + async redirects() { + return [ + { + source: "/", + destination: "/lt-pools", + permanent: true, + }, + ]; + }, + async headers() { + return [ + { + // Apply these headers to all routes. + source: "/:path*", + headers: securityHeaders, + }, + ]; + }, + }, + sentryWebpackPluginOptions + ) + ) +); \ No newline at end of file diff --git a/pages/transaction.tsx b/pages/transaction.tsx index 8f267ff..ca6156a 100644 --- a/pages/transaction.tsx +++ b/pages/transaction.tsx @@ -1,5 +1,5 @@ import { ButtonOutlined } from "@atoms/Button/Button"; -import useHashConnect from "@hooks/useHashConnect"; +import { useDAppConnector } from "@hooks/useDAppConnector"; import React, { useState } from "react"; import MainLayout from "../layout"; @@ -8,22 +8,41 @@ function Sign() { const [signedTransaction, setSignedTransaction] = useState( null ); - const { signTransaction, status } = useHashConnect(); - const handleClick = (e: React.MouseEvent) => { + const [isSigning, setIsSigning] = useState(false); + const { signTransaction, status, connect, accountId, isConnecting, error } = + useDAppConnector(); + + const handleClick = async (e: React.MouseEvent) => { e.preventDefault(); - setSignedTransaction(""); - getSignedTransaction(); - }; + setSignedTransaction(null); + + if (status !== "WALLET_CONNECTED") { + await connect(); + return; + } - const getSignedTransaction = async () => { - const trans = await signTransaction(transaction); - setSignedTransaction(trans); + setIsSigning(true); + try { + const trans = await signTransaction(transaction); + setSignedTransaction(trans); + } catch (err: any) { + console.error("Failed to sign transaction:", err); + // Error is already set in the hook + } finally { + setIsSigning(false); + } }; - if (status !== "WALLET_CONNECTED") { + if (status === "INITIALIZING") { return (
- + +
+
+ Initializing wallet connector... +
+
+
); } @@ -31,6 +50,35 @@ function Sign() {
+ {status !== "WALLET_CONNECTED" && ( +
+
+ {status === "WALLET_NOT_CONNECTED" + ? "Please connect your wallet to sign transactions" + : "Connecting to wallet..."} +
+ + {isConnecting ? "Connecting..." : "Connect Wallet"} + +
+ )} + + {accountId && ( +
+ Connected: {accountId} +
+ )} + + {error && ( +
+ {error} +
+ )} +
{signedTransaction && ( -
- {signedTransaction} +
+
Signed Transaction:
+
{signedTransaction}
)}