Skip to content

P-992 Add Solana Wallet Adapter integration#157

Merged
yosriady merged 47 commits intomainfrom
claude/add-solana-support-LMxRK
Feb 12, 2026
Merged

P-992 Add Solana Wallet Adapter integration#157
yosriady merged 47 commits intomainfrom
claude/add-solana-support-LMxRK

Conversation

@yosriady
Copy link
Copy Markdown
Contributor

@yosriady yosriady commented Feb 5, 2026

Summary

This PR adds comprehensive Solana Wallet Adapter integration to the Formo Analytics SDK, enabling tracking of Solana wallet events (connect, disconnect, signatures, and transactions) alongside existing EVM wallet tracking. This allows dApps supporting multiple chains to track all blockchain interactions in a unified analytics platform.

Key Changes

  • New Solana Integration Module (src/solana/)

    • SolanaWalletAdapterHandler.ts: Core orchestrator that hooks into Solana Wallet Adapter events for connection, transaction, and signature tracking
    • types.ts: Comprehensive TypeScript interfaces for Solana integration (wallet adapters, contexts, clusters, chain IDs)
    • address.ts: Solana-specific address validation and utilities (Base58 format, blocked addresses, address comparison)
  • Core SDK Updates (src/FormoAnalytics.ts)

    • Added optional Solana handler initialization alongside existing EVM handlers
    • Implemented setSolanaWallet(), setSolanaConnection(), and setSolanaCluster() methods for dynamic wallet updates
    • Added proper cleanup for Solana resources in cleanup() method
    • Solana tracking works additively with EVM tracking (not mutually exclusive)
  • Package Configuration (package.json)

    • Added optional peer dependencies for Solana integration:
      • @solana/wallet-adapter-base (>=0.9.0)
      • @solana/wallet-adapter-react (>=0.15.0)
      • @solana/web3.js (>=1.70.0)
  • Public API Exports (src/index.ts)

    • Exported Solana types and utilities for consumer use
    • Exported address validation helpers and chain ID mappings
  • Documentation (docs/SOLANA_INTEGRATION.md)

    • Comprehensive 500-line integration guide covering architecture, usage, configuration, and troubleshooting
    • Event mapping tables for connections, signatures, and transactions
    • Chain ID mapping (900001-900004 for Solana clusters)
    • Multi-chain setup examples and comparison with EVM modes

Implementation Details

Design Philosophy

  • Opt-in Configuration: Solana integration only activates when explicitly configured
  • Additive Model: Works alongside EVM tracking without conflicts
  • Consistent Event Model: Reuses same event types (connect, disconnect, signature, transaction) for Solana
  • Chain-Agnostic Addresses: Supports both EVM (hex) and Solana (Base58) address formats

Key Features

  • Method Wrapping: Wraps sendTransaction, signMessage, and signTransaction for automatic event tracking
  • Transaction Confirmation Polling: Polls Solana connection for transaction confirmation status (CONFIRMED/REVERTED)
  • Wallet Context Support: Works with both direct wallet adapters and useWallet() hook contexts
  • Memory Management: Limits processed signatures set to 1000 entries with automatic pruning
  • Wallet Identification: Generates RDNS-like identifiers (e.g., sol.wallet.phantom)

Chain ID Mapping

Solana clusters are mapped to high numeric IDs (900001-900004) to avoid collision with EVM chain IDs:

  • mainnet-beta: 900001
  • testnet: 900002
  • devnet: 900003
  • localnet: 900004

Event Lifecycle

Transactions flow through: STARTED → BROADCASTED → CONFIRMED/REVERTED
Signatures flow through: REQUESTED → CONFIRMED/REJECTED

Testing Considerations

  • Solana integration is completely optional and doesn't affect existing EVM functionality
  • Backward compatible - no breaking changes to existing API
  • Works with popular Solana wallets (Phantom, Solflare, Backpack, etc.)
  • Supports both mainnet and testnet environments

https://claude.ai/code/session_011PKjHn7x4Pmqwm7tojPz4E


Open with Devin

Note

Medium Risk
Touches core wallet tracking/state management and adds runtime method-wrapping + polling behavior, which could impact event correctness across both EVM and Solana if bugs slip through.

Overview
Adds an opt-in Solana integration to the SDK that tracks Solana wallet connect/disconnect, signMessage/signTransaction signatures, and sendTransaction lifecycle (including confirmation polling) via a new SolanaAdapter/SolanaManager.

Updates core analytics state handling to isolate EVM vs Solana chain state (preventing disconnects on one namespace from wiping the other) and extends address validation/event enrichment to support strict chain-aware EVM vs Solana address validation.

Exposes the Solana API/types from src/index.ts, adds optional Solana peer dependencies in package.json, adds a comprehensive docs/SOLANA_INTEGRATION.md, and includes regression tests around per-namespace chain state isolation.

Written by Cursor Bugbot for commit 1dbc1f2. This will update automatically on new commits. Configure here.

Greptile Overview

Greptile Summary

This PR successfully adds comprehensive Solana Wallet Adapter integration to enable tracking of Solana wallet events alongside existing EVM functionality. The implementation follows a solid opt-in, additive architecture that preserves backward compatibility.

Key Strengths

  • Chain isolation: EVM and Solana state are properly separated into namespaces (_chainState.evm vs _chainState.solana) to prevent cross-contamination
  • Method wrapping robustness: SolanaAdapter correctly handles StandardWalletAdapter._reset() overwrites by detecting and re-wrapping methods after connect/disconnect events
  • Context wallet switching: The syncWalletState() / checkAndRebindContextAdapter() mechanism properly handles when useWallet().wallet changes in React apps
  • Blocked address filtering: System addresses (like 11111111111111111111111111111111) are correctly filtered to prevent spurious events
  • Lazy initialization: SolanaManager enables deferred wallet setup via setSolanaWallet() for flexible integration patterns
  • Chain-aware validation: Address validation correctly uses chainId for strict Solana vs EVM discrimination (addresses previous review concerns about ambiguous 40-char hex/Base58 strings)
  • Comprehensive tests: Regression coverage for cross-chain state isolation and extensive unit tests for adapter lifecycle

Architecture

The implementation adds three new Solana modules:

  • SolanaAdapter: Low-level wallet integration with method wrapping, event listeners, and confirmation polling
  • SolanaManager: Lifecycle manager that keeps Solana logic out of core SDK
  • Solana types + utilities: Chain ID mappings (900001-900004), address validation, type guards

Integration points in core SDK:

  • FormoAnalytics: Lazy-initializes SolanaManager, exposes public solana.* API
  • validateAddress(): Now chain-aware (prefers EVM when chainId is absent to avoid misclassification)
  • EventFactory: Uses chain-aware validation for all events

Implementation Quality

The code demonstrates careful attention to edge cases:

  • Deduplicates only terminal transaction states (not intermediate "processed" states)
  • Clears stale state on setWallet() to prevent old address/chainId leaking into disconnect events
  • Polls with both standard (getSignatureStatuses) and legacy (getSignatureStatus) Connection APIs
  • Tracks and cancels polling timeouts on cleanup
  • Prevents double-wrapping the same adapter instance

Confidence Score: 4/5

  • Safe to merge with careful validation testing - core logic is solid but cross-chain edge cases need production verification
  • Score reflects strong architecture and extensive test coverage, balanced against the inherent complexity of multi-chain state management. The implementation correctly handles most known edge cases (adapter rewrapping, context switching, state isolation), and previous review concerns about address validation and cleanup have been addressed. However, the additive nature means EVM + Solana interactions in production need validation, particularly around simultaneous wallet operations and state synchronization.
  • All critical files have been reviewed. The most complex file is src/solana/SolanaAdapter.ts (method wrapping + polling logic), but it has comprehensive test coverage. Pay extra attention to multi-chain scenarios in production monitoring (simultaneous EVM + Solana wallet operations).

Important Files Changed

Filename Overview
src/solana/SolanaAdapter.ts Core Solana wallet adapter integration with method wrapping, event listeners, and transaction confirmation polling. Properly handles adapter lifecycle, rewrapping after StandardWalletAdapter resets, and context wallet switching.
src/FormoAnalytics.ts Adds optional Solana integration via SolanaManager with lazy initialization. Properly isolates EVM and Solana chain state, integrates cleanup lifecycle, and exposes public API for Solana wallet management.
src/utils/address.ts Extends address validation to support both EVM (checksummed) and Solana (Base58) formats with chainId-aware strict validation. Prefers EVM validation first when chainId is absent, avoiding ambiguous address classification.
src/solana/types.ts Comprehensive TypeScript interfaces for Solana integration including wallet adapters, contexts, clusters, and chain ID mappings (900001-900004). Includes type guards and dual Connection API support.
src/solana/SolanaManager.ts Lifecycle manager for SolanaAdapter with lazy initialization and pending config support. Clean separation of Solana-specific logic from core SDK.
src/event/EventFactory.ts Updated to validate multi-chain addresses with chainId-aware validation. Uses strict validation when chainId is known, tries EVM first when chainId is absent.

Sequence Diagram

sequenceDiagram
    participant App as dApp
    participant Formo as FormoAnalytics
    participant SolMgr as SolanaManager
    participant Adapter as SolanaAdapter
    participant Wallet as Solana Wallet
    participant Conn as Solana Connection

    App->>Formo: init({ solana: { wallet, connection, cluster } })
    Formo->>SolMgr: new SolanaManager(options)
    SolMgr->>Adapter: new SolanaAdapter(wallet, connection, cluster)
    Adapter->>Wallet: on("connect", handler)
    Adapter->>Wallet: on("disconnect", handler)
    Adapter->>Adapter: wrapAdapterMethods(sendTransaction, signMessage, signTransaction)
    Adapter->>Adapter: checkInitialConnection()
    
    Note over Wallet,App: User connects wallet
    Wallet->>Adapter: emit("connect", publicKey)
    Adapter->>Adapter: handleConnect(publicKey)
    Adapter->>Adapter: Check if blocked address
    Adapter->>Formo: connect({ chainId, address })
    
    Note over App,Wallet: User sends transaction
    App->>Wallet: sendTransaction(tx, connection)
    Wallet->>Adapter: wrappedSendTransaction(tx, connection)
    Adapter->>Formo: transaction(STARTED)
    Adapter->>Wallet: originalSendTransaction(tx, connection)
    Wallet-->>Adapter: signature
    Adapter->>Formo: transaction(BROADCASTED, signature)
    Adapter->>Adapter: pollTransactionConfirmation(signature)
    
    loop Every 2s, max 30 attempts
        Adapter->>Conn: getSignatureStatuses([signature])
        Conn-->>Adapter: status
        alt Transaction confirmed
            Adapter->>Formo: transaction(CONFIRMED, signature)
        else Transaction failed
            Adapter->>Formo: transaction(REVERTED, signature)
        end
    end
    
    Note over App,Wallet: User signs message
    App->>Wallet: signMessage(message)
    Wallet->>Adapter: wrappedSignMessage(message)
    Adapter->>Formo: signature(REQUESTED, message)
    Adapter->>Wallet: originalSignMessage(message)
    Wallet-->>Adapter: signature
    Adapter->>Formo: signature(CONFIRMED, signatureHex)
    
    Note over App,Wallet: User switches cluster
    App->>Formo: solana.setCluster("devnet")
    Formo->>SolMgr: setCluster("devnet")
    SolMgr->>Adapter: setCluster("devnet")
    Adapter->>Adapter: Update chainId (900003)
    Adapter->>Formo: chain({ chainId: 900003, address })
    
    Note over Wallet,App: User disconnects
    Wallet->>Adapter: emit("disconnect")
    Adapter->>Adapter: handleDisconnect()
    Adapter->>Formo: disconnect({ chainId, address })
Loading

This commit adds support for tracking Solana wallet events in the Formo Analytics SDK.

Features:
- SolanaWalletAdapterHandler for tracking Solana wallet events
- Support for connect/disconnect, signature, and transaction tracking
- Solana address validation utilities (Base58 format)
- Cluster-to-chainId mapping (mainnet-beta: 900001, etc.)
- Transaction confirmation polling
- Works alongside EVM tracking (not mutually exclusive)
- Dynamic wallet/connection updates for React apps

Changes:
- Add src/solana/ module with handler, types, and address utilities
- Extend Options interface with SolanaOptions
- Add setSolanaWallet, setSolanaConnection, setSolanaCluster methods
- Add optional peer dependencies for @solana/wallet-adapter-*
- Add comprehensive tests and documentation

https://claude.ai/code/session_011PKjHn7x4Pmqwm7tojPz4E
@yosriady yosriady changed the title Add Solana Wallet Adapter integration for multi-chain analytics P-992 Add Solana Wallet Adapter integration Feb 5, 2026
@linear
Copy link
Copy Markdown

linear Bot commented Feb 5, 2026

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Feb 5, 2026

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm @react-native/debugger-frontend is 96.0% likely obfuscated

Confidence: 0.96

Location: Package overview

From: pnpm-lock.yamlnpm/@react-native/debugger-frontend@0.83.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@react-native/debugger-frontend@0.83.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Comment thread src/solana/SolanaWalletAdapterHandler.ts Fixed
Comment thread src/solana/SolanaWalletAdapterHandler.ts Fixed
Comment thread src/types/base.ts Fixed
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread src/FormoAnalytics.ts
Comment on lines +874 to +879
public cleanup(): void {
logger.info("SolanaWalletAdapterHandler: Cleaning up");

this.cleanupWalletListeners();
this.processedSignatures.clear();
this.pendingTransactions.clear();

This comment was marked as outdated.

- Remove unused imports (DEFAULT_SOLANA_CHAIN_ID, getValidSolanaAddress, SolanaCluster, etc.)
- Add validateMultiChainAddress() to support both EVM and Solana addresses in connect()
- Fix orphaned setTimeout in pollTransactionConfirmation - now tracks timeout IDs and cancels them on cleanup
- Add isCleanedUp flag to prevent polls from running after cleanup

https://claude.ai/code/session_011PKjHn7x4Pmqwm7tojPz4E
Copy link
Copy Markdown

@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.

12 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +125 to +130
public setWallet(
wallet: SolanaWalletAdapter | SolanaWalletContext | null
): void {
// Clean up previous wallet listeners
this.cleanupWalletListeners();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nonexistent cleanup call
setWallet() calls this.cleanupWalletListeners(), but the only cleanup helper defined is cleanupWalletListeners() (without the extra this. prefix in the name). As written, setWallet() will throw TypeError: this.cleanupWalletListeners is not a function the first time it’s called, breaking runtime wallet updates.

Suggested change
public setWallet(
wallet: SolanaWalletAdapter | SolanaWalletContext | null
): void {
// Clean up previous wallet listeners
this.cleanupWalletListeners();
this.cleanupWalletListeners();
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/solana/SolanaWalletAdapterHandler.ts
Line: 125:130

Comment:
**Nonexistent cleanup call**
`setWallet()` calls `this.cleanupWalletListeners()`, but the only cleanup helper defined is `cleanupWalletListeners()` (without the extra `this.` prefix in the name). As written, `setWallet()` will throw `TypeError: this.cleanupWalletListeners is not a function` the first time it’s called, breaking runtime wallet updates.

```suggestion
    this.cleanupWalletListeners();
```

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

Comment thread src/FormoAnalytics.ts Outdated
Comment on lines +1977 to +1980
public setSolanaWallet(wallet: Parameters<SolanaWalletAdapterHandler["setWallet"]>[0]): void {
if (this.solanaHandler) {
this.solanaHandler.setWallet(wallet);
} else if (wallet) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Runtime crash on setSolanaWallet
FormoAnalytics.setSolanaWallet() delegates to solanaHandler.setWallet(), which currently throws due to calling a nonexistent cleanupWalletListeners() method in SolanaWalletAdapterHandler (see comment there). This makes the new public API method unusable in the common “wallet context updates” scenario.

Also appears in: src/solana/SolanaWalletAdapterHandler.ts:125.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/FormoAnalytics.ts
Line: 1977:1980

Comment:
**Runtime crash on setSolanaWallet**
`FormoAnalytics.setSolanaWallet()` delegates to `solanaHandler.setWallet()`, which currently throws due to calling a nonexistent `cleanupWalletListeners()` method in `SolanaWalletAdapterHandler` (see comment there). This makes the new public API method unusable in the common “wallet context updates” scenario.

Also appears in: `src/solana/SolanaWalletAdapterHandler.ts:125`.

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

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 3 new potential issues.

View 8 additional findings in Devin Review.

Open in Devin Review

Comment thread src/solana/SolanaAdapter.ts
Comment thread src/solana/SolanaWalletAdapterHandler.ts Outdated
Comment thread src/solana/SolanaAdapter.ts
Comment on lines +126 to +136
wallet: SolanaWalletAdapter | SolanaWalletContext | null
): void {
// Clean up previous wallet listeners
this.cleanupWalletListeners();

this.wallet = wallet;

if (this.wallet) {
this.setupWalletListeners();
}
}

This comment was marked as outdated.

Comment thread src/FormoAnalytics.ts Outdated
Comment on lines +2321 to +2331
address: string,
chainId?: number
): Address | undefined {
// If chain ID is in Solana range, validate as Solana address
const solanaChainIds = Object.values(SOLANA_CHAIN_IDS);
if (chainId && solanaChainIds.includes(chainId)) {
const validSolanaAddress = getValidSolanaAddress(address);
return validSolanaAddress || undefined;
}

// Check if it looks like a Solana address (Base58, no 0x prefix)

This comment was marked as outdated.

- EventFactory: Add validateMultiChainAddress() to support both EVM and Solana addresses
- SolanaWalletAdapterHandler: Store context/adapter references for cleanup
- SolanaWalletAdapterHandler: Add restoreOriginalMethods() helper called in setWallet() and cleanup()
- SolanaWalletAdapterHandler: Fix double-wrapping by adding setupAdapterEventListenersOnly() for contexts
- SolanaWalletAdapterHandler: Rename original method properties to originalAdapter*/originalContext*

https://claude.ai/code/session_011PKjHn7x4Pmqwm7tojPz4E

if (address && this.formo.isAutocaptureEnabled("signature")) {
// Track signature confirmed
const signatureHex = Buffer.from(signature).toString("hex");

This comment was marked as outdated.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 11 additional findings in Devin Review.

Open in Devin Review

Comment thread src/solana/SolanaWalletAdapterHandler.ts Outdated
- Replace Buffer.from().toString('hex') with uint8ArrayToHex() helper
  function for browser compatibility (Buffer is a Node.js API)
- Fix polling timeout ID memory leak by tracking currentTimeoutId and
  removing it from the Set when the poll callback fires

https://claude.ai/code/session_011PKjHn7x4Pmqwm7tojPz4E
Copy link
Copy Markdown

@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.

13 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +268 to +285
/**
* Set up listeners for a wallet context (useWallet)
*/
private setupContextListeners(context: SolanaWalletContext): void {
// For context-based wallets, we set up event listeners on the inner adapter
// but only wrap the context methods (not adapter methods) to avoid double tracking.
// The context methods are the user-facing API that we want to track.

if (context.wallet) {
// Only add event listeners (connect/disconnect) on the inner adapter
// Do NOT wrap adapter methods - we'll wrap context methods instead
this.setupAdapterEventListenersOnly(context.wallet);
}

// Wrap context methods for transaction/signature tracking
this.wrapContextMethods(context);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Context wallet adapter swap

setupContextListeners() only attaches connect/disconnect listeners to context.wallet once at initialization. In @solana/wallet-adapter-react, context.wallet can change when the user selects a different wallet; after that happens, events from the new adapter won’t be tracked, and the old adapter will keep listeners attached (leak + stale events). This needs a mechanism to rebind listeners when context.wallet changes (e.g., require callers to call setSolanaWallet on wallet changes, or add a small polling/observer strategy to detect adapter swaps and re-run setupAdapterEventListenersOnly with cleanup).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/solana/SolanaWalletAdapterHandler.ts
Line: 268:285

Comment:
**Context wallet adapter swap**

`setupContextListeners()` only attaches connect/disconnect listeners to `context.wallet` once at initialization. In `@solana/wallet-adapter-react`, `context.wallet` can change when the user selects a different wallet; after that happens, events from the new adapter won’t be tracked, and the old adapter will keep listeners attached (leak + stale events). This needs a mechanism to rebind listeners when `context.wallet` changes (e.g., require callers to call `setSolanaWallet` on wallet changes, or add a small polling/observer strategy to detect adapter swaps and re-run `setupAdapterEventListenersOnly` with cleanup).

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

Comment thread src/event/EventFactory.ts Outdated
Comment on lines +59 to +75
private validateMultiChainAddress(address: string | null | undefined): Address | null {
if (!address) {
return null;
}

// Check if it's a valid Solana address first (Base58, no 0x prefix)
if (isSolanaAddress(address)) {
return getValidSolanaAddress(address);
}

// Try EVM address validation
const validEvmAddress = getValidAddress(address);
if (validEvmAddress) {
return toChecksumAddress(validEvmAddress);
}

return null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

EVM address misclassified

validateMultiChainAddress() checks isSolanaAddress() first, so any 32–44 char string comprised solely of Base58 chars will be treated as Solana and returned as-is. This includes some valid EVM addresses without a 0x prefix (40 hex chars are a subset of Base58), which will now bypass EVM validation/checksumming and change the emitted address value. Consider preferring EVM validation first (via getValidAddress) and only falling back to Solana if EVM fails, or require a Solana chainId/context signal before accepting Base58.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/event/EventFactory.ts
Line: 59:75

Comment:
**EVM address misclassified**

`validateMultiChainAddress()` checks `isSolanaAddress()` first, so any 32–44 char string comprised solely of Base58 chars will be treated as Solana and returned as-is. This includes some valid EVM addresses *without* a `0x` prefix (40 hex chars are a subset of Base58), which will now bypass EVM validation/checksumming and change the emitted `address` value. Consider preferring EVM validation first (via `getValidAddress`) and only falling back to Solana if EVM fails, or require a Solana chainId/context signal before accepting Base58.

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

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 17 additional findings in Devin Review.

Open in Devin Review

Comment thread src/solana/SolanaAdapter.ts
Comment thread src/solana/SolanaWalletAdapterHandler.ts Outdated
Comment thread src/solana/SolanaAdapter.ts
Comment thread src/solana/address.ts Outdated
Comment thread src/FormoAnalytics.ts Outdated
Comment thread src/solana/SolanaAdapter.ts
Comment thread src/solana/SolanaAdapter.ts
- Add trackConnectEventOnly/trackDisconnectEventOnly internal methods
  to FormoAnalytics for emitting events without mutating shared state
- Update Solana handler to use internal methods, preventing EVM state
  corruption in multi-chain setups
- Fix transaction polling stopping prematurely on "processed" status
  by only deduplicating terminal states (confirmed/finalized/err)
- Fix checkInitialConnection to emit connect event for auto-connected
  wallets that were already connected at initialization
- Remove redundant SYSTEM_PROGRAM check in isBlockedSolanaAddress

https://claude.ai/code/session_011PKjHn7x4Pmqwm7tojPz4E
@cursor

This comment has been minimized.

Copy link
Copy Markdown

@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.

5 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment thread src/event/EventFactory.ts Outdated
Comment on lines +64 to +66
// Check if it's a valid Solana address first (Base58, no 0x prefix)
if (isSolanaAddress(address)) {
return getValidSolanaAddress(address);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Solana checked before EVM may misclassify EVM addresses without 0x prefix as Solana (40 hex chars are valid Base58 subset). Prefer EVM validation first or require chainId/context:

Suggested change
// Check if it's a valid Solana address first (Base58, no 0x prefix)
if (isSolanaAddress(address)) {
return getValidSolanaAddress(address);
// Try EVM address validation first
const validEvmAddress = getValidAddress(address);
if (validEvmAddress) {
return toChecksumAddress(validEvmAddress);
}
// Check if it's a valid Solana address (Base58, no 0x prefix)
if (isSolanaAddress(address)) {
return getValidSolanaAddress(address);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/event/EventFactory.ts
Line: 64:66

Comment:
Solana checked before EVM may misclassify EVM addresses without `0x` prefix as Solana (40 hex chars are valid Base58 subset). Prefer EVM validation first or require chainId/context:

```suggestion
    // Try EVM address validation first
    const validEvmAddress = getValidAddress(address);
    if (validEvmAddress) {
      return toChecksumAddress(validEvmAddress);
    }

    // Check if it's a valid Solana address (Base58, no 0x prefix)
    if (isSolanaAddress(address)) {
      return getValidSolanaAddress(address);
    }
```

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

Comment on lines +271 to +284
private setupContextListeners(context: SolanaWalletContext): void {
// For context-based wallets, we set up event listeners on the inner adapter
// but only wrap the context methods (not adapter methods) to avoid double tracking.
// The context methods are the user-facing API that we want to track.

if (context.wallet) {
// Only add event listeners (connect/disconnect) on the inner adapter
// Do NOT wrap adapter methods - we'll wrap context methods instead
this.setupAdapterEventListenersOnly(context.wallet);
}

// Wrap context methods for transaction/signature tracking
this.wrapContextMethods(context);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

setupContextListeners only attaches listeners to context.wallet once. In @solana/wallet-adapter-react, context.wallet changes when user switches wallets. After a wallet switch, new adapter events won't be tracked and old adapter keeps listeners (memory leak + stale events). Either document that callers must call setSolanaWallet on wallet changes, or add a mechanism to detect and rebind listeners when context.wallet changes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/solana/SolanaWalletAdapterHandler.ts
Line: 271:284

Comment:
`setupContextListeners` only attaches listeners to `context.wallet` once. In `@solana/wallet-adapter-react`, `context.wallet` changes when user switches wallets. After a wallet switch, new adapter events won't be tracked and old adapter keeps listeners (memory leak + stale events). Either document that callers must call `setSolanaWallet` on wallet changes, or add a mechanism to detect and rebind listeners when `context.wallet` changes.

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

Comment on lines +844 to +971
}

// Stop polling if cleaned up
if (this.isCleanedUp) {
this.pendingTransactions.delete(signature);
return;
}

try {
const result = await conn.getSignatureStatus!(signature);
const status = result.value;

if (status) {
const signatureKey = `${signature}:${status.confirmationStatus}`;

// Only deduplicate terminal states (confirmed/finalized/err) to prevent
// duplicate event emissions. Do NOT deduplicate intermediate states like
// "processed" since we need to keep polling until a terminal state.
const isTerminalState =
!!status.err ||
status.confirmationStatus === "confirmed" ||
status.confirmationStatus === "finalized";

// Check for duplicate processing of terminal states only
if (isTerminalState && this.processedSignatures.has(signatureKey)) {
return;
}
if (isTerminalState) {
this.processedSignatures.add(signatureKey);
}

if (status.err) {
// Transaction failed
logger.info(
"SolanaWalletAdapterHandler: Transaction reverted",
{
signature,
error: status.err,
}
);

this.formo.transaction({
status: TransactionStatus.REVERTED,
chainId: this.chainId,
address,
transactionHash: signature,
});

this.pendingTransactions.delete(signature);
return;
}

if (
status.confirmationStatus === "confirmed" ||
status.confirmationStatus === "finalized"
) {
// Transaction confirmed
logger.info(
"SolanaWalletAdapterHandler: Transaction confirmed",
{
signature,
confirmationStatus: status.confirmationStatus,
}
);

this.formo.transaction({
status: TransactionStatus.CONFIRMED,
chainId: this.chainId,
address,
transactionHash: signature,
});

this.pendingTransactions.delete(signature);
return;
}
}
} catch (error) {
logger.error(
"SolanaWalletAdapterHandler: Error polling transaction status",
error
);
}

attempts++;
if (attempts < maxAttempts && !this.isCleanedUp) {
currentTimeoutId = setTimeout(poll, intervalMs);
this.pollingTimeouts.add(currentTimeoutId);
} else {
// Cleanup after max attempts
this.pendingTransactions.delete(signature);
}
};

// Start polling
currentTimeoutId = setTimeout(poll, intervalMs);
this.pollingTimeouts.add(currentTimeoutId);

// Clean up old processed signatures
cleanupOldEntries(this.processedSignatures);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Polling continues until confirmation/error or 30 attempts (60s). Solana block time is ~400ms, so 2s intervals may be longer than needed. Consider reducing interval for faster confirmation detection. Note: pendingTransactions map cleans up on terminal states but not on max attempts timeout (minor memory accumulation for stuck transactions).

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/solana/SolanaWalletAdapterHandler.ts
Line: 844:971

Comment:
Polling continues until confirmation/error or 30 attempts (60s). Solana block time is ~400ms, so 2s intervals may be longer than needed. Consider reducing interval for faster confirmation detection. Note: `pendingTransactions` map cleans up on terminal states but not on max attempts timeout (minor memory accumulation for stuck transactions).

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

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

Comment on lines +886 to +901

// Only deduplicate terminal states (confirmed/finalized/err) to prevent
// duplicate event emissions. Do NOT deduplicate intermediate states like
// "processed" since we need to keep polling until a terminal state.
const isTerminalState =
!!status.err ||
status.confirmationStatus === "confirmed" ||
status.confirmationStatus === "finalized";

// Check for duplicate processing of terminal states only
if (isTerminalState && this.processedSignatures.has(signatureKey)) {
return;
}
if (isTerminalState) {
this.processedSignatures.add(signatureKey);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deduplicates only terminal states (confirmed/finalized/err) to prevent duplicate events, allowing intermediate "processed" states to continue polling until terminal state reached. This design correctly handles Solana's multi-stage confirmation model.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/solana/SolanaWalletAdapterHandler.ts
Line: 886:901

Comment:
Deduplicates only terminal states (confirmed/finalized/err) to prevent duplicate events, allowing intermediate "processed" states to continue polling until terminal state reached. This design correctly handles Solana's multi-stage confirmation model.

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

Comment thread src/FormoAnalytics.ts Outdated
Comment on lines +2401 to +2419
private validateMultiChainAddress(
address: string,
chainId?: number
): Address | undefined {
// If chain ID is in Solana range, validate as Solana address
const solanaChainIds = Object.values(SOLANA_CHAIN_IDS);
if (chainId && solanaChainIds.includes(chainId)) {
const validSolanaAddress = getValidSolanaAddress(address);
return validSolanaAddress || undefined;
}

// Check if it looks like a Solana address (Base58, no 0x prefix)
if (isSolanaAddress(address)) {
return getValidSolanaAddress(address) || undefined;
}

// Default to EVM address validation
return this.validateAndChecksumAddress(address);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same address validation order issue as EventFactory.ts:64 - checking isSolanaAddress first may misclassify EVM addresses without 0x as Solana since 40 hex chars are a Base58 subset. Should prefer EVM validation first (via validateAndChecksumAddress), then fall back to Solana only if EVM validation fails.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/FormoAnalytics.ts
Line: 2401:2419

Comment:
Same address validation order issue as `EventFactory.ts:64` - checking `isSolanaAddress` first may misclassify EVM addresses without `0x` as Solana since 40 hex chars are a Base58 subset. Should prefer EVM validation first (via `validateAndChecksumAddress`), then fall back to Solana only if EVM validation fails.

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

Comment thread src/event/EventFactory.ts
}

return null;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Address validation prioritizes Solana over EVM without chain context

Medium Severity

EventFactory.validateMultiChainAddress checks isSolanaAddress before EVM validation without any chainId context. Unlike the FormoAnalytics version which checks chainId first, this method guesses purely from format. isSolanaAddress matches any 32–44 character string containing only Base58 characters, which can overlap with non-0x-prefixed EVM addresses (about 7.6% of random hex addresses contain no 0 digit). While normal SDK-generated EVM addresses always carry the 0x prefix (making this safe in practice), the FormoAnalytics.validateMultiChainAddress has the same fallback path when chainId is absent or non-Solana — an address matching Base58 will skip EVM checksumming entirely.

Additional Locations (1)

Fix in Cursor Fix in Web

Comment thread src/solana/types.ts
transactions: SolanaTransaction[]
): Promise<SolanaTransaction[]>;
signMessage?(message: Uint8Array): Promise<Uint8Array>;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SolanaWalletContext.wallet type mismodels the real wallet-adapter-react interface

High Severity

SolanaWalletContext.wallet is typed as SolanaWalletAdapter | null, but in the real @solana/wallet-adapter-react, useWallet().wallet returns { adapter: Adapter, readyState: WalletReadyState } | null — the adapter with .on()/.off() lives at .adapter, not directly on the wallet object. setupContextListeners passes context.wallet to setupAdapterEventListenersOnly, which calls .on("connect", ...) on it. At runtime with a real wallet-adapter-react context (e.g., via type coercion), this would throw because the Wallet wrapper object lacks on/off methods. Similarly, getWalletName accesses this.wallet.wallet?.name which would be undefined on a real Wallet object (the name is at .adapter.name).

Additional Locations (1)

Fix in Cursor Fix in Web

@cursor

This comment has been minimized.

@yosriady
Copy link
Copy Markdown
Contributor Author

yosriady commented Feb 9, 2026

@cursor push f87326a

Copy link
Copy Markdown

@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.

13 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +398 to +402
// Wrap sendTransaction
context.sendTransaction = async (
transaction: SolanaTransaction,
connection: SolanaConnection,
options?: SendTransactionOptions
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cleanup breaks future polling
cleanup() sets isCleanedUp = true, and pollTransactionConfirmation() bails out early when that flag is set. If the handler is later reused via setWallet() (or lazy init in FormoAnalytics.setSolanaWallet()), isCleanedUp is never reset, so confirmation polling will never run for any subsequent transactions/signatures.

This triggers when consumers call FormoAnalytics.cleanup() (or solanaHandler.cleanup()) and later re-initialize/reuse the same FormoAnalytics instance with Solana enabled.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/solana/SolanaWalletAdapterHandler.ts
Line: 398:402

Comment:
**Cleanup breaks future polling**
`cleanup()` sets `isCleanedUp = true`, and `pollTransactionConfirmation()` bails out early when that flag is set. If the handler is later reused via `setWallet()` (or lazy init in `FormoAnalytics.setSolanaWallet()`), `isCleanedUp` is never reset, so confirmation polling will never run for any subsequent transactions/signatures.

This triggers when consumers call `FormoAnalytics.cleanup()` (or `solanaHandler.cleanup()`) and later re-initialize/reuse the same `FormoAnalytics` instance with Solana enabled.

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

Comment on lines +459 to +462
): Promise<Uint8Array> => {
const address = this.getCurrentAddress();
const messageString = new TextDecoder().decode(message);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unconditional TextDecoder usage
new TextDecoder() will throw in environments without the Web TextEncoder/TextDecoder globals (e.g., some Node/test setups). Since the SDK supports non-browser contexts, signMessage autocapture can crash when enabled.

Also occurs in wrappedSignMessage at src/solana/SolanaWalletAdapterHandler.ts:617.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/solana/SolanaWalletAdapterHandler.ts
Line: 459:462

Comment:
**Unconditional `TextDecoder` usage**
`new TextDecoder()` will throw in environments without the Web TextEncoder/TextDecoder globals (e.g., some Node/test setups). Since the SDK supports non-browser contexts, `signMessage` autocapture can crash when enabled.

Also occurs in `wrappedSignMessage` at src/solana/SolanaWalletAdapterHandler.ts:617.

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

Comment on lines +841 to +846

/**
* Poll for transaction confirmation
*/
private async pollTransactionConfirmation(
signature: string,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Polling uses nonstandard API
The code calls conn.getSignatureStatus(signature) but the common @solana/web3.js Connection API is getSignatureStatuses([signature]) (plural) rather than getSignatureStatus. With a real web3.js Connection, getSignatureStatus will be undefined, so confirmation polling will never emit CONFIRMED/REVERTED and pending transactions are only cleaned up on max-attempts.

If this is intentional (supporting a custom connection wrapper), it likely needs to also support getSignatureStatuses to work with the standard adapter stack.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/solana/SolanaWalletAdapterHandler.ts
Line: 841:846

Comment:
**Polling uses nonstandard API**
The code calls `conn.getSignatureStatus(signature)` but the common `@solana/web3.js` Connection API is `getSignatureStatuses([signature])` (plural) rather than `getSignatureStatus`. With a real web3.js `Connection`, `getSignatureStatus` will be `undefined`, so confirmation polling will never emit CONFIRMED/REVERTED and pending transactions are only cleaned up on max-attempts.

If this is intentional (supporting a custom connection wrapper), it likely needs to also support `getSignatureStatuses` to work with the standard adapter stack.

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

Comment thread src/FormoAnalytics.ts Outdated
Comment on lines +2067 to +2072
chainId: this.options.solana?.chainId,
});
}
}

/**

This comment was marked as outdated.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 19 additional findings in Devin Review.

Open in Devin Review

Comment thread src/solana/SolanaWalletAdapterHandler.ts Outdated
Comment thread src/solana/SolanaAdapter.ts
- Reset isCleanedUp flag in setWallet() to enable polling after reuse
- Add trackChainEventOnly() internal method for Solana chain events
- Update setCluster to use trackChainEventOnly to avoid EVM state corruption
- Add safeDecodeMessage() helper for Node.js environment compatibility
- Support both getSignatureStatuses (standard) and getSignatureStatus APIs
- Pass connection from initial options in setSolanaWallet lazy-init
- Add duplicate check in checkInitialConnection to prevent repeat events
- Fix SolanaWalletContext.wallet type to match real wallet-adapter-react
  (wallet is { adapter, readyState }, not the adapter directly)
- Update setupContextListeners to use context.wallet.adapter
- Update getWalletName to use wallet.adapter.name

https://claude.ai/code/session_011PKjHn7x4Pmqwm7tojPz4E
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 44 additional findings in Devin Review.

Open in Devin Review

Comment thread src/FormoAnalytics.ts Outdated
1. Fix setSolanaConnection/setSolanaCluster silent drop:
   - Add pendingSolanaConnection and pendingSolanaCluster properties
   - Store values when called before handler is initialized
   - Apply pending values when handler is lazily created via setSolanaWallet()

2. Add syncSolanaWalletState() method:
   - Public method for consumers to sync wallet state
   - Checks if adapter changed and rebinds if needed
   - Useful in React effects when wallet.wallet changes but context
     object reference stays the same
   - Ensures connect/disconnect events are tracked without waiting
     for next transaction call

https://claude.ai/code/session_011PKjHn7x4Pmqwm7tojPz4E
1. Add tests for wallet swap detection:
   - syncWalletState() with adapter change detection
   - Adapter becoming null in context
   - No-op for non-context wallets

2. Add tests for disconnect guards:
   - No disconnect event when no prior connection exists
   - Disconnect only emits with prior connection

3. Add tests for double-wrapping prevention:
   - Same adapter not double-wrapped on setWallet
   - Different adapter properly re-wrapped

4. Add tests for chainId consistency:
   - Captured chainId used for all events
   - ChainId preserved even if setCluster called mid-transaction

5. Add tests for all system address blocking:
   - System Program, Token Program, Token 2022, Associated Token,
     Rent Sysvar, Clock Sysvar

6. Simplify getPublicKey() method:
   - Both wallet types have same publicKey property
   - Reduced from 10 lines to 1 line

Test count: 515 -> 530 (+15 new tests)

https://claude.ai/code/session_011PKjHn7x4Pmqwm7tojPz4E
Comment thread test/solana/SolanaWalletAdapterHandler.spec.ts Fixed
Update test to verify that setWallet re-wraps cleanly without
causing duplicate event emissions, rather than checking method
reference equality.

https://claude.ai/code/session_011PKjHn7x4Pmqwm7tojPz4E
1. Add emitTransactionEvent() helper:
   - Consolidates repeated pattern of checking address and autocapture
   - Used in wrappedSendTransaction for STARTED/BROADCASTED/REJECTED

2. Add emitSignatureEvent() helper:
   - Same consolidation for signature events
   - Used in wrappedSignMessage and wrappedSignTransaction

3. Add registerAdapterListener() helper:
   - Simplifies listener registration with automatic unsubscriber tracking
   - Reduces boilerplate in setupAdapterEventListenersOnly

4. Simplify wrapped methods:
   - wrappedSendTransaction: 64 -> 40 lines
   - wrappedSignMessage: 48 -> 24 lines
   - wrappedSignTransaction: 47 -> 25 lines

Net reduction: 34 lines of code (-108 +74)

https://claude.ai/code/session_011PKjHn7x4Pmqwm7tojPz4E
Copy link
Copy Markdown

@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.

14 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +383 to +387
handler: (...args: unknown[]) => void
): void {
adapter.on(event as "connect", handler as () => void);
this.unsubscribers.push(() => adapter.off(event as "connect", handler as () => void));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Incorrect event listener types
registerAdapterListener() always casts the event to "connect" for both on and off, which means disconnect/error listeners are registered/removed under the wrong event name. This will prevent disconnect/error tracking and leak listeners on cleanup.

Suggested change
handler: (...args: unknown[]) => void
): void {
adapter.on(event as "connect", handler as () => void);
this.unsubscribers.push(() => adapter.off(event as "connect", handler as () => void));
}
adapter.on(event as any, handler as any);
this.unsubscribers.push(() => adapter.off(event, handler as any));
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/solana/SolanaWalletAdapterHandler.ts
Line: 383:387

Comment:
**Incorrect event listener types**
`registerAdapterListener()` always casts the event to `"connect"` for both `on` and `off`, which means `disconnect`/`error` listeners are registered/removed under the wrong event name. This will prevent disconnect/error tracking and leak listeners on cleanup.

```suggestion
    adapter.on(event as any, handler as any);
    this.unsubscribers.push(() => adapter.off(event, handler as any));
```

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

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 46 additional findings in Devin Review.

Open in Devin Review

Comment on lines +458 to +462
this.checkAndRebindContextAdapter();

if (!this.originalAdapterSendTransaction) {
throw new Error("sendTransaction not available");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 checkAndRebindContextAdapter inside wrapped methods replaces originalAdapter* references mid-flight, routing calls to the wrong adapter

When wrappedSendTransaction (and similarly wrappedSignMessage/wrappedSignTransaction) is invoked, it calls this.checkAndRebindContextAdapter() at the very start. If the context's adapter has changed since the method was wrapped, checkAndRebindContextAdapter calls restoreOriginalMethods() which clears this.originalAdapterSendTransaction (line 179), then calls wrapAdapterMethods(newAdapter) which sets this.originalAdapterSendTransaction to the new adapter's bound method.

The wrapped method then continues execution at line 460-461 and uses this.originalAdapterSendTransaction — which now points to the new adapter's method, not the old one that was originally called. The transaction/signature intended for the old adapter gets routed through the new adapter.

Root Cause and Impact

The call chain is:

  1. User calls oldAdapter.sendTransaction(tx, conn) which is the wrapped version.
  2. wrappedSendTransaction (line 458) calls checkAndRebindContextAdapter().
  3. checkAndRebindContextAdapter (line 334) calls restoreOriginalMethods() → clears this.originalAdapterSendTransaction at SolanaWalletAdapterHandler.ts:179.
  4. checkAndRebindContextAdapter (line 340) calls wrapAdapterMethods(newAdapter) → sets this.originalAdapterSendTransaction to newAdapter's sendTransaction at SolanaWalletAdapterHandler.ts:432.
  5. Back in wrappedSendTransaction, line 471 calls this.originalAdapterSendTransaction(transaction, connection, options) — this is now the new adapter's method.

Impact: A transaction or signature request could be sent through the wrong wallet adapter. This could cause the call to fail with a confusing error, or in the worst case, succeed on a different wallet than intended. The same issue affects wrappedSignMessage (line 498) and wrappedSignTransaction (line 527).

The fix should capture this.originalAdapterSendTransaction in a local variable before calling checkAndRebindContextAdapter(), so that even if the handler rebinds, the in-flight call still uses the correct original method.

Suggested change
this.checkAndRebindContextAdapter();
if (!this.originalAdapterSendTransaction) {
throw new Error("sendTransaction not available");
}
// Capture the original method BEFORE rebinding, so an in-flight call
// is not rerouted to a different adapter if the context changed.
const originalSend = this.originalAdapterSendTransaction;
this.checkAndRebindContextAdapter();
if (!originalSend) {
throw new Error("sendTransaction not available");
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

claude and others added 3 commits February 11, 2026 09:38
The registerAdapterListener() was incorrectly casting all events to
"connect" type, which would register disconnect/error listeners under
the wrong event name. Fixed by using 'any' cast to properly handle
the overloaded on/off signatures.

https://claude.ai/code/session_011PKjHn7x4Pmqwm7tojPz4E
The chainId option was removed from SolanaOptions as it was always
derived from the cluster. Updated documentation to reflect this and
document the chain ID mapping.

https://claude.ai/code/session_011PKjHn7x4Pmqwm7tojPz4E
}

if (status) {
const signatureKey = `${signature}:${status.confirmationStatus}`;

This comment was marked as outdated.

yosriady and others added 3 commits February 12, 2026 11:32
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rename validateMultiChainAddress → validateAddress
- Extract validateAddress and validateAndChecksumAddress to utils/address.ts
- Rename SolanaWalletAdapter → SolanaAdapter across all files
- Narrow public exports: remove internal constants, type guards, address utils
- Add signTransaction to docs mock wallet example

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON, but it could not run because on-demand usage is turned off. To enable Bugbot Autofix, turn on on-demand usage and set a spend limit in the Cursor dashboard.

Comment thread package.json Outdated
"wagmi": ">=2.0.0",
"@solana/wallet-adapter-base": ">=0.9.0",
"@solana/wallet-adapter-react": ">=0.15.0",
"@solana/web3.js": ">=1.70.0 <2.0.0"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Optional peer blocks web3.js v2 projects

Medium Severity

package.json adds @solana/web3.js as an optional peer but pins it to <2.0.0. Even when Solana tracking is unused, apps that already depend on @solana/web3.js@2 can hit peer-resolution failures or strict CI install errors because the version constraint is incompatible.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 42 additional findings in Devin Review.

Open in Devin Review

Comment thread src/FormoAnalytics.ts Outdated
Comment thread src/event/EventFactory.ts Outdated
Comment on lines +66 to +76
if (validEvmAddress) {
return toChecksumAddress(validEvmAddress);
}

// Check if it's a valid Solana address (Base58, no 0x prefix)
if (isSolanaAddress(address)) {
return getValidSolanaAddress(address);
}

return null;
}

This comment was marked as outdated.

yosriady and others added 2 commits February 12, 2026 14:19
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 44 additional findings in Devin Review.

Open in Devin Review

Comment thread src/FormoAnalytics.ts
Comment thread src/FormoAnalytics.ts
yosriady and others added 2 commits February 12, 2026 14:54
Rename resolveNamespace → getNamespace and make it internal to
setChainState/clearChainState. Callers can now pass either a
namespace string or a chainId number, reducing indirection at
call sites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Eliminates the redundant chainId duplication at call sites like
setChainState(chainId, { address, chainId }). When the first arg
is a number it is now implicitly used as the namespace's chainId.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON, but it could not run because on-demand usage is turned off. To enable Bugbot Autofix, turn on on-demand usage and set a spend limit in the Cursor dashboard.

Comment thread package.json Outdated
"wagmi": ">=2.0.0",
"@solana/wallet-adapter-base": ">=0.9.0",
"@solana/wallet-adapter-react": ">=0.15.0",
"@solana/web3.js": ">=1.70.0 <2.0.0"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overly strict Solana peer dependency range

Medium Severity

The new peer range for @solana/web3.js is pinned to <2.0.0, which can create peer conflicts in apps already on v2 even when the SDK only uses structural SolanaConnection typing and no direct @solana/web3.js imports. This can block installs in strict peer-dependency environments.

Fix in Cursor Fix in Web

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Feb 12, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​solana/​wallet-adapter-base@​0.9.271001007283100
Added@​solana/​wallet-adapter-react@​0.15.399910010083100

View full report

The SDK defines its own SolanaConnection interface and never imports
from @solana/web3.js directly, so the <2.0.0 cap was unnecessarily
blocking users on v2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@yosriady yosriady merged commit ec2108d into main Feb 12, 2026
14 checks passed
@yosriady yosriady deleted the claude/add-solana-support-LMxRK branch February 12, 2026 08:14
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.

3 participants