Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15,890 changes: 15,890 additions & 0 deletions dashboard/package-lock.json

Large diffs are not rendered by default.

15,069 changes: 7,473 additions & 7,596 deletions dashboard/pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dashboard/src/components/credits-added.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ type CreditsAddedProps = {
};
export default function CreditsAdded({ credits }: CreditsAddedProps) {
const { open, setOpen } = useDialog();
const { updateCreditBalance } = useBalance();
const { updateAllBalances, refreshCounter } = useBalance();

useEffect(() => {
const updateBalnceCallback = setTimeout(() => {
if (open) {
updateCreditBalance();
updateAllBalances();
}
}, 5000);
return () => {
Expand Down
92 changes: 74 additions & 18 deletions dashboard/src/components/credits-transaction-progress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { config } from "@/config/walletConfig";
import { APP_TABS, cn, formatDataBytes } from "@/lib/utils";
import { TransactionStatus, useConfig } from "@/providers/ConfigProvider";
import { useOverview } from "@/providers/OverviewProvider";
import useBalance from "@/hooks/useBalance";
import useBalance, {
dispatchTransactionCompleted,
dispatchCreditBalanceUpdated,
} from "@/hooks/useBalance";
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
import Image from "next/image";
import { Close } from "@radix-ui/react-dialog";
Expand All @@ -23,7 +26,7 @@ const CreditsTransactionProgress = () => {
const { open, setOpen } = useDialog();
const { transactionProgress, error } = useAppToast();
const { setMainTabSelected } = useOverview();
const { updateCreditBalance } = useBalance();
const { updateAllBalances, pollCreditBalanceUpdate } = useBalance();
const {
token,
setTransactionStatusList,
Expand All @@ -34,6 +37,7 @@ const CreditsTransactionProgress = () => {

// State to control initial animation
const [shouldAnimate, setShouldAnimate] = useState(false);
const [isPollingCreditBalance, setIsPollingCreditBalance] = useState(false);

// Start animation after dialog opens
useEffect(() => {
Expand Down Expand Up @@ -125,9 +129,40 @@ const CreditsTransactionProgress = () => {
)
);
setShowTransaction({ ...showTransaction, status: "completed" });

// Refresh credit balance after successful transaction
updateCreditBalance();

// Dispatch transaction completed event to update all balances
dispatchTransactionCompleted();

// Refresh token balances immediately
updateAllBalances();

// Start polling for credit balance update if we have credit amount
if (showTransaction?.creditAmount) {
console.log(
`Starting credit balance polling for ${showTransaction.creditAmount} credits...`
);
setIsPollingCreditBalance(true);
pollCreditBalanceUpdate(showTransaction.creditAmount).then(
(success) => {
setIsPollingCreditBalance(false);
if (success) {
console.log(
"Credit balance polling completed successfully"
);
// Dispatch events after successful polling
dispatchTransactionCompleted();
dispatchCreditBalanceUpdated();
} else {
console.log("Credit balance polling timed out or failed");
}
}
);
}

// Force a small delay to ensure all components have updated
setTimeout(() => {
dispatchTransactionCompleted();
}, 1000);
})
.catch((error) => {
console.log(error);
Expand Down Expand Up @@ -198,24 +233,45 @@ const CreditsTransactionProgress = () => {
{showTransaction?.tokenAmount ? (
<>
{showTransaction?.status === "completed" ? (
<Text
weight={"semibold"}
size={"2xl"}
as="div"
className="relative self-stretch text-center"
>
<>
<Text
as="span"
weight={"semibold"}
size={"2xl"}
variant={"green"}
as="div"
className="relative self-stretch text-center"
>
{showTransaction?.creditAmount
? formatDataBytes(showTransaction.creditAmount)
: ""}
<Text
as="span"
weight={"semibold"}
size={"2xl"}
variant={"green"}
>
{showTransaction?.creditAmount
? formatDataBytes(showTransaction.creditAmount)
: ""}
</Text>
Credited Successfully
</Text>
Credited Successfully
</Text>
<div className="flex flex-col items-center gap-y-2">
<Text
weight={"medium"}
size={"sm"}
variant={"secondary-grey"}
className="text-center"
>
{isPollingCreditBalance
? "Waiting for backend to update credit balance..."
: "Credit balance updated successfully!"}
</Text>
{isPollingCreditBalance && (
<LoaderCircle
className="animate-spin"
color="#B3B3B3"
size={20}
/>
)}
</div>
</>
) : (
<>
<Text
Expand Down
214 changes: 201 additions & 13 deletions dashboard/src/hooks/useBalance.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,218 @@
import { useConfig } from "@/providers/ConfigProvider";
import { useOverview } from "@/providers/OverviewProvider";
import AuthenticationService from "@/services/authentication";
import { useCallback, useState } from "react";
import { useCallback, useState, useEffect } from "react";
import { useAccount, useBalance as useWagmiBalance } from "wagmi";
import { config } from "@/config/walletConfig";
import { readContract, getBalance } from "@wagmi/core";
import { TOKEN_MAP } from "@/lib/types";
import BigNumber from "bignumber.js";

// Custom event for transaction completion
const TRANSACTION_COMPLETED_EVENT = "transactionCompleted";
const CREDIT_BALANCE_UPDATED_EVENT = "creditBalanceUpdated";

// Dispatch transaction completed event
export const dispatchTransactionCompleted = () => {
window.dispatchEvent(new CustomEvent(TRANSACTION_COMPLETED_EVENT));
};

// Dispatch credit balance updated event
export const dispatchCreditBalanceUpdated = () => {
window.dispatchEvent(new CustomEvent(CREDIT_BALANCE_UPDATED_EVENT));
};

const useBalance = () => {
const [loading, setLoading] = useState(false);
const [refreshCounter, setRefreshCounter] = useState(0);
const { setCreditBalance, creditBalance } = useOverview();
const { token } = useConfig();
const account = useAccount();

const updateCreditBalance = useCallback(async () => {
if (!token) return;
setLoading(true);
AuthenticationService.fetchUser({ token })
.then((response) => {
const mainCreditBalance = +response?.data?.credit_balance || 0;
setCreditBalance(mainCreditBalance);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
setLoading(false);
try {
const response = await AuthenticationService.fetchUser({ token });
const mainCreditBalance = +response?.data?.credit_balance || 0;
setCreditBalance(mainCreditBalance);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
}, [token, setCreditBalance]);

// Poll for credit balance updates after transaction completion
const pollCreditBalanceUpdate = useCallback(
async (expectedIncrease: number, maxAttempts: number = 30) => {
if (!token) return;

console.log(
`Starting credit balance polling. Expected increase: ${expectedIncrease}`
);

let attempts = 0;
const startTime = Date.now();
const maxWaitTime = 60000; // 60 seconds max wait time

const poll = async (): Promise<boolean> => {
attempts++;

try {
const response = await AuthenticationService.fetchUser({ token });
const currentBalance = +response?.data?.credit_balance || 0;
const previousBalance = creditBalance;
const actualIncrease = currentBalance - previousBalance;

console.log(
`Poll attempt ${attempts}: Previous: ${previousBalance}, Current: ${currentBalance}, Increase: ${actualIncrease}, Expected: ${expectedIncrease}`
);

// Check if balance has increased by the expected amount
if (actualIncrease >= expectedIncrease) {
console.log(
`Credit balance updated successfully! New balance: ${currentBalance}`
);
setCreditBalance(currentBalance);
// Dispatch event to hide the warning message
dispatchCreditBalanceUpdated();
return true;
}

// Check if we've exceeded max attempts or time
if (attempts >= maxAttempts || Date.now() - startTime > maxWaitTime) {
console.log(`Polling timeout. Final balance: ${currentBalance}`);
setCreditBalance(currentBalance);
return false;
}

// Exponential backoff: 2s, 4s, 8s, 16s, then 20s intervals
const delay = Math.min(
2000 * Math.pow(2, Math.min(attempts - 1, 3)),
20000
);

console.log(`Balance not updated yet. Retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));

return poll();
} catch (error) {
console.error(
`Error polling credit balance (attempt ${attempts}):`,
error
);

if (attempts >= maxAttempts || Date.now() - startTime > maxWaitTime) {
return false;
}

// Retry after 5 seconds on error
await new Promise((resolve) => setTimeout(resolve, 5000));
return poll();
}
};

return poll();
},
[token, creditBalance, setCreditBalance]
);

// Function to refresh AVAIL ERC20 balance on Ethereum
const refreshAvailERC20Balance = useCallback(async () => {
if (!account.address || !account.chainId) return;

try {
const availTokenAddress = TOKEN_MAP.avail?.token_address;
if (!availTokenAddress) return;

const erc20Abi = [
{
type: "function",
name: "balanceOf",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
] as const;

await readContract(config, {
address: availTokenAddress as `0x${string}`,
abi: erc20Abi,
functionName: "balanceOf",
args: [account.address],
chainId: account.chainId,
});
}, [token]);

return { loading, updateCreditBalance, creditBalance: +creditBalance };
// Force refresh by incrementing counter
setRefreshCounter((prev) => prev + 1);
} catch (error) {
console.error("Error refreshing AVAIL ERC20 balance:", error);
}
}, [account.address, account.chainId]);

// Function to refresh ETH balance
const refreshETHBalance = useCallback(async () => {
if (!account.address || !account.chainId) return;

try {
await getBalance(config, {
address: account.address,
chainId: account.chainId,
});

// Force refresh by incrementing counter
setRefreshCounter((prev) => prev + 1);
} catch (error) {
console.error("Error refreshing ETH balance:", error);
}
}, [account.address, account.chainId]);

// Function to refresh all token balances
const refreshTokenBalances = useCallback(async () => {
await Promise.all([refreshAvailERC20Balance(), refreshETHBalance()]);

// Increment refresh counter to trigger wagmi hooks to refresh
setRefreshCounter((prev) => prev + 1);
}, [refreshAvailERC20Balance, refreshETHBalance]);

// Combined function to update both credit balance and token balances
const updateAllBalances = useCallback(async () => {
console.log("Updating all balances...");
await Promise.all([updateCreditBalance(), refreshTokenBalances()]);
}, [updateCreditBalance, refreshTokenBalances]);

// Listen for transaction completion events
useEffect(() => {
const handleTransactionCompleted = () => {
console.log("Transaction completed event received, updating balances...");
updateAllBalances();
};

window.addEventListener(
TRANSACTION_COMPLETED_EVENT,
handleTransactionCompleted
);

return () => {
window.removeEventListener(
TRANSACTION_COMPLETED_EVENT,
handleTransactionCompleted
);
};
}, [updateAllBalances]);

return {
loading,
updateCreditBalance,
refreshTokenBalances,
refreshAvailERC20Balance,
refreshETHBalance,
updateAllBalances,
pollCreditBalanceUpdate,
refreshCounter,
creditBalance: +creditBalance,
};
};

export default useBalance;
Loading