Skip to content

Commit

Permalink
add helius method to PriorityFeeSubscriber (#832)
Browse files Browse the repository at this point in the history
* add helius method to PriorityFeeSubscriber

* eagerly convert PubicKey to string

* use helius from connection obj, or optional rpc url

* lints
  • Loading branch information
wphan authored Jan 27, 2024
1 parent fbd5d6a commit 7142d2a
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 60 deletions.
3 changes: 2 additions & 1 deletion sdk/src/priorityFee/averageOverSlotsStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { SolanaPriorityFeeResponse } from './solanaPriorityFeeMethod';
import { PriorityFeeStrategy } from './types';

export class AverageOverSlotsStrategy implements PriorityFeeStrategy {
calculate(samples: { slot: number; prioritizationFee: number }[]): number {
calculate(samples: SolanaPriorityFeeResponse[]): number {
if (samples.length === 0) {
return 0;
}
Expand Down
3 changes: 2 additions & 1 deletion sdk/src/priorityFee/averageStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { SolanaPriorityFeeResponse } from './solanaPriorityFeeMethod';
import { PriorityFeeStrategy } from './types';

export class AverageStrategy implements PriorityFeeStrategy {
calculate(samples: { slot: number; prioritizationFee: number }[]): number {
calculate(samples: SolanaPriorityFeeResponse[]): number {
return (
samples.reduce((a, b) => {
return a + b.prioritizationFee;
Expand Down
3 changes: 2 additions & 1 deletion sdk/src/priorityFee/ewmaStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SolanaPriorityFeeResponse } from './solanaPriorityFeeMethod';
import { PriorityFeeStrategy } from './types';

class EwmaStrategy implements PriorityFeeStrategy {
Expand All @@ -11,7 +12,7 @@ class EwmaStrategy implements PriorityFeeStrategy {
}

// samples provided in desc slot order
calculate(samples: { slot: number; prioritizationFee: number }[]): number {
calculate(samples: SolanaPriorityFeeResponse[]): number {
if (samples.length === 0) {
return 0;
}
Expand Down
51 changes: 51 additions & 0 deletions sdk/src/priorityFee/heliusPriorityFeeMethod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import fetch from 'node-fetch';

export enum HeliusPriorityLevel {
MIN = 'min', // 25th percentile
LOW = 'low', // 25th percentile
MEDIUM = 'medium', // 50th percentile
HIGH = 'high', // 75th percentile
VERY_HIGH = 'veryHigh', // 95th percentile
UNSAFE_MAX = 'unsafeMax', // 100th percentile
}

export type HeliusPriorityFeeLevels = {
[key in HeliusPriorityLevel]: number;
};

export type HeliusPriorityFeeResponse = {
jsonrpc: string;
result: {
priorityFeeEstimate?: number;
priorityFeeLevels?: HeliusPriorityFeeLevels;
};
id: string;
};

/// Fetches the priority fee from the Helius API
/// https://docs.helius.dev/solana-rpc-nodes/alpha-priority-fee-api
export async function fetchHeliusPriorityFee(
heliusRpcUrl: string,
lookbackDistance: number,
addresses: string[]
): Promise<HeliusPriorityFeeResponse> {
const response = await fetch(heliusRpcUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: '1',
method: 'getPriorityFeeEstimate',
params: [
{
accountKeys: addresses,
options: {
includeAllPriorityFeeLevels: true,
lookbackSlots: lookbackDistance,
},
},
],
}),
});
return await response.json();
}
2 changes: 2 additions & 0 deletions sdk/src/priorityFee/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export * from './ewmaStrategy';
export * from './maxOverSlotsStrategy';
export * from './maxStrategy';
export * from './priorityFeeSubscriber';
export * from './solanaPriorityFeeMethod';
export * from './heliusPriorityFeeMethod';
export * from './types';
3 changes: 2 additions & 1 deletion sdk/src/priorityFee/maxOverSlotsStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { SolanaPriorityFeeResponse } from './solanaPriorityFeeMethod';
import { PriorityFeeStrategy } from './types';

export class MaxOverSlotsStrategy implements PriorityFeeStrategy {
calculate(samples: { slot: number; prioritizationFee: number }[]): number {
calculate(samples: SolanaPriorityFeeResponse[]): number {
if (samples.length === 0) {
return 0;
}
Expand Down
156 changes: 103 additions & 53 deletions sdk/src/priorityFee/priorityFeeSubscriber.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import { Connection, PublicKey } from '@solana/web3.js';
import { PriorityFeeStrategy } from './types';
import { Connection } from '@solana/web3.js';
import {
PriorityFeeMethod,
PriorityFeeStrategy,
PriorityFeeSubscriberConfig,
} from './types';
import { AverageOverSlotsStrategy } from './averageOverSlotsStrategy';
import { MaxOverSlotsStrategy } from './maxOverSlotsStrategy';
import { fetchSolanaPriorityFee } from './solanaPriorityFeeMethod';
import {
HeliusPriorityFeeLevels,
HeliusPriorityLevel,
fetchHeliusPriorityFee,
} from './heliusPriorityFeeMethod';

export class PriorityFeeSubscriber {
connection: Connection;
frequencyMs: number;
addresses: PublicKey[];
addresses: string[];
customStrategy?: PriorityFeeStrategy;
averageStrategy = new AverageOverSlotsStrategy();
maxStrategy = new MaxOverSlotsStrategy();
priorityFeeMethod = PriorityFeeMethod.SOLANA;
lookbackDistance: number;

heliusRpcUrl?: string;
lastHeliusSample?: HeliusPriorityFeeLevels;

intervalId?: ReturnType<typeof setTimeout>;

latestPriorityFee = 0;
Expand All @@ -20,76 +34,112 @@ export class PriorityFeeSubscriber {
lastMaxStrategyResult = 0;
lastSlotSeen = 0;

/**
* @param props
* customStrategy : strategy to return the priority fee to use based on recent samples. defaults to AVERAGE.
*/
public constructor({
connection,
frequencyMs,
addresses,
customStrategy,
slotsToCheck = 10,
}: {
connection: Connection;
frequencyMs: number;
addresses: PublicKey[];
customStrategy?: PriorityFeeStrategy;
slotsToCheck?: number;
}) {
this.connection = connection;
this.frequencyMs = frequencyMs;
this.addresses = addresses;
if (!customStrategy) {
this.customStrategy = new AverageOverSlotsStrategy();
public constructor(config: PriorityFeeSubscriberConfig) {
this.connection = config.connection;
this.frequencyMs = config.frequencyMs;
this.addresses = config.addresses.map((address) => address.toBase58());
if (config.customStrategy) {
this.customStrategy = config.customStrategy;
} else {
this.customStrategy = customStrategy;
this.customStrategy = this.averageStrategy;
}
this.lookbackDistance = config.slotsToCheck ?? 50;
if (config.priorityFeeMethod) {
this.priorityFeeMethod = config.priorityFeeMethod;

if (this.priorityFeeMethod === PriorityFeeMethod.HELIUS) {
if (config.heliusRpcUrl === undefined) {
if (this.connection.rpcEndpoint.includes('helius')) {
this.heliusRpcUrl = this.connection.rpcEndpoint;
} else {
throw new Error(
'Connection must be helius, or heliusRpcUrl must be provided to use PriorityFeeMethod.HELIUS'
);
}
}
}
}

if (this.priorityFeeMethod === PriorityFeeMethod.SOLANA) {
if (this.connection === undefined) {
throw new Error(
'connection must be provided to use SOLANA priority fee API'
);
}
}
this.lookbackDistance = slotsToCheck;
}

public async subscribe(): Promise<void> {
if (this.intervalId) {
return;
}

await this.load();
this.intervalId = setInterval(this.load.bind(this), this.frequencyMs);
}

public async load(): Promise<void> {
try {
// @ts-ignore
const rpcJSONResponse: any = await this.connection._rpcRequest(
'getRecentPrioritizationFees',
[this.addresses]
);
private async loadForSolana(): Promise<void> {
const samples = await fetchSolanaPriorityFee(
this.connection!,
this.lookbackDistance,
this.addresses
);
this.latestPriorityFee = samples[0].prioritizationFee;
this.lastSlotSeen = samples[0].slot;

const results: { slot: number; prioritizationFee: number }[] =
rpcJSONResponse?.result;
this.lastAvgStrategyResult = this.averageStrategy.calculate(samples);
this.lastMaxStrategyResult = this.maxStrategy.calculate(samples);
if (this.customStrategy) {
this.lastCustomStrategyResult = this.customStrategy.calculate(samples);
}
}

if (!results.length) return;
private async loadForHelius(): Promise<void> {
const sample = await fetchHeliusPriorityFee(
this.heliusRpcUrl,
this.lookbackDistance,
this.addresses
);
this.lastHeliusSample = sample?.result?.priorityFeeLevels ?? undefined;
}

// # Sort and filter results based on the slot lookback setting
const descResults = results.sort((a, b) => b.slot - a.slot);
const mostRecentResult = descResults[0];
const cutoffSlot = mostRecentResult.slot - this.lookbackDistance;
public getHeliusPriorityFeeLevel(
level: HeliusPriorityLevel = HeliusPriorityLevel.MEDIUM
): number {
if (this.lastHeliusSample === undefined) {
return 0;
}
return this.lastHeliusSample[level];
}

const resultsToUse = descResults.filter(
(result) => result.slot >= cutoffSlot
);
public getCustomStrategyResult(): number {
return this.lastCustomStrategyResult;
}

// # Handle results
this.latestPriorityFee = mostRecentResult.prioritizationFee;
this.lastSlotSeen = mostRecentResult.slot;
public getAvgStrategyResult(): number {
return this.lastAvgStrategyResult;
}

this.lastAvgStrategyResult = this.averageStrategy.calculate(resultsToUse);
this.lastMaxStrategyResult = this.maxStrategy.calculate(resultsToUse);
if (this.customStrategy) {
this.lastCustomStrategyResult =
this.customStrategy.calculate(resultsToUse);
public getMaxStrategyResult(): number {
return this.lastMaxStrategyResult;
}

public async load(): Promise<void> {
try {
if (this.priorityFeeMethod === PriorityFeeMethod.SOLANA) {
await this.loadForSolana();
} else if (this.priorityFeeMethod === PriorityFeeMethod.HELIUS) {
await this.loadForHelius();
} else {
throw new Error(`${this.priorityFeeMethod} load not implemented`);
}
} catch (err) {
// It's possible to get here with "TypeError: failed to fetch"
const e = err as Error;
console.error(
`Error loading priority fee ${this.priorityFeeMethod}: ${e.message}\n${
e.stack ? e.stack : ''
}`
);
return;
}
}
Expand Down
28 changes: 28 additions & 0 deletions sdk/src/priorityFee/solanaPriorityFeeMethod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Connection } from '@solana/web3.js';

export type SolanaPriorityFeeResponse = {
slot: number;
prioritizationFee: number;
};

export async function fetchSolanaPriorityFee(
connection: Connection,
lookbackDistance: number,
addresses: string[]
): Promise<SolanaPriorityFeeResponse[]> {
// @ts-ignore
const rpcJSONResponse: any = await connection._rpcRequest(
'getRecentPrioritizationFees',
[addresses]
);

const results: SolanaPriorityFeeResponse[] = rpcJSONResponse?.result;

if (!results.length) return;

// Sort and filter results based on the slot lookback setting
const descResults = results.sort((a, b) => b.slot - a.slot);
const cutoffSlot = descResults[0].slot - lookbackDistance;

return descResults.filter((result) => result.slot >= cutoffSlot);
}
30 changes: 29 additions & 1 deletion sdk/src/priorityFee/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
import { Connection, PublicKey } from '@solana/web3.js';
import { SolanaPriorityFeeResponse } from './solanaPriorityFeeMethod';
import { HeliusPriorityFeeResponse } from './heliusPriorityFeeMethod';

export interface PriorityFeeStrategy {
// calculate the priority fee for a given set of samples.
// expect samples to be sorted in descending order (by slot)
calculate(samples: { slot: number; prioritizationFee: number }[]): number;
calculate(
samples: SolanaPriorityFeeResponse[] | HeliusPriorityFeeResponse
): number;
}

export enum PriorityFeeMethod {
SOLANA = 'solana',
HELIUS = 'helius',
}

export type PriorityFeeSubscriberConfig = {
/// rpc connection, optional if using priorityFeeMethod.HELIUS
connection?: Connection;
/// frequency to make RPC calls to update priority fee samples, in milliseconds
frequencyMs: number;
/// addresses you plan to write lock, used to determine priority fees
addresses: PublicKey[];
/// custom strategy to calculate priority fees, defaults to AVERAGE
customStrategy?: PriorityFeeStrategy;
/// method for fetching priority fee samples
priorityFeeMethod?: PriorityFeeMethod;
/// lookback window to determine priority fees, in slots.
slotsToCheck?: number;
/// url for helius rpc, required if using priorityFeeMethod.HELIUS
heliusRpcUrl?: string;
};
4 changes: 2 additions & 2 deletions sdk/tests/tx/priorityFeeStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('PriorityFeeStrategy', () => {
{ slot: 1, prioritizationFee: 1000 },
];
const maxOverSlots = maxOverSlotsStrategy.calculate(samples);
expect(maxOverSlots).to.equal(832);
expect(maxOverSlots).to.equal(1000);
});

it('AverageOverSlotsStrategy should calculate the average prioritization fee over slots', () => {
Expand All @@ -90,6 +90,6 @@ describe('PriorityFeeStrategy', () => {
{ slot: 1, prioritizationFee: 1000 },
];
const averageOverSlots = averageOverSlotsStrategy.calculate(samples);
expect(averageOverSlots).to.equal(454.4);
expect(averageOverSlots).to.approximately(545.33333, 0.00001);
});
});

0 comments on commit 7142d2a

Please sign in to comment.