Skip to content

Commit 6df349a

Browse files
Added Triton One improved priority fee API support (#785)
* 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 <art@wormholelabs.xyz>
1 parent f2b3167 commit 6df349a

File tree

3 files changed

+134
-26
lines changed

3 files changed

+134
-26
lines changed

core/base/src/utils/array.ts

+27
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,30 @@ export type Cartesian<L, R> =
174174
: R extends RoArray
175175
? [...{ [K in keyof R]: K extends `${number}` ? [L, R[K]] : never }]
176176
: [L, R];
177+
178+
export function median(arr: RoArray<number>, isSorted?: boolean): number;
179+
export function median(arr: RoArray<bigint>, isSorted?: boolean): bigint;
180+
export function median(arr: RoArray<number> | RoArray<bigint>, isSorted: boolean = false): number | bigint {
181+
if (arr.length === 0) throw new Error("Can't calculate median of empty array");
182+
183+
const sorted = isSorted ? arr : [...arr].sort((a, b) => (a > b ? 1 : a < b ? -1 : 0)); // handle bigint and number
184+
185+
const mid = Math.floor(sorted.length / 2);
186+
187+
if (sorted.length % 2 === 1) {
188+
return sorted[mid]!;
189+
}
190+
191+
const left = sorted[mid - 1]!;
192+
const right = sorted[mid]!;
193+
194+
if (typeof left === "bigint" && typeof right === "bigint") {
195+
return (left + right) / 2n;
196+
}
197+
198+
if (typeof left === "number" && typeof right === "number") {
199+
return (left + right) / 2;
200+
}
201+
202+
throw new Error("Can't calculate median of array with mixed number and bigint");
203+
}

core/base/src/utils/misc.ts

+12
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,15 @@ export function throws(fn: () => any): boolean {
2525
return true;
2626
}
2727
}
28+
29+
export function bound(value: number, min: number, max: number): number;
30+
export function bound(value: bigint, min: bigint, max: bigint): bigint;
31+
export function bound(
32+
value: number | bigint,
33+
min: number | bigint,
34+
max: number | bigint,
35+
): number | bigint {
36+
if (value < min) return min;
37+
if (value > max) return max;
38+
return value;
39+
}

platforms/solana/src/signer.ts

+95-26
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
VersionedTransaction,
77
PublicKey,
88
AddressLookupTableAccount,
9+
RecentPrioritizationFees,
910
} from '@solana/web3.js';
1011
import {
1112
ComputeBudgetProgram,
@@ -21,7 +22,7 @@ import type {
2122
Signer,
2223
UnsignedTransaction,
2324
} from '@wormhole-foundation/sdk-connect';
24-
import { encoding } from '@wormhole-foundation/sdk-connect';
25+
import { bound, encoding, median } from '@wormhole-foundation/sdk-connect';
2526
import { SolanaPlatform } from './platform.js';
2627
import type { SolanaChains } from './types.js';
2728
import {
@@ -380,6 +381,37 @@ export async function determineComputeBudget(
380381
return computeBudget;
381382
}
382383

384+
// Helper function to get the writable accounts from a transaction
385+
export async function getWritableAccounts(
386+
connection: Connection,
387+
transaction: Transaction | VersionedTransaction,
388+
): Promise<PublicKey[]> {
389+
if (isVersionedTransaction(transaction)) {
390+
const luts = (
391+
await Promise.all(
392+
transaction.message.addressTableLookups.map((acc) =>
393+
connection.getAddressLookupTable(acc.accountKey),
394+
),
395+
)
396+
)
397+
.map((lut) => lut.value)
398+
.filter((val) => val !== null) as AddressLookupTableAccount[];
399+
const msg = transaction.message;
400+
const keys = msg.getAccountKeys({
401+
addressLookupTableAccounts: luts ?? undefined,
402+
});
403+
return msg.compiledInstructions
404+
.flatMap((ix) => ix.accountKeyIndexes)
405+
.map((k) => (msg.isAccountWritable(k) ? keys.get(k) : null))
406+
.filter(Boolean) as PublicKey[];
407+
} else {
408+
return transaction.instructions
409+
.flatMap((ix) => ix.keys)
410+
.map((k) => (k.isWritable ? k.pubkey : null))
411+
.filter(Boolean) as PublicKey[];
412+
}
413+
}
414+
383415
/**
384416
* A helper function to determine the priority fee to use for a transaction
385417
*
@@ -405,31 +437,10 @@ export async function determinePriorityFee(
405437
let fee = minPriorityFee;
406438

407439
// Figure out which accounts need write lock
408-
let lockedWritableAccounts = [];
409-
if (isVersionedTransaction(transaction)) {
410-
const luts = (
411-
await Promise.all(
412-
transaction.message.addressTableLookups.map((acc) =>
413-
connection.getAddressLookupTable(acc.accountKey),
414-
),
415-
)
416-
)
417-
.map((lut) => lut.value)
418-
.filter((val) => val !== null) as AddressLookupTableAccount[];
419-
const msg = transaction.message;
420-
const keys = msg.getAccountKeys({
421-
addressLookupTableAccounts: luts ?? undefined,
422-
});
423-
lockedWritableAccounts = msg.compiledInstructions
424-
.flatMap((ix) => ix.accountKeyIndexes)
425-
.map((k) => (msg.isAccountWritable(k) ? keys.get(k) : null))
426-
.filter((k) => k !== null) as PublicKey[];
427-
} else {
428-
lockedWritableAccounts = transaction.instructions
429-
.flatMap((ix) => ix.keys)
430-
.map((k) => (k.isWritable ? k.pubkey : null))
431-
.filter((k) => k !== null) as PublicKey[];
432-
}
440+
const lockedWritableAccounts = await getWritableAccounts(
441+
connection,
442+
transaction,
443+
);
433444

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

475+
interface RpcResponse {
476+
jsonrpc: String;
477+
id?: String;
478+
result?: [];
479+
error?: any;
480+
}
481+
482+
// Helper function to calculate the priority fee using the Triton One API
483+
// See https://docs.triton.one/chains/solana/improved-priority-fees-api
484+
// NOTE: this is currently an experimental feature
485+
export async function determinePriorityFeeTritonOne(
486+
connection: Connection,
487+
transaction: Transaction | VersionedTransaction,
488+
percentile: number = DEFAULT_PRIORITY_FEE_PERCENTILE,
489+
multiple: number = DEFAULT_PERCENTILE_MULTIPLE,
490+
minPriorityFee: number = DEFAULT_MIN_PRIORITY_FEE,
491+
maxPriorityFee: number = DEFAULT_MAX_PRIORITY_FEE,
492+
): Promise<number> {
493+
const scaledPercentile = percentile * 10_000;
494+
495+
if (scaledPercentile < 1 || scaledPercentile > 10_000) {
496+
throw new Error('percentile must be between 0.0001 and 1');
497+
}
498+
499+
// @ts-ignore
500+
const rpcRequest = connection._rpcRequest;
501+
502+
const accounts = await getWritableAccounts(connection, transaction);
503+
504+
const args = [
505+
accounts,
506+
{
507+
percentile: scaledPercentile,
508+
},
509+
];
510+
511+
const response = (await rpcRequest(
512+
'getRecentPrioritizationFees',
513+
args,
514+
)) as RpcResponse;
515+
516+
if (response.error) {
517+
throw new Error(response.error);
518+
}
519+
520+
const recentPrioritizationFees = (
521+
response.result as RecentPrioritizationFees[]
522+
).map((e) => e.prioritizationFee);
523+
524+
if (recentPrioritizationFees.length === 0) return minPriorityFee;
525+
526+
const unboundedFee = Math.floor(
527+
median(recentPrioritizationFees) * (multiple > 0 ? multiple : 1),
528+
);
529+
530+
return bound(unboundedFee, minPriorityFee, maxPriorityFee);
531+
}
532+
464533
export class SolanaSigner<N extends Network, C extends SolanaChains = 'Solana'>
465534
implements SignOnlySigner<N, C>
466535
{

0 commit comments

Comments
 (0)