From e8f46335a175aea0d0481a643074c7c0163add8c Mon Sep 17 00:00:00 2001 From: Odilitime Date: Thu, 16 Oct 2025 20:50:46 +0000 Subject: [PATCH 01/36] .env, .elizadb* --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index ab0378b..db9e3f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ node_modules .turbo dist +.env +.elizadb +.elizadb-test From 64be54cec331b277395f93ec2ff50c2892d87cb0 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Thu, 16 Oct 2025 20:54:06 +0000 Subject: [PATCH 02/36] core to latest, bump spl-token, add spl-token-metadata --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 73f268a..30a6921 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,9 @@ "dist" ], "dependencies": { - "@elizaos/core": "^1.0.0", - "@solana/spl-token": "0.4.13", + "@elizaos/core": "latest", + "@solana/spl-token": "0.4.14", + "@solana/spl-token-metadata": "^0.1.6", "@solana/web3.js": "^1.98.0", "bignumber.js": "9.3.0", "bs58": "6.0.0", From c029a73ca5793d425464497a6e76de87c8124d77 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Thu, 16 Oct 2025 20:54:22 +0000 Subject: [PATCH 03/36] fix npm org --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5d16b65..e7b8929 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# @elizaos-plugins/plugin-solana +# @elizaos/plugin-solana Core Solana blockchain plugin for Eliza OS that provides essential services and actions for token operations, trading, and DeFi integrations. @@ -55,7 +55,7 @@ The Solana plugin serves as a foundational component of Eliza OS, bridging Solan ## Installation ```bash -npm install @elizaos-plugins/plugin-solana +npm install @elizaos/plugin-solana ``` ## Configuration @@ -80,7 +80,7 @@ const solanaEnvSchema = { ### Basic Setup ```typescript -import { solanaPlugin } from '@elizaos-plugins/plugin-solana'; +import { solanaPlugin } from '@elizaos/plugin-solana'; // Initialize the plugin const runtime = await initializeRuntime({ From edaf09ffbdaf479ec3529cbf27e65ece7a359e3b Mon Sep 17 00:00:00 2001 From: Odilitime Date: Thu, 16 Oct 2025 20:54:58 +0000 Subject: [PATCH 04/36] general update --- src/environment.ts | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/environment.ts b/src/environment.ts index c40b484..46ade51 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -24,16 +24,16 @@ import { z } from 'zod'; */ export const solanaEnvSchema = z .object({ - WALLET_SECRET_SALT: z.string().optional(), + SOLANA_SECRET_SALT: z.string().optional(), }) .and( z.union([ z.object({ - WALLET_SECRET_KEY: z.string().min(1, 'Wallet secret key is required'), - WALLET_PUBLIC_KEY: z.string().min(1, 'Wallet public key is required'), + SOLANA_PRIVATE_KEY: z.string().min(1, 'Solana secret key is required'), + SOLANA_PUBLIC_KEY: z.string().min(1, 'Solana public key is required'), }), z.object({ - WALLET_SECRET_SALT: z.string().min(1, 'Wallet secret salt is required'), + SOLANA_SECRET_SALT: z.string().min(1, 'Solana secret salt is required'), }), ]) ) @@ -42,8 +42,8 @@ export const solanaEnvSchema = z SOL_ADDRESS: z.string().min(1, 'SOL address is required'), SLIPPAGE: z.string().min(1, 'Slippage is required'), SOLANA_RPC_URL: z.string().min(1, 'RPC URL is required'), - HELIUS_API_KEY: z.string().min(1, 'Helius API key is required'), - BIRDEYE_API_KEY: z.string().min(1, 'Birdeye API key is required'), + //HELIUS_API_KEY: z.string().min(1, 'Helius API key is required'), + //BIRDEYE_API_KEY: z.string().min(1, 'Birdeye API key is required'), }) ); @@ -63,18 +63,15 @@ export type SolanaConfig = z.infer; export async function validateSolanaConfig(runtime: IAgentRuntime): Promise { try { const config = { - WALLET_SECRET_SALT: - runtime.getSetting('WALLET_SECRET_SALT') || process.env.WALLET_SECRET_SALT, - WALLET_SECRET_KEY: runtime.getSetting('WALLET_SECRET_KEY') || process.env.WALLET_SECRET_KEY, - WALLET_PUBLIC_KEY: - runtime.getSetting('SOLANA_PUBLIC_KEY') || - runtime.getSetting('WALLET_PUBLIC_KEY') || - process.env.WALLET_PUBLIC_KEY, - SOL_ADDRESS: runtime.getSetting('SOL_ADDRESS') || process.env.SOL_ADDRESS, - SLIPPAGE: runtime.getSetting('SLIPPAGE') || process.env.SLIPPAGE, - SOLANA_RPC_URL: runtime.getSetting('SOLANA_RPC_URL') || process.env.SOLANA_RPC_URL, - HELIUS_API_KEY: runtime.getSetting('HELIUS_API_KEY') || process.env.HELIUS_API_KEY, - BIRDEYE_API_KEY: runtime.getSetting('BIRDEYE_API_KEY') || process.env.BIRDEYE_API_KEY, + SOLANA_SECRET_SALT: runtime.getSetting('SOLANA_SECRET_SALT'),// wtf is this? + //SOL_ADDRESS: runtime.getSetting('SOL_ADDRESS'), + SLIPPAGE: runtime.getSetting('SLIPPAGE'), + SOLANA_RPC_URL: runtime.getSetting('SOLANA_RPC_URL'), + //HELIUS_API_KEY: runtime.getSetting('HELIUS_API_KEY'), + //BIRDEYE_API_KEY: runtime.getSetting('BIRDEYE_API_KEY'), + // optional: + SOLANA_PRIVATE_KEY: runtime.getSetting('SOLANA_PRIVATE_KEY'), + SOLANA_PUBLIC_KEY: runtime.getSetting('SOLANA_PUBLIC_KEY') }; return solanaEnvSchema.parse(config); From 296a2a795716564176e499cf301ec2b89180874d Mon Sep 17 00:00:00 2001 From: Odilitime Date: Thu, 16 Oct 2025 20:55:18 +0000 Subject: [PATCH 05/36] add declarationDir & exclude --- tsconfig.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index f156f86..7a69139 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "esModuleInterop": true, "allowImportingTsExtensions": true, "declaration": true, + "declarationDir": "dist", "emitDeclarationOnly": true, "resolveJsonModule": true, "moduleDetection": "force", @@ -20,5 +21,6 @@ "@elizaos/core/*": ["../core/src/*"] } }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] } From 70f088153fd0e41d895887bd302747021590fd77 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Thu, 16 Oct 2025 20:55:46 +0000 Subject: [PATCH 06/36] add declarationDir --- tsconfig.build.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.build.json b/tsconfig.build.json index 9a7896f..baff6c6 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -3,6 +3,7 @@ "compilerOptions": { "rootDir": "./src", "outDir": "./dist", + "declarationDir": "./dist", "strict": true, "sourceMap": true, "inlineSources": true, From 7b803fd2ba12e77bf4bd86f6e0f2f85ac085e839 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Thu, 16 Oct 2025 20:56:33 +0000 Subject: [PATCH 07/36] modernize init, export service symbols, update description --- src/index.ts | 83 ++++++++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1374470..1aae20a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,52 +1,57 @@ import type { IAgentRuntime, Plugin } from '@elizaos/core'; -import { logger } from '@elizaos/core'; +import { parseBooleanFromText } from '@elizaos/core'; + +// actions import { executeSwap } from './actions/swap'; import transferToken from './actions/transfer'; -import { SOLANA_SERVICE_NAME } from './constants'; + +// providers import { walletProvider } from './providers/wallet'; + +// service import { SolanaService } from './service'; +import { SOLANA_SERVICE_NAME } from './constants'; + export const solanaPlugin: Plugin = { name: SOLANA_SERVICE_NAME, - description: 'Solana Plugin for Eliza', - actions: [transferToken, executeSwap], - evaluators: [], - providers: [walletProvider], + description: 'Solana blockchain plugin', services: [SolanaService], init: async (_, runtime: IAgentRuntime) => { - logger.debug('solana init'); - - new Promise(async (resolve) => { - resolve(); - const asking = 'solana'; - const serviceType = 'TRADER_CHAIN'; - const maxRetries = 10; - let retries = 0; - - let traderChainService = runtime.getService(serviceType) as any; - while (!traderChainService && retries < maxRetries) { - logger.debug(`${asking} waiting for ${serviceType} service... (${retries + 1}/${maxRetries})`); - traderChainService = runtime.getService(serviceType) as any; - if (!traderChainService) { - await new Promise((waitResolve) => setTimeout(waitResolve, 1000)); - retries++; - } else { - logger.debug(`${asking} Acquired ${serviceType} service...`); - } - } - - if (traderChainService) { - const me = { - name: 'Solana services', - chain: 'solana', - service: SOLANA_SERVICE_NAME, - }; - traderChainService.registerChain(me); - logger.debug('solana init done'); - } else { - logger.debug('solana init done (standalone mode - TRADER_CHAIN service not available)'); - } - }); + + // Validation + if (!runtime.getSetting('SOLANA_RPC_URL')) { + runtime.logger.log('no SOLANA_RPC_URL, skipping plugin-solana init') + return + } + + const noActions = parseBooleanFromText(runtime.getSetting("SOLANA_NO_ACTIONS")); + if (!noActions) { + runtime.registerAction(transferToken) + runtime.registerAction(executeSwap) + } else { + runtime.logger.log('SOLANA_NO_ACTIONS is set, skipping solana actions') + } + + runtime.registerProvider(walletProvider) + + // extensions + const p = runtime.getServiceLoadPromise('INTEL_CHAIN').then( () => { + //runtime.logger.log('solana INTEL_CHAIN LOADED') + const traderChainService = runtime.getService('INTEL_CHAIN') as any; + const me = { + name: 'Solana services', + chain: 'solana', + service: SOLANA_SERVICE_NAME, + }; + traderChainService.registerChain(me); + }) + }, }; export default solanaPlugin; + +// Export additional items for use by other plugins +export { SOLANA_SERVICE_NAME } from './constants'; +export { SolanaService } from './service'; +export type { SolanaService as ISolanaService } from './service'; \ No newline at end of file From f0a04b9bd098fab7bc2fa085072a3d275fbb21d3 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Thu, 16 Oct 2025 20:56:51 +0000 Subject: [PATCH 08/36] new file per PR9 --- env.example | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 env.example diff --git a/env.example b/env.example new file mode 100644 index 0000000..e6fe4ea --- /dev/null +++ b/env.example @@ -0,0 +1,32 @@ +# Solana Plugin Environment Variables +# Copy this file to .env and fill in your actual values + +# Solana RPC URL (required) +# You can use public endpoints or get a dedicated one from providers like Helius, QuickNode, etc. +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com + +# Birdeye API Key (required for price data and portfolio valuation) +# Get your free API key from https://birdeye.so/ +BIRDEYE_API_KEY=YourBirdeyeApiKeyHere + +# Optional: Helius API Key (for enhanced RPC features) +# Get your API key from https://helius.xyz/ +HELIUS_API_KEY=YourHeliusApiKeyHere + +# Solana Private Key (base58 encoded) +# Required for sending transactions (like transfers, swaps) and for account subscriptions. +# If not provided, the plugin will operate in a read-only mode for basic portfolio viewing (if public key was previously used/cached or if functionality is adapted to not require it for reads). +# WARNING: Keep this secret and never commit to version control if you put a real one here for testing. +# For development, if this is not set, a new key may be generated by some test environments. +SOLANA_PRIVATE_KEY=YourPrivateKeyHere_Base58Encoded_e_g_5jZ2p... + +# Optional: Slippage tolerance for swaps (default: 0.5%) +SLIPPAGE=0.5 + +# Optional: SOL token address (usually the wrapped SOL address for DeFi interactions) +SOL_ADDRESS=So11111111111111111111111111111111111111112 + +# Example values for testing (replace with your actual values if needed): +# SOLANA_RPC_URL=https://api.mainnet-beta.solana.com +# BIRDEYE_API_KEY=your-birdeye-api-key-here +# SOLANA_PRIVATE_KEY= # Fill with a testnet/devnet private key for transaction tests From c5f2503b340a1858e115e4e5a04fdf3725c4c91f Mon Sep 17 00:00:00 2001 From: Odilitime Date: Thu, 16 Oct 2025 21:03:41 +0000 Subject: [PATCH 09/36] getAddressesTypes(), getCirculatingSupplies(), getDecimals(), getTokenAccountsByKeypairs(), getTokensSymbols token2022 attempt (can't read pumpfun's token still), change verifySignature prototype order, deprecate funcs, modernize cstr, type fixes, notes --- src/service.ts | 336 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 227 insertions(+), 109 deletions(-) diff --git a/src/service.ts b/src/service.ts index 6f904c1..b6eadec 100644 --- a/src/service.ts +++ b/src/service.ts @@ -1,4 +1,4 @@ -import { type IAgentRuntime, Service, logger } from '@elizaos/core'; +import { type IAgentRuntime, ServiceTypeName, Service, logger } from '@elizaos/core'; import { Connection, Keypair, @@ -6,13 +6,15 @@ import { VersionedTransaction, SendTransactionError, LAMPORTS_PER_SOL, - AccountInfo + type AccountInfo, } from '@solana/web3.js'; import { MintLayout, getMint, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, unpackAccount, getAssociatedTokenAddressSync, ExtensionType, getExtensionData, getExtensionTypes, unpackMint, } from "@solana/spl-token"; +// parses the raw Token-2022 metadata struct +import { unpack as unpackToken2022Metadata } from '@solana/spl-token-metadata'; import BigNumber from 'bignumber.js'; import { SOLANA_SERVICE_NAME, SOLANA_WALLET_DATA_CACHE_KEY } from './constants'; import { getWalletKey, KeypairResult } from './keypairUtils'; @@ -66,12 +68,12 @@ export class SolanaService extends Service { capabilityDescription = 'The agent is able to interact with the Solana blockchain, and has access to the wallet data'; - private updateInterval: NodeJS.Timer | null = null; private lastUpdate = 0; private readonly UPDATE_INTERVAL = 2 * 60_000; // 2 minutes private connection: Connection; private publicKey: PublicKey | null = null; private exchangeRegistry: Record = {}; + // probably should be an array of numbers? private subscriptions: Map = new Map(); jupiterService: any; @@ -99,30 +101,12 @@ export class SolanaService extends Service { ); this.connection = connection; - const asking = 'Solana service' - const serviceType = 'JUPITER_SERVICE' - - const getJup = async () => { - const maxRetries = 10; - let retries = 0; - - this.jupiterService = this.runtime.getService(serviceType) as any; - while (!this.jupiterService && retries < maxRetries) { - runtime.logger.debug(`${asking} waiting for ${serviceType} service... (${retries + 1}/${maxRetries})`); - this.jupiterService = this.runtime.getService(serviceType) as any; - if (!this.jupiterService) { - await new Promise((waitResolve) => setTimeout(waitResolve, 1000)); - retries++; - } else { - runtime.logger.debug(`${asking} Acquired ${serviceType} service...`); - } - } - - if (!this.jupiterService) { - runtime.logger.info(`${asking} - JUPITER_SERVICE not available (swap functionality will be limited)`); - } - } - getJup() // no wait + // jupiter support detection + // shouldn't even be here... + runtime.getServiceLoadPromise('JUPITER_SERVICE' as ServiceTypeName).then(async s => { + // now we have jupiter lets register our services + this.jupiterService = runtime.getService('JUPITER_SERVICE' as ServiceTypeName) as any; + }) // Initialize publicKey using getWalletKey getWalletKey(runtime, false) @@ -135,7 +119,8 @@ export class SolanaService extends Service { // get initial read this.updateWalletData(); // only need to update wallet if it changes - this.subscribeToAccount(this.publicKey.toBase58(), async (accountAddress, accountInfo, context) => { + // FIXME store this subscriptions somewhere... + this.subscribeToAccount(this.publicKey.toBase58(), async (accountAddress: string, accountInfo: unknown, context: unknown) => { runtime.logger.log('Updating wallet data'); await this.updateWalletData(); // non-forced (respect: UPDATE_INTERVAL) }).catch((error) => { @@ -164,7 +149,7 @@ export class SolanaService extends Service { */ async registerExchange(provider: any) { const id = Object.values(this.exchangeRegistry).length + 1; - logger.log(`Registered ${provider.name} as Solana provider #${id}`); + this.runtime.logger.success(`Registered ${provider.name} as Solana provider #${id}`); this.exchangeRegistry[id] = provider; return id; } @@ -198,6 +183,7 @@ export class SolanaService extends Service { return await response.json(); } catch (error) { logger.error(`Attempt ${i + 1} failed: ${error}`); + logger.error({ error }, `Attempt ${i + 1} failed`); lastError = error as Error; if (i < PROVIDER_CONFIG.MAX_RETRIES - 1) { await new Promise((resolve) => setTimeout(resolve, PROVIDER_CONFIG.RETRY_DELAY * 2 ** i)); @@ -220,8 +206,8 @@ export class SolanaService extends Service { return results; } - verifySolanaSignature({ - message, signatureBase64, publicKeyBase58 + verifySignature({ + publicKeyBase58, message, signatureBase64 }: { message: string; signatureBase64: string; publicKeyBase58: string; }): boolean { @@ -232,11 +218,22 @@ export class SolanaService extends Service { return nacl.sign.detached.verify(messageUint8, signature, publicKeyBytes); } + // Solana should be here, it's already in the class/service name + // deprecate + verifySolanaSignature({ + message, signatureBase64, publicKeyBase58 + }: { + message: string; signatureBase64: string; publicKeyBase58: string; + }): boolean { + this.runtime.logger.warn('verifySolanaSignature is deprecated, use verifySignature') + return this.verifySignature({ message, signatureBase64, publicKeyBase58 }) + } + // // MARK: Addresses // - public isValidSolanaAddress(address: string, onCurveOnly = false): boolean { + public isValidAddress(address: string, onCurveOnly = false): boolean { try { const pubkey = new PublicKey(address); if (onCurveOnly) { @@ -248,6 +245,13 @@ export class SolanaService extends Service { } } + // Solana should be here, it's already in the class/service name + // deprecate + public isValidSolanaAddress(address: string, onCurveOnly = false): boolean { + this.runtime.logger.warn('isValidSolanaAddress is deprecated, use isValidAddress') + return this.isValidAddress(address, onCurveOnly) + } + /** * Validates a Solana address. * @param {string | undefined} address - The address to validate. @@ -258,7 +262,7 @@ export class SolanaService extends Service { try { // Handle Solana addresses if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) { - logger.warn(`Invalid Solana address format: ${address}`); + this.runtime.logger.warn(`Invalid Solana address format: ${address}`); return false; } @@ -267,7 +271,8 @@ export class SolanaService extends Service { //logger.log(`Solana address validation: ${address}`, { isValid }); return isValid; } catch (error) { - logger.error(`Address validation error: ${address} - ${error}`); + //logger.error(`Address validation error: ${address} - ${error}`); + this.runtime.logger.error({ error }, `Address validation error: ${address}`); return false; } } @@ -276,8 +281,7 @@ export class SolanaService extends Service { private static readonly TOKEN_ACCOUNT_DATA_LENGTH = 165; private static readonly TOKEN_MINT_DATA_LENGTH = 82; - // could use batchGetMultipleAccountsInfo to get multiple - // FIXME: getAddressesTypes(addresses: string[]) + // deprecate async getAddressType(address: string): Promise { let dataLength = -1 try { @@ -324,6 +328,11 @@ export class SolanaService extends Service { return `Unknown (Data length: ${dataLength})`; } + async getAddressesTypes(addresses: string[]) { + // FIXME: use batchGetMultipleAccountsInfo to efficiently check multiple + return Promise.all(addresses.map(a => this.getAddressType(a))) + } + /** * Detect Solana public keys (Base58) in a string * @param input arbitrary text @@ -417,6 +426,7 @@ export class SolanaService extends Service { // MARK: tokens // + // deprecate async getCirculatingSupply(mint: string) { //const mintPublicKey = new PublicKey(mint); @@ -455,6 +465,11 @@ export class SolanaService extends Service { return circulating; } + async getCirculatingSupplies(mints: string[]) { + // FIXME: use batchGetMultipleAccountsInfo? to efficiently check multiple + return Promise.all(mints.map(m => this.getCirculatingSupply(m))) + } + /** * Asynchronously fetches the prices of SOL, BTC, and ETH tokens. * Uses cache to store and retrieve prices if available. @@ -465,6 +480,7 @@ export class SolanaService extends Service { const cachedValue = await this.runtime.getCache(cacheKey); // if cachedValue is JSON, parse it + // FIXME: how long do we cache this for?!? if (cachedValue) { logger.log('Cache hit for fetchPrices'); return cachedValue; @@ -520,7 +536,8 @@ export class SolanaService extends Service { undefined, // optional commitment TOKEN_2022_PROGRAM_ID // specify the extensions token program ); - console.log('getDecimal - mintInfo2022', mintInfo) + // address, mintAuthority, supply, decimals, isInitialized, freezeAuthority, tlvData + //console.log('getDecimal - mintInfo2022', mintInfo) this.decimalsCache.set(key, mintInfo.decimals); return mintInfo.decimals; } @@ -534,6 +551,11 @@ export class SolanaService extends Service { } } + public async getDecimals(mints: string[]): Promise { + const mintPublicKeys = mints.map(a => new PublicKey(a)) + return Promise.all(mintPublicKeys.map(a => this.getDecimal(a))) + } + public async getMetadataAddress(mint: PublicKey): Promise { // not an rpc call const [metadataPDA] = await PublicKey.findProgramAddress( @@ -576,53 +598,156 @@ export class SolanaService extends Service { return symbol; } + // this is all local + private parseToken2022SymbolFromMintOrPtr = (mintData: Buffer): { symbol: string | null, ptr?: PublicKey } => { + // Try inline TokenMetadata extension first + const inline = getExtensionData(ExtensionType.TokenMetadata, mintData); + if (inline) { + try { + const md = unpackToken2022Metadata(inline); + const symbol = md?.symbol?.replace(/\0/g, '').trim() || null; + return { symbol }; + } catch { + // fall through to pointer + } + } + + // Try MetadataPointer extension + const ptrExt = getExtensionData(ExtensionType.MetadataPointer, mintData) as + | { authority: Uint8Array; metadataAddress: Uint8Array } + | null; + + if (ptrExt?.metadataAddress) { + return { symbol: null, ptr: new PublicKey(ptrExt.metadataAddress) }; + } + + return { symbol: null }; + }; + // cache me public async getTokensSymbols( mints: string[] ): Promise> { + console.log('getTokensSymbols'); const mintKeys: PublicKey[] = mints.map(k => new PublicKey(k)); + + // Phase 1: Metaplex PDAs (your existing flow) const metadataAddresses: PublicKey[] = await Promise.all( mintKeys.map(mk => this.getMetadataAddress(mk)) ); const accountInfos = await this.batchGetMultipleAccountsInfo( metadataAddresses, - 'getTokensSymbols' + 'getTokensSymbols/Metaplex' ); const out: Record = {}; + const needs2022: PublicKey[] = []; + mintKeys.forEach((token, i) => { - const accountInfo = accountInfos[i]; // raw AccountInfo | null + const accountInfo = accountInfos[i]; // AccountInfo | null if (!accountInfo || !accountInfo.data) { out[token.toBase58()] = null; + console.log('getTokensSymbols - adding', token.toBase58(), 'to token2022 list') + needs2022.push(token); return; } - const data = accountInfo.data; + try { + const data = accountInfo.data as Buffer; - // Skip the 1-byte key and 32+32+4+len name fields (you can parse these if needed) - let offset = 1 + 32 + 32; + // @metaplex-foundation/mpl-token-metadata + // Minimal Metaplex parse: + // key(1) + updateAuth(32) + mint(32) + let offset = 1 + 32 + 32; - // Name (length-prefixed string) - const nameLen = data.readUInt32LE(offset); - offset += 4 + nameLen; + // name + const nameLen = data.readUInt32LE(offset); + offset += 4 + nameLen; - // Symbol (length-prefixed string) - const symbolLen = data.readUInt32LE(offset); - offset += 4; - const symbol = - data - .slice(offset, offset + symbolLen) - .toString('utf8') - .replace(/\0/g, '') || null; - out[token.toBase58()] = symbol; + // symbol + const symbolLen = data.readUInt32LE(offset); + offset += 4; + const symbol = + data.slice(offset, offset + symbolLen).toString('utf8').replace(/\0/g, '').trim() || null; + + out[token.toBase58()] = symbol; + if (!symbol) needs2022.push(token); + } catch (e) { + console.log('Metaplex parse failed; will try Token-2022:', e); + out[token.toBase58()] = null; + needs2022.push(token); + } }); + // Phase 2: Batch fetch *mint accounts* via your batch helper, then parse Token-2022 TLV + if (needs2022.length) { + const mintInfos = await this.batchGetMultipleAccountsInfo( + needs2022, + 'getTokensSymbols/Token2022' + ); + + // First pass: parse inline metadata or collect pointer addresses + const ptrsToFetch: PublicKey[] = []; + const ptrOwnerByKey = new Map(); // mint base58 -> owner key (for logging) + + needs2022.forEach((mint, idx) => { + const info = mintInfos[idx] as AccountInfo | null; + if (!info || !info.data) { + console.log('getTokensSymbols - token2022 failed', mint.toBase58()); + return; + } + if (!info.owner.equals(TOKEN_2022_PROGRAM_ID)) { + console.log('getTokensSymbols - not a token2022', mint.toBase58()); + return; + } + + const { symbol, ptr } = this.parseToken2022SymbolFromMintOrPtr(info.data); + if (symbol) { + out[mint.toBase58()] = symbol; + } else if (ptr) { + ptrsToFetch.push(ptr); + ptrOwnerByKey.set(ptr.toBase58(), mint.toBase58()); + } else { + console.log('getTokensSymbols - no TokenMetadata or pointer', mint.toBase58()); + } + }); + + // Second pass: fetch and parse pointer accounts (batch) + if (ptrsToFetch.length) { + const pointerInfos = await this.batchGetMultipleAccountsInfo( + ptrsToFetch, + 'getTokensSymbols/Token2022Pointer' + ); + + ptrsToFetch.forEach((ptrPk, idx) => { + const pinfo = pointerInfos[idx] as AccountInfo | null; + const mintB58 = ptrOwnerByKey.get(ptrPk.toBase58())!; + if (!pinfo?.data) { + console.log('getTokensSymbols - pointer account missing', ptrPk.toBase58(), 'for mint', mintB58); + return; + } + try { + const md = unpackToken2022Metadata(pinfo.data); + const symbol = md?.symbol?.replace(/\0/g, '').trim() || null; + if (symbol) { + out[mintB58] = symbol; + } else { + console.log('getTokensSymbols - pointer metadata has no symbol', ptrPk.toBase58(), 'for mint', mintB58); + } + } catch (e) { + console.log('getTokensSymbols - failed to unpack pointer metadata', ptrPk.toBase58(), e); + } + }); + } + } + return out; } - + public async getSupply(CAs: string[]) { - const mintKeys: PublicKey[] = CAs.map((ca: string) => new PublicKey(ca)); + //console.log('getSupply CAs', CAs.length) + const mintKeys: PublicKey[] = CAs.map((ca: string) => new PublicKey(ca)); const mintInfos = await this.batchGetMultipleAccountsInfo(mintKeys, 'getSupply') const results = mintInfos.map((accountInfo, idx) => { @@ -659,7 +784,7 @@ export class SolanaService extends Service { return out } - public async parseTokenAccounts(heldTokens: any [], options = {}) { + public async parseTokenAccounts(heldTokens: any [], options: { notOlderThan?: number } = {}) { // decimalsCache means we don't need all I think // we need structure token cache // stil need them for symbol @@ -693,7 +818,7 @@ export class SolanaService extends Service { let misses = 0 const fetchTokens = [] - const goodCache = {} + const goodCache: Record & { balanceUi: number }> = {} for(const i in heldTokens) { const t = heldTokens[i] if (cache[i]) { @@ -757,7 +882,7 @@ export class SolanaService extends Service { const allMintKeys: PublicKey[] = Array.from(new Set( fetchTokens.map((t: any) => t.account.data.parsed.info.mint as string) )).map(s => new PublicKey(s)) - + // const mintInfos = await this.batchGetMultipleAccountsInfo(allMintKeys, "t22-mints") @@ -837,7 +962,7 @@ export class SolanaService extends Service { // Parse the Token-2022 TokenMetadata TLV (just the Value slice) function parseToken2022MetadataTLV(ext: Buffer): { - isMutable: boolean; updateAuthority?: string; name: string; symbol: string; uri: string; + isMutable: boolean; updateAuthority?: string; mint: string; name: string; symbol: string; uri: string; additional?: unknown; } { const o = { off: 0 }; // 32B updateAuthority (all-zero = None) @@ -1105,26 +1230,26 @@ export class SolanaService extends Service { const t22Set = new Set(t22MintKeys.map(k => k.toBase58())); const results = heldTokens.map((t) => { - const mintStr = t.account.data.parsed.info.mint as string; - const mintKey = new PublicKey(mintStr); - const is2022 = t22Set.has(mintStr); + const mintStr: string = t.account.data.parsed.info.mint as string; + const mintKey: PublicKey = new PublicKey(mintStr); + const is2022: boolean = t22Set.has(mintStr); // decimals / balance (unchanged) const { amount: raw, decimals } = t.account.data.parsed.info.tokenAmount; - const balanceUi = Number(raw) / 10 ** decimals; + const balanceUi: number = Number(raw) / 10 ** decimals; // pick the right source for isMutable - const isMutable = + const isMutable: boolean | null = (is2022 && hasT22Meta.has(mintStr)) ? (t22IsMutable.get(mintStr) ?? null) : (mpIsMutable.get(mintStr) ?? null); - const symbol = + const symbol: string | null = (is2022 && hasT22Meta.has(mintStr)) ? (t22Symbols.get(mintStr) ?? null) : (mpSymbols.get(mintStr) ?? null); - let supply = mpSupply.get(mintStr) ?? null + let supply: string | number | null = mpSupply.get(mintStr) ?? null if (supply) supply = parseFloat(supply) return { @@ -1259,7 +1384,7 @@ export class SolanaService extends Service { (async () => { console.time('saveCache') for(const t of results) { - const copy = {...t} + const copy: any = {...t} delete copy.balanceUi delete copy.mint const key = 'solana_token_meta_' + t.mint @@ -1356,6 +1481,15 @@ export class SolanaService extends Service { return keypair; } + /** + * Retrieves the public key of the instance. + * + * @returns {PublicKey} The public key of the instance. + */ + public getPublicKey(): PublicKey | null { + return this.publicKey; + } + /** * Update wallet data including fetching wallet portfolio information, prices, and caching the data. * @param {boolean} [force=false] - Whether to force update the wallet data even if the update interval has not passed @@ -1399,11 +1533,11 @@ export class SolanaService extends Service { const solPriceInUSD = new BigNumber(prices.solana.usd); - const missingSymbols = data.items.filter(i => !i.symbol) + const missingSymbols = data.items.filter((i: any) => !i.symbol) //console.log('data.items', data.items) if (missingSymbols.length) { - const symbols = await this.getTokensSymbols(missingSymbols.map(i => i.address)) + const symbols: Record = await this.getTokensSymbols(missingSymbols.map((i: any) => i.address)) let missing = false for(const i in data.items) { const item = data.items[i] @@ -1520,15 +1654,6 @@ export class SolanaService extends Service { return await this.updateWalletData(true); } - /** - * Retrieves the public key of the instance. - * - * @returns {PublicKey} The public key of the instance. - */ - public getPublicKey(): PublicKey | null { - return this.publicKey; - } - // // MARK: any wallet // @@ -1567,7 +1692,7 @@ export class SolanaService extends Service { const balance = Number(amountRaw) / (10 ** decimals); const symbol = await solanaService.getTokenSymbol(ca); */ - public async getTokenAccountsByKeypair(walletAddress: PublicKey, options = {}) { + public async getTokenAccountsByKeypair(walletAddress: PublicKey, options: { notOlderThan?: number; includeZeroBalances?: boolean; } = {}) { //console.log('getTokenAccountsByKeypair', walletAddress.toString()) //console.log('publicKey', this.publicKey, 'vs', walletAddress) const key = 'solana_' + walletAddress.toString() + '_tokens' @@ -1583,7 +1708,7 @@ export class SolanaService extends Service { const diff = now - (check as any).fetchedAt // 1s - 5min cache? // FIXME: options driven... - const acceptableInMs = options.notOlderThan ?? 60_000 // default + const acceptableInMs: number = options.notOlderThan ?? 60_000 // default if (diff < acceptableInMs) { console.log('getTokenAccountsByKeypair cache HIT, its', diff.toLocaleString() + 'ms old') return (check as any).data @@ -1635,6 +1760,10 @@ export class SolanaService extends Service { } } + public async getTokenAccountsByKeypairs(walletAddresses: string[], options = {}) { + return Promise.all(walletAddresses.map(a => this.getTokenAccountsByKeypair(new PublicKey(a), options))) + } + // deprecated /* public async getBalanceByAddr(walletAddressStr: string): Promise { @@ -1846,20 +1975,9 @@ export class SolanaService extends Service { return false; } - //await connection.removeAccountChangeListener(subscriptionId); - - /* - const ws = (this.connection as any).connection._rpcWebSocket; - const success = await ws.call('accountUnsubscribe', [subscriptionId]); - - if (success) { - this.subscriptions.delete(accountAddress); - logger.log(`Unsubscribed from account ${accountAddress}`); - } + await this.connection.removeAccountChangeListener(subscriptionId); - return success; - */ - // + return true; } catch (error) { logger.error(`Error unsubscribing from account: ${error}`); throw error; @@ -2044,7 +2162,7 @@ export class SolanaService extends Service { */ // outAmount, minOutAmount, priceImpactPct - const impliedSlippageBps = ((initialQuote.outAmount - initialQuote.otherAmountThreshold) / initialQuote.outAmount) * 10_000; + const impliedSlippageBps: number = ((initialQuote.outAmount - initialQuote.otherAmountThreshold) / initialQuote.outAmount) * 10_000; console.log('impliedSlippageBps', impliedSlippageBps, 'jupSlip', initialQuote.slippageBps) // Calculate optimal buy amount using the input mint from quote @@ -2281,10 +2399,8 @@ export class SolanaService extends Service { if (inBal && outBal) { // in will be high than out in this scenario? - //const lamDiff = (outBal.uiTokenAmount?.uiAmount || 0) - (inBal.uiTokenAmount?.uiAmount || 0) - const lamDiff = inBal.uiTokenAmount.uiAmount - outBal.uiTokenAmount.uiAmount - //const diff = Number(outBal.uiTokenAmount?.amount || 0) - Number(inBal.uiTokenAmount?.amount || 0) - const diff = Number(inBal.uiTokenAmount.amount) - Number(outBal.uiTokenAmount.amount) + const lamDiff = (inBal.uiTokenAmount.uiAmount || 0) - (outBal.uiTokenAmount.uiAmount || 0) + const diff = Number(inBal.uiTokenAmount.amount || 0) - Number(outBal.uiTokenAmount.amount || 0) // we definitely didn't swap for nothing if (diff) { outAmount = diff @@ -2307,8 +2423,8 @@ export class SolanaService extends Service { } else { if (inBal && outBal) { - const lamDiff = outBal.uiTokenAmount.uiAmount - inBal.uiTokenAmount.uiAmount - const diff = Number(outBal.uiTokenAmount.amount) - Number(inBal.uiTokenAmount.amount) + const lamDiff = (outBal.uiTokenAmount.uiAmount || 0) - (inBal.uiTokenAmount.uiAmount || 0) + const diff = Number(outBal.uiTokenAmount.amount || 0) - Number(inBal.uiTokenAmount.amount || 0) // we definitely didn't swap for nothing if (diff) { outAmount = diff @@ -2389,9 +2505,9 @@ export class SolanaService extends Service { * @returns {Promise} - A promise that resolves once the Solana service has stopped. */ static async stop(runtime: IAgentRuntime) { - const client = runtime.getService(SOLANA_SERVICE_NAME); + const client = runtime.getService(SOLANA_SERVICE_NAME) as SolanaService | null; if (!client) { - logger.error('SolanaService not found'); + logger.error('SolanaService not found during static stop'); return; } await client.stop(); @@ -2402,14 +2518,16 @@ export class SolanaService extends Service { * @returns {Promise} A Promise that resolves when the update interval is stopped. */ async stop(): Promise { + this.runtime.logger.info('SolanaService: Stopping instance...'); // Unsubscribe from all accounts for (const [address] of this.subscriptions) { - await this.unsubscribeFromAccount(address); - } - - if (this.updateInterval) { - clearInterval(this.updateInterval); - this.updateInterval = null; + await this.unsubscribeFromAccount(address).catch((e) => + this.runtime.logger.error( + `Error unsubscribing from ${address} during stop:`, + e instanceof Error ? e.message : String(e) + ) + ); } + this.subscriptions.clear(); } } From 2c7f488f329149b9e2598c7e5165b171003d3e32 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Thu, 16 Oct 2025 21:16:30 +0000 Subject: [PATCH 10/36] SOL_ADDRESS isn't required --- src/environment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/environment.ts b/src/environment.ts index 46ade51..2e1e047 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -39,7 +39,7 @@ export const solanaEnvSchema = z ) .and( z.object({ - SOL_ADDRESS: z.string().min(1, 'SOL address is required'), + //SOL_ADDRESS: z.string().min(1, 'SOL address is required'), SLIPPAGE: z.string().min(1, 'Slippage is required'), SOLANA_RPC_URL: z.string().min(1, 'RPC URL is required'), //HELIUS_API_KEY: z.string().min(1, 'Helius API key is required'), From be2e9a5b84f56b03b054b5bab5e058ec37b2260c Mon Sep 17 00:00:00 2001 From: Odilitime Date: Thu, 16 Oct 2025 21:16:43 +0000 Subject: [PATCH 11/36] minor clean up --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 1aae20a..6751791 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,7 +36,7 @@ export const solanaPlugin: Plugin = { runtime.registerProvider(walletProvider) // extensions - const p = runtime.getServiceLoadPromise('INTEL_CHAIN').then( () => { + runtime.getServiceLoadPromise('INTEL_CHAIN').then( () => { //runtime.logger.log('solana INTEL_CHAIN LOADED') const traderChainService = runtime.getService('INTEL_CHAIN') as any; const me = { From 7812a627ae0bf3f704e2f84a285f3730f93766fe Mon Sep 17 00:00:00 2001 From: Odilitime Date: Thu, 16 Oct 2025 21:20:23 +0000 Subject: [PATCH 12/36] bump tsup/typescript versions --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 30a6921..c3f0886 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,8 @@ }, "devDependencies": { "prettier": "3.5.3", - "tsup": "8.4.0", - "typescript": "^5.8.2" + "tsup": "8.5.0", + "typescript": "^5.8.3" }, "scripts": { "build": "tsup", From a859be44cd61d5afe856e0fda0614db9d9502cf7 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Thu, 16 Oct 2025 21:22:59 +0000 Subject: [PATCH 13/36] go to TS 5.9.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c3f0886..e5b7601 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "devDependencies": { "prettier": "3.5.3", "tsup": "8.5.0", - "typescript": "^5.8.3" + "typescript": "5.9.3" }, "scripts": { "build": "tsup", From e93026c3b2ab695b2ca2dbef679e7bff9ca7c8e8 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Thu, 16 Oct 2025 21:23:23 +0000 Subject: [PATCH 14/36] fix ServiceTypeName type --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6751791..a309330 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import type { IAgentRuntime, Plugin } from '@elizaos/core'; +import type { IAgentRuntime, Plugin, ServiceTypeName } from '@elizaos/core'; import { parseBooleanFromText } from '@elizaos/core'; // actions @@ -36,7 +36,7 @@ export const solanaPlugin: Plugin = { runtime.registerProvider(walletProvider) // extensions - runtime.getServiceLoadPromise('INTEL_CHAIN').then( () => { + runtime.getServiceLoadPromise('INTEL_CHAIN' as ServiceTypeName).then( () => { //runtime.logger.log('solana INTEL_CHAIN LOADED') const traderChainService = runtime.getService('INTEL_CHAIN') as any; const me = { From e4c08ba3309aed77062f1af085d681670b2b8484 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Fri, 17 Oct 2025 02:11:39 +0000 Subject: [PATCH 15/36] bun build --- build.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 build.ts diff --git a/build.ts b/build.ts new file mode 100644 index 0000000..8ef83d1 --- /dev/null +++ b/build.ts @@ -0,0 +1,38 @@ +#!/usr/bin/env bun +/** + * Build script for @elizaos/plugin-bootstrap using standardized build utilities + */ + +import { createBuildRunner } from '../../build-utils'; + +// Create and run the standardized build runner +const run = createBuildRunner({ + packageName: '@elizaos/plugin-solana', + buildOptions: { + entrypoints: ['src/index.ts'], + outdir: 'dist', + target: 'bun', // instead of 'node' + format: 'esm', + strict: true, + clean: true, + external: [ + // keep third-party externals + 'dotenv','@reflink/reflink','@node-llama-cpp', + 'agentkeepalive','safe-buffer','base-x','bs58','borsh', + '@solana/buffer-layout','querystring', + '@elizaos/core','@elizaos/service-interfaces','zod', + 'node:stream/web', // optional if you reference it; bun-types has it + 'fs','path','https','http','stream','buffer' + ], + sourcemap: true, + minify: false, + generateDts: false, + }, +}); + + +// Execute the build +run().catch((error) => { + //console.error('Build script error:', error); + process.exit(1); +}); From 878a32df1e21f1cdba9e3d06967816ff24b886dd Mon Sep 17 00:00:00 2001 From: Odilitime Date: Fri, 17 Oct 2025 02:13:31 +0000 Subject: [PATCH 16/36] switch to bun build, include service-interfaces, bun-types, more deps around, tsup/engines key --- package.json | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index e5b7601..b3c0d4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "@elizaos/plugin-solana", "version": "1.2.0", + "engines": { "bun": ">=1.1.0" }, "type": "module", "main": "dist/index.js", "module": "dist/index.js", @@ -21,34 +22,41 @@ "files": [ "dist" ], + "scripts": { + "build": "bun run build.ts", + "dev": "tsup --watch", + "test": "vitest run", + "lint": "prettier --write ./src", + "clean": "rm -rf dist .turbo node_modules .turbo-tsconfig.json tsconfig.tsbuildinfo", + "format": "prettier --write ./src", + "format:check": "prettier --check ./src" + }, "dependencies": { - "@elizaos/core": "latest", "@solana/spl-token": "0.4.14", "@solana/spl-token-metadata": "^0.1.6", "@solana/web3.js": "^1.98.0", "bignumber.js": "9.3.0", "bs58": "6.0.0", - "tweetnacl": "^1.0.3", - "vitest": "3.1.3" + "tweetnacl": "^1.0.3" }, "devDependencies": { + "@elizaos/core": "latest", + "@elizaos/service-interfaces": "latest", + "bun-types": "^1.3.0", "prettier": "3.5.3", "tsup": "8.5.0", - "typescript": "5.9.3" - }, - "scripts": { - "build": "tsup", - "dev": "tsup --watch", - "test": "vitest run", - "lint": "prettier --write ./src", - "clean": "rm -rf dist .turbo node_modules .turbo-tsconfig.json tsconfig.tsbuildinfo", - "format": "prettier --write ./src", - "format:check": "prettier --check ./src" + "typescript": "^5.9.3", + "vitest": "3.1.3" }, "peerDependencies": { + "@elizaos/core": "latest", + "@elizaos/service-interfaces": "latest", "form-data": "4.0.2", "whatwg-url": "7.1.0" }, + "tsup": { + "external": ["@elizaos/core", "@elizaos/service-interfaces"] + }, "gitHead": "646c632924826e2b75c2304a75ee56959fe4a460", "agentConfig": { "pluginType": "elizaos:plugin:1.0.0", From 5a04d1a60da25b710408ba577c312f015f6c3a45 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Fri, 17 Oct 2025 02:14:03 +0000 Subject: [PATCH 17/36] modernize, fix types --- src/actions/swap.ts | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/actions/swap.ts b/src/actions/swap.ts index 76b30f0..f69d1e4 100644 --- a/src/actions/swap.ts +++ b/src/actions/swap.ts @@ -1,6 +1,7 @@ import { type Action, type ActionExample, + type ActionResult, type HandlerCallback, type IAgentRuntime, type Memory, @@ -72,11 +73,11 @@ async function swapToken( const amountBN = new BigNumber(amount); const adjustedAmount = amountBN.multipliedBy(new BigNumber(10).pow(decimals)); - logger.log('Fetching quote with params:', { + logger.log({ inputMint: inputTokenCA, outputMint: outputTokenCA, amount: adjustedAmount, - }); + }, 'Fetching quote with params:'); const quoteResponse = await fetch( `https://quote-api.jup.ag/v6/quote?inputMint=${inputTokenCA}&outputMint=${outputTokenCA}&amount=${adjustedAmount}&dynamicSlippage=true&maxAccounts=64` @@ -86,7 +87,7 @@ async function swapToken( }; if (!quoteData || quoteData.error) { - logger.error('Quote error:', quoteData); + logger.error({ quoteData },'Quote error'); throw new Error(`Failed to get quote: ${quoteData?.error || 'Unknown error'}`); } @@ -113,7 +114,7 @@ async function swapToken( }; if (!swapData || !swapData.swapTransaction) { - logger.error('Swap error:', swapData); + logger.error({ swapData }, 'Swap error'); throw new Error( `Failed to get swap transaction: ${swapData?.error || 'No swap transaction returned'}` ); @@ -121,7 +122,7 @@ async function swapToken( return swapData; } catch (error) { - logger.error('Error in swapToken:', error); + logger.error({ error }, 'Error in swapToken:'); throw error; } } @@ -155,7 +156,7 @@ async function getTokenFromWallet( return token ? token.address : null; } catch (error) { - logger.error('Error checking token in wallet:', error); + logger.error({ error }, 'Error checking token in wallet'); return null; } } @@ -250,7 +251,7 @@ export const executeSwap: Action = { state: State | undefined, _options: { [key: string]: unknown } | undefined, callback?: HandlerCallback - ): Promise => { + ): Promise => { state = await runtime.composeState(message, ['RECENT_MESSAGES']); try { @@ -293,7 +294,7 @@ export const executeSwap: Action = { (await getTokenFromWallet(runtime, response.inputTokenSymbol)) || undefined; if (!response.inputTokenCA) { callback?.({ text: 'Could not find the input token in your wallet' }); - return false; + return; } } @@ -304,13 +305,13 @@ export const executeSwap: Action = { callback?.({ text: 'Could not find the output token in your wallet', }); - return false; + return; } } if (!response.amount) { callback?.({ text: 'Please specify the amount you want to swap' }); - return false; + return; } const connection = new Connection( @@ -365,15 +366,15 @@ export const executeSwap: Action = { content: { success: true, txid }, }); - return true; + return; } catch (error) { if (error instanceof Error) { - logger.error('Error during token swap:', error); + logger.error({ error }, 'Error during token swap'); callback?.({ text: `Swap failed: ${error.message}`, content: { error: error.message }, }); - return false; + return; } throw error; } From c02c2080cbd74c15529c95a254086559d0697b19 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Fri, 17 Oct 2025 02:14:26 +0000 Subject: [PATCH 18/36] modernize, guards/fix types --- src/actions/transfer.ts | 61 +++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/src/actions/transfer.ts b/src/actions/transfer.ts index f95f918..0a0115f 100644 --- a/src/actions/transfer.ts +++ b/src/actions/transfer.ts @@ -1,6 +1,7 @@ import { type Action, type ActionExample, + type ActionResult, type Content, type HandlerCallback, type IAgentRuntime, @@ -45,19 +46,18 @@ interface TransferContent extends Content { * @param {TransferContent} content - The content to be validated for transfer. * @returns {boolean} Returns true if the content is valid for transfer, and false otherwise. */ -function isTransferContent(content: TransferContent): boolean { - logger.log('Content for transfer', content); +function isTransferContent(content: unknown): content is TransferContent { + if (!content || typeof content !== 'object') return false; + const c = content as Partial>; // Base validation - if (!content.recipient || typeof content.recipient !== 'string' || !content.amount) { - return false; - } + if (typeof c.recipient !== 'string') return false; + if (!(typeof c.amount === 'string' || typeof c.amount === 'number')) return false; - if (content.tokenAddress === 'null') { - content.tokenAddress = null; - } + // Don’t mutate here; just validate. Treat 'null' as valid string; normalize later. + if (c.tokenAddress !== null && typeof c.tokenAddress !== 'string') return false; - return typeof content.amount === 'string' || typeof content.amount === 'number'; + return true; } /** @@ -134,8 +134,8 @@ export default { 'PAY_TOKENS_SOLANA', 'PAY_SOLANA', ], - validate: async (_runtime: IAgentRuntime, message: Memory) => { - logger.log('Validating transfer from entity:', message.entityId); + validate: async (runtime: IAgentRuntime, message: Memory) => { + runtime.logger.log('Validating transfer from entity:', message.entityId); return true; }, description: 'Transfer SOL or SPL tokens to another address on Solana.', @@ -145,7 +145,7 @@ export default { state: State, _options: { [key: string]: unknown }, callback?: HandlerCallback - ): Promise => { + ): Promise => { logger.log('Starting TRANSFER handler...'); const transferPrompt = composePromptFromState({ @@ -159,6 +159,16 @@ export default { const content = parseJSONObjectFromText(result); + if (!content) { + if (callback) { + callback({ + text: 'Need a valid recipient address and amount to transfer.', + content: { error: 'Invalid transfer content' }, + }); + } + return; + } + if (!isTransferContent(content)) { if (callback) { callback({ @@ -166,11 +176,20 @@ export default { content: { error: 'Invalid transfer content' }, }); } - return false; + return; } try { const { keypair: senderKeypair } = await getWalletKey(runtime, true); + if (!senderKeypair) { + if (callback) { + callback({ + text: 'Need a valid agent address.', + content: { error: 'Invalid transfer content' }, + }); + } + return; + } const connection = new Connection( runtime.getSetting('SOLANA_RPC_URL') || 'https://api.mainnet-beta.solana.com' ); @@ -270,16 +289,22 @@ export default { } } - return true; + return; } catch (error) { - logger.error('Error during transfer:', error); + logger.error({ error },'Error during transfer'); if (callback) { + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : JSON.stringify(error); callback({ - text: `Transfer failed: ${error.message}`, - content: { error: error.message }, + text: `Transfer failed: ${message}`, + content: { error: message }, }); } - return false; + return; } }, From 47c87b8ae8928aef8d444ff67f24bdeb2c07b3cb Mon Sep 17 00:00:00 2001 From: Odilitime Date: Fri, 17 Oct 2025 02:14:42 +0000 Subject: [PATCH 19/36] newer zod uses issues not errors --- src/environment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/environment.ts b/src/environment.ts index 2e1e047..3ae2ad4 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -77,7 +77,7 @@ export async function validateSolanaConfig(runtime: IAgentRuntime): Promise `${err.path.join('.')}: ${err.message}`) .join('\n'); throw new Error(`Solana configuration validation failed:\n${errorMessages}`); From dad5abc7467893a446e86349086046c6f3be1beb Mon Sep 17 00:00:00 2001 From: Odilitime Date: Fri, 17 Oct 2025 02:14:54 +0000 Subject: [PATCH 20/36] fix types --- src/keypairUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/keypairUtils.ts b/src/keypairUtils.ts index fa93ae2..5c93041 100644 --- a/src/keypairUtils.ts +++ b/src/keypairUtils.ts @@ -44,14 +44,14 @@ export async function getWalletKey( const secretKey = bs58.decode(privateKeyString); return { keypair: Keypair.fromSecretKey(secretKey) }; } catch (e) { - logger.log('Error decoding base58 private key:', e); + logger.log({ e }, 'Error decoding base58 private key:'); try { // Then try base64 logger.log('Try decoding base64 instead'); const secretKey = Uint8Array.from(Buffer.from(privateKeyString, 'base64')); return { keypair: Keypair.fromSecretKey(secretKey) }; } catch (e2) { - logger.error('Error decoding private key: ', e2); + logger.error({ e: e2 }, 'Error decoding private key: '); throw new Error('Invalid private key format'); } } From dfbe9d09e8c586d75d38e81684e5f3d2b184b09d Mon Sep 17 00:00:00 2001 From: Odilitime Date: Fri, 17 Oct 2025 02:15:08 +0000 Subject: [PATCH 21/36] update comment to match --- tsup.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsup.config.ts b/tsup.config.ts index 3449114..d8cc1b9 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ sourcemap: true, clean: true, strict: true, - format: ['esm'], // Ensure you're targeting CommonJS + format: ['esm'], // ESM build (Bun-friendly) dts: true, external: [ 'dotenv', // Externalize dotenv to prevent bundling From 3b1065c557f4d4ef75199692e95919aea4e0bc0d Mon Sep 17 00:00:00 2001 From: Odilitime Date: Fri, 17 Oct 2025 02:15:58 +0000 Subject: [PATCH 22/36] change lib, remove paths/baseUrl, gpt reorder --- tsconfig.json | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 7a69139..2e8c616 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,24 +2,27 @@ "compilerOptions": { "outDir": "dist", "rootDir": "src", - "baseUrl": "../..", - "lib": ["ESNext"], + + // Bun runtime ≈ modern JS + Web APIs "target": "ESNext", "module": "Preserve", "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, "esModuleInterop": true, - "allowImportingTsExtensions": true, + "resolveJsonModule": true, "declaration": true, "declarationDir": "dist", "emitDeclarationOnly": true, - "resolveJsonModule": true, + + // *** Bun types only *** + "types": ["bun"], + + // Nice-to-haves for Bun + bundlers + "allowImportingTsExtensions": true, "moduleDetection": "force", - "allowArbitraryExtensions": true, - "paths": { - "@elizaos/core": ["../core/src"], - "@elizaos/core/*": ["../core/src/*"] - } + "allowArbitraryExtensions": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] From 00695c6f32c8403f6a45eb337e27907ada0fe1cb Mon Sep 17 00:00:00 2001 From: Odilitime Date: Fri, 17 Oct 2025 02:18:17 +0000 Subject: [PATCH 23/36] bun types, no declaration --- tsconfig.build.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tsconfig.build.json b/tsconfig.build.json index baff6c6..cc135b7 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -7,8 +7,8 @@ "strict": true, "sourceMap": true, "inlineSources": true, - "declaration": true, - "emitDeclarationOnly": true + "emitDeclarationOnly": true, + "types": ["bun"], }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] From 1dfee25e75e5c7c083c17fb57280c4f2dc2b216a Mon Sep 17 00:00:00 2001 From: Odilitime Date: Fri, 17 Oct 2025 02:18:49 +0000 Subject: [PATCH 24/36] ISolanaPluginServiceAPI/IWalletService extension/support, type fixes --- src/service.ts | 147 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 124 insertions(+), 23 deletions(-) diff --git a/src/service.ts b/src/service.ts index b6eadec..2dbfc1e 100644 --- a/src/service.ts +++ b/src/service.ts @@ -1,4 +1,5 @@ -import { type IAgentRuntime, ServiceTypeName, Service, logger } from '@elizaos/core'; +import { type IAgentRuntime, ServiceTypeName, Service, ServiceType, logger } from '@elizaos/core'; +import { IWalletService, WalletPortfolio as siWalletPortfolio } from '@elizaos/service-interfaces'; import { Connection, Keypair, @@ -59,14 +60,47 @@ async function setCacheExp(runtime: IAgentRuntime, key: string, val: any, ttlInS }); } +export interface ISolanaPluginServiceAPI extends Service { + executeSwap: ( + wallets: Array<{ keypair: any; amount: number }>, signal: any + ) => Promise>; + /* + executeSwap: (params: { + inputMint: string; + outputMint: string; + amount: string; // Amount in base units of input token + slippageBps: number; + payerAddress: string; // Public key of the payer (must match service's configured wallet) + priorityFeeMicroLamports?: number; + }) => Promise<{ + success: boolean; + signature?: string; + error?: string; + outAmount?: string; + inAmount?: string; + swapUsdValue?: string; + }>; + */ + //getSolBalance: (publicKey: string) => Promise; // Returns SOL balance (not lamports) + /* + getTokenBalance: ( + publicKey: string, + mintAddress: string + ) => Promise<{ amount: string; decimals: number; uiAmount: number } | null>; + */ + getPublicKey: () => PublicKey | null; // Returns base58 public key +} + /** * Service class for interacting with the Solana blockchain and accessing wallet data. * @extends Service */ -export class SolanaService extends Service { - static serviceType: string = SOLANA_SERVICE_NAME; - capabilityDescription = - 'The agent is able to interact with the Solana blockchain, and has access to the wallet data'; +export class SolanaService extends IWalletService implements ISolanaPluginServiceAPI { + readonly serviceName = SOLANA_SERVICE_NAME; + //static override readonly serviceType = ServiceType.WALLET; + //static serviceType: string = SOLANA_SERVICE_NAME; + public readonly capabilityDescription = + ('The agent is able to interact with the Solana blockchain, and has access to the wallet data' as unknown as typeof IWalletService.prototype.capabilityDescription); private lastUpdate = 0; private readonly UPDATE_INTERVAL = 2 * 60_000; // 2 minutes @@ -93,8 +127,9 @@ export class SolanaService extends Service { * Constructor for creating an instance of the class. * @param {IAgentRuntime} runtime - The runtime object that provides access to agent-specific functionality. */ - constructor(protected runtime: IAgentRuntime) { - super(); + constructor(runtime?: IAgentRuntime) { + if (!runtime) throw new Error('runtime is required for solana service') + super(runtime); this.exchangeRegistry = {}; const connection = new Connection( runtime.getSetting('SOLANA_RPC_URL') || PROVIDER_CONFIG.DEFAULT_RPC @@ -133,6 +168,64 @@ export class SolanaService extends Service { this.subscriptions = new Map(); } + // + // MARK: IWalletService + // + + /** + * Retrieves the entire portfolio of assets held by the wallet. + * @param owner - Optional: The specific wallet address/owner to query. + * @returns A promise that resolves to the wallet's portfolio. + */ + public async getPortfolio(owner?: string): Promise { + if (owner && owner !== this.publicKey?.toBase58()) { + throw new Error( + `This SolanaService instance can only get the portfolio for its configured wallet: ${this.publicKey?.toBase58()}` + ); + } + const wp: WalletPortfolio = await this.updateWalletData(true) + const out: siWalletPortfolio = { + totalValueUsd: parseFloat(wp.totalUsd), + assets: [] + } + return out; + } + + /** + * Retrieves the balance of a specific asset in the wallet. + * @param assetAddress - The mint address or native identifier ('SOL') of the asset. + * @param owner - Optional: The specific wallet address/owner to query. + * @returns A promise that resolves to the user-friendly (decimal-adjusted) balance of the asset held. + */ + public async getBalance(assetAddress: string, owner?: string): Promise { + const ownerAddress: string | undefined = owner || (await this.getPublicKey()?.toBase58()); + if (!ownerAddress) { + return -1 + } + if ( + assetAddress.toUpperCase() === 'SOL' || + assetAddress === PROVIDER_CONFIG.TOKEN_ADDRESSES.SOL + ) { + //return this.getSolBalance(ownerAddress); + const balances = await this.getBalancesByAddrs([ownerAddress]) + const balance = balances[ownerAddress] + return balance + } + //const tokenBalance = await this.getTokenBalance(ownerAddress, assetAddress); + //return tokenBalance?.uiAmount || 0; + const tokenBalances: any = await this.getTokenAccountsByKeypairs([ownerAddress]) + const balance: number = tokenBalances[ownerAddress]?.balanceUi || 0 + return balance + } + + async transferSol(from: any, to: any, lamports: number): Promise { + return '' + } + + // + // MARK: End IWalletService + // + /** * Retrieves the connection object. * @@ -755,21 +848,26 @@ export class SolanaService extends Service { return { address: CAs[idx], error: 'Account not found' }; } - const data = Uint8Array.from(Buffer.from(accountInfo.data)); - const mint = MintLayout.decode(data); - // mintAuthority, supply, decimals, isInitialized, freezeAuthorityOption, freezeAuthority - //console.log('mint', mint) + // accountInfo.data is a Node Buffer; make a Uint8Array *view* (no copy) + const buf = accountInfo.data as Buffer; + const u8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); + + // MintLayout.decode accepts Uint8Array (and Buffer). Use u8 to avoid type fuss. + const mint = MintLayout.decode(u8); + + // Normalize types + const decimals: number = mint.decimals; + const supply: bigint = BigInt(mint.supply.toString()); // ensure bigint - // Convert Buffer (little endian) to BigNumber - const supply = mint.supply; - const decimals = mint.decimals; + // bigint-safe 10^decimals + let denom = 1n; + for (let i = 0; i < decimals; i++) denom *= 10n; return { address: CAs[idx], - biSupply: supply, // or divide by 10**decimals if you want human-readable - // BigNumber is good for price for MCAP - human: new BigNumber((supply / BigInt(10 ** decimals)).toString()), - // maybe it should be a string... and they w/e use can cast it as such + biSupply: supply, // keep as bigint for exactness + // Human-readable (use BigNumber to avoid float issues for large values) + human: new BigNumber(supply.toString()).dividedBy(10 ** decimals), decimals, }; }); @@ -1066,8 +1164,11 @@ export class SolanaService extends Service { } } else { // spl token - const data = info?.data; - const header = data.subarray(0, MintLayout.span); + const buf = info!.data as Buffer; + const u8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); + + // slice the header as a Uint8Array, not Buffer + const header = u8.subarray(0, MintLayout.span); const mintData = MintLayout.decode(header); //console.log('spl mintData', mintData) const uiSupply = formatSupplyUiAmount(mintData.supply, mintData.decimals) @@ -2070,7 +2171,7 @@ export class SolanaService extends Service { * @param {any} signal - Trading signal information * @returns {Promise>} */ - public async executeSwap(wallets: Array<{ keypair: any; amount: number }>, signal: any) { + public async executeSwap(wallets: Array<{ keypair: any; amount: number }>, signal: any): Promise> { // do it in serial to avoid hitting rate limits const swapResponses = {} for(const wallet of wallets) { @@ -2491,7 +2592,7 @@ export class SolanaService extends Service { * @param {IAgentRuntime} runtime - The agent runtime to use for the Solana service. * @returns {Promise} The initialized Solana service. */ - static async start(runtime: IAgentRuntime): Promise { + static async start(runtime: IAgentRuntime): Promise { logger.log(`SolanaService start for ${runtime.character.name}`); const solanaService = new SolanaService(runtime); @@ -2504,7 +2605,7 @@ export class SolanaService extends Service { * @param {IAgentRuntime} runtime - The agent runtime. * @returns {Promise} - A promise that resolves once the Solana service has stopped. */ - static async stop(runtime: IAgentRuntime) { + static async stop(runtime: IAgentRuntime): Promise { const client = runtime.getService(SOLANA_SERVICE_NAME) as SolanaService | null; if (!client) { logger.error('SolanaService not found during static stop'); From 29a91dc7da766b2b375fbfac9e030908dc04ec24 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Fri, 17 Oct 2025 02:38:10 +0000 Subject: [PATCH 25/36] remove types --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index b3c0d4a..15f1f72 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "type": "module", "main": "dist/index.js", "module": "dist/index.js", - "types": "dist/index.d.ts", "repository": { "type": "git", "url": "git+https://github.com/elizaos-plugins/plugin-solana.git" From ecccb0906c3ef280168c407a8a1c8f14094bd48a Mon Sep 17 00:00:00 2001 From: Odilitime Date: Fri, 17 Oct 2025 02:40:07 +0000 Subject: [PATCH 26/36] make only public key required --- src/environment.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/environment.ts b/src/environment.ts index 3ae2ad4..8222f0d 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -29,11 +29,11 @@ export const solanaEnvSchema = z .and( z.union([ z.object({ - SOLANA_PRIVATE_KEY: z.string().min(1, 'Solana secret key is required'), + SOLANA_PRIVATE_KEY: z.string().min(1).optional(), SOLANA_PUBLIC_KEY: z.string().min(1, 'Solana public key is required'), }), z.object({ - SOLANA_SECRET_SALT: z.string().min(1, 'Solana secret salt is required'), + SOLANA_SECRET_SALT: z.string().min(1).optional(), }), ]) ) From 759f9cbab7b36fe31fd53e2e9fb823bcefe2e620 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Fri, 17 Oct 2025 02:40:21 +0000 Subject: [PATCH 27/36] getPublicKey is not async --- src/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service.ts b/src/service.ts index 2dbfc1e..5a65e56 100644 --- a/src/service.ts +++ b/src/service.ts @@ -198,7 +198,7 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic * @returns A promise that resolves to the user-friendly (decimal-adjusted) balance of the asset held. */ public async getBalance(assetAddress: string, owner?: string): Promise { - const ownerAddress: string | undefined = owner || (await this.getPublicKey()?.toBase58()); + const ownerAddress: string | undefined = owner || (this.getPublicKey()?.toBase58()); if (!ownerAddress) { return -1 } From a1d5e52147c15bdd443b90dd6fbcda545026ce9f Mon Sep 17 00:00:00 2001 From: Odilitime Date: Fri, 17 Oct 2025 02:46:13 +0000 Subject: [PATCH 28/36] getPortfolio - fix assets --- src/service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/service.ts b/src/service.ts index 5a65e56..3a8d6eb 100644 --- a/src/service.ts +++ b/src/service.ts @@ -186,7 +186,13 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic const wp: WalletPortfolio = await this.updateWalletData(true) const out: siWalletPortfolio = { totalValueUsd: parseFloat(wp.totalUsd), - assets: [] + assets: wp.items.map(i => ({ + address: i.address, + symbol: i.symbol, + balance: Number(i.uiAmount ?? 0), + decimals: i.decimals, + valueUsd: Number(i.valueUsd ?? 0), + })), } return out; } From f020d0e217ad2084a9ffa988708a1c218d714993 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Fri, 17 Oct 2025 03:20:06 +0000 Subject: [PATCH 29/36] implement transferSol, fetchPrices update key, getWalletKeypair use property instead of rederriving, getWalletBalances() start --- src/service.ts | 142 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 135 insertions(+), 7 deletions(-) diff --git a/src/service.ts b/src/service.ts index 3a8d6eb..3d3e4bd 100644 --- a/src/service.ts +++ b/src/service.ts @@ -224,10 +224,57 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic return balance } - async transferSol(from: any, to: any, lamports: number): Promise { - return '' - } + /** + * Transfers SOL from a specified keypair to a public key. + * The service's own wallet is used to pay transaction fees. + * @param {Keypair} from - The keypair of the account to send SOL from. + * @param {PublicKey} to - The public key of the account to send SOL to. + * @param {number} lamports - The amount of SOL to send, in lamports. + * @returns {Promise} The transaction signature. + * @throws {Error} If the transfer fails. + */ + public async transferSol(from: Keypair, to: PublicKey, lamports: number): Promise { + try { + if (!this.servicePublicKey) { + throw new Error( + 'SolanaService is not initialized with a fee payer key, cannot send transaction.' + ); + } + + const transaction = new TransactionMessage({ + payerKey: this.servicePublicKey, + recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, + instructions: [ + SystemProgram.transfer({ + fromPubkey: from.publicKey, + toPubkey: to, + lamports: lamports, + }), + ], + }).compileToV0Message(); + + const versionedTransaction = new VersionedTransaction(transaction); + + const serviceKeypair = await this.getServiceKeypair(); + versionedTransaction.sign([from, serviceKeypair]); + + const signature = await this.connection.sendTransaction(versionedTransaction, { + skipPreflight: false, + }); + const confirmation = await this.connection.confirmTransaction(signature, 'confirmed'); + if (confirmation.value.err) { + throw new Error( + `Transaction confirmation failed: ${JSON.stringify(confirmation.value.err)}` + ); + } + + return signature; + } catch (error: unknown) { + logger.error('SolanaService: transferSol failed:', error); + throw error; + } + } // // MARK: End IWalletService // @@ -575,7 +622,7 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic * @returns A Promise that resolves to an object containing the prices of SOL, BTC, and ETH tokens. */ private async fetchPrices(): Promise { - const cacheKey = 'prices'; + const cacheKey = 'prices_sol_btc_eth'; const cachedValue = await this.runtime.getCache(cacheKey); // if cachedValue is JSON, parse it @@ -1581,7 +1628,7 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic * @throws {Error} If private key is not available */ private async getWalletKeypair(): Promise { - const { keypair } = await getWalletKey(this.runtime, true); + const keypair = this.publicKey; if (!keypair) { throw new Error('Failed to get wallet keypair'); } @@ -1892,7 +1939,6 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic //console.log('walletAddressArr', walletAddressArr) const publicKeyObjs = walletAddressArr.map(k => new PublicKey(k)); //console.log('getBalancesByAddrs - getMultipleAccountsInfo') - //const accounts = await this.connection.getMultipleAccountsInfo(publicKeyObjs); const accounts = await this.batchGetMultipleAccountsInfo(publicKeyObjs, 'getBalancesByAddrs'); //console.log('getBalancesByAddrs - accounts', accounts) @@ -1976,6 +2022,88 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic // MARK: wallet Associated Token Account (ATA) // + // single wallet, list of tokens + public async getWalletBalances(publicKeyStr: string, mintAddresses: string[]): Promise<{ + amount: string; + decimals: number; + uiAmount: number; + } | null> { + + const owner = new PublicKey(publicKeyStr); + const mints = mintAddresses.map(m => new PublicKey(m)); + + // 1) Derive ATAs for both programs + const ataPairs = mints.map(mint => { + const ataLegacy = getAssociatedTokenAddressSync( + mint, owner, false, TOKEN_PROGRAM_ID + ); + const ata2022 = getAssociatedTokenAddressSync( + mint, owner, false, TOKEN_2022_PROGRAM_ID + ); + return { mint, ataLegacy, ata2022 }; + }); + + // 2) Batch fetch token accounts (both program ATAs) + const allAtaAddrs = ataPairs.flatMap(p => [p.ataLegacy, p.ata2022]); + const ataInfos = await this.batchGetMultipleAccountsInfo(allAtaAddrs, 'getWalletBalances'); + + // 3) Batch fetch mint accounts (for decimals) + //const mintInfos = await getMultiple(connection, mints, opts?.commitment); + const mintInfos = await this.batchGetMultipleAccountsInfo(mints, 'getWalletBalances'); + + // 4) Build quick lookups + const mintDecimals = new Map(); + mints.forEach((mintPk, i) => { + const acc = mintInfos[i]; + if (!acc) return; + // MintLayout.decode expects acc.data to be a Buffer of correct length + const mintData = MintLayout.decode(acc.data); + mintDecimals.set(mintPk.toBase58(), mintData.decimals); + }); + + const byAddress = new Map | null>(); + allAtaAddrs.forEach((ata, i) => { + const info = ataInfos[i]; + if (!info) { + byAddress.set(ata.toBase58(), null); + return; + } + byAddress.set(ata.toBase58(), AccountLayout.decode(info.data)); + }); + + // 5) Assemble balances; prefer legacy program over 2022 if both exist + const out: Record = {}; + + for (const { mint, ataLegacy, ata2022 } of ataPairs) { + const mintStr = mint.toBase58(); + const decimals = mintDecimals.get(mintStr); + // If we don’t know decimals (mint account not found), we can’t compute uiAmount + if (decimals === undefined) { + out[mintStr] = null; + continue; + } + + const legacy = byAddress.get(ataLegacy.toBase58()); + const tok2022 = byAddress.get(ata2022.toBase58()); + + // Choose which token account to use: + const chosen = legacy ?? tok2022; + if (!chosen) { + out[mintStr] = null; // ATA doesn’t exist → zero balance + continue; + } + + // AccountLayout amount is a u64 in little-endian buffer + const rawAmount = BigInt(chosen.amount.toString()); // AccountLayout already gives a BN-like + const amountStr = rawAmount.toString(); + const uiAmount = Number(rawAmount) / 10 ** decimals; + + out[mintStr] = { amount: amountStr, decimals, uiAmount }; + } + + return out; + } + // 5 calls to get a balance for 500 wallets public async getTokenBalanceForWallets(mint: PublicKey, walletAddresses: string[]): Promise> { const walletPubkeys = walletAddresses.map(a => new PublicKey(a)); @@ -2578,7 +2706,7 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic (swapResponses as any)[pubKey] = { success: true, outAmount, - outDecimal: await this.getDecimal(signal.targetTokenCA), + outDecimal: await this.getDecimal(new PublicKey(signal.targetTokenCA)), signature: txid, fees, swapResponse, From ae7546e052ed3876b5a851e3f97ad9eb78fbd901 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Mon, 20 Oct 2025 22:22:04 +0000 Subject: [PATCH 30/36] split SolanaWalletService from SolanaService --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index a309330..7285add 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,14 +9,14 @@ import transferToken from './actions/transfer'; import { walletProvider } from './providers/wallet'; // service -import { SolanaService } from './service'; +import { SolanaService, SolanaWalletService } from './service'; import { SOLANA_SERVICE_NAME } from './constants'; export const solanaPlugin: Plugin = { name: SOLANA_SERVICE_NAME, description: 'Solana blockchain plugin', - services: [SolanaService], + services: [SolanaService, SolanaWalletService], init: async (_, runtime: IAgentRuntime) => { // Validation @@ -53,5 +53,5 @@ export default solanaPlugin; // Export additional items for use by other plugins export { SOLANA_SERVICE_NAME } from './constants'; -export { SolanaService } from './service'; +export { SolanaService, SolanaWalletService } from './service'; export type { SolanaService as ISolanaService } from './service'; \ No newline at end of file From a5afbb533313851fb0b75f2999a487c12aab3601 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Mon, 20 Oct 2025 22:24:47 +0000 Subject: [PATCH 31/36] split SolanaWalletService from SolanaService, adjust private/public, type fixes --- src/service.ts | 280 ++++++++++++++++++++++++++++++------------------- 1 file changed, 173 insertions(+), 107 deletions(-) diff --git a/src/service.ts b/src/service.ts index 3d3e4bd..3d3e961 100644 --- a/src/service.ts +++ b/src/service.ts @@ -8,11 +8,14 @@ import { SendTransactionError, LAMPORTS_PER_SOL, type AccountInfo, + SystemProgram, + Transaction, + TransactionMessage, } from '@solana/web3.js'; import { MintLayout, getMint, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, unpackAccount, getAssociatedTokenAddressSync, ExtensionType, getExtensionData, getExtensionTypes, - unpackMint, + unpackMint, AccountLayout } from "@solana/spl-token"; // parses the raw Token-2022 metadata struct import { unpack as unpackToken2022Metadata } from '@solana/spl-token-metadata'; @@ -35,6 +38,14 @@ const PROVIDER_CONFIG = { }, }; +export type MintBalance = { + amount: string; + decimals: number; + uiAmount: number; +}; + + + const METADATA_PROGRAM_ID = new PublicKey( 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s' // Metaplex Token Metadata Program ID ); @@ -91,105 +102,38 @@ export interface ISolanaPluginServiceAPI extends Service { getPublicKey: () => PublicKey | null; // Returns base58 public key } -/** - * Service class for interacting with the Solana blockchain and accessing wallet data. - * @extends Service - */ -export class SolanaService extends IWalletService implements ISolanaPluginServiceAPI { - readonly serviceName = SOLANA_SERVICE_NAME; - //static override readonly serviceType = ServiceType.WALLET; - //static serviceType: string = SOLANA_SERVICE_NAME; - public readonly capabilityDescription = - ('The agent is able to interact with the Solana blockchain, and has access to the wallet data' as unknown as typeof IWalletService.prototype.capabilityDescription); +// split out off to keep this wrapper simple, so we can move it out of here +// it's a single unit focused on one thing (reduce scope of main service) +export class SolanaWalletService extends IWalletService { + private solanaService: SolanaService; - private lastUpdate = 0; - private readonly UPDATE_INTERVAL = 2 * 60_000; // 2 minutes - private connection: Connection; - private publicKey: PublicKey | null = null; - private exchangeRegistry: Record = {}; - // probably should be an array of numbers? - private subscriptions: Map = new Map(); - - jupiterService: any; - - // always multiple these - static readonly LAMPORTS2SOL = 1 / LAMPORTS_PER_SOL; - static readonly SOL2LAMPORTS = LAMPORTS_PER_SOL; - - // Token decimals cache - private decimalsCache = new Map([ - ['EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', 6], // USDC - ['Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', 6], // USDT - ['So11111111111111111111111111111111111111112', 9], // SOL - ]); - - /** - * Constructor for creating an instance of the class. - * @param {IAgentRuntime} runtime - The runtime object that provides access to agent-specific functionality. - */ constructor(runtime?: IAgentRuntime) { if (!runtime) throw new Error('runtime is required for solana service') super(runtime); - this.exchangeRegistry = {}; - const connection = new Connection( - runtime.getSetting('SOLANA_RPC_URL') || PROVIDER_CONFIG.DEFAULT_RPC - ); - this.connection = connection; - - // jupiter support detection - // shouldn't even be here... - runtime.getServiceLoadPromise('JUPITER_SERVICE' as ServiceTypeName).then(async s => { - // now we have jupiter lets register our services - this.jupiterService = runtime.getService('JUPITER_SERVICE' as ServiceTypeName) as any; - }) - - // Initialize publicKey using getWalletKey - getWalletKey(runtime, false) - .then(({ publicKey }) => { - if (!publicKey) { - throw new Error('Failed to initialize public key'); - } - this.publicKey = publicKey; - - // get initial read - this.updateWalletData(); - // only need to update wallet if it changes - // FIXME store this subscriptions somewhere... - this.subscribeToAccount(this.publicKey.toBase58(), async (accountAddress: string, accountInfo: unknown, context: unknown) => { - runtime.logger.log('Updating wallet data'); - await this.updateWalletData(); // non-forced (respect: UPDATE_INTERVAL) - }).catch((error) => { - logger.error('Error subscribing to agent wallet updates:', error); - }); - }) - .catch((error) => { - logger.error(`Error initializing public key: ${error}`); - }); - this.subscriptions = new Map(); + // start / stop? + // link to main service... + this.solanaService = runtime.getService('chain_solana') as SolanaService; + if (!this.solanaService) throw new Error('Solana Service is required for Solana Wallet Service') } - // - // MARK: IWalletService - // - /** * Retrieves the entire portfolio of assets held by the wallet. * @param owner - Optional: The specific wallet address/owner to query. * @returns A promise that resolves to the wallet's portfolio. */ public async getPortfolio(owner?: string): Promise { - if (owner && owner !== this.publicKey?.toBase58()) { + if (owner && owner !== this.solanaService.getPublicKey()?.toBase58()) { throw new Error( - `This SolanaService instance can only get the portfolio for its configured wallet: ${this.publicKey?.toBase58()}` + `This SolanaService instance can only get the portfolio for its configured wallet: ${this.solanaService.getPublicKey()?.toBase58()}` ); } - const wp: WalletPortfolio = await this.updateWalletData(true) + const wp: WalletPortfolio = await this.solanaService.updateWalletData(true) const out: siWalletPortfolio = { totalValueUsd: parseFloat(wp.totalUsd), assets: wp.items.map(i => ({ address: i.address, symbol: i.symbol, - balance: Number(i.uiAmount ?? 0), + balance: '' + Number(i.uiAmount ?? 0), decimals: i.decimals, valueUsd: Number(i.valueUsd ?? 0), })), @@ -204,7 +148,7 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic * @returns A promise that resolves to the user-friendly (decimal-adjusted) balance of the asset held. */ public async getBalance(assetAddress: string, owner?: string): Promise { - const ownerAddress: string | undefined = owner || (this.getPublicKey()?.toBase58()); + const ownerAddress: string | undefined = owner || (this.solanaService.getPublicKey()?.toBase58()); if (!ownerAddress) { return -1 } @@ -213,13 +157,13 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic assetAddress === PROVIDER_CONFIG.TOKEN_ADDRESSES.SOL ) { //return this.getSolBalance(ownerAddress); - const balances = await this.getBalancesByAddrs([ownerAddress]) + const balances = await this.solanaService.getBalancesByAddrs([ownerAddress]) const balance = balances[ownerAddress] return balance } //const tokenBalance = await this.getTokenBalance(ownerAddress, assetAddress); //return tokenBalance?.uiAmount || 0; - const tokenBalances: any = await this.getTokenAccountsByKeypairs([ownerAddress]) + const tokenBalances: any = await this.solanaService.getTokenAccountsByKeypairs([ownerAddress]) const balance: number = tokenBalances[ownerAddress]?.balanceUi || 0 return balance } @@ -235,15 +179,17 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic */ public async transferSol(from: Keypair, to: PublicKey, lamports: number): Promise { try { - if (!this.servicePublicKey) { + const payerKey = this.solanaService.getPublicKey() + if (!payerKey || payerKey === null) { throw new Error( 'SolanaService is not initialized with a fee payer key, cannot send transaction.' ); } + const connection = this.solanaService.getConnection() const transaction = new TransactionMessage({ - payerKey: this.servicePublicKey, - recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, + payerKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, instructions: [ SystemProgram.transfer({ fromPubkey: from.publicKey, @@ -255,14 +201,14 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic const versionedTransaction = new VersionedTransaction(transaction); - const serviceKeypair = await this.getServiceKeypair(); + const serviceKeypair = await this.solanaService.getWalletKeypair() versionedTransaction.sign([from, serviceKeypair]); - const signature = await this.connection.sendTransaction(versionedTransaction, { + const signature = await connection.sendTransaction(versionedTransaction, { skipPreflight: false, }); - const confirmation = await this.connection.confirmTransaction(signature, 'confirmed'); + const confirmation = await connection.confirmTransaction(signature, 'confirmed'); if (confirmation.value.err) { throw new Error( `Transaction confirmation failed: ${JSON.stringify(confirmation.value.err)}` @@ -271,13 +217,132 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic return signature; } catch (error: unknown) { - logger.error('SolanaService: transferSol failed:', error); + this.runtime.logger.error({ error },'SolanaService: transferSol failed'); throw error; } } - // - // MARK: End IWalletService - // + + /** + * Starts the Solana wallet service with the given agent runtime. + * + * @param {IAgentRuntime} runtime - The agent runtime to use for the Solana service. + * @returns {Promise} The initialized Solana service. + */ + static async start(runtime: IAgentRuntime): Promise { + runtime.logger.log(`SolanaWalletService start for ${runtime.character.name}`); + + const solanaWalletService = new SolanaWalletService(runtime); + return solanaWalletService; + } + + /** + * Stops the Solana wallet service. + * + * @param {IAgentRuntime} runtime - The agent runtime. + * @returns {Promise} - A promise that resolves once the Solana service has stopped. + */ + static async stop(runtime: IAgentRuntime): Promise { + const client = runtime.getService(ServiceType.WALLET) as SolanaService | null; + if (!client) { + logger.error('SolanaWalletService not found during static stop'); + return; + } + await client.stop(); + } + + /** + * @returns {Promise} A Promise that resolves when the update interval is stopped. + */ + async stop(): Promise { + } +} + +/** + * Service class for interacting with the Solana blockchain and accessing wallet data. + * @extends Service + */ +// implements ISolanaPluginServiceAPI +export class SolanaService extends Service { + static override readonly serviceType: string = SOLANA_SERVICE_NAME; + public readonly capabilityDescription = + ('The agent is able to interact with the Solana blockchain, and has access to the wallet data' as unknown as typeof IWalletService.prototype.capabilityDescription); + + private lastUpdate = 0; + private readonly UPDATE_INTERVAL = 2 * 60_000; // 2 minutes + private connection: Connection; + private publicKey: PublicKey | null = null; + private keypair: Keypair | null = null; + private exchangeRegistry: Record = {}; + // probably should be an array of numbers? + private subscriptions: Map = new Map(); + + jupiterService: any; + + // always multiple these + static readonly LAMPORTS2SOL = 1 / LAMPORTS_PER_SOL; + static readonly SOL2LAMPORTS = LAMPORTS_PER_SOL; + + // Token decimals cache + private decimalsCache = new Map([ + ['EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', 6], // USDC + ['Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', 6], // USDT + ['So11111111111111111111111111111111111111112', 9], // SOL + ]); + + /** + * Constructor for creating an instance of the class. + * @param {IAgentRuntime} runtime - The runtime object that provides access to agent-specific functionality. + */ + constructor(runtime?: IAgentRuntime) { + if (!runtime) throw new Error('runtime is required for solana service') + super(runtime); + this.exchangeRegistry = {}; + this.connection = new Connection( + runtime.getSetting('SOLANA_RPC_URL') || PROVIDER_CONFIG.DEFAULT_RPC + ); + + // jupiter support detection + // shouldn't even be here... + runtime.getServiceLoadPromise('JUPITER_SERVICE' as ServiceTypeName).then(async s => { + // now we have jupiter lets register our services + this.jupiterService = runtime.getService('JUPITER_SERVICE' as ServiceTypeName) as any; + }) + + getWalletKey(runtime, true) + .then(({ keypair }) => { + if (keypair) { + this.keypair = keypair + } + }).catch(e => { + // no private key + // not the end of the world, just somethings should be disabled... + runtime.logger.log('no useable solana private key') + }) + // Initialize publicKey using getWalletKey + // FIXME: promise for this to be ready? + getWalletKey(runtime, false) + .then(({ publicKey }) => { + if (!publicKey) { + throw new Error('Failed to initialize public key'); + } + this.publicKey = publicKey; + + // get initial read + this.updateWalletData(); + // only need to update wallet if it changes + // FIXME store this subscriptions somewhere... + this.subscribeToAccount(this.publicKey.toBase58(), async (accountAddress: string, accountInfo: unknown, context: unknown) => { + runtime.logger.log('Updating wallet data'); + await this.updateWalletData(); // non-forced (respect: UPDATE_INTERVAL) + }).catch((error) => { + logger.error('Error subscribing to agent wallet updates:', error); + }); + }) + .catch((error) => { + logger.error(`Error initializing public key: ${error}`); + }); + this.subscriptions = new Map(); + } /** * Retrieves the connection object. @@ -1627,8 +1692,8 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic * @returns {Promise} The wallet keypair * @throws {Error} If private key is not available */ - private async getWalletKeypair(): Promise { - const keypair = this.publicKey; + public async getWalletKeypair(): Promise { + const keypair = this.keypair; if (!keypair) { throw new Error('Failed to get wallet keypair'); } @@ -1649,7 +1714,7 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic * @param {boolean} [force=false] - Whether to force update the wallet data even if the update interval has not passed * @returns {Promise} The updated wallet portfolio information */ - private async updateWalletData(force = false): Promise { + public async updateWalletData(force = false): Promise { //console.log('updateWalletData - start') const now = Date.now(); @@ -1846,7 +1911,7 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic const balance = Number(amountRaw) / (10 ** decimals); const symbol = await solanaService.getTokenSymbol(ca); */ - public async getTokenAccountsByKeypair(walletAddress: PublicKey, options: { notOlderThan?: number; includeZeroBalances?: boolean; } = {}) { + public async getTokenAccountsByKeypair(walletAddress: PublicKey, options: { notOlderThan?: number; includeZeroBalances?: boolean; } = {}): Promise { //console.log('getTokenAccountsByKeypair', walletAddress.toString()) //console.log('publicKey', this.publicKey, 'vs', walletAddress) const key = 'solana_' + walletAddress.toString() + '_tokens' @@ -1914,8 +1979,13 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic } } - public async getTokenAccountsByKeypairs(walletAddresses: string[], options = {}) { - return Promise.all(walletAddresses.map(a => this.getTokenAccountsByKeypair(new PublicKey(a), options))) + public async getTokenAccountsByKeypairs(walletAddresses: string[], options = {}): Promise> { + const res = await Promise.all(walletAddresses.map(a => this.getTokenAccountsByKeypair(new PublicKey(a), options))) + const out: Record = {} + for(const i in walletAddresses) { + out[walletAddresses[i]] = res[i] + } + return out } // deprecated @@ -2023,11 +2093,7 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic // // single wallet, list of tokens - public async getWalletBalances(publicKeyStr: string, mintAddresses: string[]): Promise<{ - amount: string; - decimals: number; - uiAmount: number; - } | null> { + public async getWalletBalances(publicKeyStr: string, mintAddresses: string[]): Promise> { const owner = new PublicKey(publicKeyStr); const mints = mintAddresses.map(m => new PublicKey(m)); @@ -2727,7 +2793,7 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic * @returns {Promise} The initialized Solana service. */ static async start(runtime: IAgentRuntime): Promise { - logger.log(`SolanaService start for ${runtime.character.name}`); + runtime.logger.log(`SolanaService start for ${runtime.character.name}`); const solanaService = new SolanaService(runtime); return solanaService; @@ -2742,14 +2808,14 @@ export class SolanaService extends IWalletService implements ISolanaPluginServic static async stop(runtime: IAgentRuntime): Promise { const client = runtime.getService(SOLANA_SERVICE_NAME) as SolanaService | null; if (!client) { - logger.error('SolanaService not found during static stop'); + runtime.logger.error('SolanaService not found during static stop'); return; } await client.stop(); } /** - * Stops the update interval if it is currently running. + * Cleans up subscriptions * @returns {Promise} A Promise that resolves when the update interval is stopped. */ async stop(): Promise { From 9b98467c7baa849c253aaddd03beebdb78521186 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Mon, 20 Oct 2025 23:59:21 +0000 Subject: [PATCH 32/36] add catch to init promise --- src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 7285add..768195a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,7 +45,9 @@ export const solanaPlugin: Plugin = { service: SOLANA_SERVICE_NAME, }; traderChainService.registerChain(me); - }) + }).catch(error => { + runtime.logger.error({ error },'Failed to register with INTEL_CHAIN'); + }); }, }; From d9396e8206d0543727ed2912f881d79c5c3eb1d6 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Tue, 21 Oct 2025 00:00:30 +0000 Subject: [PATCH 33/36] getBalance fixes, clean up subscription, fix types --- src/service.ts | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/src/service.ts b/src/service.ts index 3d3e961..432097a 100644 --- a/src/service.ts +++ b/src/service.ts @@ -11,6 +11,8 @@ import { SystemProgram, Transaction, TransactionMessage, + type RpcResponseAndContext, + type ParsedAccountData, } from '@solana/web3.js'; import { MintLayout, getMint, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, unpackAccount, @@ -44,7 +46,20 @@ export type MintBalance = { uiAmount: number; }; +type KeyedParsedTokenAccount = { + pubkey: PublicKey; + account: AccountInfo; +}; +type ParsedTokenAccountsResponse = Awaited>; +/* +type ParsedTokenAccountsResponse = Promise; + }> + >> +*/ const METADATA_PROGRAM_ID = new PublicKey( 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s' // Metaplex Token Metadata Program ID @@ -163,9 +178,18 @@ export class SolanaWalletService extends IWalletService { } //const tokenBalance = await this.getTokenBalance(ownerAddress, assetAddress); //return tokenBalance?.uiAmount || 0; - const tokenBalances: any = await this.solanaService.getTokenAccountsByKeypairs([ownerAddress]) - const balance: number = tokenBalances[ownerAddress]?.balanceUi || 0 - return balance + const tokensBalances: Record = await this.solanaService.getTokenAccountsByKeypairs([ownerAddress]) + const heldTokens = tokensBalances[ownerAddress] || [] + for(const t of heldTokens) { + //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.account.data.parsed.info.mint === assetAddress) { + return t.account.data.parsed.info.tokenAmount.balanceUi; + } + } + this.runtime.logger.log('could not find', assetAddress, 'in', heldTokens) + return -1 } /** @@ -1911,7 +1935,7 @@ export class SolanaService extends Service { const balance = Number(amountRaw) / (10 ** decimals); const symbol = await solanaService.getTokenSymbol(ca); */ - public async getTokenAccountsByKeypair(walletAddress: PublicKey, options: { notOlderThan?: number; includeZeroBalances?: boolean; } = {}): Promise { + public async getTokenAccountsByKeypair(walletAddress: PublicKey, options: { notOlderThan?: number; includeZeroBalances?: boolean; } = {}): Promise { //console.log('getTokenAccountsByKeypair', walletAddress.toString()) //console.log('publicKey', this.publicKey, 'vs', walletAddress) const key = 'solana_' + walletAddress.toString() + '_tokens' @@ -1937,7 +1961,7 @@ export class SolanaService extends Service { } console.log('getTokenAccountsByKeypair - getParsedTokenAccountsByOwner', walletAddress.toString()) - const [accounts, token2022s]: [any, any] = await Promise.all([ + const [accounts, token2022s]: [ParsedTokenAccountsResponse, ParsedTokenAccountsResponse] = await Promise.all([ this.connection.getParsedTokenAccountsByOwner(walletAddress, { programId: TOKEN_PROGRAM_ID, // original SPL }), @@ -1950,10 +1974,10 @@ export class SolanaService extends Service { //console.log('haveToken22s', haveToken22s) //for(const t of token2022s.value) { console.log('t2022 account.data', t.account.data) } //const haveTokens = accounts.value.filter(account => account.account.data.parsed.info.tokenAmount.amount !== '0') - const allTokens = [...token2022s.value, ...accounts.value] + const allTokens: KeyedParsedTokenAccount[] = [...token2022s.value, ...accounts.value] // update decimalCache - const haveAllTokens = [] + const haveAllTokens: KeyedParsedTokenAccount[] = [] for(const t of allTokens) { const { amount, decimals } = t.account.data.parsed.info.tokenAmount; this.decimalsCache.set(t.account.data.parsed.info.mint, decimals); @@ -1979,9 +2003,9 @@ export class SolanaService extends Service { } } - public async getTokenAccountsByKeypairs(walletAddresses: string[], options = {}): Promise> { + public async getTokenAccountsByKeypairs(walletAddresses: string[], options = {}): Promise> { const res = await Promise.all(walletAddresses.map(a => this.getTokenAccountsByKeypair(new PublicKey(a), options))) - const out: Record = {} + const out: Record = {} for(const i in walletAddresses) { out[walletAddresses[i]] = res[i] } @@ -2277,6 +2301,7 @@ export class SolanaService extends Service { } await this.connection.removeAccountChangeListener(subscriptionId); + this.subscriptions.delete(accountAddress); return true; } catch (error) { From 60eda4b65c397d6a3e67657a43286e3503d670de Mon Sep 17 00:00:00 2001 From: Odilitime Date: Tue, 21 Oct 2025 00:11:37 +0000 Subject: [PATCH 34/36] getBalance fix, getAddressesTypes better implementation, fix warning --- src/service.ts | 69 +++++++++++++++++--------------------------------- 1 file changed, 23 insertions(+), 46 deletions(-) diff --git a/src/service.ts b/src/service.ts index 432097a..39f7d3a 100644 --- a/src/service.ts +++ b/src/service.ts @@ -173,7 +173,7 @@ export class SolanaWalletService extends IWalletService { ) { //return this.getSolBalance(ownerAddress); const balances = await this.solanaService.getBalancesByAddrs([ownerAddress]) - const balance = balances[ownerAddress] + const balance = balances[ownerAddress] ?? 0 return balance } //const tokenBalance = await this.getTokenBalance(ownerAddress, assetAddress); @@ -185,7 +185,7 @@ export class SolanaWalletService extends IWalletService { //const balance = Number(amountRaw) / (10 ** decimals); //const ca = new PublicKey(t.account.data.parsed.info.mint); if (t.account.data.parsed.info.mint === assetAddress) { - return t.account.data.parsed.info.tokenAmount.balanceUi; + return t.account.data.parsed.info.tokenAmount.uiAmount; } } this.runtime.logger.log('could not find', assetAddress, 'in', heldTokens) @@ -518,54 +518,31 @@ export class SolanaService extends Service { // deprecate async getAddressType(address: string): Promise { - let dataLength = -1 - try { - const key = 'solana_' + address + '_addressType' - const check = await this.runtime.getCache(key) - if (check) { - console.log('getAddressType - HIT') - return check - } - - const pubkey = new PublicKey(address); - console.log('getAddressType - getAccountInfo') - const accountInfo = await this.connection.getAccountInfo(pubkey); - - if (!accountInfo) { - return 'Account does not exist'; - } - - //console.log('accountInfo', accountInfo) - - dataLength = accountInfo.data.length; - - if (dataLength === 0) { - await this.runtime.setCache(key, 'Wallet') - return 'Wallet'; - } + const types = await this.getAddressesTypes([address]) + return types[address] + } - // SPL Token accounts are always 165 bytes - // User's balance of a specified token - if (dataLength === SolanaService.TOKEN_ACCOUNT_DATA_LENGTH) { - await this.runtime.setCache(key, 'Token Account') - return 'Token Account'; - } + async getAddressesTypes(addresses: string[]): Promise> { + const pubkeys = addresses.map(a => new PublicKey(a)); + const infos = await this.batchGetMultipleAccountsInfo(pubkeys, 'getAddressesTypes'); + + const resultList: string[] = addresses.map((addr, i) => { + const info = infos[i]; + if (!info) return 'Account does not exist'; + const dataLength = info.data.length; + if (dataLength === 0) return 'Wallet'; + if (dataLength === SolanaService.TOKEN_ACCOUNT_DATA_LENGTH) return 'Token Account'; + if (dataLength === SolanaService.TOKEN_MINT_DATA_LENGTH) return 'Token'; + return `Unknown (Data length: ${dataLength})`; + }); - // Token mint account - if (dataLength === SolanaService.TOKEN_MINT_DATA_LENGTH) { - await this.runtime.setCache(key, 'Token') - return 'Token'; - } - } catch(e) { - // likely bad address - console.error('solsrv:getAddressType - err', e) + const out: Record = {} + for(const i in addresses) { + const addr = addresses[i] + out[addr] = resultList[i] } - return `Unknown (Data length: ${dataLength})`; - } - async getAddressesTypes(addresses: string[]) { - // FIXME: use batchGetMultipleAccountsInfo to efficiently check multiple - return Promise.all(addresses.map(a => this.getAddressType(a))) + return out } /** From dac64af36671db604b6cd20b6c0453513fcb7f76 Mon Sep 17 00:00:00 2001 From: Odilitime Date: Tue, 21 Oct 2025 00:20:10 +0000 Subject: [PATCH 35/36] additional number warning fixes --- src/service.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/service.ts b/src/service.ts index 39f7d3a..abe4a91 100644 --- a/src/service.ts +++ b/src/service.ts @@ -148,7 +148,7 @@ export class SolanaWalletService extends IWalletService { assets: wp.items.map(i => ({ address: i.address, symbol: i.symbol, - balance: '' + Number(i.uiAmount ?? 0), + balance: Number(i.uiAmount ?? 0).toString(), decimals: i.decimals, valueUsd: Number(i.valueUsd ?? 0), })), @@ -2702,8 +2702,8 @@ export class SolanaService extends Service { if (inBal && outBal) { // in will be high than out in this scenario? - const lamDiff = (inBal.uiTokenAmount.uiAmount || 0) - (outBal.uiTokenAmount.uiAmount || 0) - const diff = Number(inBal.uiTokenAmount.amount || 0) - Number(outBal.uiTokenAmount.amount || 0) + const lamDiff = (inBal.uiTokenAmount.uiAmount ?? 0) - (outBal.uiTokenAmount.uiAmount ?? 0) + const diff = Number(inBal.uiTokenAmount.amount ?? 0) - Number(outBal.uiTokenAmount.amount ?? 0) // we definitely didn't swap for nothing if (diff) { outAmount = diff @@ -2726,8 +2726,8 @@ export class SolanaService extends Service { } else { if (inBal && outBal) { - const lamDiff = (outBal.uiTokenAmount.uiAmount || 0) - (inBal.uiTokenAmount.uiAmount || 0) - const diff = Number(outBal.uiTokenAmount.amount || 0) - Number(inBal.uiTokenAmount.amount || 0) + const lamDiff = (outBal.uiTokenAmount.uiAmount ?? 0) - (inBal.uiTokenAmount.uiAmount ?? 0) + const diff = Number(outBal.uiTokenAmount.amount ?? 0) - Number(inBal.uiTokenAmount.amount ?? 0) // we definitely didn't swap for nothing if (diff) { outAmount = diff From fcf58154be26e1f0e4ff22ff0e12916be6a5a42b Mon Sep 17 00:00:00 2001 From: Odilitime Date: Tue, 21 Oct 2025 00:50:48 +0000 Subject: [PATCH 36/36] fix name in comment --- build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.ts b/build.ts index 8ef83d1..94fbe72 100644 --- a/build.ts +++ b/build.ts @@ -1,6 +1,6 @@ #!/usr/bin/env bun /** - * Build script for @elizaos/plugin-bootstrap using standardized build utilities + * Build script for @elizaos/plugin-solana using standardized build utilities */ import { createBuildRunner } from '../../build-utils';