Skip to content

Conversation

@abheektripathy
Copy link
Member

No description provided.

- Replace bridge-ui-indexer with direct bridge API integration
- Remove localStorage transaction storage
- Use POST /v2/transaction/:tx_hash to mark transactions as initiated
- Add 20-second auto-polling for transaction updates
- Consolidate all transaction fetching into unified flow
- Fix typo: detinationBlockhash → destinationBlockhash
The bridge API doesn't provide timestamp data which causes:
- Incorrect transaction sorting (all show same time)
- Poor UX in transaction list (displays 'just now' for all)

Needs API team to add timestamp fields or implement blockchain fetch
@vercel
Copy link

vercel bot commented Jan 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
bridge-ui Ready Ready Preview, Comment Jan 14, 2026 8:22am

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 13, 2026

Greptile Overview

Greptile Summary

This PR migrates from a localStorage-based transaction system with an indexer service to a new Bridge API architecture. The changes simplify transaction management by removing complex local/remote merging logic and relying on the API as the source of truth.

Key Changes

Architecture Migration:

  • Removed bridgeIndexerBaseUrl config and all indexer service calls
  • Added new /transactions and /v2/transaction/:txHash Bridge API endpoints
  • Removed localStorage transaction persistence and merging logic (~200 lines)
  • Introduced polling mechanism in Zustand store (20s interval)

Transaction Flow:

  • Old: localStorage → Indexer → Complex merge → Display
  • New: Bridge API + Liquidity API + Wormhole → Dedupe → Display

Component Updates:

  • Simplified useTransactions hook from 203 to 77 lines
  • StatusTime component no longer calculates time estimates client-side
  • Removed Babel configuration (migrated to Next.js SWC compiler)

Critical Issues Found

🔴 Blocking Issues

  1. Missing Timestamps (services/transactions.ts:105): Bridge API doesn't provide transaction timestamps, causing ALL transactions to show current time. This completely breaks:

    • Transaction sorting (all appear in random order)
    • UI time display (shows "just now" for all)
    • User ability to track transaction history
  2. Duplicate Polling (container.tsx:24-54 + stores/transactions.ts:135-143): Two independent polling loops fetch transactions every 15s AND 20s, causing redundant API calls and wasted resources.

  3. Stale Closure Bug (stores/transactions.ts:135-143): When users switch accounts, polling continues fetching data for the OLD account because addresses are captured in closure and never update.

🟡 Type Safety Issues

  1. Type Mismatch (bridgeapi.ts:213, 266): sourceTimestamp defined as string but assigned number from .getTime(), breaking type contracts.

  2. Unsafe Status Mapping (services/transactions.ts:82): Default case passes through unknown API statuses without validation, bypassing TypeScript enum safety.

Confidence Score: 1/5

  • This PR contains critical bugs that break core functionality and should NOT be merged
  • The missing timestamp issue is a showstopper that makes the transaction list unusable - users cannot see when transactions occurred or sort them properly. Combined with the stale closure bug (wrong account data) and duplicate polling (wasted resources), this PR introduces severe regressions. While the architectural direction is good, the implementation has fundamental issues that must be fixed before merging.
  • Critical fixes required in services/transactions.ts (timestamp handling), stores/transactions.ts (stale closure), and components/common/container.tsx (duplicate polling). The Bridge API team must provide timestamp fields or an alternative solution must be implemented.

Important Files Changed

File Analysis

Filename Score Overview
stores/transactions.ts 2/5 Added polling mechanism but captures stale addresses in closure, causing incorrect data fetches when users switch accounts
services/transactions.ts 1/5 Critical bugs: sets all timestamps to current time (breaks sorting/display) and unsafe status mapping allows invalid enum values
services/bridgeapi.ts 2/5 Type mismatches: sourceTimestamp set to number instead of string as defined in Transaction type, minor typo fix
hooks/useTransactions.ts 3/5 Simplified from complex localStorage merging to API-only fetching, but has dependency array issue causing race conditions
components/common/container.tsx 2/5 Contains duplicate polling logic that conflicts with new store-based polling (creates redundant API calls every 15s + 20s)

Sequence Diagram

sequenceDiagram
    participant User
    participant Container
    participant useTransactions
    participant TransactionsStore
    participant BridgeAPI
    participant LiquidityAPI
    participant WormholeAPI

    User->>Container: Connect wallet
    Container->>TransactionsStore: fetchAllTransactions (initial)
    Container->>Container: Start 15s polling interval
    
    useTransactions->>TransactionsStore: startPolling()
    TransactionsStore->>TransactionsStore: Start 20s polling interval
    
    Note over Container,TransactionsStore: ⚠️ DUPLICATE POLLING<br/>Both intervals running!
    
    loop Every 15s (Container)
        Container->>TransactionsStore: fetchAllTransactions
    end
    
    loop Every 20s (Store)
        TransactionsStore->>TransactionsStore: fetchAllTransactions
    end
    
    TransactionsStore->>BridgeAPI: GET /transactions
    BridgeAPI-->>TransactionsStore: transactions (❌ no timestamp!)
    Note over TransactionsStore,BridgeAPI: Sets sourceTimestamp = new Date()<br/>All txns appear as "just now"
    
    TransactionsStore->>LiquidityAPI: fetchAllLiquidityBridgeTransactions
    LiquidityAPI-->>TransactionsStore: transactions with timestamps
    
    TransactionsStore->>WormholeAPI: fetchWormholeTransactions
    WormholeAPI-->>TransactionsStore: transactions
    
    TransactionsStore->>TransactionsStore: Deduplicate by hash
    TransactionsStore->>TransactionsStore: Set pendingIndexedTransactions
    
    useTransactions->>TransactionsStore: Detect pendingIndexedTransactions
    useTransactions->>TransactionsStore: setIndexedTransactions
    useTransactions->>TransactionsStore: setPendingIndexedTransactions(null)
    
    useTransactions->>useTransactions: Filter & sort transactions
    Note over useTransactions: ❌ Sort breaks because<br/>Bridge API txns have same time
    
    useTransactions-->>User: Display transactions
    
    User->>User: Switch account
    useTransactions->>TransactionsStore: stopPolling()
    useTransactions->>TransactionsStore: startPolling(newAddresses)
    Note over TransactionsStore: ❌ Stale closure!<br/>Still fetches old addresses
    
    User->>User: Initiate new transaction
    useTransactions->>BridgeAPI: POST /v2/transaction/:txHash
    Note over useTransactions,BridgeAPI: Mark transaction as initiated
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 files reviewed, 7 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +135 to +143
const intervalId = setInterval(() => {
const { setTransactionLoader } = get();
get().fetchAllTransactions({
ethAddress,
availAddress,
setTransactionLoader,
isInitialFetch: false,
});
}, 20000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The polling interval captures ethAddress and availAddress in a closure, which means these values will become stale and won't update when the user switches accounts. If a user switches from account A to account B, the polling will continue to fetch transactions for account A.

To fix this, the interval callback should read the current addresses from the store or component state dynamically rather than relying on the closure:

Suggested change
const intervalId = setInterval(() => {
const { setTransactionLoader } = get();
get().fetchAllTransactions({
ethAddress,
availAddress,
setTransactionLoader,
isInitialFetch: false,
});
}, 20000);
const intervalId = setInterval(() => {
const { setTransactionLoader, pollingIntervalId } = get();
// Don't fetch if we've been stopped
if (!pollingIntervalId) return;
// Get current addresses from params stored in state or pass them dynamically
get().fetchAllTransactions({
ethAddress,
availAddress,
setTransactionLoader,
isInitialFetch: false,
});
}, 20000);

However, a better solution would be to store the addresses in the Zustand state and have startPolling restart when addresses change, or pass a getter function that returns current addresses.

Prompt To Fix With AI
This is a comment left during a code review.
Path: stores/transactions.ts
Line: 135:143

Comment:
The polling interval captures `ethAddress` and `availAddress` in a closure, which means these values will become stale and won't update when the user switches accounts. If a user switches from account A to account B, the polling will continue to fetch transactions for account A.

To fix this, the interval callback should read the current addresses from the store or component state dynamically rather than relying on the closure:

```suggestion
    const intervalId = setInterval(() => {
      const { setTransactionLoader, pollingIntervalId } = get();
      // Don't fetch if we've been stopped
      if (!pollingIntervalId) return;
      
      // Get current addresses from params stored in state or pass them dynamically
      get().fetchAllTransactions({
        ethAddress,
        availAddress,
        setTransactionLoader,
        isInitialFetch: false,
      });
    }, 20000);
```

However, a better solution would be to store the addresses in the Zustand state and have `startPolling` restart when addresses change, or pass a getter function that returns current addresses.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +100 to +106
// TODO: Bridge API doesn't provide sourceTimestamp (created_at/timestamp field).
// This causes issues with:
// 1. Transaction sorting - all transactions appear to have the same time
// 2. UI display - shows "just now" for all transactions instead of actual time
// Solution: Request API team to add timestamp fields or fetch from blockchain
sourceTimestamp: new Date().toISOString(),
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting sourceTimestamp: new Date().toISOString() creates a critical bug. This causes ALL transactions from the Bridge API to appear with the current timestamp instead of their actual creation time. The TODO comment acknowledges this but the implementation is severely broken:

  1. All transactions show "just now" - Users can't see when transactions actually occurred
  2. Sorting is broken - Transactions can't be sorted by actual time since they all have the same (current) timestamp
  3. Type inconsistency - sourceTimestamp is defined as string in types/transaction.ts (line 92), but other parts of the codebase set it to a number via .getTime() (see bridgeapi.ts lines 213, 266)

This is a blocking issue. The Bridge API must provide timestamp data, or you need to fetch it from the blockchain. Using new Date().toISOString() is not an acceptable workaround as it breaks core functionality.

Prompt To Fix With AI
This is a comment left during a code review.
Path: services/transactions.ts
Line: 100:106

Comment:
Setting `sourceTimestamp: new Date().toISOString()` creates a critical bug. This causes ALL transactions from the Bridge API to appear with the current timestamp instead of their actual creation time. The TODO comment acknowledges this but the implementation is severely broken:

1. **All transactions show "just now"** - Users can't see when transactions actually occurred
2. **Sorting is broken** - Transactions can't be sorted by actual time since they all have the same (current) timestamp
3. **Type inconsistency** - `sourceTimestamp` is defined as `string` in types/transaction.ts (line 92), but other parts of the codebase set it to a number via `.getTime()` (see bridgeapi.ts lines 213, 266)

This is a blocking issue. The Bridge API must provide timestamp data, or you need to fetch it from the blockchain. Using `new Date().toISOString()` is not an acceptable workaround as it breaks core functionality.

How can I resolve this? If you propose a fix, please make it concise.

? encodeAddress(tx.receiver_hash)
: "0x0",
sourceTransactionHash: tx.tx_hash,
sourceTimestamp: new Date(tx.created_at).getTime(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type mismatch: Same issue as line 213 - sourceTimestamp is defined as string in types/transaction.ts but here you're assigning a number from .getTime().

Suggested change
sourceTimestamp: new Date(tx.created_at).getTime(),
sourceTimestamp: new Date(tx.created_at).toISOString(),
Prompt To Fix With AI
This is a comment left during a code review.
Path: services/bridgeapi.ts
Line: 266:266

Comment:
Type mismatch: Same issue as line 213 - `sourceTimestamp` is defined as `string` in types/transaction.ts but here you're assigning a `number` from `.getTime()`.

```suggestion
      sourceTimestamp: new Date(tx.created_at).toISOString(),
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 31 to +47
useEffect(() => {
if (!selected?.address && !address) {
const accountAddress = selected?.address || address;

if (!accountAddress) {
stopPolling();
return;
}

const loadLocalTransactions = async () => {
const accountAddress = selected?.address || address;
const storageKey = `localTransactions:${accountAddress}`;
const storedTxns = JSON.parse(
localStorage.getItem(storageKey) || "[]",
) as Transaction[];
startPolling({
ethAddress: address,
availAddress: selected?.address,
});

localTransactions.length = 0;
localTransactions.push(...storedTxns);
lastLocalTxnCountRef.current = storedTxns.length;
return () => {
stopPolling();
};

loadLocalTransactions();
}, [selected?.address, address]);
}, [selected?.address, address, startPolling, stopPolling]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useEffect has startPolling and stopPolling in its dependency array, but these are Zustand store functions that have stable references. However, the effect will re-run every time selected?.address or address changes, which causes:

  1. stopPolling() to be called
  2. startPolling() to be called with new addresses
  3. Then the cleanup function runs stopPolling() again

This creates a race condition where the polling might be stopped immediately after being started if the component re-renders. Additionally, calling startPolling on every address change is correct, but the cleanup always calling stopPolling means polling stops when the component unmounts even if the user is still logged in.

Consider removing startPolling and stopPolling from the dependency array since they're stable Zustand functions:

Suggested change
useEffect(() => {
if (!selected?.address && !address) {
const accountAddress = selected?.address || address;
if (!accountAddress) {
stopPolling();
return;
}
const loadLocalTransactions = async () => {
const accountAddress = selected?.address || address;
const storageKey = `localTransactions:${accountAddress}`;
const storedTxns = JSON.parse(
localStorage.getItem(storageKey) || "[]",
) as Transaction[];
startPolling({
ethAddress: address,
availAddress: selected?.address,
});
localTransactions.length = 0;
localTransactions.push(...storedTxns);
lastLocalTxnCountRef.current = storedTxns.length;
return () => {
stopPolling();
};
loadLocalTransactions();
}, [selected?.address, address]);
}, [selected?.address, address, startPolling, stopPolling]);
}, [selected?.address, address]);
Prompt To Fix With AI
This is a comment left during a code review.
Path: hooks/useTransactions.ts
Line: 31:47

Comment:
The useEffect has `startPolling` and `stopPolling` in its dependency array, but these are Zustand store functions that have stable references. However, the effect will re-run every time `selected?.address` or `address` changes, which causes:

1. `stopPolling()` to be called
2. `startPolling()` to be called with new addresses
3. Then the cleanup function runs `stopPolling()` again

This creates a race condition where the polling might be stopped immediately after being started if the component re-renders. Additionally, calling `startPolling` on every address change is correct, but the cleanup always calling `stopPolling` means polling stops when the component unmounts even if the user is still logged in.

Consider removing `startPolling` and `stopPolling` from the dependency array since they're stable Zustand functions:

```suggestion
  }, [selected?.address, address]);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +63 to +83
return apiTransactions.map((tx: any) => {
// Parse direction to get source and destination chains
const isEthToAvail = tx.direction === "EthAvail";
const sourceChain = isEthToAvail ? Chain.ETH : Chain.AVAIL;
const destinationChain = isEthToAvail ? Chain.AVAIL : Chain.ETH;

// Map status from API to TransactionStatus enum
let status;
switch (tx.status) {
case "ClaimReady":
status = "READY_TO_CLAIM";
break;
case "Bridged":
status = "CLAIMED";
break;
case "Pending":
status = "PENDING";
break;
default:
status = tx.status;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The status mapping has a critical flaw: the default case (line 82) returns tx.status directly without validation. If the API returns an unexpected status string, it will be assigned to the Transaction object even though it doesn't match any TransactionStatus enum value. This breaks type safety and could cause runtime errors in components that switch on transaction status.

For example, if the API returns "Processing" or "Initiated", these would be passed through unchanged and not handled properly by the UI.

Suggested change
return apiTransactions.map((tx: any) => {
// Parse direction to get source and destination chains
const isEthToAvail = tx.direction === "EthAvail";
const sourceChain = isEthToAvail ? Chain.ETH : Chain.AVAIL;
const destinationChain = isEthToAvail ? Chain.AVAIL : Chain.ETH;
// Map status from API to TransactionStatus enum
let status;
switch (tx.status) {
case "ClaimReady":
status = "READY_TO_CLAIM";
break;
case "Bridged":
status = "CLAIMED";
break;
case "Pending":
status = "PENDING";
break;
default:
status = tx.status;
}
// Map status from API to TransactionStatus enum
let status: TransactionStatus;
switch (tx.status) {
case "ClaimReady":
status = TransactionStatus.READY_TO_CLAIM;
break;
case "Bridged":
status = TransactionStatus.CLAIMED;
break;
case "Pending":
status = TransactionStatus.PENDING;
break;
default:
Logger.error(`Unknown transaction status from API: ${tx.status}`);
status = TransactionStatus.PENDING; // Fallback to safe default
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: services/transactions.ts
Line: 63:83

Comment:
The status mapping has a critical flaw: the `default` case (line 82) returns `tx.status` directly without validation. If the API returns an unexpected status string, it will be assigned to the Transaction object even though it doesn't match any `TransactionStatus` enum value. This breaks type safety and could cause runtime errors in components that switch on transaction status.

For example, if the API returns "Processing" or "Initiated", these would be passed through unchanged and not handled properly by the UI.

```suggestion
      // Map status from API to TransactionStatus enum
      let status: TransactionStatus;
      switch (tx.status) {
        case "ClaimReady":
          status = TransactionStatus.READY_TO_CLAIM;
          break;
        case "Bridged":
          status = TransactionStatus.CLAIMED;
          break;
        case "Pending":
          status = TransactionStatus.PENDING;
          break;
        default:
          Logger.error(`Unknown transaction status from API: ${tx.status}`);
          status = TransactionStatus.PENDING; // Fallback to safe default
      }
```

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 13, 2026

Additional Comments (2)

components/common/container.tsx
This component creates its own polling interval (15 seconds) while useTransactions hook also starts a separate polling interval (20 seconds via startPolling). This results in duplicate API calls - transactions will be fetched every 15 seconds from Container AND every 20 seconds from the useTransactions hook.

This polling logic should be removed from Container since the new architecture handles polling in the store/hook layer. The initial fetch can be kept if needed, but the interval should be removed:

  useEffect(() => {
    if (!address && !selected?.address) {
      return;
    }

    // Only do initial fetch, polling is handled by useTransactions hook
    const timeoutId = setTimeout(async () => {
      await fetchAllTransactions({
        ethAddress: address,
        availAddress: selected?.address,
        setTransactionLoader,
        isInitialFetch: true,
      });
    }, 500);

    return () => {
      if (timeoutId) clearTimeout(timeoutId);
    };
  }, [selected?.address, address, fetchAllTransactions, setTransactionLoader]);
Prompt To Fix With AI
This is a comment left during a code review.
Path: components/common/container.tsx
Line: 24:54

Comment:
This component creates its own polling interval (15 seconds) while `useTransactions` hook also starts a separate polling interval (20 seconds via `startPolling`). This results in duplicate API calls - transactions will be fetched every 15 seconds from Container AND every 20 seconds from the useTransactions hook.

This polling logic should be removed from Container since the new architecture handles polling in the store/hook layer. The initial fetch can be kept if needed, but the interval should be removed:

```suggestion
  useEffect(() => {
    if (!address && !selected?.address) {
      return;
    }

    // Only do initial fetch, polling is handled by useTransactions hook
    const timeoutId = setTimeout(async () => {
      await fetchAllTransactions({
        ethAddress: address,
        availAddress: selected?.address,
        setTransactionLoader,
        isInitialFetch: true,
      });
    }, 500);

    return () => {
      if (timeoutId) clearTimeout(timeoutId);
    };
  }, [selected?.address, address, fetchAllTransactions, setTransactionLoader]);
```

How can I resolve this? If you propose a fix, please make it concise.

services/bridgeapi.ts
Type mismatch: sourceTimestamp is defined as string in types/transaction.ts (line 92), but here you're setting it to new Date(tx.created_at).getTime() which returns a number.

This inconsistency will cause issues in code that expects sourceTimestamp to be a string. For example, in hooks/useTransactions.ts lines 61-62, the code does new Date(b.sourceTimestamp).getTime() which works with both string and number, but it's still a type violation.

      sourceTimestamp: new Date(tx.created_at).toISOString(),

Or update the Transaction type definition to use number instead of string for sourceTimestamp.

Prompt To Fix With AI
This is a comment left during a code review.
Path: services/bridgeapi.ts
Line: 213:213

Comment:
Type mismatch: `sourceTimestamp` is defined as `string` in types/transaction.ts (line 92), but here you're setting it to `new Date(tx.created_at).getTime()` which returns a `number`.

This inconsistency will cause issues in code that expects sourceTimestamp to be a string. For example, in hooks/useTransactions.ts lines 61-62, the code does `new Date(b.sourceTimestamp).getTime()` which works with both string and number, but it's still a type violation.

```suggestion
      sourceTimestamp: new Date(tx.created_at).toISOString(),
```

Or update the Transaction type definition to use `number` instead of `string` for sourceTimestamp.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants