Skip to content

Commit

Permalink
Added Triton One improved priority fee API support (#785)
Browse files Browse the repository at this point in the history
* Added Triton One improved priority fee API support

Added determinePriorityFeeTritonOne function

* add type back

* propagate exception

* address review feedback

* address feedback

* fix function signatures

---------

Co-authored-by: Artur Sapek <[email protected]>
  • Loading branch information
kev1n-peters and artursapek authored Jan 23, 2025
1 parent f2b3167 commit 6df349a
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 26 deletions.
27 changes: 27 additions & 0 deletions core/base/src/utils/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,30 @@ export type Cartesian<L, R> =
: R extends RoArray
? [...{ [K in keyof R]: K extends `${number}` ? [L, R[K]] : never }]
: [L, R];

export function median(arr: RoArray<number>, isSorted?: boolean): number;
export function median(arr: RoArray<bigint>, isSorted?: boolean): bigint;
export function median(arr: RoArray<number> | RoArray<bigint>, isSorted: boolean = false): number | bigint {
if (arr.length === 0) throw new Error("Can't calculate median of empty array");

const sorted = isSorted ? arr : [...arr].sort((a, b) => (a > b ? 1 : a < b ? -1 : 0)); // handle bigint and number

const mid = Math.floor(sorted.length / 2);

if (sorted.length % 2 === 1) {
return sorted[mid]!;
}

const left = sorted[mid - 1]!;
const right = sorted[mid]!;

if (typeof left === "bigint" && typeof right === "bigint") {
return (left + right) / 2n;
}

if (typeof left === "number" && typeof right === "number") {
return (left + right) / 2;
}

throw new Error("Can't calculate median of array with mixed number and bigint");
}
12 changes: 12 additions & 0 deletions core/base/src/utils/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,15 @@ export function throws(fn: () => any): boolean {
return true;
}
}

export function bound(value: number, min: number, max: number): number;
export function bound(value: bigint, min: bigint, max: bigint): bigint;
export function bound(
value: number | bigint,
min: number | bigint,
max: number | bigint,
): number | bigint {
if (value < min) return min;
if (value > max) return max;
return value;
}
121 changes: 95 additions & 26 deletions platforms/solana/src/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
VersionedTransaction,
PublicKey,
AddressLookupTableAccount,
RecentPrioritizationFees,
} from '@solana/web3.js';
import {
ComputeBudgetProgram,
Expand All @@ -21,7 +22,7 @@ import type {
Signer,
UnsignedTransaction,
} from '@wormhole-foundation/sdk-connect';
import { encoding } from '@wormhole-foundation/sdk-connect';
import { bound, encoding, median } from '@wormhole-foundation/sdk-connect';
import { SolanaPlatform } from './platform.js';
import type { SolanaChains } from './types.js';
import {
Expand Down Expand Up @@ -380,6 +381,37 @@ export async function determineComputeBudget(
return computeBudget;
}

// Helper function to get the writable accounts from a transaction
export async function getWritableAccounts(
connection: Connection,
transaction: Transaction | VersionedTransaction,
): Promise<PublicKey[]> {
if (isVersionedTransaction(transaction)) {
const luts = (
await Promise.all(
transaction.message.addressTableLookups.map((acc) =>
connection.getAddressLookupTable(acc.accountKey),
),
)
)
.map((lut) => lut.value)
.filter((val) => val !== null) as AddressLookupTableAccount[];
const msg = transaction.message;
const keys = msg.getAccountKeys({
addressLookupTableAccounts: luts ?? undefined,
});
return msg.compiledInstructions
.flatMap((ix) => ix.accountKeyIndexes)
.map((k) => (msg.isAccountWritable(k) ? keys.get(k) : null))
.filter(Boolean) as PublicKey[];
} else {
return transaction.instructions
.flatMap((ix) => ix.keys)
.map((k) => (k.isWritable ? k.pubkey : null))
.filter(Boolean) as PublicKey[];
}
}

/**
* A helper function to determine the priority fee to use for a transaction
*
Expand All @@ -405,31 +437,10 @@ export async function determinePriorityFee(
let fee = minPriorityFee;

// Figure out which accounts need write lock
let lockedWritableAccounts = [];
if (isVersionedTransaction(transaction)) {
const luts = (
await Promise.all(
transaction.message.addressTableLookups.map((acc) =>
connection.getAddressLookupTable(acc.accountKey),
),
)
)
.map((lut) => lut.value)
.filter((val) => val !== null) as AddressLookupTableAccount[];
const msg = transaction.message;
const keys = msg.getAccountKeys({
addressLookupTableAccounts: luts ?? undefined,
});
lockedWritableAccounts = msg.compiledInstructions
.flatMap((ix) => ix.accountKeyIndexes)
.map((k) => (msg.isAccountWritable(k) ? keys.get(k) : null))
.filter((k) => k !== null) as PublicKey[];
} else {
lockedWritableAccounts = transaction.instructions
.flatMap((ix) => ix.keys)
.map((k) => (k.isWritable ? k.pubkey : null))
.filter((k) => k !== null) as PublicKey[];
}
const lockedWritableAccounts = await getWritableAccounts(
connection,
transaction,
);

try {
const recentFeesResponse = await connection.getRecentPrioritizationFees({
Expand Down Expand Up @@ -461,6 +472,64 @@ export async function determinePriorityFee(
return Math.min(Math.max(fee, minPriorityFee), maxPriorityFee);
}

interface RpcResponse {
jsonrpc: String;
id?: String;
result?: [];
error?: any;
}

// Helper function to calculate the priority fee using the Triton One API
// See https://docs.triton.one/chains/solana/improved-priority-fees-api
// NOTE: this is currently an experimental feature
export async function determinePriorityFeeTritonOne(
connection: Connection,
transaction: Transaction | VersionedTransaction,
percentile: number = DEFAULT_PRIORITY_FEE_PERCENTILE,
multiple: number = DEFAULT_PERCENTILE_MULTIPLE,
minPriorityFee: number = DEFAULT_MIN_PRIORITY_FEE,
maxPriorityFee: number = DEFAULT_MAX_PRIORITY_FEE,
): Promise<number> {
const scaledPercentile = percentile * 10_000;

if (scaledPercentile < 1 || scaledPercentile > 10_000) {
throw new Error('percentile must be between 0.0001 and 1');
}

// @ts-ignore
const rpcRequest = connection._rpcRequest;

const accounts = await getWritableAccounts(connection, transaction);

const args = [
accounts,
{
percentile: scaledPercentile,
},
];

const response = (await rpcRequest(
'getRecentPrioritizationFees',
args,
)) as RpcResponse;

if (response.error) {
throw new Error(response.error);
}

const recentPrioritizationFees = (
response.result as RecentPrioritizationFees[]
).map((e) => e.prioritizationFee);

if (recentPrioritizationFees.length === 0) return minPriorityFee;

const unboundedFee = Math.floor(
median(recentPrioritizationFees) * (multiple > 0 ? multiple : 1),
);

return bound(unboundedFee, minPriorityFee, maxPriorityFee);
}

export class SolanaSigner<N extends Network, C extends SolanaChains = 'Solana'>
implements SignOnlySigner<N, C>
{
Expand Down

0 comments on commit 6df349a

Please sign in to comment.