Skip to content

Commit

Permalink
feat(blockchain-link): solana fee overhaul
Browse files Browse the repository at this point in the history
  • Loading branch information
martykan committed Nov 28, 2024
1 parent 18e2ee3 commit 9c699ab
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 35 deletions.
75 changes: 51 additions & 24 deletions packages/blockchain-link/src/workers/solana/fee.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,45 @@
import {
Address,
Base64EncodedWireTransaction,
CompiledTransactionMessage,
decompileTransactionMessage,
getBase64Decoder,
getCompiledTransactionMessageEncoder,
GetFeeForMessageApi,
GetRecentPrioritizationFeesApi,
getTransactionEncoder,
isWritableRole,
MicroLamports,
pipe,
Rpc,
SignaturesMap,
SimulateTransactionApi,
TransactionMessageBytes,
TransactionMessageBytesBase64,
} from '@solana/web3.js';

import { BigNumber } from '@trezor/utils/src/bigNumber';

const COMPUTE_BUDGET_PROGRAM_ID =
'ComputeBudget111111111111111111111111111111' as Address<'ComputeBudget111111111111111111111111111111'>;
const DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = BigInt(100_000); // micro-lamports, value taken from other wallets
// sending tokens with token account creation requires ~28K units. However we over-reserve for now
// since otherwise the transactions don't seem to go through otherwise. This can perhaps be changed
// if e.g. https://github.com/anza-xyz/agave/pull/187 is merged.
const DEFAULT_COMPUTE_UNIT_LIMIT = BigInt(200_000);
const DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = BigInt(300_000); // micro-lamports, value taken from other wallets

const stripComputeBudgetInstructions = (message: CompiledTransactionMessage) => ({
...message,
// Remove ComputeBudget instructions from the message when estimating the base fee
// since the exact priority fees are computed separately and getFeeForMessage also
// considers priority fees.
instructions: message.instructions.filter(
instruction =>
message.staticAccounts[instruction.programAddressIndex] !== COMPUTE_BUDGET_PROGRAM_ID,
),
});

export const getBaseFee = async (
api: Rpc<GetFeeForMessageApi>,
message: CompiledTransactionMessage,
) => {
const messageWithoutComputeBudget = pipe(
{
...message,
// Remove ComputeBudget instructions from the message when estimating the base fee
// since the exact priority fees are computed separately and getFeeForMessage also
// considers priority fees.
instructions: message.instructions.filter(
instruction =>
message.staticAccounts[instruction.programAddressIndex] !==
COMPUTE_BUDGET_PROGRAM_ID,
),
},
stripComputeBudgetInstructions(message),
getCompiledTransactionMessageEncoder().encode,
getBase64Decoder().decode,
) as TransactionMessageBytesBase64;
Expand All @@ -55,8 +56,9 @@ export const getBaseFee = async (
// More about Solana priority fees here:
// https://solana.com/developers/guides/advanced/how-to-use-priority-fees#how-do-i-estimate-priority-fees
export const getPriorityFee = async (
api: Rpc<GetRecentPrioritizationFeesApi>,
api: Rpc<GetRecentPrioritizationFeesApi & SimulateTransactionApi>,
compiledMessage: CompiledTransactionMessage,
signatures: SignaturesMap,
) => {
const message = decompileTransactionMessage(compiledMessage);
const affectedAccounts = new Set<Address>(
Expand All @@ -66,9 +68,34 @@ export const getPriorityFee = async (
.map(({ address }) => address),
);

const recentFees = await api.getRecentPrioritizationFees(Array.from(affectedAccounts)).send();
// Reconstruct TX for simulation
const messageWithoutComputeBudget = pipe(
stripComputeBudgetInstructions(compiledMessage),
getCompiledTransactionMessageEncoder().encode,
) as TransactionMessageBytes;
const rawTx = pipe(
{
messageBytes: messageWithoutComputeBudget,
signatures,
},
getTransactionEncoder().encode,
getBase64Decoder().decode,
) as Base64EncodedWireTransaction;

const computeUnitLimit = DEFAULT_COMPUTE_UNIT_LIMIT;
const simulated = await api
.simulateTransaction(rawTx, { commitment: 'confirmed', encoding: 'base64' })
.send();
if (simulated.value.err != null || !simulated.value.unitsConsumed) {
console.error('Could not simulate transaction:', simulated.value.err);
throw new Error(`Could not simulate transaction: ${simulated.value.err}`);
}
// Add 10% margin to the computed limit
const computeUnitLimit = new BigNumber(simulated.value.unitsConsumed.toString())
.times(1.1)
.decimalPlaces(0, BigNumber.ROUND_UP);

// Local fees from API
const recentFees = await api.getRecentPrioritizationFees(Array.from(affectedAccounts)).send();

const networkPriorityFee = recentFees
.map(a => a.prioritizationFee)
Expand All @@ -81,13 +108,13 @@ export const getPriorityFee = async (

const fee = new BigNumber(computeUnitPrice.toString())
.times(10 ** -6) // microLamports -> Lamports
.times(computeUnitLimit.toString())
.times(computeUnitLimit)
.decimalPlaces(0, BigNumber.ROUND_UP)
.toString(10);

return {
computeUnitPrice,
computeUnitLimit,
fee: BigInt(fee) as MicroLamports,
computeUnitPrice: computeUnitPrice.toString(10),
computeUnitLimit: computeUnitLimit.toString(10),
fee,
};
};
21 changes: 10 additions & 11 deletions packages/blockchain-link/src/workers/solana/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import type * as MessageTypes from '@trezor/blockchain-link-types/src/messages';
import { CustomError } from '@trezor/blockchain-link-types/src/constants/errors';
import { MESSAGES, RESPONSES } from '@trezor/blockchain-link-types/src/constants';
import { solanaUtils } from '@trezor/blockchain-link-utils';
import { createLazy } from '@trezor/utils';
import { BigNumber, createLazy } from '@trezor/utils';
import {
transformTokenInfo,
TOKEN_PROGRAM_PUBLIC_KEY,
Expand Down Expand Up @@ -405,24 +405,23 @@ const estimateFee = async (request: Request<MessageTypes.EstimateFee>) => {
if (messageHex == null) {
throw new Error('Could not estimate fee for transaction.');
}

const message = pipe(
messageHex,
getBase16Encoder().encode,
getCompiledTransactionMessageDecoder().decode,
);
const transaction = pipe(messageHex, getBase16Encoder().encode, getTransactionDecoder().decode);
const message = pipe(transaction.messageBytes, getCompiledTransactionMessageDecoder().decode);

const baseFee = await getBaseFee(api.rpc, message);
const priorityFee = await getPriorityFee(api.rpc, message);
const priorityFee = await getPriorityFee(api.rpc, message, transaction.signatures);
const accountCreationFee = isCreatingAccount
? await api.rpc.getMinimumBalanceForRentExemption(BigInt(getTokenSize())).send()
: BigInt(0);

const payload = [
{
feePerTx: `${baseFee + accountCreationFee + priorityFee.fee}`,
feePerUnit: `${priorityFee.computeUnitPrice}`,
feeLimit: `${priorityFee.computeUnitLimit}`,
feePerTx: new BigNumber(baseFee.toString())
.plus(priorityFee.fee)
.plus(accountCreationFee.toString())
.toString(10),
feePerUnit: priorityFee.computeUnitPrice,
feeLimit: priorityFee.computeUnitLimit,
},
];

Expand Down
1 change: 1 addition & 0 deletions suite-common/wallet-core/src/send/sendFormSolanaThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export const composeSolanaTransactionFeeLevelsThunk = createThunk<
fetchedFeeLimit = feeLevel.feeLimit;
} else {
// Error fetching fee, fall back on default values defined in `/packages/connect/src/data/defaultFeeLevels.ts`
console.warn('Error fetching fee, using default values.', estimatedFee.payload.error);
}

// FeeLevels are read-only, so we create a copy if need be
Expand Down

0 comments on commit 9c699ab

Please sign in to comment.