Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

solana: add priority fee to redeem transactions #1795

Merged
merged 8 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 57 additions & 94 deletions sdk/src/contexts/solana/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
import {
clusterApiUrl,
Commitment,
ComputeBudgetProgram,
Connection,
Keypair,
PublicKey,
Expand Down Expand Up @@ -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 {
Expand All @@ -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 =
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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,
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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'),
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
);
const recipientTokenAccount = getAssociatedTokenAddressSync(
mint,
recipient,
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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'),
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
);
transaction.feePayer = new PublicKey(senderAddress);

if (token === NATIVE || token.chain === SOLANA_CHAIN_NAME) {
const mint = token === NATIVE ? NATIVE_MINT : token.address;
Expand Down Expand Up @@ -1099,9 +1100,6 @@ export class SolanaContext<
}
}

transaction.add(
...(await this.determineComputeBudget(writableAddresses)),
);
transaction.add(
await createTransferNativeTokensWithRelayInstruction(
this.connection,
Expand All @@ -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,
Expand All @@ -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;
}

Expand Down Expand Up @@ -1213,38 +1210,4 @@ export class SolanaContext<
chain: 'solana',
};
}

async determineComputeBudget(
lockedWritableAccounts: PublicKey[] = [],
): Promise<TransactionInstruction[]> {
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({
kev1n-peters marked this conversation as resolved.
Show resolved Hide resolved
units: 250_000,
}),
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: fee,
}),
];
}
}
104 changes: 104 additions & 0 deletions sdk/src/contexts/solana/utils/computeBudget/index.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<TransactionInstruction[]> {
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<number> {
// 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;
}
Loading
Loading