@lifi/sdk 3.3.1 → 3.4.1

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 (50) hide show
  1. package/package.json +4 -4
  2. package/src/_cjs/config.js +6 -5
  3. package/src/_cjs/config.js.map +1 -1
  4. package/src/_cjs/core/Solana/SolanaStepExecutor.js +12 -47
  5. package/src/_cjs/core/Solana/SolanaStepExecutor.js.map +1 -1
  6. package/src/_cjs/core/Solana/connection.js +30 -9
  7. package/src/_cjs/core/Solana/connection.js.map +1 -1
  8. package/src/_cjs/core/Solana/getSolanaBalance.js +5 -5
  9. package/src/_cjs/core/Solana/getSolanaBalance.js.map +1 -1
  10. package/src/_cjs/core/Solana/sendAndConfirmTransaction.js +68 -0
  11. package/src/_cjs/core/Solana/sendAndConfirmTransaction.js.map +1 -0
  12. package/src/_cjs/index.js +8 -2
  13. package/src/_cjs/index.js.map +1 -1
  14. package/src/_cjs/utils/withDedupe.js.map +1 -1
  15. package/src/_cjs/version.js +1 -1
  16. package/src/_esm/config.js +6 -5
  17. package/src/_esm/config.js.map +1 -1
  18. package/src/_esm/core/Solana/SolanaStepExecutor.js +14 -65
  19. package/src/_esm/core/Solana/SolanaStepExecutor.js.map +1 -1
  20. package/src/_esm/core/Solana/connection.js +40 -10
  21. package/src/_esm/core/Solana/connection.js.map +1 -1
  22. package/src/_esm/core/Solana/getSolanaBalance.js +6 -6
  23. package/src/_esm/core/Solana/getSolanaBalance.js.map +1 -1
  24. package/src/_esm/core/Solana/sendAndConfirmTransaction.js +76 -0
  25. package/src/_esm/core/Solana/sendAndConfirmTransaction.js.map +1 -0
  26. package/src/_esm/index.js +3 -0
  27. package/src/_esm/index.js.map +1 -1
  28. package/src/_esm/utils/withDedupe.js.map +1 -1
  29. package/src/_esm/version.js +1 -1
  30. package/src/_types/config.d.ts +2 -2
  31. package/src/_types/config.d.ts.map +1 -1
  32. package/src/_types/core/Solana/SolanaStepExecutor.d.ts.map +1 -1
  33. package/src/_types/core/Solana/connection.d.ts +14 -3
  34. package/src/_types/core/Solana/connection.d.ts.map +1 -1
  35. package/src/_types/core/Solana/getSolanaBalance.d.ts.map +1 -1
  36. package/src/_types/core/Solana/sendAndConfirmTransaction.d.ts +13 -0
  37. package/src/_types/core/Solana/sendAndConfirmTransaction.d.ts.map +1 -0
  38. package/src/_types/index.d.ts +6 -1
  39. package/src/_types/index.d.ts.map +1 -1
  40. package/src/_types/utils/withDedupe.d.ts +1 -1
  41. package/src/_types/utils/withDedupe.d.ts.map +1 -1
  42. package/src/_types/version.d.ts +1 -1
  43. package/src/config.ts +6 -6
  44. package/src/core/Solana/SolanaStepExecutor.ts +15 -86
  45. package/src/core/Solana/connection.ts +43 -10
  46. package/src/core/Solana/getSolanaBalance.ts +27 -7
  47. package/src/core/Solana/sendAndConfirmTransaction.ts +111 -0
  48. package/src/index.ts +10 -0
  49. package/src/utils/withDedupe.ts +3 -3
  50. package/src/version.ts +1 -1
@@ -1,11 +1,6 @@
1
1
  import type { ExtendedTransactionInfo, FullStatusData } from '@lifi/types'
2
2
  import type { SignerWalletAdapter } from '@solana/wallet-adapter-base'
3
- import {
4
- type SendOptions,
5
- type SignatureResult,
6
- VersionedTransaction,
7
- } from '@solana/web3.js'
8
- import bs58 from 'bs58'
3
+ import { VersionedTransaction } from '@solana/web3.js'
9
4
  import { withTimeout } from 'viem'
10
5
  import { config } from '../../config.js'
11
6
  import { LiFiErrorCode } from '../../errors/constants.js'
@@ -13,7 +8,6 @@ import { TransactionError } from '../../errors/errors.js'
13
8
  import { getStepTransaction } from '../../services/api.js'
14
9
  import { base64ToUint8Array } from '../../utils/base64ToUint8Array.js'
15
10
  import { getTransactionFailedMessage } from '../../utils/getTransactionMessage.js'
16
- import { sleep } from '../../utils/sleep.js'
17
11
  import { BaseStepExecutor } from '../BaseStepExecutor.js'
18
12
  import { checkBalance } from '../checkBalance.js'
19
13
  import { getSubstatusMessage } from '../processMessages.js'
@@ -24,8 +18,9 @@ import type {
24
18
  TransactionParameters,
25
19
  } from '../types.js'
26
20
  import { waitForReceivingTransaction } from '../waitForReceivingTransaction.js'
27
- import { getSolanaConnection } from './connection.js'
21
+ import { callSolanaWithRetry } from './connection.js'
28
22
  import { parseSolanaErrors } from './parseSolanaErrors.js'
23
+ import { sendAndConfirmTransaction } from './sendAndConfirmTransaction.js'
29
24
 
30
25
  export interface SolanaStepExecutorOptions extends StepExecutorOptions {
31
26
  walletAdapter: SignerWalletAdapter
@@ -66,8 +61,6 @@ export class SolanaStepExecutor extends BaseStepExecutor {
66
61
 
67
62
  if (process.status !== 'DONE') {
68
63
  try {
69
- const connection = await getSolanaConnection()
70
-
71
64
  process = this.statusManager.updateProcess(
72
65
  step,
73
66
  process.type,
@@ -115,10 +108,6 @@ export class SolanaStepExecutor extends BaseStepExecutor {
115
108
  data: step.transactionRequest.data,
116
109
  }
117
110
 
118
- const blockhashResult = await connection.getLatestBlockhash({
119
- commitment: 'confirmed',
120
- })
121
-
122
111
  if (this.executionOptions?.updateTransactionRequestHook) {
123
112
  const customizedTransactionRequest: TransactionParameters =
124
113
  await this.executionOptions.updateTransactionRequestHook({
@@ -165,12 +154,11 @@ export class SolanaStepExecutor extends BaseStepExecutor {
165
154
  'PENDING'
166
155
  )
167
156
 
168
- const simulationResult = await connection.simulateTransaction(
169
- signedTx,
170
- {
171
- commitment: 'processed',
157
+ const simulationResult = await callSolanaWithRetry((connection) =>
158
+ connection.simulateTransaction(signedTx, {
159
+ commitment: 'confirmed',
172
160
  replaceRecentBlockhash: true,
173
- }
161
+ })
174
162
  )
175
163
 
176
164
  if (simulationResult.value.err) {
@@ -180,79 +168,20 @@ export class SolanaStepExecutor extends BaseStepExecutor {
180
168
  )
181
169
  }
182
170
 
183
- // Create transaction hash (signature)
184
- const txSignature = bs58.encode(signedTx.signatures[0])
185
-
186
- // A known weirdness - MAX_RECENT_BLOCKHASHES is 300
187
- // https://github.com/solana-labs/solana/blob/master/sdk/program/src/clock.rs#L123
188
- // but MAX_PROCESSING_AGE is 150
189
- // https://github.com/solana-labs/solana/blob/master/sdk/program/src/clock.rs#L129
190
- // the blockhash queue in the bank tells you 300 + current slot, but it won't be accepted 150 blocks later.
191
- // https://solana.com/docs/advanced/confirmation#transaction-expiration
192
- const lastValidBlockHeight = blockhashResult.lastValidBlockHeight - 150
193
-
194
- // In the following section, we wait and constantly check for the transaction to be confirmed
195
- // and resend the transaction if it is not confirmed within a certain time interval
196
- // thus handling tx retries on the client side rather than relying on the RPC
197
- const abortController = new AbortController()
198
- const confirmTransactionPromise = connection
199
- .confirmTransaction(
200
- {
201
- signature: txSignature,
202
- blockhash: blockhashResult.blockhash,
203
- lastValidBlockHeight: lastValidBlockHeight,
204
- abortSignal: abortController.signal,
205
- },
206
- 'confirmed'
207
- )
208
- .then((result) => result.value)
209
-
210
- let confirmedTx: SignatureResult | null = null
211
- let blockHeight = await connection.getBlockHeight()
212
-
213
- const rawTransactionOptions: SendOptions = {
214
- // We can skip preflight check after the first transaction has been sent
215
- // https://solana.com/docs/advanced/retry#the-cost-of-skipping-preflight
216
- skipPreflight: true,
217
- // Setting max retries to 0 as we are handling retries manually
218
- maxRetries: 0,
219
- // https://solana.com/docs/advanced/confirmation#use-an-appropriate-preflight-commitment-level
220
- preflightCommitment: 'confirmed',
221
- }
222
-
223
- const signedTxSerialized = signedTx.serialize()
224
-
225
- // https://solana.com/docs/advanced/retry#customizing-rebroadcast-logic
226
- while (!confirmedTx && blockHeight < lastValidBlockHeight) {
227
- await connection.sendRawTransaction(
228
- signedTxSerialized,
229
- rawTransactionOptions
230
- )
231
- confirmedTx = await Promise.race([
232
- confirmTransactionPromise,
233
- sleep(1000),
234
- ])
235
- if (confirmedTx) {
236
- break
237
- }
238
- blockHeight = await connection.getBlockHeight()
239
- }
240
-
241
- // Stop waiting for tx confirmation
242
- abortController.abort()
171
+ const confirmedTx = await sendAndConfirmTransaction(signedTx)
243
172
 
244
- if (!confirmedTx) {
173
+ if (!confirmedTx.signatureResult) {
245
174
  throw new TransactionError(
246
175
  LiFiErrorCode.TransactionExpired,
247
176
  'Transaction has expired: The block height has exceeded the maximum allowed limit.'
248
177
  )
249
178
  }
250
179
 
251
- if (confirmedTx?.err) {
180
+ if (confirmedTx.signatureResult.err) {
252
181
  const reason =
253
- typeof confirmedTx.err === 'object'
254
- ? JSON.stringify(confirmedTx.err)
255
- : confirmedTx.err
182
+ typeof confirmedTx.signatureResult.err === 'object'
183
+ ? JSON.stringify(confirmedTx.signatureResult.err)
184
+ : confirmedTx.signatureResult.err
256
185
  throw new TransactionError(
257
186
  LiFiErrorCode.TransactionFailed,
258
187
  `Transaction failed: ${reason}`
@@ -265,8 +194,8 @@ export class SolanaStepExecutor extends BaseStepExecutor {
265
194
  process.type,
266
195
  'PENDING',
267
196
  {
268
- txHash: txSignature,
269
- txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${txSignature}`,
197
+ txHash: confirmedTx.txSignature,
198
+ txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${confirmedTx.txSignature}`,
270
199
  }
271
200
  )
272
201
 
@@ -1,18 +1,51 @@
1
1
  import { ChainId } from '@lifi/types'
2
2
  import { Connection } from '@solana/web3.js'
3
- import { getRpcUrl } from '../rpc.js'
3
+ import { getRpcUrls } from '../rpc.js'
4
4
 
5
- let connection: Connection | undefined = undefined
5
+ const connections = new Map<string, Connection>()
6
6
 
7
7
  /**
8
- * getSolanaConnection is just a thin wrapper around getting the connection (RPC provider) for Solana
9
- * @returns - Solana RPC connection
8
+ * Initializes the Solana connections if they haven't been initialized yet.
9
+ * @returns - Promise that resolves when connections are initialized.
10
10
  */
11
- export const getSolanaConnection = async (): Promise<Connection> => {
12
- if (!connection) {
13
- const rpcUrl = await getRpcUrl(ChainId.SOL)
14
- connection = new Connection(rpcUrl)
15
- return connection
11
+ export const ensureConnections = async (): Promise<void> => {
12
+ const rpcUrls = await getRpcUrls(ChainId.SOL)
13
+ for (const rpcUrl of rpcUrls) {
14
+ if (!connections.get(rpcUrl)) {
15
+ const connection = new Connection(rpcUrl)
16
+ connections.set(rpcUrl, connection)
17
+ }
16
18
  }
17
- return connection
19
+ }
20
+
21
+ /**
22
+ * Wrapper around getting the connection (RPC provider) for Solana
23
+ * @returns - Solana RPC connections
24
+ */
25
+ export const getSolanaConnections = async (): Promise<Connection[]> => {
26
+ await ensureConnections()
27
+ return Array.from(connections.values())
28
+ }
29
+
30
+ /**
31
+ * Calls a function on the Connection instances with retry logic.
32
+ * @param fn - The function to call, which receives a Connection instance.
33
+ * @returns - The result of the function call.
34
+ */
35
+ export async function callSolanaWithRetry<R>(
36
+ fn: (connection: Connection) => Promise<R>
37
+ ): Promise<R> {
38
+ // Ensure connections are initialized
39
+ await ensureConnections()
40
+ let lastError: any = null
41
+ for (const connection of connections.values()) {
42
+ try {
43
+ const result = await fn(connection)
44
+ return result
45
+ } catch (error) {
46
+ lastError = error
47
+ }
48
+ }
49
+ // Throw the last encountered error
50
+ throw lastError
18
51
  }
@@ -1,7 +1,8 @@
1
1
  import type { ChainId, Token, TokenAmount } from '@lifi/types'
2
2
  import { PublicKey } from '@solana/web3.js'
3
3
  import { SolSystemProgram } from '../../constants.js'
4
- import { getSolanaConnection } from './connection.js'
4
+ import { withDedupe } from '../../utils/withDedupe.js'
5
+ import { callSolanaWithRetry } from './connection.js'
5
6
  import { TokenProgramAddress } from './types.js'
6
7
 
7
8
  export const getSolanaBalance = async (
@@ -26,15 +27,34 @@ const getSolanaBalanceDefault = async (
26
27
  tokens: Token[],
27
28
  walletAddress: string
28
29
  ): Promise<TokenAmount[]> => {
29
- const connection = await getSolanaConnection()
30
30
  const accountPublicKey = new PublicKey(walletAddress)
31
31
  const tokenProgramPublicKey = new PublicKey(TokenProgramAddress)
32
32
  const [slot, balance, tokenAccountsByOwner] = await Promise.allSettled([
33
- connection.getSlot(),
34
- connection.getBalance(accountPublicKey),
35
- connection.getParsedTokenAccountsByOwner(accountPublicKey, {
36
- programId: tokenProgramPublicKey,
37
- }),
33
+ withDedupe(
34
+ () =>
35
+ callSolanaWithRetry((connection) => connection.getSlot('confirmed')),
36
+ { id: `${getSolanaBalanceDefault.name}.getSlot` }
37
+ ),
38
+ withDedupe(
39
+ () =>
40
+ callSolanaWithRetry((connection) =>
41
+ connection.getBalance(accountPublicKey, 'confirmed')
42
+ ),
43
+ { id: `${getSolanaBalanceDefault.name}.getBalance` }
44
+ ),
45
+ withDedupe(
46
+ () =>
47
+ callSolanaWithRetry((connection) =>
48
+ connection.getParsedTokenAccountsByOwner(
49
+ accountPublicKey,
50
+ {
51
+ programId: tokenProgramPublicKey,
52
+ },
53
+ 'confirmed'
54
+ )
55
+ ),
56
+ { id: `${getSolanaBalanceDefault.name}.getParsedTokenAccountsByOwner` }
57
+ ),
38
58
  ])
39
59
  const blockNumber = slot.status === 'fulfilled' ? BigInt(slot.value) : 0n
40
60
  const solBalance = balance.status === 'fulfilled' ? BigInt(balance.value) : 0n
@@ -0,0 +1,111 @@
1
+ import type {
2
+ SendOptions,
3
+ SignatureResult,
4
+ VersionedTransaction,
5
+ } from '@solana/web3.js'
6
+ import bs58 from 'bs58'
7
+ import { sleep } from '../../utils/sleep.js'
8
+ import { getSolanaConnections } from './connection.js'
9
+
10
+ export type ConfirmedTransactionResult = {
11
+ signatureResult: SignatureResult | null
12
+ txSignature: string
13
+ }
14
+
15
+ /**
16
+ * Sends a Solana transaction to multiple RPC endpoints and returns the confirmation
17
+ * as soon as any of them confirm the transaction.
18
+ * @param signedTx - The signed transaction to send.
19
+ * @returns - The confirmation result of the transaction.
20
+ */
21
+ export async function sendAndConfirmTransaction(
22
+ signedTx: VersionedTransaction
23
+ ): Promise<ConfirmedTransactionResult> {
24
+ const connections = await getSolanaConnections()
25
+
26
+ const signedTxSerialized = signedTx.serialize()
27
+ // Create transaction hash (signature)
28
+ const txSignature = bs58.encode(signedTx.signatures[0])
29
+
30
+ if (!txSignature) {
31
+ throw new Error('Transaction signature is missing.')
32
+ }
33
+
34
+ const rawTransactionOptions: SendOptions = {
35
+ // We can skip preflight check after the first transaction has been sent
36
+ // https://solana.com/docs/advanced/retry#the-cost-of-skipping-preflight
37
+ skipPreflight: true,
38
+ // Setting max retries to 0 as we are handling retries manually
39
+ maxRetries: 0,
40
+ // https://solana.com/docs/advanced/confirmation#use-an-appropriate-preflight-commitment-level
41
+ preflightCommitment: 'confirmed',
42
+ }
43
+
44
+ for (const connection of connections) {
45
+ connection
46
+ .sendRawTransaction(signedTxSerialized, rawTransactionOptions)
47
+ .catch()
48
+ }
49
+
50
+ const abortControllers: AbortController[] = []
51
+
52
+ const confirmPromises = connections.map(async (connection) => {
53
+ const abortController = new AbortController()
54
+ abortControllers.push(abortController)
55
+ try {
56
+ const blockhashResult = await connection.getLatestBlockhash('confirmed')
57
+
58
+ const confirmTransactionPromise = connection
59
+ .confirmTransaction(
60
+ {
61
+ signature: txSignature,
62
+ blockhash: blockhashResult.blockhash,
63
+ lastValidBlockHeight: blockhashResult.lastValidBlockHeight,
64
+ abortSignal: abortController.signal,
65
+ },
66
+ 'confirmed'
67
+ )
68
+ .then((result) => result.value)
69
+
70
+ let signatureResult: SignatureResult | null = null
71
+ let blockHeight = await connection.getBlockHeight('confirmed')
72
+
73
+ while (
74
+ !signatureResult &&
75
+ blockHeight < blockhashResult.lastValidBlockHeight
76
+ ) {
77
+ await connection.sendRawTransaction(
78
+ signedTxSerialized,
79
+ rawTransactionOptions
80
+ )
81
+ signatureResult = await Promise.race([
82
+ confirmTransactionPromise,
83
+ sleep(1000),
84
+ ])
85
+
86
+ if (signatureResult || abortController.signal.aborted) {
87
+ break
88
+ }
89
+
90
+ blockHeight = await connection.getBlockHeight('confirmed')
91
+ }
92
+
93
+ abortController.abort()
94
+
95
+ return signatureResult
96
+ } catch (error) {
97
+ if (abortController.signal.aborted) {
98
+ return Promise.reject(new Error('Confirmation aborted.'))
99
+ }
100
+ throw error
101
+ }
102
+ })
103
+
104
+ const signatureResult = await Promise.any(confirmPromises).catch(() => null)
105
+
106
+ for (const abortController of abortControllers) {
107
+ abortController.abort()
108
+ }
109
+
110
+ return { signatureResult, txSignature }
111
+ }
package/src/index.ts CHANGED
@@ -11,7 +11,10 @@ export {
11
11
  revokeTokenApproval,
12
12
  setTokenAllowance,
13
13
  } from './core/EVM/setAllowance.js'
14
+ export { isEVM } from './core/EVM/types.js'
14
15
  export type {
16
+ EVMProvider,
17
+ EVMProviderOptions,
15
18
  MultisigConfig,
16
19
  MultisigTransaction,
17
20
  MultisigTxDetails,
@@ -53,7 +56,14 @@ export {
53
56
  KeypairWalletName,
54
57
  } from './core/Solana/KeypairWalletAdapter.js'
55
58
  export { Solana } from './core/Solana/Solana.js'
59
+ export { isSolana } from './core/Solana/types.js'
60
+ export type {
61
+ SolanaProvider,
62
+ SolanaProviderOptions,
63
+ } from './core/Solana/types.js'
56
64
  export { UTXO } from './core/UTXO/UTXO.js'
65
+ export { isUTXO } from './core/UTXO/types.js'
66
+ export type { UTXOProvider, UTXOProviderOptions } from './core/UTXO/types.js'
57
67
  export { createConfig } from './createConfig.js'
58
68
  export {
59
69
  checkPackageUpdates,
@@ -29,10 +29,10 @@ type WithDedupeOptions = {
29
29
  }
30
30
 
31
31
  /** Deduplicates in-flight promises. */
32
- export function withDedupe<data>(
33
- fn: () => Promise<data>,
32
+ export function withDedupe<T>(
33
+ fn: () => Promise<T>,
34
34
  { enabled = true, id }: WithDedupeOptions
35
- ): Promise<data> {
35
+ ): Promise<T> {
36
36
  if (!enabled || !id) {
37
37
  return fn()
38
38
  }
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export const name = '@lifi/sdk'
2
- export const version = '3.3.1'
2
+ export const version = '3.4.1'