172 lines
6.0 KiB
TypeScript
172 lines
6.0 KiB
TypeScript
import { Connection, Transaction} from '@solana/web3.js';
|
|
import { WalletContextState } from '@solana/wallet-adapter-react';
|
|
|
|
export interface MobileWalletTransactionOptions {
|
|
skipPreflight?: boolean;
|
|
preflightCommitment?: 'processed' | 'confirmed' | 'finalized';
|
|
maxRetries?: number;
|
|
commitment?: 'processed' | 'confirmed' | 'finalized';
|
|
}
|
|
|
|
/**
|
|
* Enhanced transaction sending for mobile wallets with better error handling
|
|
*/
|
|
export async function sendTransactionWithMobileOptimization(
|
|
transaction: Transaction,
|
|
connection: Connection,
|
|
wallet: WalletContextState,
|
|
options: MobileWalletTransactionOptions = {}
|
|
): Promise<string> {
|
|
const {
|
|
skipPreflight = false,
|
|
preflightCommitment = 'confirmed',
|
|
maxRetries = 3,
|
|
commitment = 'confirmed'
|
|
} = options;
|
|
|
|
// Ensure transaction has all required fields for mobile wallets
|
|
if (!transaction.recentBlockhash) {
|
|
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(commitment);
|
|
transaction.recentBlockhash = blockhash;
|
|
transaction.lastValidBlockHeight = lastValidBlockHeight;
|
|
}
|
|
|
|
// CRITICAL: Ensure fee payer is set correctly for mobile wallets
|
|
if (!transaction.feePayer) {
|
|
if (wallet.publicKey) {
|
|
transaction.feePayer = wallet.publicKey;
|
|
console.log('Mobile wallet utility: Set fee payer to:', wallet.publicKey.toString());
|
|
} else {
|
|
throw new Error('Wallet public key not available');
|
|
}
|
|
}
|
|
|
|
// Double-check that all required fields are present
|
|
console.log('Mobile wallet utility: Transaction validation:');
|
|
console.log('- Fee payer:', transaction.feePayer?.toString());
|
|
console.log('- Recent blockhash:', transaction.recentBlockhash);
|
|
console.log('- Last valid block height:', transaction.lastValidBlockHeight);
|
|
console.log('- Number of instructions:', transaction.instructions.length);
|
|
|
|
// Validate transaction structure for mobile wallets
|
|
if (!transaction.feePayer) {
|
|
throw new Error('Transaction missing fee payer - required for mobile wallets');
|
|
}
|
|
if (!transaction.recentBlockhash) {
|
|
throw new Error('Transaction missing recent blockhash - required for mobile wallets');
|
|
}
|
|
if (transaction.instructions.length === 0) {
|
|
throw new Error('Transaction has no instructions');
|
|
}
|
|
|
|
// Preflight check for mobile wallets
|
|
try {
|
|
console.log('Mobile wallet utility: Running preflight check...');
|
|
const { value: { err, logs } } = await connection.simulateTransaction(transaction);
|
|
if (err) {
|
|
console.warn('Mobile wallet utility: Preflight check failed:', err);
|
|
console.log('Mobile wallet utility: Preflight logs:', logs);
|
|
|
|
// For mobile wallets, we'll be more lenient with preflight failures
|
|
// as some mobile wallets handle this differently
|
|
console.log('Mobile wallet utility: Proceeding despite preflight failure (mobile wallet behavior)');
|
|
} else {
|
|
console.log('Mobile wallet utility: Preflight check passed');
|
|
}
|
|
} catch (simError) {
|
|
console.warn('Mobile wallet utility: Preflight simulation failed:', simError);
|
|
// Don't throw here, some mobile wallets handle this differently
|
|
console.log('Mobile wallet utility: Proceeding despite preflight simulation failure');
|
|
}
|
|
|
|
// Send transaction with retry logic
|
|
let lastError: Error | null = null;
|
|
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
try {
|
|
console.log(`Mobile wallet utility: Attempt ${attempt} of ${maxRetries}`);
|
|
|
|
// Ensure transaction is fresh for each attempt
|
|
if (attempt > 1) {
|
|
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(commitment);
|
|
transaction.recentBlockhash = blockhash;
|
|
transaction.lastValidBlockHeight = lastValidBlockHeight;
|
|
console.log('Mobile wallet utility: Refreshed blockhash for retry');
|
|
}
|
|
|
|
const signature = await wallet.sendTransaction(transaction, connection, {
|
|
skipPreflight,
|
|
preflightCommitment,
|
|
maxRetries: 1, // We handle retries ourselves
|
|
});
|
|
|
|
console.log('Mobile wallet utility: Transaction sent successfully, signature:', signature);
|
|
|
|
// Wait for confirmation
|
|
const confirmation = await connection.confirmTransaction({
|
|
signature,
|
|
blockhash: transaction.recentBlockhash!,
|
|
lastValidBlockHeight: transaction.lastValidBlockHeight!
|
|
}, commitment);
|
|
|
|
if (confirmation.value.err) {
|
|
throw new Error(`Transaction failed: ${confirmation.value.err}`);
|
|
}
|
|
|
|
console.log('Mobile wallet utility: Transaction confirmed successfully');
|
|
return signature;
|
|
} catch (error) {
|
|
lastError = error as Error;
|
|
console.warn(`Mobile wallet utility: Transaction attempt ${attempt} failed:`, error);
|
|
|
|
if (attempt < maxRetries) {
|
|
// Wait before retry (exponential backoff)
|
|
const waitTime = Math.pow(2, attempt) * 1000;
|
|
console.log(`Mobile wallet utility: Waiting ${waitTime}ms before retry...`);
|
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
}
|
|
}
|
|
}
|
|
|
|
throw lastError || new Error('Transaction failed after all retry attempts');
|
|
}
|
|
|
|
/**
|
|
* Check if the current device is mobile
|
|
*/
|
|
export function isMobileDevice(): boolean {
|
|
if (typeof window === 'undefined') return false;
|
|
|
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
|
navigator.userAgent
|
|
) || window.innerWidth <= 768;
|
|
}
|
|
|
|
/**
|
|
* Get optimal commitment level for mobile wallets
|
|
*/
|
|
export function getMobileCommitmentLevel(): 'processed' | 'confirmed' | 'finalized' {
|
|
return isMobileDevice() ? 'confirmed' : 'finalized';
|
|
}
|
|
|
|
/**
|
|
* Validate transaction before sending (mobile-specific checks)
|
|
*/
|
|
export function validateTransactionForMobile(transaction: Transaction): string[] {
|
|
const errors: string[] = [];
|
|
|
|
if (!transaction.recentBlockhash) {
|
|
errors.push('Transaction missing recent blockhash');
|
|
}
|
|
|
|
if (!transaction.feePayer) {
|
|
errors.push('Transaction missing fee payer');
|
|
}
|
|
|
|
if (transaction.instructions.length === 0) {
|
|
errors.push('Transaction has no instructions');
|
|
}
|
|
|
|
return errors;
|
|
}
|