diff --git a/sdk/src/contexts/solana/context.ts b/sdk/src/contexts/solana/context.ts index c2c4eb653..fd2134263 100644 --- a/sdk/src/contexts/solana/context.ts +++ b/sdk/src/contexts/solana/context.ts @@ -23,7 +23,6 @@ import { import { clusterApiUrl, Commitment, - ComputeBudgetProgram, Connection, Keypair, PublicKey, @@ -66,6 +65,7 @@ import { getClaim, getPostedMessage, } from './utils/wormhole'; +import { addComputeBudget } from './utils/computeBudget'; import { ForeignAssetCache } from '../../utils'; import { RelayerAbstract } from '../abstracts/relayer'; import { @@ -78,9 +78,6 @@ import { const SOLANA_SEQ_LOG = 'Program log: Sequence: '; const SOLANA_CHAIN_NAME = MAINNET_CONFIG.chains.solana!.key; -// Add priority fee according to 75th percentile of recent fees paid -const SOLANA_FEE_PERCENTILE = 0.75; - const SOLANA_MAINNET_EMMITER_ID = 'ec7372995d5cc8732397fb0ad35c0121e0eaa90d26f828a534cab54391b3a4f5'; const SOLANA_TESTNET_EMITTER_ID = @@ -277,18 +274,15 @@ export class SolanaContext< payerPublicKey, tokenPublicKey, ); - const transaction = new Transaction(); - transaction.add( - ...(await this.determineComputeBudget([ - tokenPublicKey, - associatedPublicKey, - ])), + const transaction = new Transaction( + await this.connection?.getLatestBlockhash(commitment), ); transaction.add(createAccountInst); - - const { blockhash } = await this.connection.getLatestBlockhash(commitment); - transaction.recentBlockhash = blockhash; transaction.feePayer = payerPublicKey; + await addComputeBudget(this.connection!, transaction, [ + tokenPublicKey, + associatedPublicKey, + ]); return transaction; } @@ -397,12 +391,9 @@ export class SolanaContext< payerPublicKey, //authority ); - const { blockhash } = await this.connection.getLatestBlockhash(commitment); - const transaction = new Transaction(); - transaction.add(...(await this.determineComputeBudget([NATIVE_MINT]))); - - transaction.recentBlockhash = blockhash; - transaction.feePayer = payerPublicKey; + const transaction = new Transaction( + await this.connection?.getLatestBlockhash(commitment), + ); transaction.add( createAncillaryAccountIx, initialBalanceTransferIx, @@ -411,6 +402,9 @@ export class SolanaContext< tokenBridgeTransferIx, closeAccountIx, ); + + transaction.feePayer = payerPublicKey; + await addComputeBudget(this.connection!, transaction, [NATIVE_MINT]); transaction.partialSign(message, ancillaryKeypair); return transaction; } @@ -531,17 +525,16 @@ export class SolanaContext< recipientAddress, recipientChainId, ); - const transaction = new Transaction(); - transaction.add( - ...(await this.determineComputeBudget([ - new PublicKey(fromAddress), - new PublicKey(mintAddress), - ])), + + const transaction = new Transaction( + await this.connection?.getLatestBlockhash(commitment), ); transaction.add(approvalIx, tokenBridgeTransferIx); - const { blockhash } = await this.connection.getLatestBlockhash(commitment); - transaction.recentBlockhash = blockhash; transaction.feePayer = new PublicKey(senderAddress); + await addComputeBudget(this.connection!, transaction, [ + new PublicKey(fromAddress), + new PublicKey(mintAddress), + ]); transaction.partialSign(message); return transaction; } @@ -901,26 +894,30 @@ export class SolanaContext< } const parsed = parseTokenTransferVaa(signedVAA); + const tokenKey = new PublicKey(parsed.tokenAddress); const isNativeSol = parsed.tokenChain === MAINNET_CHAINS.solana && - new PublicKey(parsed.tokenAddress).equals(NATIVE_MINT); - if (isNativeSol) { - return await redeemAndUnwrapOnSolana( - this.connection, - contracts.core, - contracts.token_bridge, - payerAddr, - signedVAA, - ); - } else { - return await redeemOnSolana( - this.connection, - contracts.core, - contracts.token_bridge, - payerAddr, - signedVAA, - ); - } + tokenKey.equals(NATIVE_MINT); + + const transaction = isNativeSol + ? await redeemAndUnwrapOnSolana( + this.connection, + contracts.core, + contracts.token_bridge, + payerAddr, + signedVAA, + ) + : await redeemOnSolana( + this.connection, + contracts.core, + contracts.token_bridge, + payerAddr, + signedVAA, + ); + + await addComputeBudget(this.connection!, transaction, [tokenKey]); + + return transaction; } async redeemRelay( @@ -953,8 +950,10 @@ export class SolanaContext< parsed.tokenChain, parsed.tokenAddress, ); - const transaction = new Transaction(); - transaction.add(...(await this.determineComputeBudget([mint]))); + + const transaction = new Transaction( + await this.connection?.getLatestBlockhash('finalized'), + ); const recipientTokenAccount = getAssociatedTokenAddressSync( mint, recipient, @@ -996,9 +995,8 @@ export class SolanaContext< ); } transaction.add(redeemIx); - const { blockhash } = await this.connection.getLatestBlockhash('finalized'); - transaction.recentBlockhash = blockhash; transaction.feePayer = new PublicKey(recipient); + await addComputeBudget(this.connection!, transaction, [mint]); return transaction; } @@ -1065,7 +1063,10 @@ export class SolanaContext< ); const recipientChainId = this.context.toChainId(recipientChain); const nonce = createNonce().readUint32LE(); - const transaction = new Transaction(); + const transaction = new Transaction( + await this.connection?.getLatestBlockhash('finalized'), + ); + transaction.feePayer = new PublicKey(senderAddress); if (token === NATIVE || token.chain === SOLANA_CHAIN_NAME) { const mint = token === NATIVE ? NATIVE_MINT : token.address; @@ -1099,9 +1100,6 @@ export class SolanaContext< } } - transaction.add( - ...(await this.determineComputeBudget(writableAddresses)), - ); transaction.add( await createTransferNativeTokensWithRelayInstruction( this.connection, @@ -1118,12 +1116,11 @@ export class SolanaContext< wrapToken, ), ); + + await addComputeBudget(this.connection!, transaction, writableAddresses); } else { const mint = await this.mustGetForeignAsset(token, sendingChain); - transaction.add( - ...(await this.determineComputeBudget([new PublicKey(mint)])), - ); transaction.add( await createTransferWrappedTokensWithRelayInstruction( this.connection, @@ -1139,11 +1136,11 @@ export class SolanaContext< nonce, ), ); - } - const { blockhash } = await this.connection.getLatestBlockhash('finalized'); - transaction.recentBlockhash = blockhash; - transaction.feePayer = new PublicKey(senderAddress); + await addComputeBudget(this.connection!, transaction, [ + new PublicKey(mint), + ]); + } return transaction; } @@ -1213,38 +1210,4 @@ export class SolanaContext< chain: 'solana', }; } - - async determineComputeBudget( - lockedWritableAccounts: PublicKey[] = [], - ): Promise { - let fee = 100_000; // Set fee to 100,000 microlamport by default - - try { - const recentFeesResponse = - await this.connection?.getRecentPrioritizationFees({ - lockedWritableAccounts, - }); - - if (recentFeesResponse) { - // Get 75th percentile fee paid in recent slots - const recentFees = recentFeesResponse - .map((dp) => dp.prioritizationFee) - .sort((a, b) => a - b); - fee = recentFees[Math.floor(recentFees.length * SOLANA_FEE_PERCENTILE)]; - } - } catch (e) { - console.error('Error fetching Solana recent fees', e); - } - - console.info(`Setting Solana compute unit price to ${fee} microLamports`); - - return [ - ComputeBudgetProgram.setComputeUnitLimit({ - units: 250_000, - }), - ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: fee, - }), - ]; - } } diff --git a/sdk/src/contexts/solana/utils/computeBudget/index.ts b/sdk/src/contexts/solana/utils/computeBudget/index.ts new file mode 100644 index 000000000..a342ee3e6 --- /dev/null +++ b/sdk/src/contexts/solana/utils/computeBudget/index.ts @@ -0,0 +1,104 @@ +import { + Connection, + Transaction, + TransactionInstruction, + PublicKey, + ComputeBudgetProgram, +} from '@solana/web3.js'; + +// Add priority fee according to 50th percentile of recent fees paid +const DEFAULT_FEE_PERCENTILE = 0.5; + +export async function addComputeBudget( + connection: Connection, + transaction: Transaction, + lockedWritableAccounts: PublicKey[] = [], +): Promise { + const ixs = await determineComputeBudget( + connection, + transaction, + lockedWritableAccounts, + ); + transaction.add(...ixs); +} + +export async function determineComputeBudget( + connection: Connection, + transaction: Transaction, + lockedWritableAccounts: PublicKey[] = [], + feePercentile: number = DEFAULT_FEE_PERCENTILE, +): Promise { + let computeBudget = 250_000; + let priorityFee = 1; + + try { + const simulateResponse = await connection.simulateTransaction(transaction); + + if (simulateResponse.value.err) { + console.error( + `Error simulating Solana transaction: ${simulateResponse.value.err}`, + ); + } + + if (simulateResponse?.value?.unitsConsumed) { + // Set compute budget to 120% of the units used in the simulated transaction + computeBudget = Math.round(simulateResponse.value.unitsConsumed * 1.2); + } + + priorityFee = await determinePriorityFee( + connection, + lockedWritableAccounts, + feePercentile, + ); + } catch (e) { + console.error(`Failed to get compute budget for Solana transaction: ${e}`); + } + + console.info(`Setting Solana compute unit budget to ${computeBudget} units`); + console.info( + `Setting Solana compute unit price to ${priorityFee} microLamports`, + ); + + return [ + ComputeBudgetProgram.setComputeUnitLimit({ + units: computeBudget, + }), + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: priorityFee, + }), + ]; +} + +async function determinePriorityFee( + connection: Connection, + lockedWritableAccounts: PublicKey[] = [], + percentile: number, +): Promise { + // https://twitter.com/0xMert_/status/1768669928825962706 + + let fee = 1; // Set fee to 1 microlamport by default + + try { + const recentFeesResponse = await connection.getRecentPrioritizationFees({ + lockedWritableAccounts, + }); + + if (recentFeesResponse) { + // Get 75th percentile fee paid in recent slots + const recentFees = recentFeesResponse + .map((dp) => dp.prioritizationFee) + .filter((dp) => dp > 0) + .sort((a, b) => a - b); + + if (recentFees.length > 0) { + const medianFee = + recentFees[Math.floor(recentFees.length * percentile)]; + fee = Math.max(fee, medianFee); + } + } + } catch (e) { + console.error('Error fetching Solana recent fees', e); + } + + return fee; +} diff --git a/sdk/src/contexts/solana/utils/sendAndConfirmPostVaa.ts b/sdk/src/contexts/solana/utils/sendAndConfirmPostVaa.ts index 84ad83077..83520c8c4 100644 --- a/sdk/src/contexts/solana/utils/sendAndConfirmPostVaa.ts +++ b/sdk/src/contexts/solana/utils/sendAndConfirmPostVaa.ts @@ -14,6 +14,7 @@ import { TransactionSignatureAndResponse, PreparedTransactions, } from './utils'; +import { addComputeBudget } from './computeBudget'; import { createPostVaaInstruction, createVerifySignaturesInstructions, @@ -40,9 +41,11 @@ export async function postVaaWithRetry( vaa, commitment, ); - const postVaaTransaction = unsignedTransactions.pop()!; + for (const unsignedTransaction of unsignedTransactions) { + await addComputeBudget(connection, unsignedTransaction); + } const responses = await sendAndConfirmTransactionsWithRetry( connection, modifySignTransaction(signTransaction, ...signers), @@ -51,6 +54,7 @@ export async function postVaaWithRetry( maxRetries, ); //While the signature_set is used to create the final instruction, it doesn't need to sign it. + await addComputeBudget(connection, postVaaTransaction); responses.push( ...(await sendAndConfirmTransactionsWithRetry( connection, diff --git a/wormhole-connect/src/utils/wallet/index.ts b/wormhole-connect/src/utils/wallet/index.ts index de3a39e29..9f635772f 100644 --- a/wormhole-connect/src/utils/wallet/index.ts +++ b/wormhole-connect/src/utils/wallet/index.ts @@ -4,12 +4,9 @@ import { Context, SendResult, ChainConfig, -} from '@wormhole-foundation/wormhole-connect-sdk'; -import { postVaaSolanaWithRetry, - CHAIN_ID_EVMOS, - CHAIN_ID_INJECTIVE, -} from '@certusone/wormhole-sdk'; +} from '@wormhole-foundation/wormhole-connect-sdk'; +import { CHAIN_ID_EVMOS, CHAIN_ID_INJECTIVE } from '@certusone/wormhole-sdk'; import { ContractReceipt } from 'ethers'; import { NotSupported,