167 lines
5 KiB
Markdown
167 lines
5 KiB
Markdown
# KRK Balance Loading Bug - Root Cause Analysis
|
|
|
|
## Summary
|
|
After swapping ETH→KRK on the cheats page, the KRK token balance takes 10-90+ seconds to load (or never loads), causing "Insufficient Balance" errors on the stake page. This blocked 2 of 5 user test personas from staking.
|
|
|
|
## Root Cause
|
|
|
|
### The Problem
|
|
The `useWallet()` composable fetches the KRK token balance **only** when:
|
|
1. The wallet account changes (address or chainId)
|
|
2. The blockchain chain changes
|
|
|
|
**There is NO polling mechanism** and **no automatic refresh after transactions**.
|
|
|
|
### The Flow
|
|
1. User swaps ETH→KRK in `CheatsView.vue` via `buyKrk()`
|
|
2. The swap transaction completes successfully
|
|
3. **`buyKrk()` does NOT call `wallet.loadBalance()`** after the transaction
|
|
4. User navigates to Stake page
|
|
5. Navigation doesn't trigger account/chain change events
|
|
6. `StakeHolder.vue` reads stale `wallet.balance.value` (still 0 KRK)
|
|
7. `maxStakeAmount` computed property returns 0
|
|
8. User sees "Insufficient Balance"
|
|
|
|
### Code Evidence
|
|
|
|
**useWallet.ts (lines 82-98):**
|
|
```typescript
|
|
async function loadBalance() {
|
|
logger.contract('loadBalance');
|
|
const userAddress = account.value.address;
|
|
if (!userAddress) {
|
|
return 0n;
|
|
}
|
|
let publicClient = getWalletPublicClient();
|
|
if (!publicClient) {
|
|
publicClient = await syncWalletPublicClient();
|
|
}
|
|
if (!publicClient) {
|
|
return 0n;
|
|
}
|
|
const value = (await publicClient.readContract({
|
|
abi: HarbContract.abi,
|
|
address: HarbContract.contractAddress,
|
|
functionName: 'balanceOf',
|
|
args: [userAddress],
|
|
})) as bigint;
|
|
// ... sets balance.value
|
|
}
|
|
```
|
|
|
|
**Balance refresh triggers (lines 102-154):**
|
|
- `watchAccount()` - only on address/chainId change
|
|
- `watchChainId()` - only on explicit chain switch
|
|
- **NO interval polling**
|
|
- **NO transaction completion hooks**
|
|
|
|
**CheatsView.vue buyKrk() (lines ~941-1052):**
|
|
```typescript
|
|
async function buyKrk() {
|
|
// ... performs swap transaction
|
|
await writeContract(wagmiConfig, {
|
|
abi: SWAP_ROUTER_ABI,
|
|
address: router,
|
|
functionName: 'exactInputSingle',
|
|
args: [/* swap params */],
|
|
chainId,
|
|
});
|
|
toast.success('Swap submitted. Watch your wallet for KRK.');
|
|
// ❌ MISSING: wallet.loadBalance() call here!
|
|
} finally {
|
|
swapping.value = false;
|
|
}
|
|
```
|
|
|
|
**StakeHolder.vue (lines 220-227):**
|
|
```typescript
|
|
const maxStakeAmount = computed(() => {
|
|
if (wallet.balance?.value) {
|
|
return bigInt2Number(wallet.balance.value, 18);
|
|
}
|
|
return 0; // ❌ Returns 0 when balance is stale
|
|
});
|
|
```
|
|
|
|
## Solution
|
|
|
|
### Quick Fix (Recommended)
|
|
Add `wallet.loadBalance()` call after the swap transaction completes in `CheatsView.vue`:
|
|
|
|
```typescript
|
|
async function buyKrk() {
|
|
if (!canSwap.value || swapping.value) return;
|
|
try {
|
|
swapping.value = true;
|
|
// ... existing swap logic ...
|
|
|
|
await writeContract(wagmiConfig, {
|
|
abi: SWAP_ROUTER_ABI,
|
|
address: router,
|
|
functionName: 'exactInputSingle',
|
|
args: [/* swap params */],
|
|
chainId,
|
|
});
|
|
|
|
// ✅ FIX: Refresh KRK balance after swap
|
|
const { loadBalance } = useWallet();
|
|
await loadBalance();
|
|
|
|
toast.success('Swap submitted. Watch your wallet for KRK.');
|
|
} catch (error: unknown) {
|
|
toast.error(getErrorMessage(error, 'Swap failed'));
|
|
} finally {
|
|
swapping.value = false;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Alternative Solutions (For Consideration)
|
|
|
|
1. **Add polling to useWallet():**
|
|
- Poll balance every 5-10 seconds when wallet is connected
|
|
- Pro: Auto-updates across all pages
|
|
- Con: Increased RPC calls, may hit rate limits
|
|
|
|
2. **Vue Router navigation guard:**
|
|
- Refresh balance on route enter
|
|
- Pro: Works for all navigation patterns
|
|
- Con: Slight delay on every page load
|
|
|
|
3. **Event bus for transaction completion:**
|
|
- Emit event after any token-affecting transaction
|
|
- Subscribe in useWallet to refresh balance
|
|
- Pro: Clean separation of concerns
|
|
- Con: More complex architecture
|
|
|
|
## Impact
|
|
|
|
### Severity: **CRITICAL**
|
|
- Blocks core user flow (swap → stake)
|
|
- 40% of test users affected (2/5 personas)
|
|
- Confusing UX ("I just bought tokens, why can't I stake?")
|
|
|
|
### User Experience Issues
|
|
1. No loading indicator for balance refresh
|
|
2. No error message explaining the delay
|
|
3. Users don't know to wait or refresh page
|
|
4. Some users never see balance load (likely due to wallet/RPC issues)
|
|
|
|
## Recommended Action
|
|
|
|
1. ✅ **Immediate:** Implement the quick fix (add `loadBalance()` call after swap)
|
|
2. 🔍 **Investigation:** Test why some balances "never" load - possible RPC/wallet issues?
|
|
3. 💡 **Future:** Consider adding a manual "Refresh Balance" button on stake page
|
|
4. 📊 **Monitoring:** Track balance load times in production
|
|
|
|
## Testing Checklist
|
|
|
|
- [ ] Swap ETH→KRK on cheats page
|
|
- [ ] Immediately navigate to stake page
|
|
- [ ] Verify KRK balance displays correctly within 1-2 seconds
|
|
- [ ] Test with multiple wallet providers (MetaMask, Coinbase Wallet)
|
|
- [ ] Test with slow RPC endpoints
|
|
- [ ] Verify balance updates after page navigation
|
|
|
|
## Files Modified
|
|
- `/home/debian/harb/web-app/src/views/CheatsView.vue` (add loadBalance call)
|