import { AddressLookupTableAccount, PublicKey, SystemProgram, TransactionInstruction, TransactionMessage, VersionedTransaction, type AddressLookupTableState } from '@solana/web3.js' import type { SolanaNativeTransferRequest, SolanaTokenTransferRequest, SolanaGenericTransferRequest, SolanaAccountMeta, SolanaTransactionRequest } from '@meshconnect/uwc-types' import { createTransferCheckedInstruction, createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddress, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token' import { Buffer } from 'buffer' /** * Service for handling Solana transaction operations */ export class SolanaTransactionBuilder { async buildTransferInstructions( request: SolanaTransactionRequest ): Promise { if ('tokenMint' in request) { return await this.buildSplTokenTransferInstructions( request as SolanaTokenTransferRequest ) } else if ('instructions' in request) { return await this.buildGenericTransferInstructions( request as SolanaGenericTransferRequest ) } else { return [ this.buildNativeTransferInstruction( request as SolanaNativeTransferRequest ) ] } } async getVersionedTransaction( feePayer: PublicKey, request: SolanaTransactionRequest ) { const lookupTable = this.parseLookupTable(request) return new VersionedTransaction( new TransactionMessage({ payerKey: feePayer, recentBlockhash: request.blockhash, instructions: await this.buildTransferInstructions(request) }).compileToV0Message(lookupTable) ) } /** * Build native SOL transfer */ private buildNativeTransferInstruction( request: SolanaNativeTransferRequest ): TransactionInstruction { return SystemProgram.transfer({ fromPubkey: new PublicKey(request.from), toPubkey: new PublicKey(request.to), lamports: Number(request.amount) }) } /** * Build SPL Token transfer */ private async buildSplTokenTransferInstructions( request: SolanaTokenTransferRequest ): Promise { let instructions: TransactionInstruction[] = [] // Create Public Keys const mint = new PublicKey(request.tokenMint) const from = new PublicKey(request.from) const to = new PublicKey(request.to) const fromTokenAccount = await getAssociatedTokenAddress( mint, from, false, request.tokenProgram ? new PublicKey(request.tokenProgram) : TOKEN_PROGRAM_ID ) // Destination may be an off-curve owner (e.g. an exchange/custodial PDA); // `from` stays on-curve above — it signs, and a keyless PDA cannot. const toTokenAccount = await getAssociatedTokenAddress( mint, to, true, request.tokenProgram ? new PublicKey(request.tokenProgram) : TOKEN_PROGRAM_ID ) // Create target associated token account instructions.push( createAssociatedTokenAccountIdempotentInstruction( from, toTokenAccount, to, mint, request.tokenProgram ? new PublicKey(request.tokenProgram) : TOKEN_PROGRAM_ID ) ) const programId = request.tokenProgram === TOKEN_2022_PROGRAM_ID.toBase58() ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID instructions.push( createTransferCheckedInstruction( fromTokenAccount, mint, toTokenAccount, from, request.amount, request.tokenDecimals, [], programId ) ) return instructions } private async buildGenericTransferInstructions( request: SolanaGenericTransferRequest ): Promise { const result: TransactionInstruction[] = [] const instructions = request.instructions for (let instrIndex = 0; instrIndex < instructions.length; instrIndex++) { const ix = instructions[instrIndex]! const programId = new PublicKey(ix.programId) const keys = ix.accounts.map( (meta: SolanaAccountMeta, accountIndex: number) => { if (!meta.pubKey) { throw new Error( `Account at instruction ${instrIndex}, index ${accountIndex} has no pubKey and is not fillable` ) } const resolvedPubkey: PublicKey = new PublicKey(meta.pubKey) return { pubkey: resolvedPubkey, isSigner: meta.isSigner, isWritable: meta.isWritable } } ) result.push( new TransactionInstruction({ keys, programId, data: Buffer.from(ix.data, 'base64') }) ) } if (request.additionalRequests) { const extraInstructions = await Promise.all( request.additionalRequests.map(req => this.buildTransferInstructions(req) ) ) result.push(...extraInstructions.flat()) } return result } private parseLookupTable = ( request: SolanaTransactionRequest ): AddressLookupTableAccount[] => { if (!('states' in request)) { return [] } return (request.states ?? []).map(state => { const currentState: AddressLookupTableState = { deactivationSlot: state.deactivationSlot, lastExtendedSlot: state.lastExtendedSlot, lastExtendedSlotStartIndex: state.lastExtendedStartIndex, addresses: [], ...(state.authority && { authority: new PublicKey(state.authority) }) } return new AddressLookupTableAccount({ key: new PublicKey(state.key), state: currentState }) }) } }