solana-priority-fee 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -0
- package/example.js +141 -0
- package/index.js +326 -0
- package/package.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# solana-priority-fee
|
|
2
|
+
|
|
3
|
+
Priority fee estimation for Solana with caching, real-time monitoring, and success tracking.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install solana-priority-fee @solana/web3.js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
const { Connection } = require('@solana/web3.js');
|
|
15
|
+
const { getPriorityFee } = require('solana-priority-fee');
|
|
16
|
+
|
|
17
|
+
const connection = new Connection('https://api.mainnet-beta.solana.com');
|
|
18
|
+
|
|
19
|
+
// Get a fee estimate
|
|
20
|
+
const fee = await getPriorityFee(connection, 'fast');
|
|
21
|
+
console.log(fee); // e.g., 50000 microlamports
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Fee Levels
|
|
25
|
+
|
|
26
|
+
| Level | Percentile | Use Case |
|
|
27
|
+
|-------|------------|----------|
|
|
28
|
+
| `economy` | 25th | Can wait, saving costs |
|
|
29
|
+
| `medium` | 50th | Normal transactions |
|
|
30
|
+
| `fast` | 75th | Need quick confirmation |
|
|
31
|
+
| `urgent` | 95th | Time-critical, willing to pay |
|
|
32
|
+
|
|
33
|
+
## Full API
|
|
34
|
+
|
|
35
|
+
### SolanaFeeEstimator
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
const { SolanaFeeEstimator } = require('solana-priority-fee');
|
|
39
|
+
|
|
40
|
+
const estimator = new SolanaFeeEstimator(connection, {
|
|
41
|
+
cacheMs: 5000, // Cache duration (default: 5s)
|
|
42
|
+
minFee: 1000, // Minimum fee (default: 1000)
|
|
43
|
+
maxFee: 10_000_000, // Maximum fee cap (default: 10M)
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
#### getFee(options)
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
const fee = await estimator.getFee({
|
|
51
|
+
level: 'fast', // economy | medium | fast | urgent
|
|
52
|
+
accountKeys: [pubkey], // Optional: specific accounts for better accuracy
|
|
53
|
+
useCache: true // Optional: use cached data (default: true)
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### getAllFees(accountKeys?)
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
const fees = await estimator.getAllFees();
|
|
61
|
+
// {
|
|
62
|
+
// economy: 10000,
|
|
63
|
+
// medium: 25000,
|
|
64
|
+
// fast: 50000,
|
|
65
|
+
// urgent: 150000,
|
|
66
|
+
// raw: { min, max, median, samples }
|
|
67
|
+
// }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Real-time Monitoring
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
// Start monitoring (updates each slot)
|
|
74
|
+
await estimator.startMonitoring((fees) => {
|
|
75
|
+
console.log(`Slot ${fees.slot}: medium=${fees.medium}`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Get trend analysis
|
|
79
|
+
const trend = estimator.getTrend();
|
|
80
|
+
// { trend: 'rising' | 'falling' | 'stable', change: '+15.2%' }
|
|
81
|
+
|
|
82
|
+
// Stop monitoring
|
|
83
|
+
await estimator.stopMonitoring();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### Success Tracking
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
// Record transaction results
|
|
90
|
+
estimator.recordResult(50000, 'fast', true); // landed
|
|
91
|
+
estimator.recordResult(50000, 'fast', false); // dropped
|
|
92
|
+
|
|
93
|
+
// Get success rates per level
|
|
94
|
+
const rates = estimator.getSuccessRates();
|
|
95
|
+
// { fast: { total: 10, landed: 8, rate: '80.0%' }, ... }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### Stats
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
const stats = estimator.getStats();
|
|
102
|
+
// {
|
|
103
|
+
// requests: 100,
|
|
104
|
+
// cacheHits: 85,
|
|
105
|
+
// cacheHitRate: '85.0%',
|
|
106
|
+
// isMonitoring: true,
|
|
107
|
+
// ...
|
|
108
|
+
// }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Helper Functions
|
|
112
|
+
|
|
113
|
+
#### createPriorityFeeInstruction
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
const { createPriorityFeeInstruction } = require('solana-priority-fee');
|
|
117
|
+
|
|
118
|
+
const ix = await createPriorityFeeInstruction(connection, 'fast');
|
|
119
|
+
|
|
120
|
+
const tx = new Transaction().add(
|
|
121
|
+
ix,
|
|
122
|
+
// ... your other instructions
|
|
123
|
+
);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Account-Specific Fees
|
|
127
|
+
|
|
128
|
+
For more accurate estimates, pass the accounts your transaction writes to:
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
const fee = await estimator.getFee({
|
|
132
|
+
level: 'fast',
|
|
133
|
+
accountKeys: [poolAddress, userTokenAccount]
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This uses `getRecentPrioritizationFees` with `lockedWritableAccounts` for account-specific data.
|
|
138
|
+
|
|
139
|
+
## Example: Full Transaction
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
const { Connection, Transaction, SystemProgram } = require('@solana/web3.js');
|
|
143
|
+
const { createPriorityFeeInstruction, SolanaFeeEstimator } = require('solana-priority-fee');
|
|
144
|
+
|
|
145
|
+
const connection = new Connection('https://api.mainnet-beta.solana.com');
|
|
146
|
+
const estimator = new SolanaFeeEstimator(connection);
|
|
147
|
+
|
|
148
|
+
async function sendWithPriorityFee(fromKeypair, toAddress, amount) {
|
|
149
|
+
// Get priority fee instruction
|
|
150
|
+
const priorityFeeIx = await createPriorityFeeInstruction(connection, 'fast', [toAddress]);
|
|
151
|
+
|
|
152
|
+
// Build transaction
|
|
153
|
+
const tx = new Transaction().add(
|
|
154
|
+
priorityFeeIx,
|
|
155
|
+
SystemProgram.transfer({
|
|
156
|
+
fromPubkey: fromKeypair.publicKey,
|
|
157
|
+
toPubkey: toAddress,
|
|
158
|
+
lamports: amount
|
|
159
|
+
})
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Send and confirm
|
|
163
|
+
const sig = await connection.sendTransaction(tx, [fromKeypair]);
|
|
164
|
+
const result = await connection.confirmTransaction(sig);
|
|
165
|
+
|
|
166
|
+
// Track result for success rate analysis
|
|
167
|
+
estimator.recordResult(
|
|
168
|
+
priorityFeeIx.data.readBigUInt64LE(1), // fee from instruction
|
|
169
|
+
'fast',
|
|
170
|
+
result.value.err === null
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
return sig;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT
|
package/example.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const { Connection, PublicKey, Transaction, SystemProgram, Keypair } = require('@solana/web3.js');
|
|
2
|
+
const { SolanaFeeEstimator, getPriorityFee, createPriorityFeeInstruction } = require('./src/index');
|
|
3
|
+
|
|
4
|
+
// Connect to Solana
|
|
5
|
+
const connection = new Connection('https://api.mainnet-beta.solana.com');
|
|
6
|
+
|
|
7
|
+
async function basicUsage() {
|
|
8
|
+
console.log('\n=== Basic Usage ===\n');
|
|
9
|
+
|
|
10
|
+
// Quick one-liner
|
|
11
|
+
const fee = await getPriorityFee(connection, 'fast');
|
|
12
|
+
console.log('Quick estimate (fast):', fee, 'microlamports');
|
|
13
|
+
|
|
14
|
+
// Get all levels
|
|
15
|
+
const estimator = new SolanaFeeEstimator(connection);
|
|
16
|
+
const allFees = await estimator.getAllFees();
|
|
17
|
+
console.log('All fee levels:', allFees);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function accountSpecificFees() {
|
|
21
|
+
console.log('\n=== Account-Specific Fees ===\n');
|
|
22
|
+
|
|
23
|
+
// For specific accounts you're writing to (more accurate)
|
|
24
|
+
const poolAddress = new PublicKey('8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6');
|
|
25
|
+
|
|
26
|
+
const estimator = new SolanaFeeEstimator(connection);
|
|
27
|
+
const fee = await estimator.getFee({
|
|
28
|
+
level: 'fast',
|
|
29
|
+
accountKeys: [poolAddress]
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
console.log('Account-specific fee:', fee, 'microlamports');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function withCaching() {
|
|
36
|
+
console.log('\n=== Caching Demo ===\n');
|
|
37
|
+
|
|
38
|
+
const estimator = new SolanaFeeEstimator(connection, {
|
|
39
|
+
cacheMs: 10000 // 10 second cache
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// First call - fetches from network
|
|
43
|
+
console.time('First call');
|
|
44
|
+
await estimator.getFee({ level: 'medium' });
|
|
45
|
+
console.timeEnd('First call');
|
|
46
|
+
|
|
47
|
+
// Second call - from cache
|
|
48
|
+
console.time('Second call (cached)');
|
|
49
|
+
await estimator.getFee({ level: 'medium' });
|
|
50
|
+
console.timeEnd('Second call (cached)');
|
|
51
|
+
|
|
52
|
+
console.log('Stats:', estimator.getStats());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function realtimeMonitoring() {
|
|
56
|
+
console.log('\n=== Real-time Monitoring ===\n');
|
|
57
|
+
|
|
58
|
+
const estimator = new SolanaFeeEstimator(connection);
|
|
59
|
+
|
|
60
|
+
// Start monitoring (updates on each slot)
|
|
61
|
+
await estimator.startMonitoring((fees) => {
|
|
62
|
+
console.log(`Slot ${fees.slot}: economy=${fees.economy}, medium=${fees.medium}, fast=${fees.fast}`);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Let it run for 30 seconds
|
|
66
|
+
console.log('Monitoring for 30 seconds...');
|
|
67
|
+
await new Promise(resolve => setTimeout(resolve, 30000));
|
|
68
|
+
|
|
69
|
+
// Check trend
|
|
70
|
+
console.log('Trend:', estimator.getTrend());
|
|
71
|
+
|
|
72
|
+
// Stop monitoring
|
|
73
|
+
await estimator.stopMonitoring();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function buildTransaction() {
|
|
77
|
+
console.log('\n=== Building a Transaction ===\n');
|
|
78
|
+
|
|
79
|
+
// Get priority fee instruction
|
|
80
|
+
const priorityFeeIx = await createPriorityFeeInstruction(connection, 'fast');
|
|
81
|
+
|
|
82
|
+
// Build your transaction
|
|
83
|
+
const tx = new Transaction().add(
|
|
84
|
+
priorityFeeIx,
|
|
85
|
+
// ... add your other instructions here
|
|
86
|
+
// SystemProgram.transfer({ ... })
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
console.log('Transaction built with priority fee instruction');
|
|
90
|
+
console.log('Instructions:', tx.instructions.length);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function successTracking() {
|
|
94
|
+
console.log('\n=== Success Rate Tracking ===\n');
|
|
95
|
+
|
|
96
|
+
const estimator = new SolanaFeeEstimator(connection);
|
|
97
|
+
|
|
98
|
+
// Simulate some transaction results
|
|
99
|
+
// In real usage, call this after each transaction confirms/fails
|
|
100
|
+
estimator.recordResult(50000, 'fast', true); // landed
|
|
101
|
+
estimator.recordResult(50000, 'fast', true); // landed
|
|
102
|
+
estimator.recordResult(50000, 'fast', false); // dropped
|
|
103
|
+
estimator.recordResult(10000, 'economy', true);
|
|
104
|
+
estimator.recordResult(10000, 'economy', false);
|
|
105
|
+
estimator.recordResult(10000, 'economy', false);
|
|
106
|
+
|
|
107
|
+
console.log('Success rates:', estimator.getSuccessRates());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function customConfiguration() {
|
|
111
|
+
console.log('\n=== Custom Configuration ===\n');
|
|
112
|
+
|
|
113
|
+
const estimator = new SolanaFeeEstimator(connection, {
|
|
114
|
+
cacheMs: 3000, // 3 second cache
|
|
115
|
+
minFee: 5000, // Minimum 5000 microlamports
|
|
116
|
+
maxFee: 5_000_000, // Cap at 5M microlamports
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const fees = await estimator.getAllFees();
|
|
120
|
+
console.log('Fees with custom config:', fees);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Run examples
|
|
124
|
+
async function main() {
|
|
125
|
+
try {
|
|
126
|
+
await basicUsage();
|
|
127
|
+
await accountSpecificFees();
|
|
128
|
+
await withCaching();
|
|
129
|
+
await buildTransaction();
|
|
130
|
+
await successTracking();
|
|
131
|
+
await customConfiguration();
|
|
132
|
+
|
|
133
|
+
// Uncomment to run real-time monitoring (takes 30 seconds)
|
|
134
|
+
// await realtimeMonitoring();
|
|
135
|
+
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('Error:', error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
main();
|
package/index.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
const { Connection } = require('@solana/web3.js');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SolanaFeeEstimator - Priority fee estimation with caching and monitoring
|
|
5
|
+
*/
|
|
6
|
+
class SolanaFeeEstimator {
|
|
7
|
+
constructor(connection, options = {}) {
|
|
8
|
+
this.connection = connection;
|
|
9
|
+
|
|
10
|
+
// Configuration
|
|
11
|
+
this.config = {
|
|
12
|
+
cacheMs: options.cacheMs ?? 5000, // Cache duration
|
|
13
|
+
sampleSize: options.sampleSize ?? 150, // Recent slots to analyze
|
|
14
|
+
minFee: options.minFee ?? 1000, // Minimum fee (microlamports)
|
|
15
|
+
maxFee: options.maxFee ?? 10_000_000, // Maximum fee cap
|
|
16
|
+
...options
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Percentiles for each level
|
|
20
|
+
this.levels = {
|
|
21
|
+
economy: 0.25,
|
|
22
|
+
medium: 0.50,
|
|
23
|
+
fast: 0.75,
|
|
24
|
+
urgent: 0.95
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Cache
|
|
28
|
+
this.cache = {
|
|
29
|
+
global: null,
|
|
30
|
+
accounts: new Map(),
|
|
31
|
+
lastUpdate: 0
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Stats tracking
|
|
35
|
+
this.stats = {
|
|
36
|
+
requests: 0,
|
|
37
|
+
cacheHits: 0,
|
|
38
|
+
estimates: [], // { level, fee, timestamp }
|
|
39
|
+
confirmed: [], // { level, fee, landed, timestamp }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// WebSocket subscription
|
|
43
|
+
this.subscription = null;
|
|
44
|
+
this.realtimeFees = [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get priority fee recommendation
|
|
49
|
+
*/
|
|
50
|
+
async getFee(options = {}) {
|
|
51
|
+
const {
|
|
52
|
+
level = 'medium',
|
|
53
|
+
accountKeys = [],
|
|
54
|
+
useCache = true
|
|
55
|
+
} = options;
|
|
56
|
+
|
|
57
|
+
this.stats.requests++;
|
|
58
|
+
|
|
59
|
+
// Check cache
|
|
60
|
+
const cacheKey = accountKeys.length > 0
|
|
61
|
+
? accountKeys.sort().join(',')
|
|
62
|
+
: 'global';
|
|
63
|
+
|
|
64
|
+
if (useCache && this._isCacheValid(cacheKey)) {
|
|
65
|
+
this.stats.cacheHits++;
|
|
66
|
+
return this._calculateFee(this._getCachedFees(cacheKey), level);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Fetch fresh data
|
|
70
|
+
const fees = await this._fetchFees(accountKeys);
|
|
71
|
+
this._updateCache(cacheKey, fees);
|
|
72
|
+
|
|
73
|
+
const estimate = this._calculateFee(fees, level);
|
|
74
|
+
|
|
75
|
+
// Track estimate
|
|
76
|
+
this.stats.estimates.push({
|
|
77
|
+
level,
|
|
78
|
+
fee: estimate,
|
|
79
|
+
timestamp: Date.now()
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return estimate;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get all fee levels at once
|
|
87
|
+
*/
|
|
88
|
+
async getAllFees(accountKeys = []) {
|
|
89
|
+
const fees = await this._fetchFees(accountKeys);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
economy: this._calculateFee(fees, 'economy'),
|
|
93
|
+
medium: this._calculateFee(fees, 'medium'),
|
|
94
|
+
fast: this._calculateFee(fees, 'fast'),
|
|
95
|
+
urgent: this._calculateFee(fees, 'urgent'),
|
|
96
|
+
raw: {
|
|
97
|
+
min: fees.length > 0 ? fees[0] : this.config.minFee,
|
|
98
|
+
max: fees.length > 0 ? fees[fees.length - 1] : this.config.minFee,
|
|
99
|
+
median: fees.length > 0 ? fees[Math.floor(fees.length / 2)] : this.config.minFee,
|
|
100
|
+
samples: fees.length
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Start real-time monitoring via WebSocket
|
|
107
|
+
*/
|
|
108
|
+
async startMonitoring(callback) {
|
|
109
|
+
if (this.subscription) {
|
|
110
|
+
console.warn('Already monitoring');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Subscribe to slot changes
|
|
115
|
+
this.subscription = this.connection.onSlotChange(async (slotInfo) => {
|
|
116
|
+
try {
|
|
117
|
+
const fees = await this._fetchFees([]);
|
|
118
|
+
this._updateCache('global', fees);
|
|
119
|
+
|
|
120
|
+
const allFees = {
|
|
121
|
+
slot: slotInfo.slot,
|
|
122
|
+
economy: this._calculateFee(fees, 'economy'),
|
|
123
|
+
medium: this._calculateFee(fees, 'medium'),
|
|
124
|
+
fast: this._calculateFee(fees, 'fast'),
|
|
125
|
+
urgent: this._calculateFee(fees, 'urgent')
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
this.realtimeFees.push(allFees);
|
|
129
|
+
|
|
130
|
+
// Keep last 100 samples
|
|
131
|
+
if (this.realtimeFees.length > 100) {
|
|
132
|
+
this.realtimeFees.shift();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (callback) callback(allFees);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error('Monitoring error:', err);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
console.log('Fee monitoring started');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Stop real-time monitoring
|
|
146
|
+
*/
|
|
147
|
+
async stopMonitoring() {
|
|
148
|
+
if (this.subscription) {
|
|
149
|
+
await this.connection.removeSlotChangeListener(this.subscription);
|
|
150
|
+
this.subscription = null;
|
|
151
|
+
console.log('Fee monitoring stopped');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Record transaction result for accuracy tracking
|
|
157
|
+
*/
|
|
158
|
+
recordResult(fee, level, landed) {
|
|
159
|
+
this.stats.confirmed.push({
|
|
160
|
+
fee,
|
|
161
|
+
level,
|
|
162
|
+
landed,
|
|
163
|
+
timestamp: Date.now()
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Keep last 1000 results
|
|
167
|
+
if (this.stats.confirmed.length > 1000) {
|
|
168
|
+
this.stats.confirmed.shift();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get success rates per level
|
|
174
|
+
*/
|
|
175
|
+
getSuccessRates() {
|
|
176
|
+
const rates = {};
|
|
177
|
+
|
|
178
|
+
for (const level of Object.keys(this.levels)) {
|
|
179
|
+
const results = this.stats.confirmed.filter(r => r.level === level);
|
|
180
|
+
const landed = results.filter(r => r.landed).length;
|
|
181
|
+
|
|
182
|
+
rates[level] = {
|
|
183
|
+
total: results.length,
|
|
184
|
+
landed,
|
|
185
|
+
rate: results.length > 0 ? (landed / results.length * 100).toFixed(1) + '%' : 'N/A'
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return rates;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get trend analysis
|
|
194
|
+
*/
|
|
195
|
+
getTrend() {
|
|
196
|
+
if (this.realtimeFees.length < 10) {
|
|
197
|
+
return { trend: 'insufficient_data', change: 0 };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const recent = this.realtimeFees.slice(-10);
|
|
201
|
+
const older = this.realtimeFees.slice(-20, -10);
|
|
202
|
+
|
|
203
|
+
if (older.length === 0) {
|
|
204
|
+
return { trend: 'insufficient_data', change: 0 };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const recentAvg = recent.reduce((sum, f) => sum + f.medium, 0) / recent.length;
|
|
208
|
+
const olderAvg = older.reduce((sum, f) => sum + f.medium, 0) / older.length;
|
|
209
|
+
|
|
210
|
+
const change = ((recentAvg - olderAvg) / olderAvg) * 100;
|
|
211
|
+
|
|
212
|
+
let trend = 'stable';
|
|
213
|
+
if (change > 20) trend = 'rising';
|
|
214
|
+
else if (change < -20) trend = 'falling';
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
trend,
|
|
218
|
+
change: change.toFixed(1) + '%',
|
|
219
|
+
recentAvg: Math.round(recentAvg),
|
|
220
|
+
olderAvg: Math.round(olderAvg)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get cache and request statistics
|
|
226
|
+
*/
|
|
227
|
+
getStats() {
|
|
228
|
+
return {
|
|
229
|
+
requests: this.stats.requests,
|
|
230
|
+
cacheHits: this.stats.cacheHits,
|
|
231
|
+
cacheHitRate: this.stats.requests > 0
|
|
232
|
+
? ((this.stats.cacheHits / this.stats.requests) * 100).toFixed(1) + '%'
|
|
233
|
+
: 'N/A',
|
|
234
|
+
estimatesTracked: this.stats.estimates.length,
|
|
235
|
+
resultsTracked: this.stats.confirmed.length,
|
|
236
|
+
realtimeSamples: this.realtimeFees.length,
|
|
237
|
+
isMonitoring: this.subscription !== null
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Private methods
|
|
242
|
+
|
|
243
|
+
async _fetchFees(accountKeys) {
|
|
244
|
+
const options = accountKeys.length > 0
|
|
245
|
+
? { lockedWritableAccounts: accountKeys }
|
|
246
|
+
: {};
|
|
247
|
+
|
|
248
|
+
const fees = await this.connection.getRecentPrioritizationFees(options);
|
|
249
|
+
|
|
250
|
+
return fees
|
|
251
|
+
.map(f => f.prioritizationFee)
|
|
252
|
+
.filter(f => f > 0)
|
|
253
|
+
.sort((a, b) => a - b);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_calculateFee(fees, level) {
|
|
257
|
+
if (fees.length === 0) return this.config.minFee;
|
|
258
|
+
|
|
259
|
+
const percentile = this.levels[level] ?? 0.5;
|
|
260
|
+
const index = Math.floor(fees.length * percentile);
|
|
261
|
+
const fee = fees[Math.min(index, fees.length - 1)];
|
|
262
|
+
|
|
263
|
+
return Math.max(this.config.minFee, Math.min(fee, this.config.maxFee));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
_isCacheValid(key) {
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
|
|
269
|
+
if (key === 'global') {
|
|
270
|
+
return this.cache.global && (now - this.cache.lastUpdate) < this.config.cacheMs;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const cached = this.cache.accounts.get(key);
|
|
274
|
+
return cached && (now - cached.timestamp) < this.config.cacheMs;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
_getCachedFees(key) {
|
|
278
|
+
if (key === 'global') return this.cache.global;
|
|
279
|
+
return this.cache.accounts.get(key)?.fees ?? [];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_updateCache(key, fees) {
|
|
283
|
+
const now = Date.now();
|
|
284
|
+
|
|
285
|
+
if (key === 'global') {
|
|
286
|
+
this.cache.global = fees;
|
|
287
|
+
this.cache.lastUpdate = now;
|
|
288
|
+
} else {
|
|
289
|
+
this.cache.accounts.set(key, { fees, timestamp: now });
|
|
290
|
+
|
|
291
|
+
// Limit account cache size
|
|
292
|
+
if (this.cache.accounts.size > 100) {
|
|
293
|
+
const oldest = [...this.cache.accounts.entries()]
|
|
294
|
+
.sort((a, b) => a[1].timestamp - b[1].timestamp)[0];
|
|
295
|
+
this.cache.accounts.delete(oldest[0]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Helper function for quick one-off estimates
|
|
303
|
+
*/
|
|
304
|
+
async function getPriorityFee(connection, level = 'medium', accountKeys = []) {
|
|
305
|
+
const estimator = new SolanaFeeEstimator(connection);
|
|
306
|
+
return estimator.getFee({ level, accountKeys });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Create compute budget instruction with estimated fee
|
|
311
|
+
*/
|
|
312
|
+
async function createPriorityFeeInstruction(connection, level = 'medium', accountKeys = []) {
|
|
313
|
+
const { ComputeBudgetProgram } = require('@solana/web3.js');
|
|
314
|
+
|
|
315
|
+
const fee = await getPriorityFee(connection, level, accountKeys);
|
|
316
|
+
|
|
317
|
+
return ComputeBudgetProgram.setComputeUnitPrice({
|
|
318
|
+
microLamports: fee
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = {
|
|
323
|
+
SolanaFeeEstimator,
|
|
324
|
+
getPriorityFee,
|
|
325
|
+
createPriorityFeeInstruction
|
|
326
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "solana-priority-fee",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Priority fee estimation for Solana with caching, monitoring, and success tracking",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"example": "node example.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"solana",
|
|
11
|
+
"priority-fee",
|
|
12
|
+
"transaction",
|
|
13
|
+
"web3",
|
|
14
|
+
"blockchain"
|
|
15
|
+
],
|
|
16
|
+
"author": "",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@solana/web3.js": "^1.98.4"
|
|
20
|
+
}
|
|
21
|
+
}
|