@lifi/sdk 3.13.7 → 3.14.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.
Files changed (68) hide show
  1. package/package.json +3 -3
  2. package/src/_cjs/core/Solana/SolanaStepExecutor.js +65 -19
  3. package/src/_cjs/core/Solana/SolanaStepExecutor.js.map +1 -1
  4. package/src/_cjs/core/Solana/connection.js +12 -4
  5. package/src/_cjs/core/Solana/connection.js.map +1 -1
  6. package/src/_cjs/core/Solana/jito/JitoConnection.js +97 -0
  7. package/src/_cjs/core/Solana/jito/JitoConnection.js.map +1 -0
  8. package/src/_cjs/core/Solana/jito/constants.js +14 -0
  9. package/src/_cjs/core/Solana/jito/constants.js.map +1 -0
  10. package/src/_cjs/core/Solana/jito/sendAndConfirmBundle.js +70 -0
  11. package/src/_cjs/core/Solana/jito/sendAndConfirmBundle.js.map +1 -0
  12. package/src/_cjs/index.js.map +1 -1
  13. package/src/_cjs/request.js +7 -3
  14. package/src/_cjs/request.js.map +1 -1
  15. package/src/_cjs/services/api.js +8 -1
  16. package/src/_cjs/services/api.js.map +1 -1
  17. package/src/_cjs/utils/uint8ArrayToBase64.js +11 -0
  18. package/src/_cjs/utils/uint8ArrayToBase64.js.map +1 -0
  19. package/src/_cjs/version.js +1 -1
  20. package/src/_esm/core/Solana/SolanaStepExecutor.js +91 -19
  21. package/src/_esm/core/Solana/SolanaStepExecutor.js.map +1 -1
  22. package/src/_esm/core/Solana/connection.js +16 -4
  23. package/src/_esm/core/Solana/connection.js.map +1 -1
  24. package/src/_esm/core/Solana/jito/JitoConnection.js +139 -0
  25. package/src/_esm/core/Solana/jito/JitoConnection.js.map +1 -0
  26. package/src/_esm/core/Solana/jito/constants.js +12 -0
  27. package/src/_esm/core/Solana/jito/constants.js.map +1 -0
  28. package/src/_esm/core/Solana/jito/sendAndConfirmBundle.js +81 -0
  29. package/src/_esm/core/Solana/jito/sendAndConfirmBundle.js.map +1 -0
  30. package/src/_esm/index.js.map +1 -1
  31. package/src/_esm/request.js +7 -3
  32. package/src/_esm/request.js.map +1 -1
  33. package/src/_esm/services/api.js +10 -2
  34. package/src/_esm/services/api.js.map +1 -1
  35. package/src/_esm/utils/uint8ArrayToBase64.js +10 -0
  36. package/src/_esm/utils/uint8ArrayToBase64.js.map +1 -0
  37. package/src/_esm/version.js +1 -1
  38. package/src/_types/core/Solana/SolanaStepExecutor.d.ts +18 -0
  39. package/src/_types/core/Solana/SolanaStepExecutor.d.ts.map +1 -1
  40. package/src/_types/core/Solana/connection.d.ts +8 -1
  41. package/src/_types/core/Solana/connection.d.ts.map +1 -1
  42. package/src/_types/core/Solana/jito/JitoConnection.d.ts +85 -0
  43. package/src/_types/core/Solana/jito/JitoConnection.d.ts.map +1 -0
  44. package/src/_types/core/Solana/jito/constants.d.ts +2 -0
  45. package/src/_types/core/Solana/jito/constants.d.ts.map +1 -0
  46. package/src/_types/core/Solana/jito/sendAndConfirmBundle.d.ts +15 -0
  47. package/src/_types/core/Solana/jito/sendAndConfirmBundle.d.ts.map +1 -0
  48. package/src/_types/index.d.ts +1 -1
  49. package/src/_types/index.d.ts.map +1 -1
  50. package/src/_types/request.d.ts.map +1 -1
  51. package/src/_types/services/api.d.ts +1 -1
  52. package/src/_types/services/api.d.ts.map +1 -1
  53. package/src/_types/types/internal.d.ts +3 -0
  54. package/src/_types/types/internal.d.ts.map +1 -1
  55. package/src/_types/utils/uint8ArrayToBase64.d.ts +2 -0
  56. package/src/_types/utils/uint8ArrayToBase64.d.ts.map +1 -0
  57. package/src/_types/version.d.ts +1 -1
  58. package/src/core/Solana/SolanaStepExecutor.ts +130 -29
  59. package/src/core/Solana/connection.ts +23 -5
  60. package/src/core/Solana/jito/JitoConnection.ts +190 -0
  61. package/src/core/Solana/jito/constants.ts +11 -0
  62. package/src/core/Solana/jito/sendAndConfirmBundle.ts +110 -0
  63. package/src/index.ts +6 -1
  64. package/src/request.ts +9 -3
  65. package/src/services/api.ts +19 -12
  66. package/src/types/internal.ts +6 -0
  67. package/src/utils/uint8ArrayToBase64.ts +12 -0
  68. package/src/version.ts +1 -1
@@ -0,0 +1,190 @@
1
+ import { Connection, type VersionedTransaction } from '@solana/web3.js'
2
+ import { uint8ArrayToBase64 } from '../../../utils/uint8ArrayToBase64.js'
3
+ import { JITO_TIP_ACCOUNTS } from './constants.js'
4
+
5
+ export type SimulateBundleResult = {
6
+ value: {
7
+ summary: 'succeeded' | { failed: { error: any; tx_signature: string } }
8
+ transactionResults: Array<{
9
+ err: any
10
+ logs: string[] | null
11
+ unitsConsumed?: number
12
+ }>
13
+ }
14
+ }
15
+
16
+ export type BundleStatus = {
17
+ bundle_id: string
18
+ transactions: string[]
19
+ slot: number
20
+ confirmation_status: 'processed' | 'confirmed' | 'finalized'
21
+ err:
22
+ | {
23
+ Ok: null
24
+ }
25
+ | any
26
+ }
27
+
28
+ export type BundleStatusResult = {
29
+ context: {
30
+ slot: number
31
+ }
32
+ value: BundleStatus[]
33
+ }
34
+
35
+ /**
36
+ * Makes a direct RPC request to an endpoint
37
+ *
38
+ */
39
+ async function rpcRequest<T>(
40
+ endpoint: string,
41
+ method: string,
42
+ params: any[]
43
+ ): Promise<T> {
44
+ const response = await fetch(endpoint, {
45
+ method: 'POST',
46
+ headers: {
47
+ 'Content-Type': 'application/json',
48
+ },
49
+ body: JSON.stringify({
50
+ jsonrpc: '2.0',
51
+ id: 1,
52
+ method,
53
+ params,
54
+ }),
55
+ })
56
+ if (!response.ok) {
57
+ throw new Error(`Jito RPC Error: ${response.status} ${response.statusText}`)
58
+ }
59
+ const data = await response.json()
60
+ if (data.error) {
61
+ throw new Error(`Jito RPC Error: ${data.error.message}`)
62
+ }
63
+ return data.result
64
+ }
65
+
66
+ /**
67
+ * Extended Connection class with Jito bundle support
68
+ * Adds simulateBundle, sendBundle, and getTipAccounts methods
69
+ */
70
+ export class JitoConnection extends Connection {
71
+ private tipAccountsCache: string[] | null = null
72
+
73
+ /**
74
+ * Check if an RPC endpoint supports Jito bundles
75
+ * @param rpcUrl - The RPC endpoint URL to check
76
+ * @returns true if the endpoint supports Jito bundle methods
77
+ */
78
+ static async isJitoRpc(rpcUrl: string): Promise<boolean> {
79
+ try {
80
+ // method exists if the request is successfull and doesn't throw an error
81
+ await rpcRequest(rpcUrl, 'getTipAccounts', [])
82
+ return true
83
+ } catch {
84
+ return false
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Makes a direct RPC request to the Jito-enabled endpoint
90
+ */
91
+ protected async rpcRequest<T>(method: string, params: any[]): Promise<T> {
92
+ try {
93
+ return await rpcRequest(this.rpcEndpoint, method, params)
94
+ } catch (error) {
95
+ console.error(`Jito RPC request failed: ${method}`, {
96
+ endpoint: this.rpcEndpoint,
97
+ params,
98
+ error,
99
+ })
100
+ throw error
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Serialize a transaction to base64 for RPC submission
106
+ */
107
+ private serializeTransaction(transaction: VersionedTransaction): string {
108
+ return uint8ArrayToBase64(transaction.serialize())
109
+ }
110
+
111
+ /**
112
+ * Get the tip accounts from the Jito endpoint, using fallbacks if results are empty
113
+ * Results are cached to avoid repeated RPC calls
114
+ */
115
+ async getTipAccounts(): Promise<string[]> {
116
+ if (this.tipAccountsCache) {
117
+ return this.tipAccountsCache
118
+ }
119
+
120
+ try {
121
+ const accounts = await this.rpcRequest<string[]>('getTipAccounts', [])
122
+ if (!accounts.length) {
123
+ throw new Error('RPC has no tip accounts')
124
+ }
125
+ this.tipAccountsCache = accounts
126
+ return accounts
127
+ } catch (error) {
128
+ const fallbackAccounts = JITO_TIP_ACCOUNTS
129
+ console.warn(
130
+ `Failed to fetch tip accounts from RPC, using fallback`,
131
+ error
132
+ )
133
+ return fallbackAccounts
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Get a random Jito tip account to reduce contention
139
+ */
140
+ async getRandomTipAccount(): Promise<string> {
141
+ const accounts = await this.getTipAccounts()
142
+ return accounts[Math.floor(Math.random() * accounts.length)]
143
+ }
144
+
145
+ /**
146
+ * Manually refresh the tip accounts cache
147
+ * Useful for long-running processes that may need updated tip accounts
148
+ */
149
+ async refreshTipAccounts(): Promise<string[]> {
150
+ this.tipAccountsCache = null
151
+ return this.getTipAccounts()
152
+ }
153
+
154
+ /**
155
+ * Simulate a bundle before sending it
156
+ * @param bundle - Array of signed transactions
157
+ * @returns Simulation result
158
+ */
159
+ async simulateBundle(
160
+ bundle: VersionedTransaction[]
161
+ ): Promise<SimulateBundleResult> {
162
+ const encodedTransactions = bundle.map((tx) =>
163
+ this.serializeTransaction(tx)
164
+ )
165
+ return this.rpcRequest<SimulateBundleResult>('simulateBundle', [
166
+ { encodedTransactions },
167
+ ])
168
+ }
169
+
170
+ /**
171
+ * Send a bundle to the Jito block engine
172
+ * @param bundle - Array of signed transactions
173
+ * @returns Bundle UUID
174
+ */
175
+ async sendBundle(bundle: VersionedTransaction[]): Promise<string> {
176
+ const encodedTransactions = bundle.map((tx) =>
177
+ this.serializeTransaction(tx)
178
+ )
179
+ return this.rpcRequest<string>('sendBundle', [encodedTransactions])
180
+ }
181
+
182
+ /**
183
+ * Get the status of submitted bundles
184
+ * @param bundleIds - Array of bundle UUIDs to check
185
+ * @returns Bundle status information
186
+ */
187
+ async getBundleStatuses(bundleIds: string[]): Promise<BundleStatusResult> {
188
+ return this.rpcRequest<BundleStatusResult>('getBundleStatuses', [bundleIds])
189
+ }
190
+ }
@@ -0,0 +1,11 @@
1
+ // Jito Tip accounts gotten from https://jito-foundation.gitbook.io/mev/mev-payment-and-distribution/on-chain-addresses
2
+ export const JITO_TIP_ACCOUNTS = [
3
+ 'Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY',
4
+ 'DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL',
5
+ '96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5',
6
+ '3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT',
7
+ 'HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe',
8
+ 'ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49',
9
+ 'ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt',
10
+ 'DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh',
11
+ ]
@@ -0,0 +1,110 @@
1
+ import type { SignatureResult, VersionedTransaction } from '@solana/web3.js'
2
+ import { sleep } from '../../../utils/sleep.js'
3
+ import { getJitoConnections } from '../connection.js'
4
+
5
+ export type BundleResult = {
6
+ bundleId: string
7
+ txSignatures: string[]
8
+ signatureResults: (SignatureResult | null)[]
9
+ }
10
+
11
+ /**
12
+ * Send and confirm a bundle of transactions using Jito
13
+ * Automatically selects a Jito-enabled RPC connection and polls for confirmation
14
+ * across multiple Jito RPCs in parallel
15
+ * @param signedTransactions - an Array of signed transactions
16
+ * @returns - {@link BundleResult} object containing Bundle ID, transaction signatures, and confirmation results
17
+ */
18
+ export async function sendAndConfirmBundle(
19
+ signedTransactions: VersionedTransaction[]
20
+ ): Promise<BundleResult> {
21
+ const jitoConnections = await getJitoConnections()
22
+
23
+ if (jitoConnections.length === 0) {
24
+ throw new Error(
25
+ 'No Jito-enabled RPC connection available for bundle submission'
26
+ )
27
+ }
28
+
29
+ const abortController = new AbortController()
30
+
31
+ const confirmPromises = jitoConnections.map(async (jitoConnection) => {
32
+ try {
33
+ // Send initial bundle for this connection
34
+ let bundleId: string
35
+ try {
36
+ bundleId = await jitoConnection.sendBundle(signedTransactions)
37
+ } catch (_) {
38
+ return null
39
+ }
40
+
41
+ const [blockhashResult, initialBlockHeight] = await Promise.all([
42
+ jitoConnection.getLatestBlockhash('confirmed'),
43
+ jitoConnection.getBlockHeight('confirmed'),
44
+ ])
45
+ let currentBlockHeight = initialBlockHeight
46
+
47
+ while (
48
+ currentBlockHeight < blockhashResult.lastValidBlockHeight &&
49
+ !abortController.signal.aborted
50
+ ) {
51
+ const statusResponse = await jitoConnection.getBundleStatuses([
52
+ bundleId,
53
+ ])
54
+
55
+ const bundleStatus = statusResponse.value[0]
56
+
57
+ // Check if bundle is confirmed or finalized
58
+ if (
59
+ bundleStatus &&
60
+ (bundleStatus.confirmation_status === 'confirmed' ||
61
+ bundleStatus.confirmation_status === 'finalized')
62
+ ) {
63
+ // Bundle confirmed! Extract transaction signatures from bundle status
64
+ const txSignatures = bundleStatus.transactions
65
+ // Fetch individual signature results
66
+ const sigResponse =
67
+ await jitoConnection.getSignatureStatuses(txSignatures)
68
+
69
+ if (!sigResponse?.value || !Array.isArray(sigResponse.value)) {
70
+ // Keep polling, if can't find signature results.
71
+ continue
72
+ }
73
+
74
+ // Immediately abort all other connections when we find a result
75
+ abortController.abort()
76
+ return {
77
+ bundleId,
78
+ txSignatures,
79
+ signatureResults: sigResponse.value,
80
+ }
81
+ }
82
+
83
+ await sleep(400)
84
+ if (!abortController.signal.aborted) {
85
+ currentBlockHeight = await jitoConnection.getBlockHeight('confirmed')
86
+ }
87
+ }
88
+
89
+ return null
90
+ } catch (error) {
91
+ if (abortController.signal.aborted) {
92
+ return null // Don't treat abortion as an error
93
+ }
94
+ throw error
95
+ }
96
+ })
97
+
98
+ // Wait for first successful confirmation
99
+ const result = await Promise.any(confirmPromises).catch(() => null)
100
+
101
+ if (!abortController.signal.aborted) {
102
+ abortController.abort()
103
+ }
104
+
105
+ if (!result) {
106
+ throw new Error('Failed to send and confirm bundle')
107
+ }
108
+
109
+ return result
110
+ }
package/src/index.ts CHANGED
@@ -129,7 +129,12 @@ export type {
129
129
  QuoteRequestFromAmount,
130
130
  QuoteRequestToAmount,
131
131
  } from './services/types.js'
132
- export type { RPCUrls, SDKBaseConfig, SDKConfig } from './types/internal.js'
132
+ export type {
133
+ RequestInterceptor,
134
+ RPCUrls,
135
+ SDKBaseConfig,
136
+ SDKConfig,
137
+ } from './types/internal.js'
133
138
  export { checkPackageUpdates } from './utils/checkPackageUpdates.js'
134
139
  export { convertQuoteToRoute } from './utils/convertQuoteToRoute.js'
135
140
  export { fetchTxErrorDetails } from './utils/fetchTxErrorDetails.js'
package/src/request.ts CHANGED
@@ -23,7 +23,8 @@ export const request = async <T = Response>(
23
23
  retries: requestSettings.retries,
24
24
  }
25
25
  ): Promise<T> => {
26
- const { userId, integrator, widgetVersion, apiKey } = config.get()
26
+ const { userId, integrator, widgetVersion, apiKey, requestInterceptor } =
27
+ config.get()
27
28
 
28
29
  if (!integrator) {
29
30
  throw new SDKError(
@@ -70,6 +71,10 @@ export const request = async <T = Response>(
70
71
  'x-lifi-integrator': integrator,
71
72
  }
72
73
 
74
+ if (requestInterceptor) {
75
+ options = await requestInterceptor(options)
76
+ }
77
+
73
78
  const response: Response = await fetch(
74
79
  url,
75
80
  stripExtendRequestInitProperties(options)
@@ -81,9 +86,10 @@ export const request = async <T = Response>(
81
86
 
82
87
  return await response.json()
83
88
  } catch (error) {
84
- if (options.retries > 0 && (error as HTTPError).status === 500) {
89
+ const retries = options.retries ?? 0
90
+ if (retries > 0 && (error as HTTPError).status === 500) {
85
91
  await sleep(500)
86
- return request<T>(url, { ...options, retries: options.retries - 1 })
92
+ return request<T>(url, { ...options, retries: retries - 1 })
87
93
  }
88
94
 
89
95
  await (error as HTTPError).buildAdditionalDetails?.()
@@ -1,5 +1,5 @@
1
1
  import {
2
- type ChainId,
2
+ ChainId,
3
3
  type ChainKey,
4
4
  type ChainsRequest,
5
5
  type ChainsResponse,
@@ -247,17 +247,24 @@ export const getStepTransaction = async (
247
247
  console.warn('SDK Validation: Invalid Step', step)
248
248
  }
249
249
 
250
- return await request<LiFiStep>(
251
- `${config.get().apiUrl}/advanced/stepTransaction`,
252
- {
253
- method: 'POST',
254
- headers: {
255
- 'Content-Type': 'application/json',
256
- },
257
- body: JSON.stringify(step),
258
- signal: options?.signal,
259
- }
260
- )
250
+ const _config = config.get()
251
+ let requestUrl = `${_config.apiUrl}/advanced/stepTransaction`
252
+ const isJitoBundleEnabled = Boolean(_config.routeOptions?.jitoBundle)
253
+
254
+ if (isJitoBundleEnabled && step.action.fromChainId === ChainId.SOL) {
255
+ // add jitoBundle param to url if from chain is SVM and jitoBundle is enabled in config
256
+ const queryParams = new URLSearchParams({ jitoBundle: 'true' })
257
+ requestUrl = `${requestUrl}?${queryParams}`
258
+ }
259
+
260
+ return await request<LiFiStep>(requestUrl, {
261
+ method: 'POST',
262
+ headers: {
263
+ 'Content-Type': 'application/json',
264
+ },
265
+ body: JSON.stringify(step),
266
+ signal: options?.signal,
267
+ })
261
268
  }
262
269
 
263
270
  /**
@@ -1,5 +1,10 @@
1
1
  import type { ChainId, ExtendedChain, RouteOptions } from '@lifi/types'
2
2
  import type { SDKProvider } from '../core/types.js'
3
+ import type { ExtendedRequestInit } from './request.js'
4
+
5
+ export type RequestInterceptor = (
6
+ request: ExtendedRequestInit
7
+ ) => ExtendedRequestInit | Promise<ExtendedRequestInit>
3
8
 
4
9
  export interface SDKBaseConfig {
5
10
  apiKey?: string
@@ -14,6 +19,7 @@ export interface SDKBaseConfig {
14
19
  widgetVersion?: string
15
20
  preloadChains: boolean
16
21
  debug: boolean
22
+ requestInterceptor?: RequestInterceptor
17
23
  }
18
24
 
19
25
  export interface SDKConfig extends Partial<Omit<SDKBaseConfig, 'integrator'>> {
@@ -0,0 +1,12 @@
1
+ export function uint8ArrayToBase64(bytes: Uint8Array): string {
2
+ // Node.js environment
3
+ if (typeof Buffer !== 'undefined') {
4
+ return Buffer.from(bytes).toString('base64')
5
+ }
6
+
7
+ // Browser environment
8
+ const binaryString = Array.from(bytes, (byte) =>
9
+ String.fromCharCode(byte)
10
+ ).join('')
11
+ return btoa(binaryString)
12
+ }
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export const name = '@lifi/sdk'
2
- export const version = '3.13.7'
2
+ export const version = '3.14.0'