@atomiqlabs/chain-solana 12.0.14 → 12.0.15

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.
Files changed (114) hide show
  1. package/LICENSE +201 -201
  2. package/dist/index.d.ts +29 -29
  3. package/dist/index.js +45 -45
  4. package/dist/solana/SolanaChainType.d.ts +11 -11
  5. package/dist/solana/SolanaChainType.js +2 -2
  6. package/dist/solana/SolanaChains.d.ts +20 -20
  7. package/dist/solana/SolanaChains.js +25 -25
  8. package/dist/solana/SolanaInitializer.d.ts +18 -18
  9. package/dist/solana/SolanaInitializer.js +63 -63
  10. package/dist/solana/btcrelay/SolanaBtcRelay.d.ts +228 -228
  11. package/dist/solana/btcrelay/SolanaBtcRelay.js +441 -441
  12. package/dist/solana/btcrelay/headers/SolanaBtcHeader.d.ts +29 -29
  13. package/dist/solana/btcrelay/headers/SolanaBtcHeader.js +34 -34
  14. package/dist/solana/btcrelay/headers/SolanaBtcStoredHeader.d.ts +46 -46
  15. package/dist/solana/btcrelay/headers/SolanaBtcStoredHeader.js +78 -78
  16. package/dist/solana/btcrelay/program/programIdl.json +671 -671
  17. package/dist/solana/chain/SolanaAction.d.ts +26 -26
  18. package/dist/solana/chain/SolanaAction.js +86 -86
  19. package/dist/solana/chain/SolanaChainInterface.d.ts +65 -65
  20. package/dist/solana/chain/SolanaChainInterface.js +125 -125
  21. package/dist/solana/chain/SolanaModule.d.ts +14 -14
  22. package/dist/solana/chain/SolanaModule.js +13 -13
  23. package/dist/solana/chain/modules/SolanaAddresses.d.ts +8 -8
  24. package/dist/solana/chain/modules/SolanaAddresses.js +22 -22
  25. package/dist/solana/chain/modules/SolanaBlocks.d.ts +28 -28
  26. package/dist/solana/chain/modules/SolanaBlocks.js +72 -72
  27. package/dist/solana/chain/modules/SolanaEvents.d.ts +68 -68
  28. package/dist/solana/chain/modules/SolanaEvents.js +238 -238
  29. package/dist/solana/chain/modules/SolanaFees.d.ts +121 -121
  30. package/dist/solana/chain/modules/SolanaFees.js +379 -379
  31. package/dist/solana/chain/modules/SolanaSignatures.d.ts +23 -23
  32. package/dist/solana/chain/modules/SolanaSignatures.js +39 -39
  33. package/dist/solana/chain/modules/SolanaSlots.d.ts +31 -31
  34. package/dist/solana/chain/modules/SolanaSlots.js +68 -68
  35. package/dist/solana/chain/modules/SolanaTokens.d.ts +136 -136
  36. package/dist/solana/chain/modules/SolanaTokens.js +248 -248
  37. package/dist/solana/chain/modules/SolanaTransactions.d.ts +124 -124
  38. package/dist/solana/chain/modules/SolanaTransactions.js +323 -323
  39. package/dist/solana/events/SolanaChainEvents.d.ts +88 -88
  40. package/dist/solana/events/SolanaChainEvents.js +256 -256
  41. package/dist/solana/events/SolanaChainEventsBrowser.d.ts +75 -75
  42. package/dist/solana/events/SolanaChainEventsBrowser.js +172 -172
  43. package/dist/solana/program/SolanaProgramBase.d.ts +40 -40
  44. package/dist/solana/program/SolanaProgramBase.js +43 -43
  45. package/dist/solana/program/SolanaProgramModule.d.ts +8 -8
  46. package/dist/solana/program/SolanaProgramModule.js +11 -11
  47. package/dist/solana/program/modules/SolanaProgramEvents.d.ts +53 -53
  48. package/dist/solana/program/modules/SolanaProgramEvents.js +114 -114
  49. package/dist/solana/swaps/SolanaSwapData.d.ts +71 -71
  50. package/dist/solana/swaps/SolanaSwapData.js +292 -292
  51. package/dist/solana/swaps/SolanaSwapModule.d.ts +10 -10
  52. package/dist/solana/swaps/SolanaSwapModule.js +11 -11
  53. package/dist/solana/swaps/SolanaSwapProgram.d.ts +224 -224
  54. package/dist/solana/swaps/SolanaSwapProgram.js +570 -570
  55. package/dist/solana/swaps/SwapTypeEnum.d.ts +11 -11
  56. package/dist/solana/swaps/SwapTypeEnum.js +42 -42
  57. package/dist/solana/swaps/modules/SolanaDataAccount.d.ts +94 -94
  58. package/dist/solana/swaps/modules/SolanaDataAccount.js +231 -231
  59. package/dist/solana/swaps/modules/SolanaLpVault.d.ts +71 -71
  60. package/dist/solana/swaps/modules/SolanaLpVault.js +173 -173
  61. package/dist/solana/swaps/modules/SwapClaim.d.ts +129 -129
  62. package/dist/solana/swaps/modules/SwapClaim.js +291 -291
  63. package/dist/solana/swaps/modules/SwapInit.d.ts +217 -217
  64. package/dist/solana/swaps/modules/SwapInit.js +519 -519
  65. package/dist/solana/swaps/modules/SwapRefund.d.ts +82 -82
  66. package/dist/solana/swaps/modules/SwapRefund.js +262 -262
  67. package/dist/solana/swaps/programIdl.json +945 -945
  68. package/dist/solana/swaps/programTypes.d.ts +943 -943
  69. package/dist/solana/swaps/programTypes.js +945 -945
  70. package/dist/solana/wallet/SolanaKeypairWallet.d.ts +9 -9
  71. package/dist/solana/wallet/SolanaKeypairWallet.js +33 -33
  72. package/dist/solana/wallet/SolanaSigner.d.ts +11 -11
  73. package/dist/solana/wallet/SolanaSigner.js +17 -17
  74. package/dist/utils/Utils.d.ts +53 -53
  75. package/dist/utils/Utils.js +170 -170
  76. package/package.json +41 -41
  77. package/src/index.ts +36 -36
  78. package/src/solana/SolanaChainType.ts +27 -27
  79. package/src/solana/SolanaChains.ts +23 -23
  80. package/src/solana/SolanaInitializer.ts +102 -102
  81. package/src/solana/btcrelay/SolanaBtcRelay.ts +589 -589
  82. package/src/solana/btcrelay/headers/SolanaBtcHeader.ts +57 -57
  83. package/src/solana/btcrelay/headers/SolanaBtcStoredHeader.ts +102 -102
  84. package/src/solana/btcrelay/program/programIdl.json +670 -670
  85. package/src/solana/chain/SolanaAction.ts +108 -108
  86. package/src/solana/chain/SolanaChainInterface.ts +192 -192
  87. package/src/solana/chain/SolanaModule.ts +20 -20
  88. package/src/solana/chain/modules/SolanaAddresses.ts +20 -20
  89. package/src/solana/chain/modules/SolanaBlocks.ts +78 -78
  90. package/src/solana/chain/modules/SolanaEvents.ts +270 -270
  91. package/src/solana/chain/modules/SolanaFees.ts +450 -450
  92. package/src/solana/chain/modules/SolanaSignatures.ts +39 -39
  93. package/src/solana/chain/modules/SolanaSlots.ts +82 -82
  94. package/src/solana/chain/modules/SolanaTokens.ts +307 -307
  95. package/src/solana/chain/modules/SolanaTransactions.ts +365 -365
  96. package/src/solana/events/SolanaChainEvents.ts +299 -299
  97. package/src/solana/events/SolanaChainEventsBrowser.ts +209 -209
  98. package/src/solana/program/SolanaProgramBase.ts +79 -79
  99. package/src/solana/program/SolanaProgramModule.ts +15 -15
  100. package/src/solana/program/modules/SolanaProgramEvents.ts +155 -155
  101. package/src/solana/swaps/SolanaSwapData.ts +430 -430
  102. package/src/solana/swaps/SolanaSwapModule.ts +16 -16
  103. package/src/solana/swaps/SolanaSwapProgram.ts +854 -854
  104. package/src/solana/swaps/SwapTypeEnum.ts +29 -29
  105. package/src/solana/swaps/modules/SolanaDataAccount.ts +307 -307
  106. package/src/solana/swaps/modules/SolanaLpVault.ts +215 -215
  107. package/src/solana/swaps/modules/SwapClaim.ts +389 -389
  108. package/src/solana/swaps/modules/SwapInit.ts +663 -663
  109. package/src/solana/swaps/modules/SwapRefund.ts +323 -323
  110. package/src/solana/swaps/programIdl.json +944 -944
  111. package/src/solana/swaps/programTypes.ts +1885 -1885
  112. package/src/solana/wallet/SolanaKeypairWallet.ts +36 -36
  113. package/src/solana/wallet/SolanaSigner.ts +24 -24
  114. package/src/utils/Utils.ts +180 -180
@@ -1,451 +1,451 @@
1
- import {
2
- ComputeBudgetProgram,
3
- Connection,
4
- ParsedNoneModeBlockResponse,
5
- PublicKey,
6
- SendOptions,
7
- SystemInstruction,
8
- SystemProgram,
9
- Transaction
10
- } from "@solana/web3.js";
11
- import {getLogger, SolanaTxUtils} from "../../../utils/Utils";
12
-
13
- const MAX_FEE_AGE = 5000;
14
-
15
- export type FeeBribeData = {
16
- address: string,
17
- endpoint: string,
18
- getBribeFee?: (original: bigint) => bigint
19
- };
20
-
21
- export class SolanaFees {
22
-
23
- private readonly connection: Connection;
24
- private readonly maxFeeMicroLamports: bigint;
25
- private readonly numSamples: number;
26
- private readonly period: number;
27
- private useHeliusApi: "yes" | "no" | "auto";
28
- private heliusApiSupported: boolean = true;
29
- private readonly heliusFeeLevel: "min" | "low" | "medium" | "high" | "veryHigh" | "unsafeMax";
30
- private readonly bribeData?: FeeBribeData;
31
- private readonly getStaticFee?: (original: bigint) => bigint;
32
-
33
- private readonly logger = getLogger("SolanaFees: ");
34
-
35
- private blockFeeCache: {
36
- timestamp: number,
37
- feeRate: Promise<bigint>
38
- } = null;
39
-
40
- constructor(
41
- connection: Connection,
42
- maxFeeMicroLamports: number = 250000,
43
- numSamples: number = 8,
44
- period: number = 150,
45
- useHeliusApi: "yes" | "no" | "auto" = "auto",
46
- heliusFeeLevel: "min" | "low" | "medium" | "high" | "veryHigh" | "unsafeMax" = "veryHigh",
47
- getStaticFee?: (feeRate: bigint) => bigint,
48
- bribeData?: FeeBribeData,
49
- ) {
50
- this.connection = connection;
51
- this.maxFeeMicroLamports = BigInt(maxFeeMicroLamports);
52
- this.numSamples = numSamples;
53
- this.period = period;
54
- this.useHeliusApi = useHeliusApi;
55
- this.heliusFeeLevel = heliusFeeLevel;
56
- this.bribeData = bribeData;
57
- this.getStaticFee = getStaticFee;
58
- }
59
-
60
- /**
61
- * Returns solana block with transactionDetails="signatures"
62
- *
63
- * @param slot
64
- * @private
65
- */
66
- private async getBlockWithSignature(slot: number): Promise<ParsedNoneModeBlockResponse & {signatures: string[]}> {
67
- const response = await (this.connection as any)._rpcRequest("getBlock", [
68
- slot,
69
- {
70
- encoding: "json",
71
- transactionDetails: "signatures",
72
- commitment: "confirmed",
73
- rewards: true
74
- }
75
- ]);
76
-
77
- if(response.error!=null) {
78
- if(response.error.code===-32004 || response.error.code===-32007 || response.error.code===-32009 || response.error.code===-32014) {
79
- return null;
80
- }
81
- throw new Error(response.error.message);
82
- }
83
-
84
- return response.result;
85
- }
86
-
87
- /**
88
- * Returns fee estimate from Helius API - only works with Helius RPC, return null for all other RPC providers
89
- *
90
- * @param mutableAccounts
91
- * @private
92
- */
93
- private async getPriorityFeeEstimate(mutableAccounts: PublicKey[]): Promise<{
94
- "min": number,
95
- "low": number,
96
- "medium": number,
97
- "high": number,
98
- "veryHigh": number,
99
- "unsafeMax": number
100
- } | null> {
101
- //Try to use getPriorityFeeEstimate api of Helius
102
- const response = await (this.connection as any)._rpcRequest("getPriorityFeeEstimate", [
103
- {
104
- "accountKeys": mutableAccounts.map(e => e.toBase58()),
105
- "options": {
106
- "includeAllPriorityFeeLevels": true
107
- }
108
- }
109
- ]).catch(e => {
110
- //Catching not supported errors
111
- if(e.message!=null && (e.message.includes("-32601") || e.message.includes("-32600"))) {
112
- return {
113
- error: {
114
- code: -32601,
115
- message: e.message
116
- }
117
- };
118
- }
119
- throw e;
120
- });
121
-
122
- if(response.error!=null) {
123
- //Catching not supported errors
124
- if(response.error.code!==-32601 && response.error.code!==-32600) throw new Error(response.error.message);
125
- return null;
126
- }
127
-
128
- return response.result.priorityFeeLevels;
129
- }
130
-
131
- /**
132
- * Sends the transaction over Jito
133
- *
134
- * @param tx
135
- * @param options
136
- * @private
137
- * @returns {Promise<string>} transaction signature
138
- */
139
- private async sendJitoTx(tx: Buffer, options?: SendOptions): Promise<string> {
140
- if(this.bribeData?.endpoint==null) throw new Error("Jito endpoint not specified!");
141
- if(options==null) options = {};
142
- const request = await fetch(this.bribeData.endpoint, {
143
- method: "POST",
144
- body: JSON.stringify({
145
- jsonrpc: "2.0",
146
- id: 1,
147
- method: "sendTransaction",
148
- params: [tx.toString("base64"), {
149
- ...options,
150
- encoding: "base64"
151
- }],
152
- }),
153
- headers: {
154
- "Content-Type": "application/json"
155
- }
156
- });
157
-
158
- if(request.ok) {
159
- const parsedResponse = await request.json();
160
- return parsedResponse.result;
161
- }
162
-
163
- throw new Error(await request.text());
164
- }
165
-
166
- /**
167
- * Checks whether the transaction should be sent over Jito, returns the fee paid to Jito in case the transaction
168
- * should be sent over Jito, returns null if the transaction shouldn't be sent over Jito
169
- *
170
- * @param parsedTx
171
- * @private
172
- */
173
- private getJitoTxFee(parsedTx: Transaction): bigint | null {
174
- const lastIx = parsedTx.instructions[parsedTx.instructions.length-1];
175
-
176
- if(!lastIx.programId.equals(SystemProgram.programId)) return null;
177
- if(SystemInstruction.decodeInstructionType(lastIx)!=="Transfer") return null;
178
-
179
- const decodedIxData = SystemInstruction.decodeTransfer(lastIx);
180
- if(decodedIxData.toPubkey.toBase58()!==this.bribeData?.address) return null;
181
- return decodedIxData.lamports;
182
- }
183
-
184
- /**
185
- * Gets the mean microLamports/CU fee rate for the block at a specific slot
186
- *
187
- * @param slot
188
- * @private
189
- */
190
- private async getBlockMeanFeeRate(slot: number): Promise<bigint | null> {
191
- const block = await this.getBlockWithSignature(slot);
192
- if(block==null) return null;
193
-
194
- const blockComission = block.rewards.find(e => e.rewardType==="Fee");
195
- const totalBlockFees: bigint = BigInt(blockComission.lamports) * 2n;
196
-
197
- //Subtract per-signature fees to get pure compute fees
198
- const totalTransactionBaseFees = BigInt(block.signatures.length) * 5000n;
199
- const computeFees = totalBlockFees - totalTransactionBaseFees;
200
-
201
- //Total compute fees in micro lamports
202
- const computeFeesMicroLamports = computeFees * 1000000n;
203
- //micro lamports per CU considering block was full (48M compute units)
204
- const perCUMicroLamports = computeFeesMicroLamports / 48000000n;
205
-
206
- this.logger.debug("getBlockMeanFeeRate(): slot: "+slot+" total reward: "+totalBlockFees.toString(10)+
207
- " total transactions: "+block.signatures.length+" computed fee: "+perCUMicroLamports);
208
-
209
- return perCUMicroLamports;
210
- }
211
-
212
- /**
213
- * Manually gets global fee rate by sampling random blocks over the last period
214
- *
215
- * @private
216
- * @returns {Promise<BN>} sampled mean microLamports/CU fee over the last period
217
- */
218
- private async _getGlobalFeeRate(): Promise<bigint> {
219
- let slot = await this.connection.getSlot();
220
-
221
- const slots: number[] = [];
222
-
223
- for(let i=0;i<this.period;i++) {
224
- slots.push(slot-i);
225
- }
226
-
227
- const promises: Promise<bigint>[] = [];
228
- for(let i=0;i<this.numSamples;i++) {
229
- promises.push((async () => {
230
- let feeRate: bigint = null;
231
- while(feeRate==null) {
232
- if(slots.length===0) throw new Error("Ran out of slots to check!");
233
- const index = Math.floor(Math.random()*slots.length);
234
- const slotNumber = slots[index];
235
- slots.splice(index, 1);
236
- feeRate = await this.getBlockMeanFeeRate(slotNumber);
237
- }
238
- return feeRate;
239
- })());
240
- }
241
-
242
- const meanFees = await Promise.all(promises);
243
-
244
- let min = null;
245
- meanFees.forEach(e => min==null || min > e ? min = e : 0);
246
-
247
- if(min!=null) this.logger.debug("_getGlobalFeeRate(): slot: "+slot+" global fee minimum: "+min.toString(10));
248
-
249
- return min;
250
- }
251
-
252
- /**
253
- * Gets the combined microLamports/CU fee rate (from localized & global fee market)
254
- *
255
- * @param mutableAccounts
256
- * @private
257
- */
258
- private async _getFeeRate(mutableAccounts: PublicKey[]): Promise<bigint> {
259
- if(this.useHeliusApi==="yes" || (this.useHeliusApi==="auto" && this.heliusApiSupported)) {
260
- //Try to use getPriorityFeeEstimate api of Helius
261
- const fees = await this.getPriorityFeeEstimate(mutableAccounts);
262
- if(fees!=null) {
263
- let calculatedFee = BigInt(fees[this.heliusFeeLevel]);
264
- if(calculatedFee < 8000n) calculatedFee = 8000n;
265
- if(calculatedFee > this.maxFeeMicroLamports) calculatedFee = this.maxFeeMicroLamports;
266
- return calculatedFee;
267
- }
268
- this.logger.warn("_getFeeRate(): tried fetching fees from Helius API, not supported," +
269
- " falling back to client-side fee estimation");
270
- this.heliusApiSupported = false;
271
- }
272
-
273
- const [globalFeeRate, localFeeRate] = await Promise.all([
274
- this.getGlobalFeeRate(),
275
- this.connection.getRecentPrioritizationFees({
276
- lockedWritableAccounts: mutableAccounts
277
- }).then(resp => {
278
- let lamports = 0;
279
- for(let i=20;i>=0;i--) {
280
- const data = resp[resp.length-i-1];
281
- if(data!=null) lamports = Math.min(lamports, data.prioritizationFee);
282
- }
283
- return BigInt(lamports);
284
- })
285
- ]);
286
-
287
- let fee = globalFeeRate;
288
- if(fee < localFeeRate) fee = localFeeRate;
289
- if(fee < 8000n) fee = 8000n;
290
- if(fee > this.maxFeeMicroLamports) fee = this.maxFeeMicroLamports;
291
-
292
- return fee;
293
- }
294
-
295
- /**
296
- * Gets global fee rate, with caching
297
- *
298
- * @returns {Promise<BN>} global fee rate microLamports/CU
299
- */
300
- public getGlobalFeeRate(): Promise<bigint> {
301
- if(this.blockFeeCache==null || Date.now() - this.blockFeeCache.timestamp > MAX_FEE_AGE) {
302
- let obj = {
303
- timestamp: Date.now(),
304
- feeRate: null
305
- };
306
- obj.feeRate = this._getGlobalFeeRate().catch(e => {
307
- if(this.blockFeeCache===obj) this.blockFeeCache=null;
308
- throw e;
309
- });
310
- this.blockFeeCache = obj;
311
- }
312
-
313
- return this.blockFeeCache.feeRate;
314
- }
315
-
316
- /**
317
- * Gets the combined microLamports/CU fee rate (from localized & global fee market), cached & adjusted as for
318
- * when bribe and/or static fee should be included, format: <uLamports/CU>;<static fee lamport>[;<bribe address>]
319
- *
320
- * @param mutableAccounts
321
- * @private
322
- */
323
- public async getFeeRate(mutableAccounts: PublicKey[]): Promise<string> {
324
- let feeMicroLamportPerCU = await this._getFeeRate(mutableAccounts);
325
- if(this.bribeData?.getBribeFee!=null) feeMicroLamportPerCU = this.bribeData.getBribeFee(feeMicroLamportPerCU);
326
-
327
- let fee: string = feeMicroLamportPerCU.toString(10);
328
- if(this.getStaticFee!=null) {
329
- fee += ";"+this.getStaticFee(feeMicroLamportPerCU);
330
- } else {
331
- fee += ";0"
332
- }
333
-
334
- if(this.bribeData?.address) {
335
- fee += ";"+this.bribeData.address;
336
- }
337
-
338
- this.logger.debug("getFeeRate(): calculated fee: "+fee);
339
-
340
- return fee;
341
- }
342
-
343
- /**
344
- * Calculates the total priority fee paid for a given compute budget at a given fee rate
345
- *
346
- * @param computeUnits
347
- * @param feeRate
348
- * @param includeStaticFee whether the include the static/base part of the fee rate
349
- */
350
- public getPriorityFee(computeUnits: number, feeRate: string, includeStaticFee: boolean = true): bigint {
351
- if(feeRate==null) return 0n;
352
-
353
- const hashArr = feeRate.split("#");
354
- if(hashArr.length>1) {
355
- feeRate = hashArr[0];
356
- }
357
-
358
- const arr = feeRate.split(";");
359
- const cuPrice = BigInt(arr[0]);
360
- const staticFee = includeStaticFee ? BigInt(arr[1]) : 0n;
361
-
362
- return staticFee + (cuPrice * BigInt(computeUnits) / 1000000n);
363
- }
364
-
365
- /**
366
- * Applies fee rate to a transaction at the beginning of the transaction (has to be called after
367
- * feePayer is set for the tx), specifically adds the setComputeUnitLimit & setComputeUnitPrice instruction
368
- *
369
- * @param tx
370
- * @param computeBudget
371
- * @param feeRate
372
- */
373
- public applyFeeRateBegin(tx: Transaction, computeBudget: number, feeRate: string): boolean {
374
- if(feeRate==null) return false;
375
-
376
- const hashArr = feeRate.split("#");
377
- if(hashArr.length>1) {
378
- feeRate = hashArr[0];
379
- }
380
-
381
- if(computeBudget!=null && computeBudget>0) tx.add(ComputeBudgetProgram.setComputeUnitLimit({
382
- units: computeBudget,
383
- }));
384
-
385
- //Check if bribe is included
386
- const arr = feeRate.split(";");
387
- if(arr.length>2) {
388
-
389
- } else {
390
- let fee: bigint = BigInt(arr[0]);
391
- if(arr.length>1) {
392
- const staticFee = BigInt(arr[1]);
393
- const cuBigInt = BigInt(computeBudget || (200000*SolanaTxUtils.getNonComputeBudgetIxs(tx)));
394
- const staticFeePerCU = staticFee*BigInt(1000000)/cuBigInt;
395
- fee += staticFeePerCU;
396
- }
397
- tx.add(ComputeBudgetProgram.setComputeUnitPrice({
398
- microLamports: fee
399
- }));
400
- }
401
- }
402
-
403
- /**
404
- * Applies fee rate to the end of the transaction (has to be called after feePayer is set for the tx),
405
- * specifically adds the bribe SystemProgram.transfer instruction
406
- *
407
- * @param tx
408
- * @param computeBudget
409
- * @param feeRate
410
- */
411
- public applyFeeRateEnd(tx: Transaction, computeBudget: number, feeRate: string): boolean {
412
- if(feeRate==null) return false;
413
-
414
- const hashArr = feeRate.split("#");
415
- if(hashArr.length>1) {
416
- feeRate = hashArr[0];
417
- }
418
-
419
- //Check if bribe is included
420
- const arr = feeRate.split(";");
421
- if(arr.length>2) {
422
- const cuBigInt = BigInt(computeBudget || (200000*(SolanaTxUtils.getNonComputeBudgetIxs(tx)+1)));
423
- const cuPrice = BigInt(arr[0]);
424
- const staticFee = BigInt(arr[1]);
425
- const bribeAddress = new PublicKey(arr[2]);
426
- tx.add(SystemProgram.transfer({
427
- fromPubkey: tx.feePayer,
428
- toPubkey: bribeAddress,
429
- lamports: staticFee + (cuBigInt*cuPrice/BigInt(1000000))
430
- }));
431
- return;
432
- }
433
- }
434
-
435
- /**
436
- * Checks if the transaction should be submitted over Jito and if yes submits it
437
- *
438
- * @param tx
439
- * @param options
440
- * @returns {Promise<string | null>} null if the transaction was not sent over Jito, tx signature when tx was sent over Jito
441
- */
442
- submitTx(tx: Buffer, options?: SendOptions): Promise<string | null> {
443
- const parsedTx = Transaction.from(tx);
444
- const jitoFee = this.getJitoTxFee(parsedTx);
445
- if(jitoFee==null) return null;
446
-
447
- this.logger.info("submitTx(): sending tx over Jito, signature: "+parsedTx.signature+" fee: "+jitoFee.toString(10));
448
- return this.sendJitoTx(tx, options);
449
- }
450
-
1
+ import {
2
+ ComputeBudgetProgram,
3
+ Connection,
4
+ ParsedNoneModeBlockResponse,
5
+ PublicKey,
6
+ SendOptions,
7
+ SystemInstruction,
8
+ SystemProgram,
9
+ Transaction
10
+ } from "@solana/web3.js";
11
+ import {getLogger, SolanaTxUtils} from "../../../utils/Utils";
12
+
13
+ const MAX_FEE_AGE = 5000;
14
+
15
+ export type FeeBribeData = {
16
+ address: string,
17
+ endpoint: string,
18
+ getBribeFee?: (original: bigint) => bigint
19
+ };
20
+
21
+ export class SolanaFees {
22
+
23
+ private readonly connection: Connection;
24
+ private readonly maxFeeMicroLamports: bigint;
25
+ private readonly numSamples: number;
26
+ private readonly period: number;
27
+ private useHeliusApi: "yes" | "no" | "auto";
28
+ private heliusApiSupported: boolean = true;
29
+ private readonly heliusFeeLevel: "min" | "low" | "medium" | "high" | "veryHigh" | "unsafeMax";
30
+ private readonly bribeData?: FeeBribeData;
31
+ private readonly getStaticFee?: (original: bigint) => bigint;
32
+
33
+ private readonly logger = getLogger("SolanaFees: ");
34
+
35
+ private blockFeeCache: {
36
+ timestamp: number,
37
+ feeRate: Promise<bigint>
38
+ } = null;
39
+
40
+ constructor(
41
+ connection: Connection,
42
+ maxFeeMicroLamports: number = 250000,
43
+ numSamples: number = 8,
44
+ period: number = 150,
45
+ useHeliusApi: "yes" | "no" | "auto" = "auto",
46
+ heliusFeeLevel: "min" | "low" | "medium" | "high" | "veryHigh" | "unsafeMax" = "veryHigh",
47
+ getStaticFee?: (feeRate: bigint) => bigint,
48
+ bribeData?: FeeBribeData,
49
+ ) {
50
+ this.connection = connection;
51
+ this.maxFeeMicroLamports = BigInt(maxFeeMicroLamports);
52
+ this.numSamples = numSamples;
53
+ this.period = period;
54
+ this.useHeliusApi = useHeliusApi;
55
+ this.heliusFeeLevel = heliusFeeLevel;
56
+ this.bribeData = bribeData;
57
+ this.getStaticFee = getStaticFee;
58
+ }
59
+
60
+ /**
61
+ * Returns solana block with transactionDetails="signatures"
62
+ *
63
+ * @param slot
64
+ * @private
65
+ */
66
+ private async getBlockWithSignature(slot: number): Promise<ParsedNoneModeBlockResponse & {signatures: string[]}> {
67
+ const response = await (this.connection as any)._rpcRequest("getBlock", [
68
+ slot,
69
+ {
70
+ encoding: "json",
71
+ transactionDetails: "signatures",
72
+ commitment: "confirmed",
73
+ rewards: true
74
+ }
75
+ ]);
76
+
77
+ if(response.error!=null) {
78
+ if(response.error.code===-32004 || response.error.code===-32007 || response.error.code===-32009 || response.error.code===-32014) {
79
+ return null;
80
+ }
81
+ throw new Error(response.error.message);
82
+ }
83
+
84
+ return response.result;
85
+ }
86
+
87
+ /**
88
+ * Returns fee estimate from Helius API - only works with Helius RPC, return null for all other RPC providers
89
+ *
90
+ * @param mutableAccounts
91
+ * @private
92
+ */
93
+ private async getPriorityFeeEstimate(mutableAccounts: PublicKey[]): Promise<{
94
+ "min": number,
95
+ "low": number,
96
+ "medium": number,
97
+ "high": number,
98
+ "veryHigh": number,
99
+ "unsafeMax": number
100
+ } | null> {
101
+ //Try to use getPriorityFeeEstimate api of Helius
102
+ const response = await (this.connection as any)._rpcRequest("getPriorityFeeEstimate", [
103
+ {
104
+ "accountKeys": mutableAccounts.map(e => e.toBase58()),
105
+ "options": {
106
+ "includeAllPriorityFeeLevels": true
107
+ }
108
+ }
109
+ ]).catch(e => {
110
+ //Catching not supported errors
111
+ if(e.message!=null && (e.message.includes("-32601") || e.message.includes("-32600"))) {
112
+ return {
113
+ error: {
114
+ code: -32601,
115
+ message: e.message
116
+ }
117
+ };
118
+ }
119
+ throw e;
120
+ });
121
+
122
+ if(response.error!=null) {
123
+ //Catching not supported errors
124
+ if(response.error.code!==-32601 && response.error.code!==-32600) throw new Error(response.error.message);
125
+ return null;
126
+ }
127
+
128
+ return response.result.priorityFeeLevels;
129
+ }
130
+
131
+ /**
132
+ * Sends the transaction over Jito
133
+ *
134
+ * @param tx
135
+ * @param options
136
+ * @private
137
+ * @returns {Promise<string>} transaction signature
138
+ */
139
+ private async sendJitoTx(tx: Buffer, options?: SendOptions): Promise<string> {
140
+ if(this.bribeData?.endpoint==null) throw new Error("Jito endpoint not specified!");
141
+ if(options==null) options = {};
142
+ const request = await fetch(this.bribeData.endpoint, {
143
+ method: "POST",
144
+ body: JSON.stringify({
145
+ jsonrpc: "2.0",
146
+ id: 1,
147
+ method: "sendTransaction",
148
+ params: [tx.toString("base64"), {
149
+ ...options,
150
+ encoding: "base64"
151
+ }],
152
+ }),
153
+ headers: {
154
+ "Content-Type": "application/json"
155
+ }
156
+ });
157
+
158
+ if(request.ok) {
159
+ const parsedResponse = await request.json();
160
+ return parsedResponse.result;
161
+ }
162
+
163
+ throw new Error(await request.text());
164
+ }
165
+
166
+ /**
167
+ * Checks whether the transaction should be sent over Jito, returns the fee paid to Jito in case the transaction
168
+ * should be sent over Jito, returns null if the transaction shouldn't be sent over Jito
169
+ *
170
+ * @param parsedTx
171
+ * @private
172
+ */
173
+ private getJitoTxFee(parsedTx: Transaction): bigint | null {
174
+ const lastIx = parsedTx.instructions[parsedTx.instructions.length-1];
175
+
176
+ if(!lastIx.programId.equals(SystemProgram.programId)) return null;
177
+ if(SystemInstruction.decodeInstructionType(lastIx)!=="Transfer") return null;
178
+
179
+ const decodedIxData = SystemInstruction.decodeTransfer(lastIx);
180
+ if(decodedIxData.toPubkey.toBase58()!==this.bribeData?.address) return null;
181
+ return decodedIxData.lamports;
182
+ }
183
+
184
+ /**
185
+ * Gets the mean microLamports/CU fee rate for the block at a specific slot
186
+ *
187
+ * @param slot
188
+ * @private
189
+ */
190
+ private async getBlockMeanFeeRate(slot: number): Promise<bigint | null> {
191
+ const block = await this.getBlockWithSignature(slot);
192
+ if(block==null) return null;
193
+
194
+ const blockComission = block.rewards.find(e => e.rewardType==="Fee");
195
+ const totalBlockFees: bigint = BigInt(blockComission.lamports) * 2n;
196
+
197
+ //Subtract per-signature fees to get pure compute fees
198
+ const totalTransactionBaseFees = BigInt(block.signatures.length) * 5000n;
199
+ const computeFees = totalBlockFees - totalTransactionBaseFees;
200
+
201
+ //Total compute fees in micro lamports
202
+ const computeFeesMicroLamports = computeFees * 1000000n;
203
+ //micro lamports per CU considering block was full (48M compute units)
204
+ const perCUMicroLamports = computeFeesMicroLamports / 48000000n;
205
+
206
+ this.logger.debug("getBlockMeanFeeRate(): slot: "+slot+" total reward: "+totalBlockFees.toString(10)+
207
+ " total transactions: "+block.signatures.length+" computed fee: "+perCUMicroLamports);
208
+
209
+ return perCUMicroLamports;
210
+ }
211
+
212
+ /**
213
+ * Manually gets global fee rate by sampling random blocks over the last period
214
+ *
215
+ * @private
216
+ * @returns {Promise<BN>} sampled mean microLamports/CU fee over the last period
217
+ */
218
+ private async _getGlobalFeeRate(): Promise<bigint> {
219
+ let slot = await this.connection.getSlot();
220
+
221
+ const slots: number[] = [];
222
+
223
+ for(let i=0;i<this.period;i++) {
224
+ slots.push(slot-i);
225
+ }
226
+
227
+ const promises: Promise<bigint>[] = [];
228
+ for(let i=0;i<this.numSamples;i++) {
229
+ promises.push((async () => {
230
+ let feeRate: bigint = null;
231
+ while(feeRate==null) {
232
+ if(slots.length===0) throw new Error("Ran out of slots to check!");
233
+ const index = Math.floor(Math.random()*slots.length);
234
+ const slotNumber = slots[index];
235
+ slots.splice(index, 1);
236
+ feeRate = await this.getBlockMeanFeeRate(slotNumber);
237
+ }
238
+ return feeRate;
239
+ })());
240
+ }
241
+
242
+ const meanFees = await Promise.all(promises);
243
+
244
+ let min = null;
245
+ meanFees.forEach(e => min==null || min > e ? min = e : 0);
246
+
247
+ if(min!=null) this.logger.debug("_getGlobalFeeRate(): slot: "+slot+" global fee minimum: "+min.toString(10));
248
+
249
+ return min;
250
+ }
251
+
252
+ /**
253
+ * Gets the combined microLamports/CU fee rate (from localized & global fee market)
254
+ *
255
+ * @param mutableAccounts
256
+ * @private
257
+ */
258
+ private async _getFeeRate(mutableAccounts: PublicKey[]): Promise<bigint> {
259
+ if(this.useHeliusApi==="yes" || (this.useHeliusApi==="auto" && this.heliusApiSupported)) {
260
+ //Try to use getPriorityFeeEstimate api of Helius
261
+ const fees = await this.getPriorityFeeEstimate(mutableAccounts);
262
+ if(fees!=null) {
263
+ let calculatedFee = BigInt(fees[this.heliusFeeLevel]);
264
+ if(calculatedFee < 8000n) calculatedFee = 8000n;
265
+ if(calculatedFee > this.maxFeeMicroLamports) calculatedFee = this.maxFeeMicroLamports;
266
+ return calculatedFee;
267
+ }
268
+ this.logger.warn("_getFeeRate(): tried fetching fees from Helius API, not supported," +
269
+ " falling back to client-side fee estimation");
270
+ this.heliusApiSupported = false;
271
+ }
272
+
273
+ const [globalFeeRate, localFeeRate] = await Promise.all([
274
+ this.getGlobalFeeRate(),
275
+ this.connection.getRecentPrioritizationFees({
276
+ lockedWritableAccounts: mutableAccounts
277
+ }).then(resp => {
278
+ let lamports = 0;
279
+ for(let i=20;i>=0;i--) {
280
+ const data = resp[resp.length-i-1];
281
+ if(data!=null) lamports = Math.min(lamports, data.prioritizationFee);
282
+ }
283
+ return BigInt(lamports);
284
+ })
285
+ ]);
286
+
287
+ let fee = globalFeeRate;
288
+ if(fee < localFeeRate) fee = localFeeRate;
289
+ if(fee < 8000n) fee = 8000n;
290
+ if(fee > this.maxFeeMicroLamports) fee = this.maxFeeMicroLamports;
291
+
292
+ return fee;
293
+ }
294
+
295
+ /**
296
+ * Gets global fee rate, with caching
297
+ *
298
+ * @returns {Promise<BN>} global fee rate microLamports/CU
299
+ */
300
+ public getGlobalFeeRate(): Promise<bigint> {
301
+ if(this.blockFeeCache==null || Date.now() - this.blockFeeCache.timestamp > MAX_FEE_AGE) {
302
+ let obj = {
303
+ timestamp: Date.now(),
304
+ feeRate: null
305
+ };
306
+ obj.feeRate = this._getGlobalFeeRate().catch(e => {
307
+ if(this.blockFeeCache===obj) this.blockFeeCache=null;
308
+ throw e;
309
+ });
310
+ this.blockFeeCache = obj;
311
+ }
312
+
313
+ return this.blockFeeCache.feeRate;
314
+ }
315
+
316
+ /**
317
+ * Gets the combined microLamports/CU fee rate (from localized & global fee market), cached & adjusted as for
318
+ * when bribe and/or static fee should be included, format: <uLamports/CU>;<static fee lamport>[;<bribe address>]
319
+ *
320
+ * @param mutableAccounts
321
+ * @private
322
+ */
323
+ public async getFeeRate(mutableAccounts: PublicKey[]): Promise<string> {
324
+ let feeMicroLamportPerCU = await this._getFeeRate(mutableAccounts);
325
+ if(this.bribeData?.getBribeFee!=null) feeMicroLamportPerCU = this.bribeData.getBribeFee(feeMicroLamportPerCU);
326
+
327
+ let fee: string = feeMicroLamportPerCU.toString(10);
328
+ if(this.getStaticFee!=null) {
329
+ fee += ";"+this.getStaticFee(feeMicroLamportPerCU);
330
+ } else {
331
+ fee += ";0"
332
+ }
333
+
334
+ if(this.bribeData?.address) {
335
+ fee += ";"+this.bribeData.address;
336
+ }
337
+
338
+ this.logger.debug("getFeeRate(): calculated fee: "+fee);
339
+
340
+ return fee;
341
+ }
342
+
343
+ /**
344
+ * Calculates the total priority fee paid for a given compute budget at a given fee rate
345
+ *
346
+ * @param computeUnits
347
+ * @param feeRate
348
+ * @param includeStaticFee whether the include the static/base part of the fee rate
349
+ */
350
+ public getPriorityFee(computeUnits: number, feeRate: string, includeStaticFee: boolean = true): bigint {
351
+ if(feeRate==null) return 0n;
352
+
353
+ const hashArr = feeRate.split("#");
354
+ if(hashArr.length>1) {
355
+ feeRate = hashArr[0];
356
+ }
357
+
358
+ const arr = feeRate.split(";");
359
+ const cuPrice = BigInt(arr[0]);
360
+ const staticFee = includeStaticFee ? BigInt(arr[1]) : 0n;
361
+
362
+ return staticFee + (cuPrice * BigInt(computeUnits) / 1000000n);
363
+ }
364
+
365
+ /**
366
+ * Applies fee rate to a transaction at the beginning of the transaction (has to be called after
367
+ * feePayer is set for the tx), specifically adds the setComputeUnitLimit & setComputeUnitPrice instruction
368
+ *
369
+ * @param tx
370
+ * @param computeBudget
371
+ * @param feeRate
372
+ */
373
+ public applyFeeRateBegin(tx: Transaction, computeBudget: number, feeRate: string): boolean {
374
+ if(feeRate==null) return false;
375
+
376
+ const hashArr = feeRate.split("#");
377
+ if(hashArr.length>1) {
378
+ feeRate = hashArr[0];
379
+ }
380
+
381
+ if(computeBudget!=null && computeBudget>0) tx.add(ComputeBudgetProgram.setComputeUnitLimit({
382
+ units: computeBudget,
383
+ }));
384
+
385
+ //Check if bribe is included
386
+ const arr = feeRate.split(";");
387
+ if(arr.length>2) {
388
+
389
+ } else {
390
+ let fee: bigint = BigInt(arr[0]);
391
+ if(arr.length>1) {
392
+ const staticFee = BigInt(arr[1]);
393
+ const cuBigInt = BigInt(computeBudget || (200000*SolanaTxUtils.getNonComputeBudgetIxs(tx)));
394
+ const staticFeePerCU = staticFee*BigInt(1000000)/cuBigInt;
395
+ fee += staticFeePerCU;
396
+ }
397
+ tx.add(ComputeBudgetProgram.setComputeUnitPrice({
398
+ microLamports: fee
399
+ }));
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Applies fee rate to the end of the transaction (has to be called after feePayer is set for the tx),
405
+ * specifically adds the bribe SystemProgram.transfer instruction
406
+ *
407
+ * @param tx
408
+ * @param computeBudget
409
+ * @param feeRate
410
+ */
411
+ public applyFeeRateEnd(tx: Transaction, computeBudget: number, feeRate: string): boolean {
412
+ if(feeRate==null) return false;
413
+
414
+ const hashArr = feeRate.split("#");
415
+ if(hashArr.length>1) {
416
+ feeRate = hashArr[0];
417
+ }
418
+
419
+ //Check if bribe is included
420
+ const arr = feeRate.split(";");
421
+ if(arr.length>2) {
422
+ const cuBigInt = BigInt(computeBudget || (200000*(SolanaTxUtils.getNonComputeBudgetIxs(tx)+1)));
423
+ const cuPrice = BigInt(arr[0]);
424
+ const staticFee = BigInt(arr[1]);
425
+ const bribeAddress = new PublicKey(arr[2]);
426
+ tx.add(SystemProgram.transfer({
427
+ fromPubkey: tx.feePayer,
428
+ toPubkey: bribeAddress,
429
+ lamports: staticFee + (cuBigInt*cuPrice/BigInt(1000000))
430
+ }));
431
+ return;
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Checks if the transaction should be submitted over Jito and if yes submits it
437
+ *
438
+ * @param tx
439
+ * @param options
440
+ * @returns {Promise<string | null>} null if the transaction was not sent over Jito, tx signature when tx was sent over Jito
441
+ */
442
+ submitTx(tx: Buffer, options?: SendOptions): Promise<string | null> {
443
+ const parsedTx = Transaction.from(tx);
444
+ const jitoFee = this.getJitoTxFee(parsedTx);
445
+ if(jitoFee==null) return null;
446
+
447
+ this.logger.info("submitTx(): sending tx over Jito, signature: "+parsedTx.signature+" fee: "+jitoFee.toString(10));
448
+ return this.sendJitoTx(tx, options);
449
+ }
450
+
451
451
  }