Skip to content

feat: transaction action/providers/task, better CA lookup, rate limiting#27

Open
odilitime wants to merge 4 commits into1.xfrom
odi-prod
Open

feat: transaction action/providers/task, better CA lookup, rate limiting#27
odilitime wants to merge 4 commits into1.xfrom
odi-prod

Conversation

@odilitime
Copy link
Member

@odilitime odilitime commented Feb 11, 2026

Note

Medium Risk
Touches core Solana RPC/service logic and adds long-running background task workflows with caching/retry behavior, which can impact performance and correctness under real RPC rate limits despite good test coverage.

Overview
Adds transaction signature lookup via a new LOOKUP_SOLANA_SIGNATURE action and SOLANA_SIGNATURE_LOOKUP provider, extracting signatures from messages and returning parsed transaction details (status, fees, balance changes, instructions).

Introduces RPC-only historical transaction fetching in SolanaService with pagination, batching + concurrency control (p-limit), caching/checkpointing, progress callbacks, and exponential-backoff retry/circuit breaker logic; this is also exposed as a background FETCH_SOLANA_TRANSACTIONS task worker with status polling (getTransactionFetchStatus/waitForTransactionFetch) and optional task chaining.

Improves reliability by adding a token-bucket RateLimiter for RPC calls (configurable via SOLANA_RPC_RATE_LIMIT) and a getTransactionWithCostUnitsFallback to handle Solana RPC responses missing meta.costUnits. Updates CA lookup decimals to return a keyed map, expands wallet provider to query arbitrary addresses from messages, and tightens runtime setting handling/types; adds docs and test coverage for the new behaviors.

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

Summary by CodeRabbit

  • New Features

    • Lookup Solana transaction signatures and display detailed, human-readable transaction reports
    • Bulk, paginated transaction fetching with caching, progress reporting, background task execution and status polling
    • Query multiple wallet addresses and create wallets from the app
    • Built-in rate limiting to improve RPC reliability
    • Plugin banner and ASCII-art plugin header for nicer startup display
  • Documentation

    • Added comprehensive guides for transaction fetching and task-chaining patterns
  • Tests

    • New and expanded tests covering transaction fetching, fallbacks, pagination, caching, and decimals mapping

odilitime and others added 3 commits November 6, 2025 21:50
…getDecimals fix

- Add LOOKUP_SOLANA_SIGNATURE action and signatureLookupProvider for tx lookups
- Add transaction fetching system with pagination, caching, retry/backoff, and task worker
- Add RPC rate limiter (configurable via SOLANA_RPC_RATE_LIMIT) with retry on 429s
- Fix getDecimals to return Record<string, number> instead of number[] to prevent address misalignment
- Simplify ca-lookup provider to use keyed decimals directly
- Add rate-limiter utility, ascii-art banner, plugin-banner utils
- Enhance wallet provider with address detection and multi-wallet query support
- Type-safe runtime.getSetting() usage across actions and keypairUtils
- Resolve merge conflicts in build.ts, package.json, tsconfig.json
- Add p-limit dependency, update tsconfig to use bun-types

Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings February 11, 2026 00:00
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

Walkthrough

Adds a Solana transaction-fetching pipeline (paginated, cached, retrying) with rate limiting and background task workers, signature lookup action/provider, expanded wallet/balance APIs, new types, utilities, docs, tests, and build/config updates.

Changes

Cohort / File(s) Summary
Documentation
TASK_CHAINING_EXAMPLES.md, TRANSACTION_FETCHING.md
New comprehensive guides for task chaining and Solana transaction fetching, usage examples, configuration, best practices, and monitoring.
Build & Config
.gitignore, tsconfig.json, build.ts, package.json
Expanded .gitignore, added rootDir in tsconfig, simplified build flow (removed typecheck), dependency updates (added p-limit, swapped types), and minor package.json text escapes.
Types
src/types.ts
New transaction-fetching type definitions (FetchTransactionOptions, FetchProgress, CachedTransactionData, TransactionRecord, FetchTransactionResult, FetchTransactionsResult, FetchTransactionsTaskOptions).
Core Service
src/service.ts, src/rate-limiter.ts, src/types.ts
Major service expansion: rate limiter, fetchSignatures/fetchTransactions flows, batching, checkpointing, caching, backoff/retries, RPC health gating, background task worker registration, wallet creation/balances, and many new public methods and helpers.
Providers
src/providers/signature-lookup.ts, src/providers/wallet.ts, src/providers/ca-lookup.ts
Added signature lookup provider; wallet provider extended for explicit multi-address queries, agent-wallet detection, and new result shape; ca-lookup adjusted decimals to address-keyed mapping.
Actions
src/actions/lookup-signature.ts, src/actions/swap.ts, src/actions/transfer.ts
New LOOKUP_SOLANA_SIGNATURE action with base58 validation and formatted transaction output; swap/transfer guard SOLANA_RPC_URL with string-coercion fallback.
Utilities & Helpers
src/ascii-art.ts, src/keypairUtils.ts, src/utils/plugin-banner.ts
New ASCII art constant, getSettingAsString helper for safe string coercion, and plugin-banner utilities for ANSI banners and settings table.
Entry & Integration
src/index.ts
Registered new action/provider, adjusted plugin name usage to literal 'solana', improved localnet RPC handling, removed prior CA-lookup conditional registration.
Tests
__tests__/providers/ca-lookup.test.ts, __tests__/service/transactions.test.ts, __tests__/service/transactionFallback.test.ts
Updated/added tests covering decimals mapping change, extensive transaction-fetching behaviors (pagination, caching, retries, dedupe, progress), and costUnits fallback behavior.
New Files
src/actions/lookup-signature.ts, src/providers/signature-lookup.ts, src/rate-limiter.ts, src/ascii-art.ts, src/utils/plugin-banner.ts
Several new modules implementing signature lookup action/provider, token-bucket rate limiter, banner/ASCII art, and related helpers.

Sequence Diagram

sequenceDiagram
    actor User as User/Client
    participant Runtime as IAgentRuntime
    participant Service as SolanaService
    participant RateLim as RateLimiter
    participant RPC as Solana RPC
    participant Cache as Cache Layer
    participant Worker as Background Worker

    User->>Service: fetchTransactionsForAddresses(addresses, options)
    activate Service
    Service->>Cache: check cached results
    alt cache hit (and not force refresh)
        Cache-->>Service: return cached data
    else
        Service->>Service: paginate signatures per address
        loop per signature page
            Service->>RateLim: acquire token
            RateLim-->>Service: granted
            Service->>RPC: getSignaturesForAddress()
            RPC-->>Service: signatures + cursor
            Service->>Cache: store checkpoint
            Service->>Runtime: onProgress callback
        end
        Service->>Service: dedupe signatures
        Service->>Service: batch fetch transactions
        loop per batch
            Service->>RateLim: acquire token
            RateLim-->>Service: granted
            Service->>RPC: getParsedTransactions()
            alt RPC transient error
                Service->>Service: retryWithBackoff()
            end
            RPC-->>Service: parsed transactions
            Service->>Cache: store results
            Service->>Runtime: onProgress callback
        end
        Service->>Worker: enqueue onComplete tasks (optional)
        Worker-->>Service: acknowledge
    end
    Service-->>User: FetchTransactionsResult
    deactivate Service
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~80 minutes

Poem

🐰 I hopped through RPCs, nibbling rate-limit crumbs,
Batched every signature until the ledger hums,
Checkpoints tucked snug, retries soft and spry,
Background workers whisk tasks by and by,
A rabbit’s cheer — no transaction slips by!

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: transaction action/provider/task functionality, improved CA lookup, and rate limiting implementation.
Docstring Coverage ✅ Passed Docstring coverage is 91.30% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into 1.x

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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-prod

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.

@odilitime odilitime changed the title idk stuff feat: transaction action/providers/task, better CA lookup, rate limiting Feb 11, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds richer Solana runtime capabilities (transaction history fetching + lookup) and introduces new infra (rate limiting, task worker integration), along with TypeScript/build configuration updates to support the new functionality.

Changes:

  • Add RPC-based transaction history fetching (pagination, batching, caching, retries) with background task worker support.
  • Add signature lookup action/provider and expand wallet provider to query arbitrary addresses from message text.
  • Introduce a token-bucket rate limiter and update build/tsconfig/deps/tests/docs accordingly.

Reviewed changes

Copilot reviewed 19 out of 21 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
tsconfig.json Adjust TS root/types/includes/excludes for Bun/Node + build script support.
build.ts Update build flow (clean script usage, declaration generation behavior).
package.json Add p-limit, swap type dependencies to Node + bun-types.
src/service.ts Major: rate limiting, new transaction-fetching APIs + task worker, various refactors.
src/types.ts Add transaction fetching-related types.
src/rate-limiter.ts New token-bucket rate limiter used for RPC throttling.
src/providers/wallet.ts Expand provider to detect addresses / “own wallet” queries and query arbitrary wallets.
src/providers/signature-lookup.ts New provider to extract signatures and fetch tx info.
src/actions/lookup-signature.ts New action to look up tx signatures mentioned in messages.
src/providers/ca-lookup.ts Adapt to getDecimals() returning a keyed map.
src/keypairUtils.ts Normalize settings to string values via helper.
src/index.ts Register new action/provider; adjust plugin naming/initialization.
src/utils/plugin-banner.ts New banner utility (currently not wired into plugin).
src/ascii-art.ts New ASCII art constant (currently not referenced).
tests/service/transactions.test.ts New tests for transaction fetching (pagination/batching/caching/retries).
tests/providers/ca-lookup.test.ts Update tests for new decimals return shape + new mapping test.
TRANSACTION_FETCHING.md New documentation for transaction fetching and tasks usage.
TASK_CHAINING_EXAMPLES.md New task chaining examples for post-fetch workflows.
.gitignore Expand ignored build/temp/IDE/cache artifacts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 414 to 417
const rateLimitPerSecond = parseInt(
String(rateLimitSetting ?? '10'),
10
);
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

SOLANA_RPC_RATE_LIMIT parsing can produce NaN (e.g., if set to a non-numeric string), which will propagate into the token bucket math and effectively break rate limiting. After parseInt, validate with Number.isFinite and clamp to a sensible minimum (e.g., >= 1) before creating the limiter.

Suggested change
const rateLimitPerSecond = parseInt(
String(rateLimitSetting ?? '10'),
10
);
let rateLimitPerSecond = parseInt(
String(rateLimitSetting ?? '10'),
10
);
if (!Number.isFinite(rateLimitPerSecond) || rateLimitPerSecond < 1) {
rateLimitPerSecond = 10;
}

Copilot uses AI. Check for mistakes.
return await this.connection.getSignaturesForAddress(publicKey, {
limit,
before,
until: options.until,
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

fetchSignaturesForAddress computes a commitment from options but never passes it into getSignaturesForAddress, so callers can't actually control the commitment level. Pass commitment in the RPC config so FetchTransactionOptions.commitment is honored.

Suggested change
until: options.until,
until: options.until,
commitment,

Copilot uses AI. Check for mistakes.

export const solanaPlugin: Plugin = {
name: SOLANA_SERVICE_NAME,
name: 'solana',
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

solanaPlugin.name was changed to 'solana' while the service type / lookup key remains SOLANA_SERVICE_NAME = 'chain_solana' (used throughout routes/providers via runtime.getService('chain_solana')). If the runtime uses Plugin.name as the identifier for enabling/configuring plugins, this creates a breaking inconsistency. Consider keeping name: SOLANA_SERVICE_NAME (or updating the constant + all lookups/docs together).

Suggested change
name: 'solana',
name: SOLANA_SERVICE_NAME,

Copilot uses AI. Check for mistakes.
*/
export function displayPluginBanner(runtime: IAgentRuntime, config: PluginBannerConfig): void {
const border = `+${'-'.repeat(78)}+`;
const emptyLine = `|${' '.repeat(78)}|`;
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

In plugin-banner.ts, emptyLine is declared but never used. Removing unused variables keeps the banner utility easier to maintain (and avoids future noUnusedLocals issues if enabled).

Suggested change
const emptyLine = `|${' '.repeat(78)}|`;

Copilot uses AI. Check for mistakes.
Comment on lines 49 to 51
import bs58 from 'bs58';
import nacl from 'tweetnacl';
import nacl from "tweetnacl";

Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

This file mixes quote styles (import nacl from "tweetnacl";) while the rest of the file predominantly uses single quotes. Keeping a consistent quoting style avoids noisy diffs and aligns with typical formatter expectations.

Copilot uses AI. Check for mistakes.
*/
function pad(str: string, length: number, align: 'left' | 'center' | 'right' = 'left'): string {
const cleanStr = stripAnsi(str);
const ansiExtra = str.length - cleanStr.length;
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

Unused variable ansiExtra.

Suggested change
const ansiExtra = str.length - cleanStr.length;

Copilot uses AI. Check for mistakes.
// Settings table
const tableLines = generateSettingsTable(runtime, config.settings);
for (const line of tableLines) {
const cleanLine = stripAnsi(line);
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

Unused variable cleanLine.

Suggested change
const cleanLine = stripAnsi(line);

Copilot uses AI. Check for mistakes.
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const result = await fn();
consecutiveFailures = 0;
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

The value assigned to consecutiveFailures here is unused.

Suggested change
consecutiveFailures = 0;

Copilot uses AI. Check for mistakes.
);

if (signatures.length === 0) {
hasMore = false;
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

The value assigned to hasMore here is unused.

Copilot uses AI. Check for mistakes.
if (options.maxTransactions) {
const remaining = options.maxTransactions - allSignatures.length;
if (remaining <= 0) {
hasMore = false;
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

The value assigned to hasMore here is unused.

Copilot uses AI. Check for mistakes.
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: 13

🤖 Fix all issues with AI agents
In `@__tests__/service/transactions.test.ts`:
- Around line 266-300: The test is inconsistent about deduplication: update the
mocks and assertions so they match the intended behavior of
SolanaService.fetchTransactionsForAddresses — either assert deduped totals
(expect result.totalTransactions to be 9) or rename the test/comment to indicate
totalTransactions is the non-deduped aggregate and then assert accordingly; also
make the signature mock objects returned by
mockConnection.getSignaturesForAddress include full ConfirmedSignatureInfo
fields (err, memo, blockTime, confirmationStatus) and ensure
mockConnection.getParsedTransactions returns realistic parsed transactions used
by fetchTransactionsForAddresses so the service code won't access undefined
fields.

In `@build.ts`:
- Around line 37-43: The catch block around the TypeScript declaration
generation (the try invoking $`tsc --project tsconfig.build.json` and the catch
that currently only console.warns) is swallowing errors; update it to log the
actual error output (include the caught error variable) and, when running in CI
(check process.env.CI truthiness), fail the build by calling process.exit(1) so
broken/missing .d.ts files don’t get shipped, while preserving the current
non-failing behavior for local development by only warning when CI is not set;
keep the existing timing message using dtsStart but augment logs with the error
details and an explicit non-zero exit in CI.

In `@src/actions/lookup-signature.ts`:
- Around line 200-215: The catch block defines a local const named `message`
which shadows the existing handler parameter `message: Memory`; rename the local
variable to `errorMessage` (or `errMsg`) and update its usages inside the catch
(the ternary logic, the callback text template `Failed to look up transaction:
...`, and the callback `content: { error: ... }`) so the shadowing is removed
and the callback still receives the same string value.
- Around line 68-91: The current lamport arithmetic (postBalance - preBalance)
can overflow Number and lose precision; update the balance delta logic in
lookup-signature.ts (the block using tx.meta.preBalances, tx.meta.postBalances,
balanceChanges array and the change variable) to use BigInt for subtraction
(e.g., cast pre/post balances to BigInt, compute delta as BigInt) and store
change as a BigInt; then format SOL output by converting the BigInt lamport
delta into a decimal SOL string (integer division and remainder with 1e9 to
produce up to 9 fractional digits and preserve sign) instead of using a JS
Number for solChange so no precision is lost when building the info string.

In `@src/index.ts`:
- Around line 35-36: The current logic passes null into parseBooleanFromText for
non-string settings causing truthy booleans to be treated as unset; update the
handling around runtime.getSetting so parseBooleanFromText receives a proper
value: either pass the string when typeof noActionsRaw === 'string' or convert
booleans to "true"/"false" (or directly map boolean to true/false) before
calling parseBooleanFromText; modify the code referencing noActionsRaw and
noActions (and the call to parseBooleanFromText) so boolean settings are honored
rather than converted to null.

In `@src/providers/signature-lookup.ts`:
- Around line 104-108: The current handling in signature-lookup.ts for
ix.parsed.type === 'transferChecked' concatenates both tokenAmount.uiAmount and
the raw tokenAmount.amount which produces confusing output; update the block
that builds the info string for transferChecked (where transferInfo is read) to
use only the human-readable value (e.g., tokenAmount.uiAmount or uiAmountString
if available) and drop the raw integer amount, or if you need precision include
decimals via tokenAmount.decimals to format a single readable amount; ensure the
updated message remains "Token Transfer: <readableAmount> from <source> to
<destination>" and keep the same variable names (ix, transferInfo, tokenAmount)
so the change is localized.

In `@src/rate-limiter.ts`:
- Around line 86-111: The queued requests lose their requested token amount
because acquire() creates a queueEntry without the count and processQueue()
always decrements by 1; modify acquire() to include the requested count in the
queue entry (e.g., queueEntry.count = count) and update processQueue() to read
entry.count when checking availability and when deducting tokens (subtract
entry.count from available tokens) and to resolve the promise appropriately;
ensure the timeout removal/reject logic still references the same queueEntry so
queued entries with their count are cleaned up correctly.

In `@src/service.ts`:
- Around line 3865-3877: The cache is storing the full options object
(cacheData.options) which may include a non-serializable onProgress callback
from FetchTransactionOptions; update the code to strip callbacks before caching
by creating a serializable options payload (e.g., safeOptions) that omits
onProgress and any function-valued fields, use that safeOptions in cacheData
instead of the original options, and then call
this.runtime.setCache(this.getTxCacheKey(address), cacheData) with the sanitized
cacheData so serialization will not fail.
- Around line 412-418: The parsed SOLANA_RPC_RATE_LIMIT may be NaN if
runtime.getSetting returns a non-numeric string; update the initialization
around parseInt and createRateLimiterPerSecond to validate and sanitize the
value: call runtime.getSetting('SOLANA_RPC_RATE_LIMIT'), attempt to parse it
(using parseInt or Number), check isFinite/Number.isInteger and that it’s > 0,
and if validation fails fall back to a safe default (e.g., 10) before passing to
createRateLimiterPerSecond so this.rpcRateLimiter always receives a valid
positive integer (optionally clamp a maximum/minimum). Keep the checks adjacent
to the existing parse logic so runtime.getSetting, parseInt (or conversion), and
createRateLimiterPerSecond are the referenced symbols to update.
- Around line 2595-2620: Replace the two hardcoded wrapped SOL mint strings with
the canonical constant: use PROVIDER_CONFIG.TOKEN_ADDRESSES.SOL wherever the
code builds wallet balance strings (notably in walletAddressToLLMString and the
earlier balance-string assembly that prepends "Wallet Address" and token lines);
update both occurrences so the mint address and CSV row use the constant instead
of the incorrect literal, ensuring the displayed token label and address come
from PROVIDER_CONFIG.TOKEN_ADDRESSES.SOL (the code paths around
parseTokenAccounts, getBalancesByAddrs, and getTokenAccountsByKeypair will then
use the correct SOL mint).
- Around line 3636-3646: The RPC call to this.connection.getSignaturesForAddress
inside the retryWithBackoff loop is missing the commitment argument, so it uses
the connection default instead of the configured commitment; update the call in
the loop to pass the commitment variable as the third parameter (keeping the
existing options object with limit, before, until: options.until) so
getSignaturesForAddress(publicKey, { limit, before, until: options.until },
commitment) is invoked; adjust the call located in the retryWithBackoff wrapper
where getSignaturesForAddress is used (refer to
connection.getSignaturesForAddress, publicKey, commitment, options.until,
retryWithBackoff).

In `@src/utils/plugin-banner.ts`:
- Around line 94-99: truncate() currently counts bytes including ANSI escape
sequences and may cut an escape mid-sequence (used by processCustomArt), so
change truncate to operate on visible characters: strip ANSI codes (use the same
/\x1b\[[0-9;]*m/ regex) to measure visible length, and when truncation is needed
produce a result that preserves full ANSI sequences by iterating through the
original string emitting escapes unchanged while consuming only visible
characters until maxLength - suffix.length is reached, then append suffix;
update calls from processCustomArt to rely on this visible-character-aware
truncate.
- Around line 104-109: The maskValue function currently exposes 4-character
secrets because it returns value.substring(0, 4) with zero '*' when value.length
=== 4; update maskValue so it always appends at least one masking character for
lengths >= 4 (e.g., change the '*' repeat call to use Math.max(1,
Math.min(value.length - 4, 8)) or equivalent) so substring(0, 4) + '*'... never
yields the unmasked secret.
🧹 Nitpick comments (11)
src/actions/transfer.ts (1)

193-196: Consistent with swap.ts — consider a shared helper.

Same RPC URL resolution pattern duplicated here. This works correctly, but if more actions adopt it, a small utility (e.g., getSolanaConnection(runtime)) would reduce duplication and centralize the default URL.

src/utils/plugin-banner.ts (1)

239-243: cleanLine is computed but never used.

Line 240 computes cleanLine via stripAnsi but only paddedLine is referenced afterward.

Suggested fix
     for (const line of tableLines) {
-        const cleanLine = stripAnsi(line);
         const paddedLine = pad(line, 78);
         output.push(`${ANSI.gray}|${ANSI.reset}${paddedLine}${ANSI.gray}|${ANSI.reset}`);
     }
src/types.ts (1)

249-249: Consider narrowing err type from any.

TransactionRecord.err is typed as any. The Solana SDK uses TransactionError | null for transaction errors. A narrower type would improve downstream type safety.

Suggested type narrowing
-  err?: any;
+  err?: Record<string, any> | null;

Or import and use the SDK's TransactionError type if available.

TASK_CHAINING_EXAMPLES.md (1)

1-374: Documentation looks comprehensive and well-structured.

The examples cover a good range of patterns for task chaining workflows. One minor note: in Pattern 6 (line 269), error.message is accessed without a type guard in the catch block, which would fail in strict TypeScript. Since these are illustrative examples, consider adding a type annotation or guard (e.g., error instanceof Error ? error.message : String(error)) to keep the examples copy-pasteable.

src/actions/lookup-signature.ts (1)

128-216: Handler mixes runtime.logger (line 162) and imported logger (line 201).

Minor inconsistency — pick one for uniformity within the same function.

src/rate-limiter.ts (1)

59-68: tryAcquire is marked async but contains no await.

This wraps the return value in an unnecessary Promise. Consider making it synchronous or at minimum noting that the async is intentional for interface consistency.

src/providers/wallet.ts (1)

39-49: Broad regex may match non-address base58 strings, but PublicKey validation mitigates this.

The regex on line 40 (/\b[1-9A-HJ-NP-Za-km-z]{32,44}\b/g) will match any 32–44 char base58 string in the message, potentially including fragments of transaction signatures or other data. The PublicKey constructor validation on line 96 is a good safety net. Also, the toLowerCase() call on line 72 is redundant since ownWalletKeywords values are already lowercase.

src/service.ts (4)

3621-3708: Signature fetching doesn't go through the RPC rate limiter.

fetchSignaturesForAddress calls getSignaturesForAddress via retryWithBackoff, but unlike batchGetMultipleAccountsInfo, it doesn't call this.rpcRateLimiter.acquire() before each RPC call. For wallets with many pages of signatures, this could overwhelm the RPC endpoint. Consider adding rate limiter acquisition inside the retryWithBackoff callback.

Proposed fix
       let signatures = await this.retryWithBackoff(
         async () => {
+          await this.rpcRateLimiter.acquire(1, 30000);
           return await this.connection.getSignaturesForAddress(publicKey, {
             limit,
             before,
             until: options.until,
           });
         },
         `getSignaturesForAddress(${address}, page ${pageCount + 1})`
       );

3716-3781: Same missing rate limiter for getParsedTransactions + static analysis note.

Similar to signature fetching, fetchTransactionsForSignatures doesn't acquire rate limiter tokens before getParsedTransactions calls. Also, the commitment parameter (line 3718) is unused — consider either using it or removing it.

Regarding the static analysis hint on line 3774: batchResults.forEach((batch) => transactions.push(...batch)) — the push() return value is implicitly returned from the arrow. Use a block body or for...of to satisfy the linter.

Proposed fix for rate limiting and linter
     const batchPromises = batches.map((batch, batchIndex) =>
       limit(async () => {
         const batchResults = await this.retryWithBackoff(
           async () => {
+            await this.rpcRateLimiter.acquire(1, 30000);
             return await this.connection.getParsedTransactions(batch, {
               maxSupportedTransactionVersion: 0,
             });
           },
-    batchResults.forEach((batch) => transactions.push(...batch));
+    for (const batch of batchResults) {
+      transactions.push(...batch);
+    }

3835-3841: Health check failure is not a gate — execution proceeds regardless.

If checkRpcHealth() fails, the code waits 5 seconds and then continues anyway. This essentially makes the health check a "soft delay" rather than a meaningful guard. Consider retrying the health check or returning an error if the RPC is confirmed unhealthy.


2411-2431: secretKey.fill(0) provides limited security benefit.

Line 2421 zeroes the Uint8Array after bs58.encode has already created the base58 string in privateKey. The string remains in memory until GC. The fill(0) is a reasonable best-effort, but don't rely on it as a security guarantee — document this limitation or add a note.

Comment on lines +266 to +300
it('should handle multi-wallet fetching with deduplication', async () => {
const service = new SolanaService(mockRuntime);
(service as any).connection = mockConnection;

const address1 = Keypair.generate().publicKey.toBase58();
const address2 = Keypair.generate().publicKey.toBase58();

// Both wallets share some transactions
const sharedSig = 'shared-sig'.padEnd(88, '0');
const sigs1 = [sharedSig, ...generateMockSignatures(5).map((s) => s.signature)];
const sigs2 = [sharedSig, ...generateMockSignatures(3).map((s) => s.signature)];

mockConnection.getSignaturesForAddress = mock(async (pubkey: PublicKey) => {
const addr = pubkey.toBase58();
if (addr === address1) {
return sigs1.map((sig) => ({ signature: sig, slot: 1000 }));
} else if (addr === address2) {
return sigs2.map((sig) => ({ signature: sig, slot: 1000 }));
}
return [];
});

mockConnection.getParsedTransactions = mock(async (sigs: string[]) =>
sigs.map((sig) => generateMockTransaction(sig))
);

const result = await service.fetchTransactionsForAddresses([address1, address2]);

// Wallet 1 has 6 sigs, Wallet 2 has 4 sigs = 10 total transactions
// Unique signatures: 6 from wallet1 + 4 from wallet2 - 1 shared = 9 unique, but the Set tracks all 10 since we add per-wallet
// The deduplication Set counts unique signatures across all results
expect(result.totalTransactions).toBe(10);
expect(result.totalSignatures).toBeGreaterThanOrEqual(6); // At least 6 unique (from wallet 1)
expect(Object.keys(result.results)).toHaveLength(2);
});
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 | 🟡 Minor

Multi-wallet deduplication test comment contradicts itself — verify expected behavior.

The comment on lines 294–296 acknowledges that the shared signature should yield 9 unique entries but then asserts totalTransactions is 10, stating "the Set tracks all 10 since we add per-wallet." If the service is supposed to deduplicate across wallets, the expected value should be 9. If totalTransactions is intentionally the sum of per-wallet counts (not deduped), the test name and comment should clarify that.

Also, the mock return values on lines 281/283 ({ signature: sig, slot: 1000 }) are missing ConfirmedSignatureInfo fields like err, memo, blockTime, and confirmationStatus, which could cause issues if the service accesses them.

Suggested clarification for the assertion

Either fix the expected value to reflect true deduplication:

-        // Wallet 1 has 6 sigs, Wallet 2 has 4 sigs = 10 total transactions
-        // Unique signatures: 6 from wallet1 + 4 from wallet2 - 1 shared = 9 unique, but the Set tracks all 10 since we add per-wallet
-        // The deduplication Set counts unique signatures across all results
-        expect(result.totalTransactions).toBe(10);
+        // Wallet 1 has 6 sigs, Wallet 2 has 4 sigs, 1 shared = 9 unique
+        expect(result.totalTransactions).toBe(9);

Or rename/reword to clarify that totalTransactions is the aggregate (non-deduped) count and that deduplication is reflected elsewhere (e.g., in totalSignatures).

🤖 Prompt for AI Agents
In `@__tests__/service/transactions.test.ts` around lines 266 - 300, The test is
inconsistent about deduplication: update the mocks and assertions so they match
the intended behavior of SolanaService.fetchTransactionsForAddresses — either
assert deduped totals (expect result.totalTransactions to be 9) or rename the
test/comment to indicate totalTransactions is the non-deduped aggregate and then
assert accordingly; also make the signature mock objects returned by
mockConnection.getSignaturesForAddress include full ConfirmedSignatureInfo
fields (err, memo, blockTime, confirmationStatus) and ensure
mockConnection.getParsedTransactions returns realistic parsed transactions used
by fetchTransactionsForAddresses so the service code won't access undefined
fields.

Comment on lines +37 to +43
try {
await $`tsc --project tsconfig.build.json`;
console.log(`✅ Declarations generated in ${((Date.now() - dtsStart) / 1000).toFixed(2)}s`);
} catch (error) {
console.warn(`⚠️ TypeScript declaration generation had errors (${((Date.now() - dtsStart) / 1000).toFixed(2)}s)`);
console.warn(" Build will continue - fix type errors when possible");
}
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

Swallowing declaration-generation errors risks shipping broken types.

If tsc fails, the dist/ directory may contain partial or missing .d.ts files. Any downstream consumer of this package (e.g., import type { SolanaService }) would get confusing errors at compile time.

Consider at minimum logging the actual error output, and failing the build in CI (e.g., check an env var like CI) while allowing local dev to continue:

Proposed improvement
   try {
     await $`tsc --project tsconfig.build.json`;
     console.log(`✅ Declarations generated in ${((Date.now() - dtsStart) / 1000).toFixed(2)}s`);
   } catch (error) {
-    console.warn(`⚠️  TypeScript declaration generation had errors (${((Date.now() - dtsStart) / 1000).toFixed(2)}s)`);
-    console.warn("   Build will continue - fix type errors when possible");
+    console.warn(`⚠️  TypeScript declaration generation had errors (${((Date.now() - dtsStart) / 1000).toFixed(2)}s)`);
+    if (process.env.CI) {
+      throw new Error("Declaration generation failed in CI — fix type errors before merging");
+    }
+    console.warn("   Build will continue locally — fix type errors before publishing");
   }
📝 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
try {
await $`tsc --project tsconfig.build.json`;
console.log(`✅ Declarations generated in ${((Date.now() - dtsStart) / 1000).toFixed(2)}s`);
} catch (error) {
console.warn(`⚠️ TypeScript declaration generation had errors (${((Date.now() - dtsStart) / 1000).toFixed(2)}s)`);
console.warn(" Build will continue - fix type errors when possible");
}
try {
await $`tsc --project tsconfig.build.json`;
console.log(`✅ Declarations generated in ${((Date.now() - dtsStart) / 1000).toFixed(2)}s`);
} catch (error) {
console.warn(`⚠️ TypeScript declaration generation had errors (${((Date.now() - dtsStart) / 1000).toFixed(2)}s)`);
if (process.env.CI) {
throw new Error("Declaration generation failed in CI — fix type errors before merging");
}
console.warn(" Build will continue locally — fix type errors before publishing");
}
🤖 Prompt for AI Agents
In `@build.ts` around lines 37 - 43, The catch block around the TypeScript
declaration generation (the try invoking $`tsc --project tsconfig.build.json`
and the catch that currently only console.warns) is swallowing errors; update it
to log the actual error output (include the caught error variable) and, when
running in CI (check process.env.CI truthiness), fail the build by calling
process.exit(1) so broken/missing .d.ts files don’t get shipped, while
preserving the current non-failing behavior for local development by only
warning when CI is not set; keep the existing timing message using dtsStart but
augment logs with the error details and an explicit non-zero exit in CI.

Comment on lines +68 to +91
if (tx.meta.preBalances && tx.meta.postBalances) {
const balanceChanges: Array<{ account: string; change: number }> = [];
if (tx.transaction && tx.transaction.message && tx.transaction.message.accountKeys) {
const accountKeys = tx.transaction.message.accountKeys;
for (let i = 0; i < Math.min(accountKeys.length, tx.meta.preBalances.length); i++) {
const preBalance = tx.meta.preBalances[i];
const postBalance = tx.meta.postBalances[i];
const change = postBalance - preBalance;
if (change !== 0) {
const account = accountKeys[i];
const accountStr = typeof account === 'string' ? account : account.pubkey?.toString() || 'Unknown';
balanceChanges.push({ account: accountStr, change });
}
}
}

if (balanceChanges.length > 0) {
info += `\nBalance Changes:\n`;
balanceChanges.forEach(({ account, change }) => {
const solChange = change / 1e9;
info += ` ${account}: ${solChange >= 0 ? '+' : ''}${solChange} SOL\n`;
});
}
}
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 | 🟡 Minor

Potential precision loss with large lamport values.

On line 75, postBalance - preBalance operates on raw lamport values. Solana lamport values can exceed Number.MAX_SAFE_INTEGER (~9×10¹⁵) for very large balances, causing silent precision loss. Consider using BigInt or BigNumber for the arithmetic.

Proposed fix
-      if (tx.meta.preBalances && tx.meta.postBalances) {
-        const balanceChanges: Array<{ account: string; change: number }> = [];
+      if (tx.meta.preBalances && tx.meta.postBalances) {
+        const balanceChanges: Array<{ account: string; change: bigint }> = [];
         if (tx.transaction && tx.transaction.message && tx.transaction.message.accountKeys) {
           const accountKeys = tx.transaction.message.accountKeys;
           for (let i = 0; i < Math.min(accountKeys.length, tx.meta.preBalances.length); i++) {
-            const preBalance = tx.meta.preBalances[i];
-            const postBalance = tx.meta.postBalances[i];
-            const change = postBalance - preBalance;
-            if (change !== 0) {
+            const preBalance = BigInt(tx.meta.preBalances[i]);
+            const postBalance = BigInt(tx.meta.postBalances[i]);
+            const change = postBalance - preBalance;
+            if (change !== 0n) {
               const account = accountKeys[i];
               const accountStr = typeof account === 'string' ? account : account.pubkey?.toString() || 'Unknown';
               balanceChanges.push({ account: accountStr, change });

And update the formatting:

         balanceChanges.forEach(({ account, change }) => {
-          const solChange = change / 1e9;
-          info += `  ${account}: ${solChange >= 0 ? '+' : ''}${solChange} SOL\n`;
+          const solChange = Number(change) / 1e9;
+          info += `  ${account}: ${solChange >= 0 ? '+' : ''}${solChange} SOL\n`;
         });
📝 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
if (tx.meta.preBalances && tx.meta.postBalances) {
const balanceChanges: Array<{ account: string; change: number }> = [];
if (tx.transaction && tx.transaction.message && tx.transaction.message.accountKeys) {
const accountKeys = tx.transaction.message.accountKeys;
for (let i = 0; i < Math.min(accountKeys.length, tx.meta.preBalances.length); i++) {
const preBalance = tx.meta.preBalances[i];
const postBalance = tx.meta.postBalances[i];
const change = postBalance - preBalance;
if (change !== 0) {
const account = accountKeys[i];
const accountStr = typeof account === 'string' ? account : account.pubkey?.toString() || 'Unknown';
balanceChanges.push({ account: accountStr, change });
}
}
}
if (balanceChanges.length > 0) {
info += `\nBalance Changes:\n`;
balanceChanges.forEach(({ account, change }) => {
const solChange = change / 1e9;
info += ` ${account}: ${solChange >= 0 ? '+' : ''}${solChange} SOL\n`;
});
}
}
if (tx.meta.preBalances && tx.meta.postBalances) {
const balanceChanges: Array<{ account: string; change: bigint }> = [];
if (tx.transaction && tx.transaction.message && tx.transaction.message.accountKeys) {
const accountKeys = tx.transaction.message.accountKeys;
for (let i = 0; i < Math.min(accountKeys.length, tx.meta.preBalances.length); i++) {
const preBalance = BigInt(tx.meta.preBalances[i]);
const postBalance = BigInt(tx.meta.postBalances[i]);
const change = postBalance - preBalance;
if (change !== 0n) {
const account = accountKeys[i];
const accountStr = typeof account === 'string' ? account : account.pubkey?.toString() || 'Unknown';
balanceChanges.push({ account: accountStr, change });
}
}
}
if (balanceChanges.length > 0) {
info += `\nBalance Changes:\n`;
balanceChanges.forEach(({ account, change }) => {
const solChange = Number(change) / 1e9;
info += ` ${account}: ${solChange >= 0 ? '+' : ''}${solChange} SOL\n`;
});
}
}
🤖 Prompt for AI Agents
In `@src/actions/lookup-signature.ts` around lines 68 - 91, The current lamport
arithmetic (postBalance - preBalance) can overflow Number and lose precision;
update the balance delta logic in lookup-signature.ts (the block using
tx.meta.preBalances, tx.meta.postBalances, balanceChanges array and the change
variable) to use BigInt for subtraction (e.g., cast pre/post balances to BigInt,
compute delta as BigInt) and store change as a BigInt; then format SOL output by
converting the BigInt lamport delta into a decimal SOL string (integer division
and remainder with 1e9 to produce up to 9 fractional digits and preserve sign)
instead of using a JS Number for solChange so no precision is lost when building
the info string.

Comment on lines +200 to +215
} catch (error) {
logger.error({ error }, 'Error looking up Solana signature');
if (callback) {
const message =
error instanceof Error
? error.message
: typeof error === 'string'
? error
: JSON.stringify(error);
callback({
text: `Failed to look up transaction: ${message}`,
content: { error: message },
});
}
return;
}
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 | 🟡 Minor

Variable message shadows the handler parameter.

On line 203, const message shadows the message: Memory parameter from line 130. While it doesn't cause a bug here, it's confusing and error-prone. Rename to errorMessage or errMsg.

Proposed fix
-        const message =
+        const errorMessage =
           error instanceof Error
             ? error.message
             : typeof error === 'string'
               ? error
               : JSON.stringify(error);
         callback({
-          text: `Failed to look up transaction: ${message}`,
-          content: { error: message },
+          text: `Failed to look up transaction: ${errorMessage}`,
+          content: { error: errorMessage },
         });
📝 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
} catch (error) {
logger.error({ error }, 'Error looking up Solana signature');
if (callback) {
const message =
error instanceof Error
? error.message
: typeof error === 'string'
? error
: JSON.stringify(error);
callback({
text: `Failed to look up transaction: ${message}`,
content: { error: message },
});
}
return;
}
} catch (error) {
logger.error({ error }, 'Error looking up Solana signature');
if (callback) {
const errorMessage =
error instanceof Error
? error.message
: typeof error === 'string'
? error
: JSON.stringify(error);
callback({
text: `Failed to look up transaction: ${errorMessage}`,
content: { error: errorMessage },
});
}
return;
}
🤖 Prompt for AI Agents
In `@src/actions/lookup-signature.ts` around lines 200 - 215, The catch block
defines a local const named `message` which shadows the existing handler
parameter `message: Memory`; rename the local variable to `errorMessage` (or
`errMsg`) and update its usages inside the catch (the ternary logic, the
callback text template `Failed to look up transaction: ...`, and the callback
`content: { error: ... }`) so the shadowing is removed and the callback still
receives the same string value.

Comment on lines +35 to +36
const noActionsRaw = runtime.getSetting("SOLANA_NO_ACTIONS");
const noActions = parseBooleanFromText(typeof noActionsRaw === 'string' ? noActionsRaw : null);
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 | 🟡 Minor

parseBooleanFromText receives null for non-string settings — verify intent.

If SOLANA_NO_ACTIONS is set to a non-string truthy value (e.g., true as a boolean), it will be converted to null before parsing, causing parseBooleanFromText to return null. Since !null === true, actions will be registered despite the setting being truthy. This is likely fine given getSetting typically returns strings, but worth confirming the behavior is intentional.

🤖 Prompt for AI Agents
In `@src/index.ts` around lines 35 - 36, The current logic passes null into
parseBooleanFromText for non-string settings causing truthy booleans to be
treated as unset; update the handling around runtime.getSetting so
parseBooleanFromText receives a proper value: either pass the string when typeof
noActionsRaw === 'string' or convert booleans to "true"/"false" (or directly map
boolean to true/false) before calling parseBooleanFromText; modify the code
referencing noActionsRaw and noActions (and the call to parseBooleanFromText) so
boolean settings are honored rather than converted to null.

Comment on lines +2595 to +2620
balanceStr += 'Wallet Address: ' + pubKey + '\n'
balanceStr += ' Token Address (Symbol)\n'
balanceStr += ' So11111111111111111111111111111111111111111 ($sol) balance: ' + (solBal ?? 'unknown') + '\n'
const tokens = await this.parseTokenAccounts(heldTokens) // options
for (const ca in tokens) {
const t = tokens[ca]
balanceStr += ' ' + ca + ' ($' + t.symbol + ') balance: ' + t.balanceUi + '\n'
}
balanceStr += '\n'
return balanceStr
}

async walletAddressToLLMString(pubKey: string): Promise<string> {
let balanceStr = ''
// get wallet contents
const pubKeyObj = new PublicKey(pubKey)
const [balances, heldTokens] = await Promise.all([
this.getBalancesByAddrs([pubKey]),
this.getTokenAccountsByKeypair(pubKeyObj),
]);
//console.log('balances', balances)
const solBal = balances[pubKey]
balanceStr += 'Wallet Address: ' + pubKey + '\n'
balanceStr += 'Current wallet contents in csv format:\n'
balanceStr += 'Token Address,Symbol,Balance\n'
balanceStr += 'So11111111111111111111111111111111111111111,sol,' + (solBal ?? 'unknown') + '\n'
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

Incorrect wrapped SOL mint address — will display wrong token label.

Lines 2597 and 2620 use So11111111111111111111111111111111111111111 (43 chars, ending in 1), but the canonical wrapped SOL mint address is So11111111111111111111111111111111111111112 (44 chars, ending in 2) — as correctly defined in PROVIDER_CONFIG.TOKEN_ADDRESSES.SOL on line 58. Use the constant instead of a hardcoded (incorrect) string.

Proposed fix
-    balanceStr += '  So11111111111111111111111111111111111111111 ($sol) balance: ' + (solBal ?? 'unknown') + '\n'
+    balanceStr += '  ' + PROVIDER_CONFIG.TOKEN_ADDRESSES.SOL + ' ($sol) balance: ' + (solBal ?? 'unknown') + '\n'
-    balanceStr += 'So11111111111111111111111111111111111111111,sol,' + (solBal ?? 'unknown') + '\n'
+    balanceStr += PROVIDER_CONFIG.TOKEN_ADDRESSES.SOL + ',sol,' + (solBal ?? 'unknown') + '\n'
📝 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
balanceStr += 'Wallet Address: ' + pubKey + '\n'
balanceStr += ' Token Address (Symbol)\n'
balanceStr += ' So11111111111111111111111111111111111111111 ($sol) balance: ' + (solBal ?? 'unknown') + '\n'
const tokens = await this.parseTokenAccounts(heldTokens) // options
for (const ca in tokens) {
const t = tokens[ca]
balanceStr += ' ' + ca + ' ($' + t.symbol + ') balance: ' + t.balanceUi + '\n'
}
balanceStr += '\n'
return balanceStr
}
async walletAddressToLLMString(pubKey: string): Promise<string> {
let balanceStr = ''
// get wallet contents
const pubKeyObj = new PublicKey(pubKey)
const [balances, heldTokens] = await Promise.all([
this.getBalancesByAddrs([pubKey]),
this.getTokenAccountsByKeypair(pubKeyObj),
]);
//console.log('balances', balances)
const solBal = balances[pubKey]
balanceStr += 'Wallet Address: ' + pubKey + '\n'
balanceStr += 'Current wallet contents in csv format:\n'
balanceStr += 'Token Address,Symbol,Balance\n'
balanceStr += 'So11111111111111111111111111111111111111111,sol,' + (solBal ?? 'unknown') + '\n'
balanceStr += 'Wallet Address: ' + pubKey + '\n'
balanceStr += ' Token Address (Symbol)\n'
balanceStr += ' ' + PROVIDER_CONFIG.TOKEN_ADDRESSES.SOL + ' ($sol) balance: ' + (solBal ?? 'unknown') + '\n'
const tokens = await this.parseTokenAccounts(heldTokens) // options
for (const ca in tokens) {
const t = tokens[ca]
balanceStr += ' ' + ca + ' ($' + t.symbol + ') balance: ' + t.balanceUi + '\n'
}
balanceStr += '\n'
return balanceStr
}
async walletAddressToLLMString(pubKey: string): Promise<string> {
let balanceStr = ''
// get wallet contents
const pubKeyObj = new PublicKey(pubKey)
const [balances, heldTokens] = await Promise.all([
this.getBalancesByAddrs([pubKey]),
this.getTokenAccountsByKeypair(pubKeyObj),
]);
//console.log('balances', balances)
const solBal = balances[pubKey]
balanceStr += 'Wallet Address: ' + pubKey + '\n'
balanceStr += 'Current wallet contents in csv format:\n'
balanceStr += 'Token Address,Symbol,Balance\n'
balanceStr += PROVIDER_CONFIG.TOKEN_ADDRESSES.SOL + ',sol,' + (solBal ?? 'unknown') + '\n'
🤖 Prompt for AI Agents
In `@src/service.ts` around lines 2595 - 2620, Replace the two hardcoded wrapped
SOL mint strings with the canonical constant: use
PROVIDER_CONFIG.TOKEN_ADDRESSES.SOL wherever the code builds wallet balance
strings (notably in walletAddressToLLMString and the earlier balance-string
assembly that prepends "Wallet Address" and token lines); update both
occurrences so the mint address and CSV row use the constant instead of the
incorrect literal, ensuring the displayed token label and address come from
PROVIDER_CONFIG.TOKEN_ADDRESSES.SOL (the code paths around parseTokenAccounts,
getBalancesByAddrs, and getTokenAccountsByKeypair will then use the correct SOL
mint).

Comment on lines +3636 to +3646
while (hasMore) {
let signatures = await this.retryWithBackoff(
async () => {
return await this.connection.getSignaturesForAddress(publicKey, {
limit,
before,
until: options.until,
});
},
`getSignaturesForAddress(${address}, page ${pageCount + 1})`
);
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

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the code context around line 3628-3650
cat -n src/service.ts | sed -n '3620,3660p'

Repository: elizaos-plugins/plugin-solana

Length of output: 1659


🏁 Script executed:

# Also search for how getSignaturesForAddress is used elsewhere in the codebase
rg "getSignaturesForAddress" -B 5 -A 5 --type ts

Repository: elizaos-plugins/plugin-solana

Length of output: 11823


🌐 Web query:

@solana/web3.js getSignaturesForAddress method signature commitment parameter

💡 Result:

In @solana/web3.js, the method is:

connection.getSignaturesForAddress(
  address: PublicKey,
  options?: SignaturesForAddressOptions,
  commitment?: Finality,
): Promise<ConfirmedSignatureInfo[]>

[1]

  • options does not include commitment; it only supports before, until, limit, and minContextSlot. [2]
  • commitment is the 3rd argument (Finality). [1]

Example:

const sigs = await connection.getSignaturesForAddress(
  address,
  { limit: 100, before },
  'finalized',
);

Note: the underlying JSON-RPC method takes commitment inside its config object, but that’s at the RPC layer (not the web3.js options type). [3]

Sources:
[1] Connection.getSignaturesForAddress typedoc signature (solana-foundation.github.io)
[2] SignaturesForAddressOptions fields (solana-foundation.github.io)
[3] Solana JSON-RPC getSignaturesForAddress params (config includes commitment) (solana.com)

Citations:


Add commitment as the third argument to getSignaturesForAddress call.

The commitment variable is set on line 3628 but never passed to the RPC call on line 3639. According to the @solana/web3.js library, getSignaturesForAddress accepts commitment as the third argument (separate from the options object), not as part of the options. Signatures will currently be fetched at the connection's default commitment level instead of the configured one.

Fix location (lines 3639-3643)

Change:

return await this.connection.getSignaturesForAddress(publicKey, {
  limit,
  before,
  until: options.until,
});

To:

return await this.connection.getSignaturesForAddress(publicKey, {
  limit,
  before,
  until: options.until,
}, commitment);
🤖 Prompt for AI Agents
In `@src/service.ts` around lines 3636 - 3646, The RPC call to
this.connection.getSignaturesForAddress inside the retryWithBackoff loop is
missing the commitment argument, so it uses the connection default instead of
the configured commitment; update the call in the loop to pass the commitment
variable as the third parameter (keeping the existing options object with limit,
before, until: options.until) so getSignaturesForAddress(publicKey, { limit,
before, until: options.until }, commitment) is invoked; adjust the call located
in the retryWithBackoff wrapper where getSignaturesForAddress is used (refer to
connection.getSignaturesForAddress, publicKey, commitment, options.until,
retryWithBackoff).

Comment on lines +3865 to +3877
// Cache result
const cacheTtl = options.cacheTtl || TX_FETCH_CONFIG.DEFAULT_CACHE_TTL;
const cacheData: CachedTransactionData = {
address,
signatures,
transactions,
lastSignature: signatures[signatures.length - 1],
fetchedAt: Date.now(),
ttl: cacheTtl,
options,
};

await this.runtime.setCache(this.getTxCacheKey(address), cacheData);
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 | 🟡 Minor

Caching options object that may contain non-serializable onProgress callback.

Line 3874 stores the original options in cacheData, but options can contain an onProgress function (per the FetchTransactionOptions type). Depending on the cache implementation, this will either throw a serialization error, silently drop the function, or store null. Consider stripping callbacks before caching.

Proposed fix
+      // Strip non-serializable fields before caching
+      const { onProgress, ...serializableOptions } = options;
       const cacheData: CachedTransactionData = {
         address,
         signatures,
         transactions,
         lastSignature: signatures[signatures.length - 1],
         fetchedAt: Date.now(),
         ttl: cacheTtl,
-        options,
+        options: serializableOptions,
       };
🤖 Prompt for AI Agents
In `@src/service.ts` around lines 3865 - 3877, The cache is storing the full
options object (cacheData.options) which may include a non-serializable
onProgress callback from FetchTransactionOptions; update the code to strip
callbacks before caching by creating a serializable options payload (e.g.,
safeOptions) that omits onProgress and any function-valued fields, use that
safeOptions in cacheData instead of the original options, and then call
this.runtime.setCache(this.getTxCacheKey(address), cacheData) with the sanitized
cacheData so serialization will not fail.

Comment on lines +94 to +99
function truncate(str: string, maxLength: number, suffix: string = '...'): string {
if (str.length <= maxLength) {
return str;
}
return str.substring(0, maxLength - suffix.length) + suffix;
}
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 | 🟡 Minor

truncate() can break ANSI escape sequences mid-stream.

When called on strings containing ANSI codes (e.g., in processCustomArt at line 145), substring may cut in the middle of an escape sequence like \x1b[35m, producing garbled terminal output. Consider stripping ANSI before measuring length, or truncating based on visible character count.

🤖 Prompt for AI Agents
In `@src/utils/plugin-banner.ts` around lines 94 - 99, truncate() currently counts
bytes including ANSI escape sequences and may cut an escape mid-sequence (used
by processCustomArt), so change truncate to operate on visible characters: strip
ANSI codes (use the same /\x1b\[[0-9;]*m/ regex) to measure visible length, and
when truncation is needed produce a result that preserves full ANSI sequences by
iterating through the original string emitting escapes unchanged while consuming
only visible characters until maxLength - suffix.length is reached, then append
suffix; update calls from processCustomArt to rely on this
visible-character-aware truncate.

Comment on lines +104 to +109
function maskValue(value: string): string {
if (!value || value.length < 4) {
return '***';
}
return value.substring(0, 4) + '*'.repeat(Math.min(value.length - 4, 8));
}
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 | 🟡 Minor

4-character secrets are fully exposed by maskValue.

When value.length === 4, the function returns value.substring(0, 4) + zero * repeats, i.e., the entire value unmasked.

Suggested fix
 function maskValue(value: string): string {
-    if (!value || value.length < 4) {
+    if (!value || value.length <= 8) {
         return '***';
     }
     return value.substring(0, 4) + '*'.repeat(Math.min(value.length - 4, 8));
 }
📝 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
function maskValue(value: string): string {
if (!value || value.length < 4) {
return '***';
}
return value.substring(0, 4) + '*'.repeat(Math.min(value.length - 4, 8));
}
function maskValue(value: string): string {
if (!value || value.length <= 8) {
return '***';
}
return value.substring(0, 4) + '*'.repeat(Math.min(value.length - 4, 8));
}
🤖 Prompt for AI Agents
In `@src/utils/plugin-banner.ts` around lines 104 - 109, The maskValue function
currently exposes 4-character secrets because it returns value.substring(0, 4)
with zero '*' when value.length === 4; update maskValue so it always appends at
least one masking character for lengths >= 4 (e.g., change the '*' repeat call
to use Math.max(1, Math.min(value.length - 4, 8)) or equivalent) so substring(0,
4) + '*'... never yields the unmasked secret.

resolve: () => void;
reject: (error: Error) => void;
timestamp: number;
}> = [];
Copy link

Choose a reason for hiding this comment

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

Rate limiter queue ignores requested token count

Medium Severity

The acquire(count) method accepts a count parameter, but when the request is queued, the count is not stored in the queue entry. When processQueue later resolves queued entries, it always deducts exactly 1 token (this.tokens -= 1) regardless of the originally requested count. This means acquire(5) would only consume 1 token when dequeued, violating the rate limiting contract.

Additional Locations (1)

Fix in Cursor Fix in Web

async () => {
return await this.connection.getParsedTransactions(batch, {
maxSupportedTransactionVersion: 0,
});
Copy link

Choose a reason for hiding this comment

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

Transaction fetch ignores commitment level parameter

Medium Severity

fetchTransactionsForSignatures accepts a commitment parameter but never passes it to getParsedTransactions. The caller in getTransactionsForAddress explicitly passes options.commitment || TX_FETCH_CONFIG.TRANSACTION_COMMITMENT (defaulting to 'finalized'), but it's silently ignored. This means the Connection's default commitment is always used, which may differ from what was requested.

Fix in Cursor Fix in Web

}

return Array.from(results);
}
Copy link

Choose a reason for hiding this comment

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

Duplicated signature extraction function across two files

Low Severity

extractSignaturesFromText is identically implemented in both src/actions/lookup-signature.ts and src/providers/signature-lookup.ts. Similarly, formatTransactionInfo appears in both files with slight variations. Extracting these into a shared utility would reduce maintenance burden and avoid the risk of divergent bug fixes.

Additional Locations (1)

Fix in Cursor Fix in Web


// Output banner
runtime.logger.info('\n' + output.join('\n'));
}
Copy link

Choose a reason for hiding this comment

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

Unused exported functions and modules never imported

Low Severity

Several new exported symbols are never imported anywhere in the codebase: displayPluginBanner and createSetting from plugin-banner.ts, SOLANA_ART from ascii-art.ts, and createRateLimiterPerMinute from rate-limiter.ts. These are dead code that adds to bundle size and maintenance burden.

Additional Locations (2)

Fix in Cursor Fix in Web

"types": [
"bun"
"bun-types",
"node"
Copy link

Choose a reason for hiding this comment

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

tsconfig changes contradict existing type safety tests

Medium Severity

The types array changed from ["bun"] to ["bun-types", "node"], which directly contradicts assertions in __tests__/tsconfig.test.ts. That test expects types to contain 'bun' (not 'bun-types') and explicitly asserts types must NOT contain 'node' because Node types conflict with Bun types. These tests will now fail, and the build script change that swallows TypeScript errors masks this breakage.

Fix in Cursor Fix in Web

before,
until: options.until,
});
},
Copy link

Choose a reason for hiding this comment

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

Signature fetching also ignores computed commitment parameter

Medium Severity

In fetchSignaturesForAddress, the commitment variable is computed on line 3628 from options (defaulting to TX_FETCH_CONFIG.SIGNATURE_COMMITMENT = 'confirmed') but is never passed to getSignaturesForAddress. That RPC call defaults to 'finalized', meaning recently confirmed-but-not-finalized transactions will be silently excluded when the caller expected 'confirmed' level results.

Fix in Cursor Fix in Web

} catch (error) {
console.warn(`⚠️ TypeScript declaration generation had errors (${((Date.now() - dtsStart) / 1000).toFixed(2)}s)`);
console.warn(" Build will continue - fix type errors when possible");
}
Copy link

Choose a reason for hiding this comment

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

Build script swallows TypeScript errors silently

Medium Severity

The build script now wraps tsc in a try/catch and continues on failure, and the pre-build typecheck step (typecheck()) has been removed entirely. TypeScript type errors no longer fail the build, meaning broken types and logic errors that the type system would catch can silently ship to production.

Fix in Cursor Fix in Web

…lback

- Resolve stash merge conflicts in package.json, src/index.ts, src/actions/swap.ts, src/service.ts
- Move @elizaos/core to workspace:* dependency (monorepo resolution)
- Remove @elizaos/service-interfaces; import IWalletService/WalletPortfolio from @elizaos/core
- Add getTransactionWithCostUnitsFallback() for Solana RPC edge case where meta.costUnits is missing
- Use costUnits fallback in executeSwap transaction detail fetch

Co-authored-by: Cursor <cursoragent@cursor.com>
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

🤖 Fix all issues with AI agents
In `@src/service.ts`:
- Around line 930-962: The catch block in batchGetMultipleAccountsInfo currently
only applies exponential backoff when isRateLimit is true, causing immediate
retries for other transient errors; modify the catch handling so that when
isRateLimit is false but retryCount < maxRetries - 1 you still wait using the
same/backoff logic (use baseDelay, retryCount and Math.pow(2, retryCount - 1)
bounded by 32000) before continuing, and keep the existing logging pattern (use
this.runtime.logger.warn) to indicate non-rate-limit retry and delay; ensure
retryCount is incremented consistently and the final throw logic (when
retryCount >= maxRetries - 1) remains unchanged.
- Around line 182-205: The function getTransactionWithCostUnitsFallback
currently calls (connection as any).rpcRequest which relies on an undocumented
internal API; replace that call with the documented private method _rpcRequest
(or perform a direct JSON-RPC HTTP call) to avoid fragile any-casting: locate
the rpcRequest usage inside getTransactionWithCostUnitsFallback, change to using
connection._rpcRequest (adding a single-line `@ts-ignore` above the call to
suppress TS error) or implement a direct JSON-RPC POST using the same params
(signature and { ...GET_TRANSACTION_OPTIONS, encoding: 'json' }), and preserve
the existing handling of result.meta.costUnits and error branching for
COST_UNITS_ERROR_FRAGMENT while keeping GET_TRANSACTION_OPTIONS intact.
🧹 Nitpick comments (6)
package.json (1)

76-76: Inconsistent use of smart apostrophe (\u2019) in description strings.

Lines 76 and 94 use \u2019 (curly/smart apostrophe) while the rest of the file uses plain ASCII. This is likely a copy-paste artifact. Consider using a standard ASCII apostrophe (') for consistency.

Proposed fix
-        "description": "Salt used to derive or encrypt the Solana wallet\u2019s secret key; required if the direct secret key is not provided.",
+        "description": "Salt used to derive or encrypt the Solana wallet's secret key; required if the direct secret key is not provided.",
-        "description": "Alternative name accepted by runtime for the wallet\u2019s public key.",
+        "description": "Alternative name accepted by runtime for the wallet's public key.",

Also applies to: 94-94

src/service.ts (5)

3808-3812: Static analysis: forEach callback implicitly returns a value.

transactions.push(...batch) returns the new array length, which becomes the implicit return of the forEach callback. Use a block body or a simple for...of loop.

Proposed fix
-    batchResults.forEach((batch) => transactions.push(...batch));
+    for (const batch of batchResults) {
+      transactions.push(...batch);
+    }

4043-4046: Static analysis: forEach callback implicitly returns a value from Set.add().

Same pattern as above — globalSignatureSet.add(sig) returns the Set, creating an implicit return in the forEach callback.

Proposed fix
-        result.signatures.forEach((sig) => globalSignatureSet.add(sig));
+        for (const sig of result.signatures) {
+          globalSignatureSet.add(sig);
+        }

476-561: Task worker stores potentially large transaction results in cache and task metadata.

Line 511-516 stores the full FetchTransactionsResult (which can contain thousands of parsed transactions) into cache, and line 530 embeds it into the chained task's metadata. This could cause memory pressure or exceed cache/storage size limits.

Consider storing only a summary or reference (e.g., the cache keys for the per-address results) rather than the full transaction data in task metadata.


3984-4013: waitForTransactionFetch cannot distinguish "task not found" from "task pending".

When status is null (line 3994), the method assumes the task hasn't started yet and keeps polling. If the caller passes an invalid taskId, this will silently poll until the 30-minute timeout. Consider adding an early exit or returning a distinct "not found" status after a reasonable initial wait.


2448-2468: secretKey.fill(0) provides limited memory-clearing benefit in JavaScript.

Line 2458 zeroes the Uint8Array backing the keypair, which is good practice. However, bs58.encode(newKeypair.secretKey) on line 2455 has already created a string copy that cannot be cleared from JS memory (strings are immutable and GC'd). The fill(0) gives a false sense of security — just noting for awareness.

Comment on lines +182 to +205
export async function getTransactionWithCostUnitsFallback(
connection: Connection,
signature: string
): Promise<VersionedTransactionResponse | null> {
try {
return await connection.getTransaction(signature, GET_TRANSACTION_OPTIONS);
} catch (error) {
if (error instanceof Error && error.message.includes(COST_UNITS_ERROR_FRAGMENT)) {
logger.warn(
'solana::getTransaction - missing meta.costUnits. Retrying via raw RPC request with patch.'
);
const rawResponse = await (connection as any).rpcRequest('getTransaction', [
signature,
{ ...GET_TRANSACTION_OPTIONS, encoding: 'json' },
]);
const result = rawResponse?.result ?? null;
if (result?.meta && result.meta.costUnits == null) {
result.meta.costUnits = 0;
}
return (result as VersionedTransactionResponse) ?? null;
}
throw error;
}
}
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

🧩 Analysis chain

🏁 Script executed:

# Verify the file and code location exists
if [ -f "src/service.ts" ]; then
  echo "=== File exists, checking lines 182-205 ==="
  sed -n '182,205p' src/service.ts
else
  echo "File not found, searching for similar functions"
  find . -name "*.ts" -type f | head -20
fi

Repository: elizaos-plugins/plugin-solana

Length of output: 1008


🌐 Web query:

@solana/web3.js Connection rpcRequest method documentation private API

💡 Result:

Connection.rpcRequest is not a public/documented API in @solana/web3.js v1.x.

  • The generated TypeDoc for Connection does not list any rpcRequest method (only public/protected members like _buildArgs, getBalance, etc.), which indicates rpcRequest isn’t part of the supported surface. [1]
  • In practice, people who need “raw RPC” access use the internal (private/TS-hidden) method connection._rpcRequest (note the leading underscore). A common workaround is /* @ts-ignore */ const rpcRequest = connection._rpcRequest; and then calling rpcRequest("someMethod", params). [2]
  • If you want a supported approach, call Solana RPC “directly” via JSON-RPC HTTP from your app (POST { jsonrpc: "2.0", id, method, params }), using the official RPC method docs as reference. [3]

Bottom line: there is no official “rpcRequest” documentation because it’s a private/internal API; prefer direct JSON-RPC HTTP calls (or a newer supported client stack) unless you accept the risk of breakage when web3.js changes. [1][3]

References: [1] [2] [3]


🌐 Web query:

@solana/web3.js Connection class public methods TypeScript definition

💡 Result:

The canonical TypeScript surface for @solana/web3.js’s Connection class is the generated API docs (TypeDoc), which are built directly from the library’s TS source (src/connection.ts). The public API is:

export class Connection {
  constructor(endpoint: string, commitmentOrConfig?: Commitment | ConnectionConfig);

  // accessors / properties
  get commitment(): Commitment | undefined;
  get rpcEndpoint(): string;
  getBlockHeight: (commitmentOrConfig?: Commitment | GetBlockHeightConfig) => Promise<number>;

  // methods (public)
  _buildArgs(...): any[];
  confirmTransaction(...): Promise<...>;
  getAccountInfo(...): Promise<...>;
  getAccountInfoAndContext(...): Promise<...>;
  getAddressLookupTable(...): Promise<...>;
  getBalance(...): Promise<...>;
  getBalanceAndContext(...): Promise<...>;
  getBlock(...): Promise<...>;
  getBlockProduction(...): Promise<...>;
  getBlocks(...): Promise<...>;
  getBlockSignatures(...): Promise<...>;
  getBlockTime(...): Promise<...>;
  getClusterNodes(...): Promise<...>;
  getConfirmedBlock(...): Promise<...>;
  getConfirmedBlockSignatures(...): Promise<...>;
  getConfirmedSignaturesForAddress(...): Promise<...>;
  getConfirmedSignaturesForAddress2(...): Promise<...>;
  getConfirmedTransaction(...): Promise<...>;
  getEpochInfo(...): Promise<...>;
  getEpochSchedule(...): Promise<...>;
  getFeeCalculatorForBlockhash(...): Promise<...>;
  getFeeForMessage(...): Promise<...>;
  getFirstAvailableBlock(...): Promise<...>;
  getGenesisHash(...): Promise<...>;
  getInflationGovernor(...): Promise<...>;
  getInflationRate(...): Promise<...>;
  getInflationReward(...): Promise<...>;
  getLargestAccounts(...): Promise<...>;
  getLatestBlockhash(...): Promise<...>;
  getLatestBlockhashAndContext(...): Promise<...>;
  getLeaderSchedule(...): Promise<...>;
  getMinimumBalanceForRentExemption(...): Promise<...>;
  getMinimumLedgerSlot(...): Promise<...>;
  getMultipleAccountsInfo(...): Promise<...>;
  getMultipleAccountsInfoAndContext(...): Promise<...>;
  getMultipleParsedAccounts(...): Promise<...>;
  getNonce(...): Promise<...>;
  getNonceAndContext(...): Promise<...>;
  getParsedAccountInfo(...): Promise<...>;
  getParsedBlock(...): Promise<...>;
  getParsedConfirmedTransaction(...): Promise<...>;
  getParsedConfirmedTransactions(...): Promise<...>;
  getParsedProgramAccounts(...): Promise<...>;
  getParsedTokenAccountsByOwner(...): Promise<...>;
  getParsedTransaction(...): Promise<...>;
  getParsedTransactions(...): Promise<...>;
  getProgramAccounts(...): Promise<...>;
  getRecentBlockhash(...): Promise<...>;
  getRecentBlockhashAndContext(...): Promise<...>;
  getRecentPerformanceSamples(...): Promise<...>;
  getRecentPrioritizationFees(...): Promise<...>;
  getSignaturesForAddress(...): Promise<...>;
  getSignatureStatus(...): Promise<...>;
  getSignatureStatuses(...): Promise<...>;
  getSlot(...): Promise<...>;
  getSlotLeader(...): Promise<...>;
  getSlotLeaders(...): Promise<...>;
  getStakeActivation(...): Promise<...>;
  getStakeMinimumDelegation(...): Promise<...>;
  getSupply(...): Promise<...>;
  getTokenAccountBalance(...): Promise<...>;
  getTokenAccountsByOwner(...): Promise<...>;
  getTokenLargestAccounts(...): Promise<...>;
  getTokenSupply(...): Promise<...>;
  getTotalSupply(...): Promise<...>;
  getTransaction(...): Promise<...>;
  getTransactionCount(...): Promise<...>;
  getTransactions(...): Promise<...>;
  getVersion(...): Promise<...>;
  getVoteAccounts(...): Promise<...>;
  isBlockhashValid(...): Promise<...>;
  onAccountChange(...): number;
  onLogs(...): number;
  onProgramAccountChange(...): number;
  onRootChange(...): number;
  onSignature(...): number;
  onSignatureWithOptions(...): number;
  onSlotChange(...): number;
  onSlotUpdate(...): number;
  removeAccountChangeListener(...): Promise<void>;
  removeOnLogsListener(...): Promise<void>;
  removeProgramAccountChangeListener(...): Promise<void>;
  removeRootChangeListener(...): Promise<void>;
  removeSignatureListener(...): Promise<void>;
  removeSlotChangeListener(...): Promise<void>;
  removeSlotUpdateListener(...): Promise<void>;
  requestAirdrop(...): Promise<TransactionSignature>;
  sendEncodedTransaction(...): Promise<TransactionSignature>;
  sendRawTransaction(...): Promise<TransactionSignature>;
  sendTransaction(...): Promise<TransactionSignature>;
  simulateTransaction(...): Promise<...>;
}

For the exact parameter/return types for each method (the full .d.ts-equivalent signatures), use the per-method entries in the official Connection class docs. [1]

Source: Solana Foundation @solana/web3.js TypeDoc for Connection (methods index + signatures). [1]

[1] https://solana-foundation.github.io/solana-web3.js/classes/Connection.html


getTransactionWithCostUnitsFallback uses an undocumented internal RPC API.

(connection as any).rpcRequest(...) (line 193) relies on a private/undocumented method of the Connection class. This method is not exposed in the official TypeScript definitions and lacks any documented contract. It may break silently on @solana/web3.js upgrades. If direct RPC access is required, use JSON-RPC HTTP calls directly or the documented _rpcRequest private method with proper @ts-ignore handling instead of casting to any.

🤖 Prompt for AI Agents
In `@src/service.ts` around lines 182 - 205, The function
getTransactionWithCostUnitsFallback currently calls (connection as
any).rpcRequest which relies on an undocumented internal API; replace that call
with the documented private method _rpcRequest (or perform a direct JSON-RPC
HTTP call) to avoid fragile any-casting: locate the rpcRequest usage inside
getTransactionWithCostUnitsFallback, change to using connection._rpcRequest
(adding a single-line `@ts-ignore` above the call to suppress TS error) or
implement a direct JSON-RPC POST using the same params (signature and {
...GET_TRANSACTION_OPTIONS, encoding: 'json' }), and preserve the existing
handling of result.meta.costUnits and error branching for
COST_UNITS_ERROR_FRAGMENT while keeping GET_TRANSACTION_OPTIONS intact.

Comment on lines +930 to +962
} catch (error: any) {
lastError = error;

// Check if it's a rate limit error (429)
const isRateLimit =
error?.message?.includes('429') ||
error?.message?.includes('Too Many Requests') ||
error?.message?.includes('max usage reached') ||
error?.code === -32429 ||
error?.status === 429;

if (isRateLimit && retryCount < maxRetries - 1) {
retryCount++;
// Exponential backoff: 2s, 4s, 8s, 16s, 32s
const delay = Math.min(baseDelay * Math.pow(2, retryCount - 1), 32000);
this.runtime.logger.warn(
`[SolanaService] Rate limit hit in batchGetMultipleAccountsInfo(${label}), retrying in ${delay}ms (attempt ${retryCount}/${maxRetries})`
);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}

// For non-rate-limit errors or final retry, throw
if (retryCount >= maxRetries - 1) {
this.runtime.logger.error(
`[SolanaService] Failed to fetch accounts after ${maxRetries} attempts:`,
error
);
throw error;
}

retryCount++;
}
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

Non-rate-limit errors are retried without any backoff delay.

When isRateLimit is false and retryCount < maxRetries - 1, the loop retries immediately without sleeping. This can hammer a failing RPC node with rapid-fire requests for transient (non-429) errors like network timeouts.

Proposed fix — add a delay for non-rate-limit retries too
-          // For non-rate-limit errors or final retry, throw
-          if (retryCount >= maxRetries - 1) {
+          // For non-rate-limit errors, still apply backoff
+          if (retryCount >= maxRetries - 1) {
             this.runtime.logger.error(
               `[SolanaService] Failed to fetch accounts after ${maxRetries} attempts:`,
               error
             );
             throw error;
           }
 
           retryCount++;
+          const delay = Math.min(baseDelay * Math.pow(2, retryCount - 1), 32000);
+          this.runtime.logger.warn(
+            `[SolanaService] Non-rate-limit error in batchGetMultipleAccountsInfo(${label}), retrying in ${delay}ms (attempt ${retryCount}/${maxRetries})`
+          );
+          await new Promise(resolve => setTimeout(resolve, delay));
📝 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
} catch (error: any) {
lastError = error;
// Check if it's a rate limit error (429)
const isRateLimit =
error?.message?.includes('429') ||
error?.message?.includes('Too Many Requests') ||
error?.message?.includes('max usage reached') ||
error?.code === -32429 ||
error?.status === 429;
if (isRateLimit && retryCount < maxRetries - 1) {
retryCount++;
// Exponential backoff: 2s, 4s, 8s, 16s, 32s
const delay = Math.min(baseDelay * Math.pow(2, retryCount - 1), 32000);
this.runtime.logger.warn(
`[SolanaService] Rate limit hit in batchGetMultipleAccountsInfo(${label}), retrying in ${delay}ms (attempt ${retryCount}/${maxRetries})`
);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
// For non-rate-limit errors or final retry, throw
if (retryCount >= maxRetries - 1) {
this.runtime.logger.error(
`[SolanaService] Failed to fetch accounts after ${maxRetries} attempts:`,
error
);
throw error;
}
retryCount++;
}
} catch (error: any) {
lastError = error;
// Check if it's a rate limit error (429)
const isRateLimit =
error?.message?.includes('429') ||
error?.message?.includes('Too Many Requests') ||
error?.message?.includes('max usage reached') ||
error?.code === -32429 ||
error?.status === 429;
if (isRateLimit && retryCount < maxRetries - 1) {
retryCount++;
// Exponential backoff: 2s, 4s, 8s, 16s, 32s
const delay = Math.min(baseDelay * Math.pow(2, retryCount - 1), 32000);
this.runtime.logger.warn(
`[SolanaService] Rate limit hit in batchGetMultipleAccountsInfo(${label}), retrying in ${delay}ms (attempt ${retryCount}/${maxRetries})`
);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
// For non-rate-limit errors, still apply backoff
if (retryCount >= maxRetries - 1) {
this.runtime.logger.error(
`[SolanaService] Failed to fetch accounts after ${maxRetries} attempts:`,
error
);
throw error;
}
retryCount++;
const delay = Math.min(baseDelay * Math.pow(2, retryCount - 1), 32000);
this.runtime.logger.warn(
`[SolanaService] Non-rate-limit error in batchGetMultipleAccountsInfo(${label}), retrying in ${delay}ms (attempt ${retryCount}/${maxRetries})`
);
await new Promise(resolve => setTimeout(resolve, delay));
}
🤖 Prompt for AI Agents
In `@src/service.ts` around lines 930 - 962, The catch block in
batchGetMultipleAccountsInfo currently only applies exponential backoff when
isRateLimit is true, causing immediate retries for other transient errors;
modify the catch handling so that when isRateLimit is false but retryCount <
maxRetries - 1 you still wait using the same/backoff logic (use baseDelay,
retryCount and Math.pow(2, retryCount - 1) bounded by 32000) before continuing,
and keep the existing logging pattern (use this.runtime.logger.warn) to indicate
non-rate-limit retry and delay; ensure retryCount is incremented consistently
and the final throw logic (when retryCount >= maxRetries - 1) remains unchanged.

Copy link

@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 3 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

this.tokens -= 1;
entry.resolve();
}
}
Copy link

Choose a reason for hiding this comment

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

Rate limiter queue ignores requested token count

Low Severity

The acquire method accepts a count parameter but does not store it in the queue entry. When processQueue drains the queue, it always deducts exactly 1 token per entry (line 131: this.tokens -= 1), regardless of the original count requested. If acquire(N) is called with N > 1 and falls through to the queue, only 1 token is consumed instead of N, violating the rate-limiting contract.

Fix in Cursor Fix in Web

throw error;
}

retryCount++;
Copy link

Choose a reason for hiding this comment

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

Non-rate-limit errors retry in tight loop without delay

Medium Severity

In batchGetMultipleAccountsInfo, the comment says "For non-rate-limit errors or final retry, throw" but the code only throws when retryCount >= maxRetries - 1. For non-rate-limit errors (e.g., network failures, deserialization errors) with retryCount below that threshold, execution falls through to retryCount++ and retries immediately with no delay. This creates up to 4 rapid-fire retry attempts against a potentially failing endpoint.

Fix in Cursor Fix in Web

}

return info;
}
Copy link

Choose a reason for hiding this comment

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

Duplicated signature extraction and formatting functions across files

Low Severity

extractSignaturesFromText is identically implemented in both lookup-signature.ts (action) and signature-lookup.ts (provider). formatTransactionInfo is also nearly identical across both files. These could be extracted into a shared utility to avoid divergent bug fixes.

Additional Locations (1)

Fix in Cursor Fix in Web

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