diff --git a/apps/tangle-dapp/public/data/credits-tree.json b/apps/tangle-dapp/public/data/credits-tree.json
new file mode 100644
index 0000000000..1e32288c15
--- /dev/null
+++ b/apps/tangle-dapp/public/data/credits-tree.json
@@ -0,0 +1,25 @@
+{
+ "epochId": "1",
+ "root": "0x573194fb391684e4863d727812079b5df367353a88268e8f3473ec0faecf678e",
+ "totalValue": "3000",
+ "entryCount": 2,
+ "startTs": "1767084038",
+ "endTs": "1767688838",
+ "epochSeconds": "604800",
+ "tntToken": "0x5eb3bc0a489c5a8288765d2336659ebca68fcd00",
+ "creditsPerTnt": "1",
+ "entries": {
+ "0x15d34aaf54267db7d7c367839aaf71a00a2c6a65": {
+ "amount": "1000",
+ "proof": [
+ "0x77cae3945c7ee93ef32d231aa007bf98eecc25ed440cf40263f8b987156321e4"
+ ]
+ },
+ "0x90f79bf6eb2c4f870365e785982e1f101e93b906": {
+ "amount": "2000",
+ "proof": [
+ "0xb1566ed9df667651f12a257e29b09690e01dc5f2488dd5c2fe8b16a95b7955f7"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/tangle-dapp/src/data/credits/useCredits.ts b/apps/tangle-dapp/src/data/credits/useCredits.ts
index a2a4071b17..f514ce5f91 100644
--- a/apps/tangle-dapp/src/data/credits/useCredits.ts
+++ b/apps/tangle-dapp/src/data/credits/useCredits.ts
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
-import { useReadContract, useChainId } from 'wagmi';
+import { useChainId, useReadContract } from 'wagmi';
import { useQuery } from '@tanstack/react-query';
-import { type Hex, zeroAddress } from 'viem';
+import { type Hex, type Address } from 'viem';
import useEvmAddress from '@tangle-network/tangle-shared-ui/hooks/useEvmAddress';
import CREDITS_MERKLE_ABI from '@tangle-network/tangle-shared-ui/abi/creditsMerkle';
import {
@@ -84,7 +84,7 @@ export default function useCredits() {
error: rootError,
refetch: refetchRoot,
} = useReadContract({
- address: creditsAddress ?? zeroAddress,
+ address: creditsAddress as Address,
abi: CREDITS_MERKLE_ABI,
functionName: 'merkleRoots',
args: claimData ? [claimData.epochId] : undefined,
@@ -100,16 +100,17 @@ export default function useCredits() {
error: claimedError,
refetch: refetchClaimed,
} = useReadContract({
- address: creditsAddress ?? zeroAddress,
+ address: creditsAddress as Address,
abi: CREDITS_MERKLE_ABI,
functionName: 'claimed',
args:
claimData && activeEvmAddress
- ? [claimData.epochId, activeEvmAddress as `0x${string}`]
+ ? [claimData.epochId, activeEvmAddress as Address]
: undefined,
query: {
enabled:
isSupportedNetwork && claimData !== null && activeEvmAddress !== null,
+ staleTime: 10000,
refetchInterval: 10000,
},
});
@@ -118,7 +119,7 @@ export default function useCredits() {
if (!claimData || !onchainRoot) {
return false;
}
- return claimData.root.toLowerCase() === (onchainRoot as Hex).toLowerCase();
+ return claimData.root.toLowerCase() === onchainRoot.toLowerCase();
}, [claimData, onchainRoot]);
const proofValid = useMemo(() => {
diff --git a/apps/tangle-dapp/src/features/claimCredits/components/ClaimCreditsButton.tsx b/apps/tangle-dapp/src/features/claimCredits/components/ClaimCreditsButton.tsx
index e7495e88ef..75a4169b58 100644
--- a/apps/tangle-dapp/src/features/claimCredits/components/ClaimCreditsButton.tsx
+++ b/apps/tangle-dapp/src/features/claimCredits/components/ClaimCreditsButton.tsx
@@ -1,15 +1,14 @@
import { CrossCircledIcon } from '@radix-ui/react-icons';
-import { TANGLE_TOKEN_DECIMALS } from '@tangle-network/dapp-config';
import { Spinner } from '@tangle-network/icons';
import { SparklingIcon } from '@tangle-network/icons';
import { TxStatus } from '@tangle-network/tangle-shared-ui/hooks/useSubstrateTx';
-import { BN } from '@polkadot/util';
import {
- AmountFormatStyle,
Button,
- formatDisplayAmount,
Typography,
TextField,
+ Tooltip,
+ TooltipBody,
+ TooltipTrigger,
} from '@tangle-network/ui-components';
import {
Dropdown,
@@ -22,6 +21,10 @@ import useClaimCreditsTx from '../../../data/credits/useClaimCreditsTx';
import { meetsMinimumClaimThreshold } from '../../../utils/creditConstraints';
import CreditVelocityTooltip from './CreditVelocityTooltip';
+const SECONDS_PER_DAY = 86400;
+const SECONDS_PER_HOUR = 3600;
+const SECONDS_PER_MINUTE = 60;
+
const ClaimCreditsButton = () => {
const { data, error, refetch, isPending, isSupportedNetwork } = useCredits();
const [offchainAccountId, setOffchainAccountId] = useState('');
@@ -32,11 +35,7 @@ const ClaimCreditsButton = () => {
return '0';
}
- return formatDisplayAmount(
- new BN(data.amount.toString()),
- TANGLE_TOKEN_DECIMALS,
- AmountFormatStyle.SHORT,
- );
+ return data.amount.toString();
}, [data]);
const meetsMinimumThreshold = useMemo(() => {
@@ -49,9 +48,9 @@ const ClaimCreditsButton = () => {
}
const total = Number(data.timeRemaining);
- const days = Math.floor(total / 86400);
- const hours = Math.floor((total % 86400) / 3600);
- const minutes = Math.floor((total % 3600) / 60);
+ const days = Math.floor(total / SECONDS_PER_DAY);
+ const hours = Math.floor((total % SECONDS_PER_DAY) / SECONDS_PER_HOUR);
+ const minutes = Math.floor((total % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE);
if (days > 0) return `${days}d ${hours}h`;
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
@@ -71,31 +70,44 @@ const ClaimCreditsButton = () => {
return error.message;
}, [error]);
+ const buttonContent = (
+
+ ) : error || isUnavailable ? (
+
+ ) : (
+
+ )
+ }
+ >
+
+ {isPending
+ ? 'Fetching credits...'
+ : isUnavailable
+ ? 'Credits unavailable'
+ : error
+ ? 'Error'
+ : formattedCredits}
+
+
+ );
+
return (
-
- ) : error || isUnavailable ? (
-
- ) : (
-
- )
- }
- >
-
- {isPending
- ? 'Fetching credits...'
- : isUnavailable
- ? 'Credits unavailable'
- : error
- ? errorLabel
- : formattedCredits}
-
-
+ {error ? (
+
+ {buttonContent}
+
+ {errorLabel}
+
+
+ ) : (
+ buttonContent
+ )}
{isUnavailable ? (
@@ -227,7 +239,15 @@ const CreditsButton = ({
}
if (execute === null || !credits || !epochId || !merkleProof) {
- return null;
+ if (process.env.NODE_ENV === 'development') {
+ console.warn('Claim credits: Missing required data', {
+ hasExecute: execute !== null,
+ hasCredits: !!credits,
+ hasEpochId: !!epochId,
+ hasMerkleProof: !!merkleProof,
+ });
+ }
+ return;
}
try {
@@ -241,7 +261,9 @@ const CreditsButton = ({
await refetchCredits();
setOffchainAccountId('');
} catch (error) {
- console.error('Failed to claim credits:', error);
+ if (process.env.NODE_ENV === 'development') {
+ console.error('Failed to claim credits:', error);
+ }
}
}, [
credits,
diff --git a/apps/tangle-dapp/src/features/claimCredits/components/CreditVelocityTooltip.tsx b/apps/tangle-dapp/src/features/claimCredits/components/CreditVelocityTooltip.tsx
index 5b0d0785f9..c9873c2357 100644
--- a/apps/tangle-dapp/src/features/claimCredits/components/CreditVelocityTooltip.tsx
+++ b/apps/tangle-dapp/src/features/claimCredits/components/CreditVelocityTooltip.tsx
@@ -1,4 +1,4 @@
-import { FC } from 'react';
+import { FC, useMemo } from 'react';
import { InfoCircledIcon } from '@radix-ui/react-icons';
import { Typography } from '@tangle-network/ui-components/typography/Typography';
import {
@@ -26,19 +26,30 @@ const CreditVelocityTooltip: FC = ({
currentAmount,
tokenSymbol = 'TNT',
}) => {
- const creditsNeeded = getCreditsNeededForMinimum(currentAmount);
+ const creditsNeeded = useMemo(
+ () => getCreditsNeededForMinimum(currentAmount),
+ [currentAmount],
+ );
+ // Convert decimal values to token units (multiply by 10^decimals) for BN
const formattedMinimum = formatDisplayAmount(
- new BN(MINIMUM_CLAIMABLE_CREDITS.toString()),
+ new BN(
+ BigInt(
+ Math.round(MINIMUM_CLAIMABLE_CREDITS * 10 ** TANGLE_TOKEN_DECIMALS),
+ ).toString(),
+ ),
TANGLE_TOKEN_DECIMALS,
AmountFormatStyle.SHORT,
);
- const formattedCreditsNeeded = formatDisplayAmount(
- new BN(creditsNeeded.toString()),
- TANGLE_TOKEN_DECIMALS,
- AmountFormatStyle.SHORT,
- );
+ // Format creditsNeeded to avoid floating-point display artifacts
+ const formattedCreditsNeeded = useMemo(() => {
+ if (creditsNeeded === 0) {
+ return '0';
+ }
+ // Format to max 4 decimal places, removing trailing zeros
+ return creditsNeeded.toFixed(4).replace(/\.?0+$/, '');
+ }, [creditsNeeded]);
return (
@@ -55,7 +66,7 @@ const CreditVelocityTooltip: FC = ({
You need at least {formattedMinimum} {tokenSymbol} to claim credits.
- {creditsNeeded !== BigInt(0) && (
+ {creditsNeeded !== 0 && (
= MINIMUM_CLAIMABLE_CREDITS;
+ // Scale the amount for comparison to avoid floating-point precision issues
+ const scaledAmount = amount * CREDIT_SCALE;
+ return scaledAmount >= MINIMUM_CLAIMABLE_CREDITS_SCALED;
};
/**
* Calculates how much more credits are needed to reach the minimum threshold.
+ * Returns 0 if the minimum threshold is already met.
*/
export const getCreditsNeededForMinimum = (
amount: bigint | null | undefined,
-): bigint => {
+): number => {
if (!amount) {
return MINIMUM_CLAIMABLE_CREDITS;
}
- if (amount >= MINIMUM_CLAIMABLE_CREDITS) {
- return BigInt(0);
+ // For display purposes, safe to convert to Number since credit amounts
+ // are raw amounts (not wei) and expected to be within safe integer range
+ const numAmount = Number(amount);
+
+ if (numAmount >= MINIMUM_CLAIMABLE_CREDITS) {
+ return 0;
}
- return MINIMUM_CLAIMABLE_CREDITS - amount;
+ // Round to avoid floating-point precision artifacts
+ const scale = Number(CREDIT_SCALE);
+ return Math.round((MINIMUM_CLAIMABLE_CREDITS - numAmount) * scale) / scale;
};
diff --git a/libs/dapp-config/src/contracts.ts b/libs/dapp-config/src/contracts.ts
index 911c38f1bd..3a97ad149f 100644
--- a/libs/dapp-config/src/contracts.ts
+++ b/libs/dapp-config/src/contracts.ts
@@ -33,6 +33,9 @@ export const SP1_VERIFIER_GATEWAY = {
};
// Local Anvil deployment addresses (from LocalTestnet.s.sol deployment)
+// IMPORTANT: These are deterministic addresses based on deployer nonce when
+// running LocalTestnet.s.sol on a fresh Anvil. If running multiple times or
+// with a different deployer, addresses may differ.
export const LOCAL_CONTRACTS: ContractAddresses = {
tangle: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
multiAssetDelegation: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
@@ -40,7 +43,7 @@ export const LOCAL_CONTRACTS: ContractAddresses = {
operatorStatusRegistry: '0x8f86403A4DE0BB5791fa46B8e795C547942fE4Cf',
rewardVaults: '0x21dF544947ba3E8b3c32561399E88B52Dc8b2823',
inflationPool: '0xD8a5a9b31c3C0232E196d518E89Fd8bF83AcAd43',
- credits: '0x1fA02b2d6A771842690194Cf62D91bdd92BfE28d',
+ credits: '0x5081a39b8A5f0E35a8D959395a630b68B74Dd30f',
liquidDelegationFactory: '0x8F4ec854Dd12F1fe79500a1f53D0cbB30f9b6134',
};