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

add helius method to PriorityFeeSubscriber #832

Merged
merged 6 commits into from
Jan 27, 2024
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
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);
});
});
Loading