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

Added Triton One improved priority fee API support #785

Merged
merged 6 commits into from
Jan 23, 2025
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
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
Loading