Skip to content

multichain/exchange api work, tx system refactor and token2022 improvements#24

Draft
odilitime wants to merge 2 commits into1.xfrom
odi-service
Draft

multichain/exchange api work, tx system refactor and token2022 improvements#24
odilitime wants to merge 2 commits into1.xfrom
odi-service

Conversation

@odilitime
Copy link
Member

@odilitime odilitime commented Oct 31, 2025

Note

Adds exchange-driven swap execution, robust Token‑2022 metadata parsing, and a refactored transaction/portfolio flow; also registers Solana nets with INTEL_CHAIN.

  • Solana Service:
    • Exchange API: Add registry and doSwapOnExchange, plus getSwapAmounts; basic selectExchange.
    • Transaction refactor: Introduce buildAndSendTransaction and unify send via sendTx; add transferSplToken and executeCustomTransaction; update transferSol to new flow.
    • Token‑2022 support: Implement TLV parsing (parseTLVExtensions, parseToken2022MetadataExtension, parseToken2022SymbolFromMintOrPtr); enhance getTokensSymbols and parseTokenAccounts (supply/decimals caching, isMutable, program detection).
    • Wallet/portfolio: Add subscription-driven refresh in ensurePublicKey; add getCachedData; streamline updateWalletData fallback; improve getBalancesByAddrs handling and add getPubkeyFromSecret.
  • Wallet Service:
    • Lazy-init linkage to main service; delegate transferSol; make balance lookups tolerate nulls; getTokenAccountsByKeypairs returns nullable arrays.
  • Plugin (index.ts):
    • Register multiple Solana networks (mainnet, devnet, testnet, localnet) with INTEL_CHAIN including RPC URLs.

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

Summary by CodeRabbit

  • New Features
    • Added support for multiple Solana networks (mainnet, devnet, testnet, localnet)
    • Implemented token swap and exchange functionality
    • Enhanced token metadata handling and support
    • Added wallet data caching and refresh capabilities
    • Enabled direct SOL and SPL token transfers with improved transaction reliability

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 31, 2025

Walkthrough

This change introduces multi-network Solana chain registration and substantially refactors wallet and token service logic. The chain registration now supports mainnet, devnet, testnet, and localnet. The SolanaService gains swap orchestration, advanced token metadata parsing (including Token-2022 TLV support), transaction construction utilities, and expanded wallet data retrieval.

Changes

Cohort / File(s) Summary
Multi-Network Chain Registration
src/index.ts
Replaced single Solana chain registration with loop-based registration for mainnet, devnet, testnet, and localnet. Each network includes chainType, chainNet, and rpcUrl fields. Localnet RPC sourced from settings with fallback to http://127.0.0.1:8899.
Wallet Service Refactoring
src/service.ts (SolanaWalletService)
Introduced lazy-loading of SolanaService with runtime activation. Renamed private _solanaService to solanaService. Updated getPortfolio, getBalance, and added getCachedData and updateWalletData methods with guard checks and caching logic.
Swap & Exchange Integration
src/service.ts (SolanaService)
Added swap orchestration: selectExchange, doSwapOnExchange, and getSwapAmounts methods. Integrated Jupiter service and Intel chain hooks for exchange coordination. Added SolanaSwapWalletSet and SwapWalletSet data structures.
Token Metadata & Decimals
src/service.ts (SolanaService)
Enhanced token symbol resolution with Token-2022 TLV parsing: parseTLVExtensions, parseToken2022MetadataExtension, parseToken2022SymbolFromMintOrPtr. Expanded getTokensSymbols and getDecimal to support TLV data and on-chain pointers.
Transaction & Transfer Utilities
src/service.ts (SolanaService)
Added buildAndSendTransaction, transferSol, transferSplToken, executeCustomTransaction, and sendTx for generalized transaction construction and signing with improved error handling.
Wallet & Account Utilities
src/service.ts (SolanaService)
Added getTokenAccountsByKeypair, getTokenAccountsByKeypairs, getBalancesByAddrs, walletAddressToHumanString, walletAddressToLLMString, getPubkeyFromSecret, getTokenBalanceForWallets, and related helpers for wallet data retrieval and formatting.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant SolanaWalletService
    participant SolanaService
    participant Exchange/Jupiter
    participant Blockchain

    User->>SolanaWalletService: getPortfolio()
    activate SolanaWalletService
    SolanaWalletService->>SolanaService: lazy-load (if not initialized)
    activate SolanaService
    SolanaWalletService->>SolanaService: getPortfolio()
    SolanaService->>Blockchain: fetch token accounts
    SolanaService->>Blockchain: fetch balances
    SolanaService-->>SolanaWalletService: portfolio data
    deactivate SolanaService
    SolanaWalletService-->>User: portfolio (cached or fresh)
    deactivate SolanaWalletService

    User->>SolanaWalletService: initiate swap
    activate SolanaWalletService
    SolanaWalletService->>SolanaService: selectExchange()
    SolanaService->>Exchange/Jupiter: register/select
    SolanaWalletService->>SolanaService: doSwapOnExchange(exchHndl, walletSet)
    activate SolanaService
    SolanaService->>SolanaService: getSwapAmounts(txDetails)
    SolanaService->>SolanaService: buildAndSendTransaction(instructions, signers)
    SolanaService->>Blockchain: sign & send transaction
    Blockchain-->>SolanaService: transaction confirmation
    SolanaService->>SolanaService: parse fees & amounts from receipt
    SolanaService-->>SolanaWalletService: swap result
    deactivate SolanaService
    SolanaWalletService-->>User: swap confirmation
    deactivate SolanaWalletService
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • src/service.ts: Extensive refactoring with 20+ new public methods across swap orchestration, token metadata parsing (TLV/Token-2022 support), and transaction utilities. Review focus areas:
    • Swap execution flow (doSwapOnExchange, getSwapAmounts) and Jupiter integration correctness
    • TLV parsing logic for Token-2022 metadata and symbol resolution (potential edge cases with on-chain pointers)
    • Transaction construction (buildAndSendTransaction, sendTx) error handling and signer compatibility
    • Lazy-loading initialization and null-check guards in SolanaWalletService
    • Caching strategy in getCachedData and updateWalletData for performance impact
  • src/index.ts: Relatively straightforward loop refactor, but verify localnet RPC URL fallback logic and chainNet field mappings

Poem

🐰 Hoppy hops through networks newfold,
Four chains bloom: mainnet, devnet, bold!
Swaps dance through Jupiter's trade,
Tokens and TLVs now paraded,
Transaction builders craft with care—hop hop! 🌟

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "multichain/exchange api work, tx system refactor and token2022 improvements" is fully aligned with the changeset. The title accurately captures the three major areas of work present in the changes: multichain registration for Solana networks (mainnet, devnet, testnet, localnet) in src/index.ts, exchange and swap orchestration capabilities in src/service.ts, transaction system refactoring through new helper methods like buildAndSendTransaction and sendTx, and Token-2022 improvements including TLV parsing and enhanced metadata handling. The title is concise, specific, and clearly conveys the scope of changes without vague terminology, allowing teammates scanning history to understand the primary focus areas of the contribution.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch odi-service

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

* @returns A promise that resolves to the wallet's portfolio.
*/
public async getPortfolio(owner?: string): Promise<siWalletPortfolio> {
if (!this.solanaService) throw new Error('Solana Service is required for Solana Wallet Service: getPortfolio')
Copy link

Choose a reason for hiding this comment

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

Bug: Bug

The SolanaWalletService has a race condition: its constructor asynchronously loads this.solanaService, but public methods like getPortfolio and getBalance perform synchronous checks. This causes methods to fail if called before the service is fully initialized, as the previous lazy-loading pattern was replaced without updating these checks.

Fix in Cursor Fix in Web

const quoteResponse = await exService.getQuote({
inputMint: src.assetRef,
outputMint: trg.assetRef,
amount: w.inAmount,
Copy link

Choose a reason for hiding this comment

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

Bug: Bug

In doSwapOnExchange, w.inAmount is rounded for validation, but the original unrounded value is used in the getQuote API call. This inconsistency means the API may receive an unvalidated fractional amount, potentially leading to precision issues or unexpected swap behavior.

Fix in Cursor Fix in Web

//const decimals = t.account.data.parsed.info.tokenAmount.decimals;
//const balance = Number(amountRaw) / (10 ** decimals);
//const ca = new PublicKey(t.account.data.parsed.info.mint);
if (t === null) continue
Copy link

Choose a reason for hiding this comment

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

Bug: Bug

The getBalance method includes an unnecessary null check (if (t === null) continue) when iterating heldTokens. Since heldTokens is typed as KeyedParsedTokenAccount[], its elements are guaranteed to be non-null, making this check always false.

Fix in Cursor Fix in Web

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 5f9d3cf and c438b70.

📒 Files selected for processing (2)
  • src/index.ts (1 hunks)
  • src/service.ts (46 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot

Comment on lines +2347 to 2364
const walletPubkeys = walletAddresses.map(a => new PublicKey(a));
const atAs = walletPubkeys.map(w => getAssociatedTokenAddressSync(mint, w));
const balances: Record<string, number> = {};

// Convert to base58 strings for secure storage
const publicKey = newKeypair.publicKey.toBase58();
const privateKey = bs58.encode(newKeypair.secretKey);
// fetch mint decimals once
const decimals = await this.getDecimal(mint);

// Clear the keypair from memory
newKeypair.secretKey.fill(0);
// fetch ATAs in batches
const infos = await this.batchGetMultipleAccountsInfo(atAs, 'getTokenBalanceForWallets');

return {
publicKey,
privateKey,
};
} catch (error) {
logger.error(`Error creating wallet: ${error}`);
throw new Error('Failed to create new wallet');
}
return {
publicKey,
privateKey,
};
} catch (error) {
logger.error(`Error creating wallet: ${error}`);
throw new Error('Failed to create new wallet');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix getTokenBalanceForWallets return logic

The new getTokenBalanceForWallets body now returns { publicKey, privateKey }, but those identifiers are undefined here and the method never emits the balance map it promises. This is a compile-time/runtime blocker. We need to iterate the fetched accounts, decode their balances, and return the populated balances object.

Apply this diff to restore the intended behaviour:

     // fetch ATAs in batches
     const infos = await this.batchGetMultipleAccountsInfo(atAs, 'getTokenBalanceForWallets');
-
-      return {
-        publicKey,
-        privateKey,
-      };
-    } catch (error) {
-      logger.error(`Error creating wallet: ${error}`);
-      throw new Error('Failed to create new wallet');
-    }
-  }
+    for (let i = 0; i < infos.length; i += 1) {
+      const info = infos[i];
+      const owner = walletAddresses[i];
+      if (!owner) {
+        continue;
+      }
+
+      if (!info) {
+        balances[owner] = 0;
+        continue;
+      }
+
+      try {
+        const programId = info.owner.equals(TOKEN_2022_PROGRAM_ID) ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
+        const account = unpackAccount(atAs[i], info, programId);
+        const rawAmount = Number(account.amount);
+        balances[owner] = rawAmount / 10 ** decimals;
+      } catch (err) {
+        this.runtime.logger.warn({ mint: mint.toBase58(), owner }, 'getTokenBalanceForWallets: failed to decode account');
+        balances[owner] = 0;
+      }
+    }
+
+    return balances;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const walletPubkeys = walletAddresses.map(a => new PublicKey(a));
const atAs = walletPubkeys.map(w => getAssociatedTokenAddressSync(mint, w));
const balances: Record<string, number> = {};
// Convert to base58 strings for secure storage
const publicKey = newKeypair.publicKey.toBase58();
const privateKey = bs58.encode(newKeypair.secretKey);
// fetch mint decimals once
const decimals = await this.getDecimal(mint);
// Clear the keypair from memory
newKeypair.secretKey.fill(0);
// fetch ATAs in batches
const infos = await this.batchGetMultipleAccountsInfo(atAs, 'getTokenBalanceForWallets');
return {
publicKey,
privateKey,
};
} catch (error) {
logger.error(`Error creating wallet: ${error}`);
throw new Error('Failed to create new wallet');
}
return {
publicKey,
privateKey,
};
} catch (error) {
logger.error(`Error creating wallet: ${error}`);
throw new Error('Failed to create new wallet');
}
const walletPubkeys = walletAddresses.map(a => new PublicKey(a));
const atAs = walletPubkeys.map(w => getAssociatedTokenAddressSync(mint, w));
const balances: Record<string, number> = {};
// fetch mint decimals once
const decimals = await this.getDecimal(mint);
// fetch ATAs in batches
const infos = await this.batchGetMultipleAccountsInfo(atAs, 'getTokenBalanceForWallets');
for (let i = 0; i < infos.length; i += 1) {
const info = infos[i];
const owner = walletAddresses[i];
if (!owner) {
continue;
}
if (!info) {
balances[owner] = 0;
continue;
}
try {
const programId = info.owner.equals(TOKEN_2022_PROGRAM_ID) ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
const account = unpackAccount(atAs[i], info, programId);
const rawAmount = Number(account.amount);
balances[owner] = rawAmount / 10 ** decimals;
} catch (err) {
this.runtime.logger.warn({ mint: mint.toBase58(), owner }, 'getTokenBalanceForWallets: failed to decode account');
balances[owner] = 0;
}
}
return balances;
}
🤖 Prompt for AI Agents
In src/service.ts around lines 2347 to 2364, the method currently returns
undefined { publicKey, privateKey } instead of the promised balances map;
replace that stray return with logic that iterates the fetched account infos
(infos) in the same order as atAs/walletAddresses, for each info decode its
token balance using the fetched decimals (use tokenAmount.uiAmount or compute
amount / 10**decimals) or default to 0 for missing/null accounts, populate
balances[walletAddress] = decodedBalance, and finally return the populated
balances object; ensure any try/catch around this block does not return
unrelated values and throws a clear error on failure.

Comment on lines +3119 to +3154
public async transferSplToken(from: Keypair, to: PublicKey, mint: PublicKey, amount: number): Promise<string> {
const serviceKeypair = await this.getWalletKeypair();

// Get token decimals
const decimals = await this.getDecimal(mint);
const adjustedAmount = BigInt(amount * 10 ** decimals);

// Get associated token addresses
const senderATA = getAssociatedTokenAddressSync(mint, from.publicKey);
const recipientATA = getAssociatedTokenAddressSync(mint, to);

const instructions: TransactionInstruction[] = [];

// Check if recipient ATA exists, create if not
const recipientATAInfo = await this.connection.getAccountInfo(recipientATA);
if (!recipientATAInfo) {
instructions.push(
createAssociatedTokenAccountInstruction(
from.publicKey,
recipientATA,
to,
mint
)
);
}

// Add transfer instruction
instructions.push(
createTransferInstruction(
senderATA,
recipientATA,
from.publicKey,
adjustedAmount
)
);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Token-2022 transfers fail without program-aware ATAs

When transferring SPL tokens you always derive ATAs and build instructions under the legacy TOKEN_PROGRAM_ID. For Token-2022 mints the associated accounts are seeded with TOKEN_2022_PROGRAM_ID, so the addresses and instructions produced here are wrong and the transaction will be rejected. Pull the mint’s owner, choose the correct program id, and pass it through to ATA derivation and instruction builders.

Apply this diff so Token-2022 mints work correctly:

-    // Get associated token addresses
-    const senderATA = getAssociatedTokenAddressSync(mint, from.publicKey);
-    const recipientATA = getAssociatedTokenAddressSync(mint, to);
+    const mintInfo = await this.connection.getAccountInfo(mint);
+    if (!mintInfo) {
+      throw new Error(`Mint ${mint.toBase58()} not found`);
+    }
+    const tokenProgramId = mintInfo.owner.equals(TOKEN_2022_PROGRAM_ID) ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
+
+    // Get associated token addresses
+    const senderATA = getAssociatedTokenAddressSync(mint, from.publicKey, false, tokenProgramId);
+    const recipientATA = getAssociatedTokenAddressSync(mint, to, false, tokenProgramId);
@@
-        createAssociatedTokenAccountInstruction(
-          from.publicKey,
-          recipientATA,
-          to,
-          mint
-        )
+        createAssociatedTokenAccountInstruction(
+          from.publicKey,
+          recipientATA,
+          to,
+          mint,
+          tokenProgramId
+        )
       );
@@
-      createTransferInstruction(
-        senderATA,
-        recipientATA,
-        from.publicKey,
-        adjustedAmount
-      )
+      createTransferInstruction(
+        senderATA,
+        recipientATA,
+        from.publicKey,
+        adjustedAmount,
+        [],
+        tokenProgramId
+      )

@odilitime
Copy link
Member Author

ok service.ts got messed up in this PR. Looking at resolving

@odilitime odilitime marked this pull request as draft November 12, 2025 02:46
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.

1 participant