From dcddbcb1b406e2644c724599ff13de7b21ac7aae Mon Sep 17 00:00:00 2001 From: blockgroot <170620375+blockgroot@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:53:20 +0530 Subject: [PATCH 1/4] fix: wallet connection errors --- context/HashConnectProvider.tsx | 77 ++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 26 deletions(-) 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; }; From 1f530ff026a9e147e6c52805e28b12631f242096 Mon Sep 17 00:00:00 2001 From: blockgroot <170620375+blockgroot@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:55:17 +0530 Subject: [PATCH 2/4] fix config --- next.config.js | 164 ++++++++++++++++--------------------------------- 1 file changed, 53 insertions(+), 111 deletions(-) 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 From e489d61bd133f0b716a1de4db07436802cde89fc Mon Sep 17 00:00:00 2001 From: blockgroot <170620375+blockgroot@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:35:07 +0530 Subject: [PATCH 3/4] fix --- hooks/useDAppConnector.tsx | 350 +++++++++++++++++++++++++++++++++++++ pages/transaction.tsx | 90 ++++++++-- 2 files changed, 424 insertions(+), 16 deletions(-) create mode 100644 hooks/useDAppConnector.tsx diff --git a/hooks/useDAppConnector.tsx b/hooks/useDAppConnector.tsx new file mode 100644 index 0000000..928baa5 --- /dev/null +++ b/hooks/useDAppConnector.tsx @@ -0,0 +1,350 @@ +import { useState, useEffect, useCallback } from "react"; +import { config } from "../config/config"; +import { Transaction, LedgerId, AccountId } from "@hashgraph/sdk"; + +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; +} + +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(() => { + // Only initialize on client side + if (typeof window === "undefined") return; + + // Suppress WalletConnect deep link errors (harmless - happens when no mobile wallet app is installed) + const originalError = console.error.bind(console); + const errorHandler = (message: any, ...args: any[]) => { + if ( + typeof message === "string" && + (message.includes("Failed to launch 'wc:") || + message.includes("scheme does not have a registered handler")) + ) { + // Suppress this specific error - it's harmless + return; + } + originalError(message, ...args); + }; + console.error = errorHandler; + + let mounted = true; + + const initializeDAppConnector = async () => { + try { + setStatus("INITIALIZING"); + + // Dynamically import to avoid SSR issues + const { + DAppConnector, + HederaChainId, + HederaJsonRpcMethod, + HederaSessionEvent, + } = await import("@hashgraph/hedera-wallet-connect"); + + const projectId = "f891b15efe53e71351537c2c62cd024d"; + + if (!projectId) { + throw new Error( + "NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID is not set. Please set it in your environment variables." + ); + } + + // Get network from config + const networkName = config.network.name; + const ledgerId = + networkName === "mainnet" + ? LedgerId.MAINNET + : networkName === "testnet" + ? LedgerId.TESTNET + : 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 while keeping full control of your staked tokens.", + url: + typeof window !== "undefined" + ? window.location.origin + : "https://hedera.staderlabs.com", + icons: ["https://hedera.staderlabs.com/static/stader_logo.svg"], + }; + + const connector = new DAppConnector( + metadata, + ledgerId, + projectId, + Object.values(HederaJsonRpcMethod), + [HederaSessionEvent.AccountsChanged, HederaSessionEvent.ChainChanged], + [chainId] + ); + + await connector.init({ logger: "error" }); + + // Check if already connected + const checkConnection = () => { + if (connector.signers && connector.signers.length > 0) { + const firstSigner = connector.signers[0]; + const accId = firstSigner.getAccountId()?.toString() || null; + if (mounted) { + setAccountId(accId); + setSigner(firstSigner); + setStatus("WALLET_CONNECTED"); + setError(null); + } + return true; + } else { + if (mounted) { + setAccountId(null); + setSigner(null); + setStatus("WALLET_NOT_CONNECTED"); + } + return false; + } + }; + + // Initial check + checkConnection(); + + // Poll for connection changes (since event API might not be available) + const connectionInterval = setInterval(() => { + if (mounted) { + checkConnection(); + } + }, 1000); + + // Store interval for cleanup + (connector as any)._connectionInterval = connectionInterval; + + if (mounted) { + setDAppConnector(connector); + } + } catch (err: any) { + console.error("Failed to initialize DAppConnector:", err); + if (mounted) { + setError(err.message || "Failed to initialize wallet connector"); + setStatus("WALLET_NOT_CONNECTED"); + } + } + }; + + initializeDAppConnector(); + + return () => { + mounted = false; + if (dAppConnector && (dAppConnector as any)._connectionInterval) { + clearInterval((dAppConnector as any)._connectionInterval); + } + // Restore original console.error + console.error = originalError; + }; + }, []); + + const connect = useCallback(async () => { + if (!dAppConnector) { + setError("DAppConnector is not initialized. Please refresh the page."); + return; + } + + try { + setIsConnecting(true); + setError(null); + await dAppConnector.openModal(); + + // Wait a bit for the connection to establish + // The polling in useEffect will detect the connection + setTimeout(() => { + setIsConnecting(false); + }, 2000); + } catch (err: any) { + console.error("Failed to connect wallet:", err); + setError(err.message || "Failed to connect wallet"); + setIsConnecting(false); + } + }, [dAppConnector]); + + const signTransaction = useCallback( + async (transactionString: string): Promise => { + if (!transactionString || transactionString.trim() === "") { + throw new Error("Transaction string is empty"); + } + + if (!signer) { + throw new Error( + "No wallet connected. Please connect your wallet first." + ); + } + + if (!dAppConnector) { + throw new Error("DAppConnector is not initialized."); + } + + try { + setError(null); + + // Validate and convert base64 string to transaction bytes + const trimmedTransaction = transactionString.trim(); + + if (!trimmedTransaction) { + throw new Error("Transaction string is empty"); + } + + // Validate base64 format and get transaction buffer + let transactionBuffer: Buffer; + try { + transactionBuffer = Buffer.from(trimmedTransaction, "base64"); + + if (transactionBuffer.length === 0) { + throw new Error( + "Invalid transaction: decoded bytes are empty. Please check the base64 format." + ); + } + + // Log transaction size for debugging + console.log(`Transaction bytes length: ${transactionBuffer.length}`); + } catch (base64Error: any) { + throw new Error( + `Invalid base64 format: ${base64Error.message}. Please ensure the transaction string is valid base64.` + ); + } + + const transactionBytes = new Uint8Array(transactionBuffer); + + // Deserialize transaction from bytes + // This will throw if the bytes are incomplete or corrupted + let transaction: Transaction; + try { + transaction = Transaction.fromBytes(transactionBytes); + + // Validate that transaction was deserialized correctly + if (!transaction) { + throw new Error( + "Failed to deserialize transaction. Please check the transaction format." + ); + } + } catch (deserializeError: any) { + // Handle deserialization errors specifically + if ( + deserializeError.message?.includes("index out of range") || + deserializeError.message?.includes("out of range") + ) { + throw new Error( + `Transaction bytes are incomplete or corrupted. ` + + `Expected more bytes but only received ${transactionBuffer.length} bytes. ` + + `Please ensure the full transaction base64 string is provided. ` + + `The transaction may be truncated or missing data.` + ); + } + // Re-throw other deserialization errors + throw new Error( + `Failed to deserialize transaction: ${ + deserializeError.message || "Unknown error" + }` + ); + } + + // Always try to freeze the transaction with the signer first + // This ensures the transaction is properly set up with node account IDs and bodyBytes + // If the transaction is already frozen, this will fail, and we'll handle that case + try { + await transaction.freezeWithSigner(signer); + } catch (freezeError: any) { + const errorMessage = freezeError.message || ""; + + // If the transaction is already frozen/immutable, that's okay - proceed to signing + if ( + errorMessage.includes("immutable") || + errorMessage.includes("frozen") || + errorMessage.includes("already") + ) { + // Transaction is already frozen - we can't freeze it again, but we can try to sign it + console.warn( + "Transaction is already frozen. Proceeding to sign without re-freezing." + ); + + // Verify transaction has bodyBytes before attempting to sign + const hasBodyBytes = (transaction as any).bodyBytes !== undefined; + if (!hasBodyBytes) { + throw new Error( + "Transaction is frozen but does not have bodyBytes. " + + "The transaction may have been frozen incorrectly or is in an invalid state. " + + "Please ensure the transaction was properly created and frozen with the correct signer." + ); + } + // Transaction is frozen and has bodyBytes - proceed to signing + } else { + // Some other error occurred during freezing - re-throw it + throw new Error( + `Failed to freeze transaction with signer: ${errorMessage}` + ); + } + } + + // Verify transaction has bodyBytes before signing + if (!(transaction as any).bodyBytes) { + throw new Error( + "Transaction does not have bodyBytes. It must be frozen before signing." + ); + } + + // Sign the transaction (does NOT execute) + // This will open the wallet modal for user to approve/reject + // The signer's signTransaction method requires the transaction to have bodyBytes + const signedTransaction = await signer.signTransaction(transaction); + + // Convert signed transaction to bytes and return as base64 string + const signedTransactionBytes = signedTransaction.toBytes(); + return Buffer.from(signedTransactionBytes).toString("base64"); + } catch (err: any) { + console.error("Error signing transaction:", err); + + // Provide more specific error messages for common issues + let errorMessage = + err.message || "Failed to sign transaction. Please try again."; + + if ( + err.message?.includes("index out of range") || + err.message?.includes("out of range") + ) { + errorMessage = + `Transaction bytes are incomplete or corrupted. ` + + `The transaction base64 string appears to be truncated or missing data. ` + + `Please ensure you provide the complete transaction string.`; + } + + setError(errorMessage); + throw new Error(errorMessage); + } + }, + [signer, dAppConnector] + ); + + return { + connect, + signTransaction, + status, + accountId, + isConnecting, + error, + }; +}; 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}
)}
From e0b0574c0f3ad10e6b8f64dadd2ab28b79b6964f Mon Sep 17 00:00:00 2001 From: blockgroot <170620375+blockgroot@users.noreply.github.com> Date: Tue, 20 Jan 2026 10:02:49 +0530 Subject: [PATCH 4/4] use json rpc method to sign --- hooks/useDAppConnector.tsx | 283 ++++++++----------------------------- 1 file changed, 59 insertions(+), 224 deletions(-) diff --git a/hooks/useDAppConnector.tsx b/hooks/useDAppConnector.tsx index 928baa5..d6444ac 100644 --- a/hooks/useDAppConnector.tsx +++ b/hooks/useDAppConnector.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from "react"; +import { LedgerId } from "@hashgraph/sdk"; import { config } from "../config/config"; -import { Transaction, LedgerId, AccountId } from "@hashgraph/sdk"; type WalletStatus = | "INITIALIZING" @@ -16,6 +16,8 @@ interface UseDAppConnectorReturn { error: string | null; } +const PROJECT_ID = "f891b15efe53e71351537c2c62cd024d"; + export const useDAppConnector = (): UseDAppConnectorReturn => { const [status, setStatus] = useState("INITIALIZING"); const [accountId, setAccountId] = useState(null); @@ -25,31 +27,16 @@ export const useDAppConnector = (): UseDAppConnectorReturn => { const [signer, setSigner] = useState(null); useEffect(() => { - // Only initialize on client side if (typeof window === "undefined") return; - // Suppress WalletConnect deep link errors (harmless - happens when no mobile wallet app is installed) - const originalError = console.error.bind(console); - const errorHandler = (message: any, ...args: any[]) => { - if ( - typeof message === "string" && - (message.includes("Failed to launch 'wc:") || - message.includes("scheme does not have a registered handler")) - ) { - // Suppress this specific error - it's harmless - return; - } - originalError(message, ...args); - }; - console.error = errorHandler; - let mounted = true; + let connectionInterval: NodeJS.Timeout; const initializeDAppConnector = async () => { try { setStatus("INITIALIZING"); - // Dynamically import to avoid SSR issues + // Dynamic import for client-side only const { DAppConnector, HederaChainId, @@ -57,23 +44,9 @@ export const useDAppConnector = (): UseDAppConnectorReturn => { HederaSessionEvent, } = await import("@hashgraph/hedera-wallet-connect"); - const projectId = "f891b15efe53e71351537c2c62cd024d"; - - if (!projectId) { - throw new Error( - "NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID is not set. Please set it in your environment variables." - ); - } - - // Get network from config const networkName = config.network.name; const ledgerId = - networkName === "mainnet" - ? LedgerId.MAINNET - : networkName === "testnet" - ? LedgerId.TESTNET - : LedgerId.TESTNET; - + networkName === "mainnet" ? LedgerId.MAINNET : LedgerId.TESTNET; const chainId = networkName === "mainnet" ? HederaChainId.Mainnet @@ -82,18 +55,15 @@ export const useDAppConnector = (): UseDAppConnectorReturn => { const metadata = { name: "Stader HBAR staking", description: - "Liquid staking with Stader. Stake HBAR with Stader to earn rewards while keeping full control of your staked tokens.", - url: - typeof window !== "undefined" - ? window.location.origin - : "https://hedera.staderlabs.com", + "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, - projectId, + PROJECT_ID, Object.values(HederaJsonRpcMethod), [HederaSessionEvent.AccountsChanged, HederaSessionEvent.ChainChanged], [chainId] @@ -101,48 +71,37 @@ export const useDAppConnector = (): UseDAppConnectorReturn => { await connector.init({ logger: "error" }); - // Check if already connected const checkConnection = () => { if (connector.signers && connector.signers.length > 0) { const firstSigner = connector.signers[0]; const accId = firstSigner.getAccountId()?.toString() || null; - if (mounted) { + + if (mounted && accId) { setAccountId(accId); setSigner(firstSigner); setStatus("WALLET_CONNECTED"); setError(null); } - return true; - } else { - if (mounted) { - setAccountId(null); - setSigner(null); - setStatus("WALLET_NOT_CONNECTED"); - } - return false; + } else if (mounted) { + setAccountId(null); + setSigner(null); + setStatus("WALLET_NOT_CONNECTED"); } }; - // Initial check checkConnection(); - // Poll for connection changes (since event API might not be available) - const connectionInterval = setInterval(() => { - if (mounted) { - checkConnection(); - } + connectionInterval = setInterval(() => { + if (mounted) checkConnection(); }, 1000); - // Store interval for cleanup - (connector as any)._connectionInterval = connectionInterval; - if (mounted) { setDAppConnector(connector); } - } catch (err: any) { + } catch (err) { console.error("Failed to initialize DAppConnector:", err); if (mounted) { - setError(err.message || "Failed to initialize wallet connector"); + setError("Failed to initialize wallet connector"); setStatus("WALLET_NOT_CONNECTED"); } } @@ -152,191 +111,67 @@ export const useDAppConnector = (): UseDAppConnectorReturn => { return () => { mounted = false; - if (dAppConnector && (dAppConnector as any)._connectionInterval) { - clearInterval((dAppConnector as any)._connectionInterval); + if (connectionInterval) { + clearInterval(connectionInterval); } - // Restore original console.error - console.error = originalError; }; }, []); const connect = useCallback(async () => { if (!dAppConnector) { - setError("DAppConnector is not initialized. Please refresh the page."); - return; + throw new Error( + "DAppConnector is not initialized. Please refresh the page." + ); } + setIsConnecting(true); + setError(null); + try { - setIsConnecting(true); - setError(null); await dAppConnector.openModal(); - - // Wait a bit for the connection to establish - // The polling in useEffect will detect the connection - setTimeout(() => { - setIsConnecting(false); - }, 2000); - } catch (err: any) { - console.error("Failed to connect wallet:", err); - setError(err.message || "Failed to connect wallet"); - setIsConnecting(false); + } 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 (!transactionString || transactionString.trim() === "") { - throw new Error("Transaction string is empty"); - } - - if (!signer) { - throw new Error( - "No wallet connected. Please connect your wallet first." - ); - } - - if (!dAppConnector) { - throw new Error("DAppConnector is not initialized."); + if (!signer || !accountId || !dAppConnector) { + const errorMsg = + "Wallet not connected. Please connect your wallet first."; + setError(errorMsg); + throw new Error(errorMsg); } try { - setError(null); - - // Validate and convert base64 string to transaction bytes - const trimmedTransaction = transactionString.trim(); - - if (!trimmedTransaction) { - throw new Error("Transaction string is empty"); - } - - // Validate base64 format and get transaction buffer - let transactionBuffer: Buffer; - try { - transactionBuffer = Buffer.from(trimmedTransaction, "base64"); - - if (transactionBuffer.length === 0) { - throw new Error( - "Invalid transaction: decoded bytes are empty. Please check the base64 format." - ); - } - - // Log transaction size for debugging - console.log(`Transaction bytes length: ${transactionBuffer.length}`); - } catch (base64Error: any) { - throw new Error( - `Invalid base64 format: ${base64Error.message}. Please ensure the transaction string is valid base64.` - ); - } - - const transactionBytes = new Uint8Array(transactionBuffer); - - // Deserialize transaction from bytes - // This will throw if the bytes are incomplete or corrupted - let transaction: Transaction; - try { - transaction = Transaction.fromBytes(transactionBytes); - - // Validate that transaction was deserialized correctly - if (!transaction) { - throw new Error( - "Failed to deserialize transaction. Please check the transaction format." - ); - } - } catch (deserializeError: any) { - // Handle deserialization errors specifically - if ( - deserializeError.message?.includes("index out of range") || - deserializeError.message?.includes("out of range") - ) { - throw new Error( - `Transaction bytes are incomplete or corrupted. ` + - `Expected more bytes but only received ${transactionBuffer.length} bytes. ` + - `Please ensure the full transaction base64 string is provided. ` + - `The transaction may be truncated or missing data.` - ); - } - // Re-throw other deserialization errors - throw new Error( - `Failed to deserialize transaction: ${ - deserializeError.message || "Unknown error" - }` - ); - } - - // Always try to freeze the transaction with the signer first - // This ensures the transaction is properly set up with node account IDs and bodyBytes - // If the transaction is already frozen, this will fail, and we'll handle that case - try { - await transaction.freezeWithSigner(signer); - } catch (freezeError: any) { - const errorMessage = freezeError.message || ""; - - // If the transaction is already frozen/immutable, that's okay - proceed to signing - if ( - errorMessage.includes("immutable") || - errorMessage.includes("frozen") || - errorMessage.includes("already") - ) { - // Transaction is already frozen - we can't freeze it again, but we can try to sign it - console.warn( - "Transaction is already frozen. Proceeding to sign without re-freezing." - ); - - // Verify transaction has bodyBytes before attempting to sign - const hasBodyBytes = (transaction as any).bodyBytes !== undefined; - if (!hasBodyBytes) { - throw new Error( - "Transaction is frozen but does not have bodyBytes. " + - "The transaction may have been frozen incorrectly or is in an invalid state. " + - "Please ensure the transaction was properly created and frozen with the correct signer." - ); - } - // Transaction is frozen and has bodyBytes - proceed to signing - } else { - // Some other error occurred during freezing - re-throw it - throw new Error( - `Failed to freeze transaction with signer: ${errorMessage}` - ); - } - } - - // Verify transaction has bodyBytes before signing - if (!(transaction as any).bodyBytes) { - throw new Error( - "Transaction does not have bodyBytes. It must be frozen before signing." - ); - } - - // Sign the transaction (does NOT execute) - // This will open the wallet modal for user to approve/reject - // The signer's signTransaction method requires the transaction to have bodyBytes - const signedTransaction = await signer.signTransaction(transaction); - - // Convert signed transaction to bytes and return as base64 string - const signedTransactionBytes = signedTransaction.toBytes(); - return Buffer.from(signedTransactionBytes).toString("base64"); - } catch (err: any) { - console.error("Error signing transaction:", err); - - // Provide more specific error messages for common issues - let errorMessage = - err.message || "Failed to sign transaction. Please try again."; - - if ( - err.message?.includes("index out of range") || - err.message?.includes("out of range") - ) { - errorMessage = - `Transaction bytes are incomplete or corrupted. ` + - `The transaction base64 string appears to be truncated or missing data. ` + - `Please ensure you provide the complete transaction string.`; - } + const { HederaJsonRpcMethod } = await import( + "@hashgraph/hedera-wallet-connect" + ); - setError(errorMessage); - throw new Error(errorMessage); + 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, dAppConnector] + [signer, accountId, dAppConnector] ); return {