From db1e55b09d1d7f85adad5fd3b825b73e7bb683aa Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Sun, 19 Jan 2025 17:36:47 -0600 Subject: [PATCH 1/6] Added Triton One improved priority fee API support Added determinePriorityFeeTritonOne function --- platforms/solana/src/signer.ts | 137 ++++++++++++++++++++++++++------- 1 file changed, 111 insertions(+), 26 deletions(-) diff --git a/platforms/solana/src/signer.ts b/platforms/solana/src/signer.ts index 5f0febfa6..9fed1dfa1 100644 --- a/platforms/solana/src/signer.ts +++ b/platforms/solana/src/signer.ts @@ -1,4 +1,4 @@ -import type { +import { Connection, SendOptions, Transaction, @@ -6,6 +6,7 @@ import type { VersionedTransaction, PublicKey, AddressLookupTableAccount, + RecentPrioritizationFees, } from '@solana/web3.js'; import { ComputeBudgetProgram, @@ -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 { + 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((k) => !!k) as PublicKey[]; + } else { + return transaction.instructions + .flatMap((ix) => ix.keys) + .map((k) => (k.isWritable ? k.pubkey : null)) + .filter((k) => !!k) as PublicKey[]; + } +} + /** * A helper function to determine the priority fee to use for a transaction * @@ -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({ @@ -461,6 +472,80 @@ 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 +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 { + // Start with min fee + let fee = minPriorityFee; + + // @ts-ignore + const rpcRequest = connection._rpcRequest; + + const accounts = await getWritableAccounts(connection, transaction); + + const args = [ + accounts, + { + percentile: percentile * 10_000, // between 1 and 10_000 + }, + ]; + + try { + const response = (await rpcRequest( + 'getRecentPrioritizationFees', + args, + )) as RpcResponse; + + if (response.error) { + throw new Error(response.error); + } + + const recentPrioritizationFees = + response.result as RecentPrioritizationFees[]; + + if (recentPrioritizationFees.length > 0) { + const sortedFees = recentPrioritizationFees.sort( + (a, b) => a.prioritizationFee - b.prioritizationFee, + ); + + // Calculate the median + const half = Math.floor(sortedFees.length / 2); + if (sortedFees.length % 2 === 0) { + fee = Math.floor( + (sortedFees[half - 1]!.prioritizationFee + + sortedFees[half]!.prioritizationFee) / + 2, + ); + } else { + fee = sortedFees[half]!.prioritizationFee; + } + + if (multiple > 0) { + fee *= multiple; + } + } + } catch (e) { + console.error('Error fetching Triton One Solana recent fees', e); + } + + // Bound the return value by the parameters passed + return Math.min(Math.max(fee, minPriorityFee), maxPriorityFee); +} + export class SolanaSigner implements SignOnlySigner { From 395d17d8aad93143d5b09d2d4d8637831ab0c493 Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Sun, 19 Jan 2025 17:51:00 -0600 Subject: [PATCH 2/6] add type back --- platforms/solana/src/signer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platforms/solana/src/signer.ts b/platforms/solana/src/signer.ts index 9fed1dfa1..1d357e400 100644 --- a/platforms/solana/src/signer.ts +++ b/platforms/solana/src/signer.ts @@ -1,4 +1,4 @@ -import { +import type { Connection, SendOptions, Transaction, From 5b646e09d500422f6516fc463a4781d4dc4bd198 Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Mon, 20 Jan 2025 10:42:22 -0600 Subject: [PATCH 3/6] propagate exception --- platforms/solana/src/signer.ts | 62 ++++++++++++++++------------------ 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/platforms/solana/src/signer.ts b/platforms/solana/src/signer.ts index 1d357e400..27f1b6e02 100644 --- a/platforms/solana/src/signer.ts +++ b/platforms/solana/src/signer.ts @@ -403,12 +403,12 @@ export async function getWritableAccounts( return msg.compiledInstructions .flatMap((ix) => ix.accountKeyIndexes) .map((k) => (msg.isAccountWritable(k) ? keys.get(k) : null)) - .filter((k) => !!k) as PublicKey[]; + .filter(Boolean) as PublicKey[]; } else { return transaction.instructions .flatMap((ix) => ix.keys) .map((k) => (k.isWritable ? k.pubkey : null)) - .filter((k) => !!k) as PublicKey[]; + .filter(Boolean) as PublicKey[]; } } @@ -481,6 +481,7 @@ interface RpcResponse { // 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, @@ -504,42 +505,39 @@ export async function determinePriorityFeeTritonOne( }, ]; - try { - const response = (await rpcRequest( - 'getRecentPrioritizationFees', - args, - )) as RpcResponse; + const response = (await rpcRequest( + 'getRecentPrioritizationFees', + args, + )) as RpcResponse; - if (response.error) { - throw new Error(response.error); - } + if (response.error) { + // Allow the error to propagate so the caller can handle it and fall-back if needed + throw new Error(response.error); + } - const recentPrioritizationFees = - response.result as RecentPrioritizationFees[]; + const recentPrioritizationFees = + response.result as RecentPrioritizationFees[]; - if (recentPrioritizationFees.length > 0) { - const sortedFees = recentPrioritizationFees.sort( - (a, b) => a.prioritizationFee - b.prioritizationFee, - ); + if (recentPrioritizationFees.length > 0) { + const sortedFees = recentPrioritizationFees.sort( + (a, b) => a.prioritizationFee - b.prioritizationFee, + ); - // Calculate the median - const half = Math.floor(sortedFees.length / 2); - if (sortedFees.length % 2 === 0) { - fee = Math.floor( - (sortedFees[half - 1]!.prioritizationFee + - sortedFees[half]!.prioritizationFee) / - 2, - ); - } else { - fee = sortedFees[half]!.prioritizationFee; - } + // Calculate the median + const half = Math.floor(sortedFees.length / 2); + if (sortedFees.length % 2 === 0) { + fee = Math.floor( + (sortedFees[half - 1]!.prioritizationFee + + sortedFees[half]!.prioritizationFee) / + 2, + ); + } else { + fee = sortedFees[half]!.prioritizationFee; + } - if (multiple > 0) { - fee *= multiple; - } + if (multiple > 0) { + fee *= multiple; } - } catch (e) { - console.error('Error fetching Triton One Solana recent fees', e); } // Bound the return value by the parameters passed From 7e9837afc0cd5cce0d010c1c6cb52408f026f6c5 Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Mon, 20 Jan 2025 18:44:13 -0600 Subject: [PATCH 4/6] address review feedback --- core/base/src/utils/array.ts | 28 ++++++++++++++++++++++++ core/base/src/utils/misc.ts | 4 ++++ platforms/solana/src/signer.ts | 40 ++++++++++------------------------ 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/core/base/src/utils/array.ts b/core/base/src/utils/array.ts index ddd9bf49d..c374016f2 100644 --- a/core/base/src/utils/array.ts +++ b/core/base/src/utils/array.ts @@ -174,3 +174,31 @@ export type Cartesian = : R extends RoArray ? [...{ [K in keyof R]: K extends `${number}` ? [L, R[K]] : never }] : [L, R]; + +export const median = (arr: T[], isSorted: boolean = false): T => { + if (arr.length === 0) throw new Error("Can't calculate median of empty array"); + + const sorted = isSorted + ? arr + : [...arr].sort((a, b) => { + // handle bigint and number + return a > b ? 1 : a < b ? -1 : 0; + }); + + 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) as T; + } else if (typeof left === "number" && typeof right === "number") { + return ((left + right) / 2) as T; + } else { + throw new Error("Can't calculate median of array with mixed number and bigint"); + } +}; \ No newline at end of file diff --git a/core/base/src/utils/misc.ts b/core/base/src/utils/misc.ts index bb8d17f03..3001902cb 100644 --- a/core/base/src/utils/misc.ts +++ b/core/base/src/utils/misc.ts @@ -25,3 +25,7 @@ export function throws(fn: () => any): boolean { return true; } } + +export function bound(value: T, min: T, max: T): T { + return min > value ? min : max < value ? max : value; +} \ No newline at end of file diff --git a/platforms/solana/src/signer.ts b/platforms/solana/src/signer.ts index 27f1b6e02..44c9d50a8 100644 --- a/platforms/solana/src/signer.ts +++ b/platforms/solana/src/signer.ts @@ -22,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 { @@ -490,8 +490,9 @@ export async function determinePriorityFeeTritonOne( minPriorityFee: number = DEFAULT_MIN_PRIORITY_FEE, maxPriorityFee: number = DEFAULT_MAX_PRIORITY_FEE, ): Promise { - // Start with min fee - let fee = minPriorityFee; + if (percentile <= 0 || percentile > 1) { + throw new Error('percentile must be between 0 and 1'); + } // @ts-ignore const rpcRequest = connection._rpcRequest; @@ -511,37 +512,20 @@ export async function determinePriorityFeeTritonOne( )) as RpcResponse; if (response.error) { - // Allow the error to propagate so the caller can handle it and fall-back if needed throw new Error(response.error); } - const recentPrioritizationFees = - response.result as RecentPrioritizationFees[]; + const recentPrioritizationFees = ( + response.result as RecentPrioritizationFees[] + ).map((e) => e.prioritizationFee); - if (recentPrioritizationFees.length > 0) { - const sortedFees = recentPrioritizationFees.sort( - (a, b) => a.prioritizationFee - b.prioritizationFee, - ); + if (recentPrioritizationFees.length === 0) return minPriorityFee; - // Calculate the median - const half = Math.floor(sortedFees.length / 2); - if (sortedFees.length % 2 === 0) { - fee = Math.floor( - (sortedFees[half - 1]!.prioritizationFee + - sortedFees[half]!.prioritizationFee) / - 2, - ); - } else { - fee = sortedFees[half]!.prioritizationFee; - } - - if (multiple > 0) { - fee *= multiple; - } - } + const unboundedFee = Math.floor( + median(recentPrioritizationFees) * (multiple > 0 ? multiple : 1), + ); - // Bound the return value by the parameters passed - return Math.min(Math.max(fee, minPriorityFee), maxPriorityFee); + return bound(unboundedFee, minPriorityFee, maxPriorityFee); } export class SolanaSigner From 778df30bed43e9ec28256b65aa85c39a10fb06ef Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Wed, 22 Jan 2025 08:25:34 -0600 Subject: [PATCH 5/6] address feedback --- core/base/src/utils/array.ts | 25 ++++++++++++------------- core/base/src/utils/misc.ts | 14 +++++++++++--- platforms/solana/src/signer.ts | 8 +++++--- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/core/base/src/utils/array.ts b/core/base/src/utils/array.ts index c374016f2..e7af1c3e7 100644 --- a/core/base/src/utils/array.ts +++ b/core/base/src/utils/array.ts @@ -175,15 +175,12 @@ export type Cartesian = ? [...{ [K in keyof R]: K extends `${number}` ? [L, R[K]] : never }] : [L, R]; -export const median = (arr: T[], isSorted: boolean = false): T => { +export function median(arr: number[], isSorted?: boolean): number; +export function median(arr: bigint[], isSorted?: boolean): bigint; +export function median(arr: (number | 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) => { - // handle bigint and number - return a > b ? 1 : a < b ? -1 : 0; - }); + 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); @@ -195,10 +192,12 @@ export const median = (arr: T[], isSorted: boolean = const right = sorted[mid]!; if (typeof left === "bigint" && typeof right === "bigint") { - return ((left + right) / 2n) as T; - } else if (typeof left === "number" && typeof right === "number") { - return ((left + right) / 2) as T; - } else { - throw new Error("Can't calculate median of array with mixed number and bigint"); + return (left + right) / 2n; } -}; \ No newline at end of file + + 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"); +} diff --git a/core/base/src/utils/misc.ts b/core/base/src/utils/misc.ts index 3001902cb..816c802fe 100644 --- a/core/base/src/utils/misc.ts +++ b/core/base/src/utils/misc.ts @@ -26,6 +26,14 @@ export function throws(fn: () => any): boolean { } } -export function bound(value: T, min: T, max: T): T { - return min > value ? min : max < value ? max : value; -} \ No newline at end of file +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; +} diff --git a/platforms/solana/src/signer.ts b/platforms/solana/src/signer.ts index 44c9d50a8..6b6eaaf16 100644 --- a/platforms/solana/src/signer.ts +++ b/platforms/solana/src/signer.ts @@ -490,8 +490,10 @@ export async function determinePriorityFeeTritonOne( minPriorityFee: number = DEFAULT_MIN_PRIORITY_FEE, maxPriorityFee: number = DEFAULT_MAX_PRIORITY_FEE, ): Promise { - if (percentile <= 0 || percentile > 1) { - throw new Error('percentile must be between 0 and 1'); + const scaledPercentile = percentile * 10_000; + + if (scaledPercentile < 1 || scaledPercentile > 10_000) { + throw new Error('percentile must be between 0.0001 and 1'); } // @ts-ignore @@ -502,7 +504,7 @@ export async function determinePriorityFeeTritonOne( const args = [ accounts, { - percentile: percentile * 10_000, // between 1 and 10_000 + percentile: scaledPercentile, }, ]; From 4717d36adb82137f9dc347bfcb47636427062f07 Mon Sep 17 00:00:00 2001 From: Artur Sapek Date: Thu, 23 Jan 2025 11:30:31 -0500 Subject: [PATCH 6/6] fix function signatures --- core/base/src/utils/array.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/base/src/utils/array.ts b/core/base/src/utils/array.ts index e7af1c3e7..d20a93de5 100644 --- a/core/base/src/utils/array.ts +++ b/core/base/src/utils/array.ts @@ -175,9 +175,9 @@ export type Cartesian = ? [...{ [K in keyof R]: K extends `${number}` ? [L, R[K]] : never }] : [L, R]; -export function median(arr: number[], isSorted?: boolean): number; -export function median(arr: bigint[], isSorted?: boolean): bigint; -export function median(arr: (number | bigint)[], isSorted: boolean = false): number | bigint { +export function median(arr: RoArray, isSorted?: boolean): number; +export function median(arr: RoArray, isSorted?: boolean): bigint; +export function median(arr: RoArray | RoArray, 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