import { Transaction, VersionedTransaction, Connection, PublicKey } from '@solana/web3.js' import type { SolanaNativeTransferRequest, NetworkRpcMap, NetworkId, SolanaTokenTransferRequest, SolanaGenericTransferRequest, SolanaTransactionRequest } from '@meshconnect/uwc-types' import { parseError } from '../../utils/error-utils' import { SolanaTransactionBuilder } from './solana-transaction-builder' import type { ExtendedStandardWalletAdapter, SolanaAdapter } from '../../wallet-standard-discovery' /** * Service for handling Solana transaction operations */ export class SolanaTransactionService { private networkRpcMap: NetworkRpcMap private builder: SolanaTransactionBuilder constructor(networkRpcMap: NetworkRpcMap = {}) { this.networkRpcMap = networkRpcMap this.builder = new SolanaTransactionBuilder() } /** * Send a transaction for Solana */ async sendTransaction( request: SolanaTransactionRequest, adapter: SolanaAdapter, networkId?: NetworkId ): Promise { try { // Handle different types of Solana transactions if ('tokenMint' in request) { return await this.sendSplTokenTransaction( adapter, request as SolanaTokenTransferRequest, networkId ) } else if ('from' in request) { return await this.sendNativeTransaction( adapter, request as SolanaNativeTransferRequest, networkId ) } else if ('instructions' in request) { return await this.sendGenericTransferWithInstructions( adapter, request as SolanaGenericTransferRequest, networkId ) } else { throw new Error('Invalid transaction request type') } } catch (error) { parseError(error) } } /** * Helper: Validate wallet address matches */ private async validateWalletMatch( adapter: SolanaAdapter, expectedAddress: string ): Promise { const base58PublicKey = await adapter.publicKey?.toBase58() if (base58PublicKey !== expectedAddress) { throw new Error('From address does not match connected wallet') } return base58PublicKey } /** * Helper: Get RPC URL with fallback */ private getRpcUrl(networkId?: NetworkId): string { return networkId && this.networkRpcMap[networkId] ? this.networkRpcMap[networkId] : 'https://api.mainnet-beta.solana.com' } /** * Two mutually exclusive paths: * 1. sendSerializedTransaction(Uint8Array, string) — Comlink bridge safe. * 2. sendTransaction(tx, Connection) — direct adapters only. */ private async executeTransaction( adapter: SolanaAdapter, serializedTx: Uint8Array, transaction: Transaction | VersionedTransaction, rpcUrl: string ): Promise { const extendedAdapter = adapter as ExtendedStandardWalletAdapter if (extendedAdapter.sendSerializedTransaction) { const signature = await extendedAdapter.sendSerializedTransaction( serializedTx, rpcUrl ) if (signature === undefined) { throw new Error(`Couldn't get the transaction signature`) } return signature } const connection = new Connection(rpcUrl) const signature = await adapter.sendTransaction(transaction, connection) if (signature === undefined) { throw new Error(`Couldn't get the transaction signature`) } return signature } /** * Send native SOL transfer */ private async sendNativeTransaction( adapter: SolanaAdapter, request: SolanaNativeTransferRequest, networkId?: NetworkId ): Promise { try { // Validate wallet await this.validateWalletMatch(adapter, request.from) // Blockhash is required if (!request.blockhash) { throw new Error('Blockhash is required for Solana transactions') } // Create transaction const transaction = new Transaction() transaction.instructions = await this.builder.buildTransferInstructions(request) transaction.recentBlockhash = request.blockhash transaction.feePayer = new PublicKey(request.from) const rpcUrl = this.getRpcUrl(networkId) const serializedTx = transaction.serialize({ requireAllSignatures: false, verifySignatures: false }) return await this.executeTransaction( adapter, serializedTx, transaction, rpcUrl ) } catch (error) { parseError(error) } } /** * Send SPL token transfer */ private async sendSplTokenTransaction( adapter: SolanaAdapter, request: SolanaTokenTransferRequest, networkId?: NetworkId ): Promise { try { // Validate wallet await this.validateWalletMatch(adapter, request.from) // Blockhash is required if (!request.blockhash) { throw new Error('Blockhash is required for Solana transactions') } // Create transaction const transaction = new Transaction() transaction.instructions = await this.builder.buildTransferInstructions(request) transaction.recentBlockhash = request.blockhash transaction.feePayer = new PublicKey(request.from) const rpcUrl = this.getRpcUrl(networkId) const serializedTx = transaction.serialize({ requireAllSignatures: false, verifySignatures: false }) return await this.executeTransaction( adapter, serializedTx, transaction, rpcUrl ) } catch (error) { parseError(error) } } /** * Send native SOL transfer */ private async sendGenericTransferWithInstructions( adapter: SolanaAdapter, request: SolanaGenericTransferRequest, networkId?: NetworkId ): Promise { try { // Validate wallet await this.validateWalletMatch(adapter, request.feePayer) // Blockhash is required if (!request.blockhash) { throw new Error('Blockhash is required for Solana transactions') } // Create versioned transaction const transaction = await this.builder.getVersionedTransaction( new PublicKey(request.feePayer), request ) const rpcUrl = this.getRpcUrl(networkId) const serializedTx = transaction.serialize() return await this.executeTransaction( adapter, serializedTx, transaction, rpcUrl ) } catch (error) { parseError(error) } } }