UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

206 lines 8.79 kB
import { ComputeBudgetProgram, Keypair, Transaction, } from '@solana/web3.js'; import { rootLogger, sleep } from '@hyperlane-xyz/utils'; import { SEALEVEL_PRIORITY_FEES } from '../../consts/sealevel.js'; /** * Keypair-based SVM transaction signer */ export class KeypairSvmTransactionSigner { publicKey; keypair; constructor(privateKey) { this.keypair = Keypair.fromSecretKey(privateKey); this.publicKey = this.keypair.publicKey; } async signTransaction(transaction) { transaction.partialSign(this.keypair); return transaction; } } export class SvmMultiProtocolSignerAdapter { chainName; signer; svmProvider; config; logger = rootLogger.child({ module: 'SvmMultiProtocolSignerAdapter', }); constructor(chainName, signer, multiProtocolProvider, config) { this.chainName = chainName; this.signer = signer; this.svmProvider = multiProtocolProvider.getSolanaWeb3Provider(chainName); this.config = { maxConfirmationAttempts: config?.maxConfirmationAttempts ?? 30, pollingDelayMs: config?.pollingDelayMs ?? 1000, commitment: config?.commitment ?? 'confirmed', enableBlockhashResubmit: config?.enableBlockhashResubmit ?? true, retryOnRpcErrors: config?.retryOnRpcErrors ?? true, }; } publicKey() { return this.signer.publicKey; } async address() { return this.signer.publicKey.toBase58(); } /** * Build and send a transaction from raw instructions */ async buildAndSendTransaction(instructions, options) { const tx = this.buildTransaction(instructions, options); return this.signAndConfirm(tx); } /** * Send and confirm a pre-built transaction (IMultiProtocolSigner interface) */ async sendAndConfirmTransaction(tx, _options) { return this.signAndConfirm(tx.transaction, tx.extraSigners); } // ============ Private Methods ============ /** * Build transaction from instructions with optional priority fees */ buildTransaction(instructions, options) { const tx = new Transaction(); // Add priority fee if enabled and not already present const includePriorityFee = options?.includePriorityFee ?? true; if (includePriorityFee) { const hasPriorityFeeIx = instructions.some((ix) => ix.programId.equals(ComputeBudgetProgram.programId)); if (!hasPriorityFeeIx) { const priorityFee = options?.priorityFee ?? SEALEVEL_PRIORITY_FEES[this.chainName]; if (priorityFee) { tx.add(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priorityFee, })); } } } // Add all instructions instructions.forEach((ix) => tx.add(ix)); tx.feePayer = this.signer.publicKey; return tx; } /** * Sign and confirm transaction with blockhash resubmit on expiry */ async signAndConfirm(transaction, extraSigners) { // Get initial blockhash const { blockhash, lastValidBlockHeight } = await this.svmProvider.getLatestBlockhash(this.config.commitment); transaction.recentBlockhash = blockhash; if (!transaction.feePayer) { transaction.feePayer = this.signer.publicKey; } // Sign with extra signers first (e.g., randomWallet for Sealevel transferRemote). // Uses partialSign to avoid clearing any existing signatures. // This re-signs with the fresh blockhash, overwriting the adapter's pre-sign. for (const signer of extraSigners ?? []) { transaction.partialSign(signer); } // Sign with main signer (uses partialSign via KeypairSvmTransactionSigner) const signedTx = await this.signer.signTransaction(transaction); // Send initial transaction const signature = await this.sendRawTransaction(signedTx); // Poll for confirmation with optional resubmit const result = await this.pollForConfirmation(signature, signedTx, lastValidBlockHeight, extraSigners); return result; } /** * Poll for transaction confirmation with blockhash expiry handling */ async pollForConfirmation(initialSignature, transaction, lastValidBlockHeight, extraSigners) { let signature = initialSignature; let attempts = 0; let currentLastValidBlockHeight = lastValidBlockHeight; while (attempts < this.config.maxConfirmationAttempts) { await sleep(this.config.pollingDelayMs); attempts++; try { // Check and handle blockhash expiry const resubmitResult = await this.checkAndResubmitIfExpired(signature, transaction, currentLastValidBlockHeight, extraSigners); if (resubmitResult) { signature = resubmitResult.signature; currentLastValidBlockHeight = resubmitResult.lastValidBlockHeight; continue; } // Check if transaction is confirmed const isConfirmed = await this.checkTransactionConfirmation(signature); if (isConfirmed) { this.logger.info(`Transaction ${signature} confirmed after ${attempts} attempts`); return signature; } } catch (error) { // If it's a transaction failure error, rethrow immediately if (error instanceof Error && error.message.includes('Transaction failed')) { throw error; } // Handle RPC errors based on config if (!this.config.retryOnRpcErrors) { throw error; } // Log RPC errors but continue polling (temporary issues) this.logger.warn(`Polling attempt ${attempts} failed: ${error}`); } } throw new Error(`Transaction confirmation timeout after ${this.config.maxConfirmationAttempts} attempts`); } /** * Check if blockhash expired and resubmit transaction if needed * Returns new signature and lastValidBlockHeight if resubmitted, null otherwise */ async checkAndResubmitIfExpired(signature, transaction, lastValidBlockHeight, extraSigners) { if (!this.config.enableBlockhashResubmit) { return null; } const currentBlockHeight = await this.svmProvider.getBlockHeight(); if (currentBlockHeight <= lastValidBlockHeight) { return null; // Blockhash still valid } this.logger.warn(`Blockhash expired at block ${lastValidBlockHeight}, current ${currentBlockHeight}. Resubmitting...`); // Get fresh blockhash and resubmit const { blockhash, lastValidBlockHeight: newLastValid } = await this.svmProvider.getLatestBlockhash(this.config.commitment); transaction.recentBlockhash = blockhash; // Re-sign extra signers with new blockhash for (const signer of extraSigners ?? []) { transaction.partialSign(signer); } const signedTx = await this.signer.signTransaction(transaction); const newSignature = await this.sendRawTransaction(signedTx); this.logger.info(`Resubmitted with signature: ${newSignature}`); return { signature: newSignature, lastValidBlockHeight: newLastValid, }; } /** * Check if transaction is confirmed * Returns true if confirmed, false if pending * Throws if transaction failed */ async checkTransactionConfirmation(signature) { const status = await this.svmProvider.getSignatureStatus(signature, { searchTransactionHistory: true, }); if (!status.value) { return false; // Transaction not yet seen } // Check for transaction error if (status.value.err) { throw new Error(`Transaction failed: ${JSON.stringify(status.value.err)}`); } // Check if confirmed at required commitment level const confirmationStatus = status.value.confirmationStatus; return (confirmationStatus === this.config.commitment || confirmationStatus === 'finalized'); } /** * Send signed transaction to network */ async sendRawTransaction(transaction) { return await this.svmProvider.sendRawTransaction(transaction.serialize(), { skipPreflight: false, maxRetries: 3, }); } } //# sourceMappingURL=solana-web3js.js.map