@exodus/solana-api 3.30.4 → 3.30.6

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/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [3.30.6](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.30.5...@exodus/solana-api@3.30.6) (2026-03-30)
7
+
8
+ **Note:** Version bump only for package @exodus/solana-api
9
+
10
+
11
+
12
+
13
+
14
+ ## [3.30.5](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.30.4...@exodus/solana-api@3.30.5) (2026-03-27)
15
+
16
+ **Note:** Version bump only for package @exodus/solana-api
17
+
18
+
19
+
20
+
21
+
6
22
  ## [3.30.4](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.30.3...@exodus/solana-api@3.30.4) (2026-03-25)
7
23
 
8
24
  **Note:** Version bump only for package @exodus/solana-api
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-api",
3
- "version": "3.30.4",
3
+ "version": "3.30.6",
4
4
  "description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Solana",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -33,7 +33,7 @@
33
33
  "@exodus/fetch": "^1.7.3",
34
34
  "@exodus/models": "^13.0.0",
35
35
  "@exodus/simple-retry": "^0.0.6",
36
- "@exodus/solana-lib": "^3.20.1",
36
+ "@exodus/solana-lib": "^3.22.3",
37
37
  "@exodus/solana-meta": "^2.0.2",
38
38
  "@exodus/timer": "^1.1.1",
39
39
  "debug": "^4.1.1",
@@ -49,7 +49,7 @@
49
49
  "@exodus/assets-testing": "^1.0.0",
50
50
  "@exodus/solana-web3.js": "^1.63.1-exodus.9-rc3"
51
51
  },
52
- "gitHead": "49d472ae5c602ba68329227e0e0cd3a9b4568fba",
52
+ "gitHead": "0d8639ef50e192df1f302f0ac7ff378d919cc536",
53
53
  "bugs": {
54
54
  "url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
55
55
  },
package/src/api.js CHANGED
@@ -12,9 +12,10 @@ import {
12
12
  findAssociatedTokenAddress,
13
13
  getMetadataAccount,
14
14
  getTransactionSimulationParams,
15
+ isSystemProgram,
16
+ isTokenProgram,
15
17
  SOL_DECIMAL,
16
18
  STAKE_PROGRAM_ID,
17
- SYSTEM_PROGRAM_ID,
18
19
  TOKEN_2022_PROGRAM_ID,
19
20
  TOKEN_PROGRAM_ID,
20
21
  } from '@exodus/solana-lib'
@@ -455,11 +456,7 @@ export class Api {
455
456
  const value = accountInfo || (await this.getAccountInfo(address))
456
457
  const owner = value?.owner // program owner
457
458
  if (!owner) return false // not initialized account (or purged)
458
- return ![
459
- SYSTEM_PROGRAM_ID.toBase58(),
460
- TOKEN_PROGRAM_ID.toBase58(),
461
- TOKEN_2022_PROGRAM_ID.toBase58(),
462
- ].includes(owner)
459
+ return !(isSystemProgram(owner) || isTokenProgram(owner))
463
460
  }
464
461
 
465
462
  async fetchValidatedDelegation({ delegatedAddress, expectedDelegate }) {
@@ -552,7 +549,7 @@ export class Api {
552
549
  lamports: value.lamports,
553
550
  }
554
551
 
555
- if (account.owner === SYSTEM_PROGRAM_ID.toBase58()) return 'solana'
552
+ if (isSystemProgram(account.owner)) return 'solana'
556
553
  if (account.owner === TOKEN_PROGRAM_ID.toBase58()) return 'token'
557
554
  if (account.owner === TOKEN_2022_PROGRAM_ID.toBase58()) return 'token-2022'
558
555
  return null
@@ -1,7 +1,7 @@
1
1
  import { memoizeLruCache } from '@exodus/asset-lib'
2
2
  import { isNil, memoize, omitBy } from '@exodus/basic-utils'
3
3
  import wretch from '@exodus/fetch/wretch'
4
- import { SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@exodus/solana-lib'
4
+ import { isSystemProgram, isTokenProgram, TOKEN_PROGRAM_ID } from '@exodus/solana-lib'
5
5
  import ms from 'ms'
6
6
  import urljoin from 'url-join'
7
7
 
@@ -205,11 +205,7 @@ export class ClarityApi extends RpcApi {
205
205
  const value = accountInfo || (await this.getAccountInfo(address))
206
206
  const owner = value?.owner // program owner
207
207
  if (!owner) return false // not initialized account (or purged)
208
- return ![
209
- SYSTEM_PROGRAM_ID.toBase58(),
210
- TOKEN_PROGRAM_ID.toBase58(),
211
- TOKEN_2022_PROGRAM_ID.toBase58(),
212
- ].includes(owner)
208
+ return !(isSystemProgram(owner) || isTokenProgram(owner))
213
209
  }
214
210
 
215
211
  ataOwnershipChangedCached = memoizeLruCache(
package/src/index.js CHANGED
@@ -33,5 +33,5 @@ const serverApi = new Api({ assets }) // TODO: remove it, clean every use from p
33
33
  export default serverApi // TODO: remove it
34
34
 
35
35
  export { Api } from './api.js'
36
- export { WsApi } from './ws-api.js'
36
+ export { WsApi, normalizeTransactionNotificationResult } from './ws-api.js'
37
37
  export { ClarityApi } from './clarity-api.js'
package/src/rpc-api.js CHANGED
@@ -10,8 +10,9 @@ import {
10
10
  findAssociatedTokenAddress,
11
11
  getMetadataAccount,
12
12
  getTransactionSimulationParams,
13
+ isSystemProgram,
14
+ isTokenProgram,
13
15
  SOL_DECIMAL,
14
- SYSTEM_PROGRAM_ID as SYSTEM_PROGRAM_ID_KEY,
15
16
  TOKEN_2022_PROGRAM_ID as TOKEN_2022_PROGRAM_ID_KEY,
16
17
  TOKEN_PROGRAM_ID as TOKEN_PROGRAM_ID_KEY,
17
18
  } from '@exodus/solana-lib'
@@ -24,8 +25,7 @@ import {
24
25
  fetchValidatedDelegation as _fetchValidatedDelegation,
25
26
  } from './tx-log/delegation-utils.js'
26
27
 
27
- const [SYSTEM_PROGRAM_ID, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID] = [
28
- SYSTEM_PROGRAM_ID_KEY.toBase58(),
28
+ const [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID] = [
29
29
  TOKEN_PROGRAM_ID_KEY.toBase58(),
30
30
  TOKEN_2022_PROGRAM_ID_KEY.toBase58(),
31
31
  ]
@@ -217,7 +217,7 @@ export class RpcApi {
217
217
 
218
218
  async isSpl(address) {
219
219
  const { owner } = await this.getAccountInfo(address)
220
- return [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID].includes(owner)
220
+ return isTokenProgram(owner)
221
221
  }
222
222
 
223
223
  async getRawAccountInfo({ address, encoding = 'jsonParsed' }) {
@@ -292,7 +292,7 @@ export class RpcApi {
292
292
  lamports: value.lamports,
293
293
  }
294
294
 
295
- if (account.owner === SYSTEM_PROGRAM_ID) return 'solana'
295
+ if (isSystemProgram(account.owner)) return 'solana'
296
296
  if (account.owner === TOKEN_PROGRAM_ID) return 'token'
297
297
  if (account.owner === TOKEN_2022_PROGRAM_ID) return 'token-2022'
298
298
  return null
@@ -2,6 +2,7 @@ import delay from 'delay'
2
2
  import assert from 'minimalistic-assert'
3
3
 
4
4
  import { DEFAULT_POOL_ADDRESS } from '../account-state.js'
5
+ import { normalizeTransactionNotificationResult } from '../ws-api.js'
5
6
  import { SolanaClarityMonitor } from './clarity-monitor.js'
6
7
 
7
8
  const DEFAULT_REMOTE_CONFIG = {
@@ -251,9 +252,8 @@ export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
251
252
 
252
253
  // Populate tokenAccountsByOwner from this tx first (e.g. new ATA created in same tx)
253
254
  // so we can parse and update history; otherwise "cannot parse tx" would trigger for new receives
254
- const rawTransaction =
255
- data.params.result?.value?.transaction ?? data.params.result?.transaction
256
- const txTokenAccounts = this.wsApi.getTokenAccountsFromTxMeta(rawTransaction, address)
255
+ const txDetails = normalizeTransactionNotificationResult(data.params.result)
256
+ const txTokenAccounts = this.wsApi.getTokenAccountsFromTxMeta(txDetails, address)
257
257
  let tokenAccountsByOwnerList = this.tokenAccountsByOwner[walletAccount] || []
258
258
  for (const txAcc of txTokenAccounts) {
259
259
  if (
@@ -295,7 +295,8 @@ export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
295
295
  assets: this.assets,
296
296
  tokens: this.clarityApi.tokens,
297
297
  tokenAccountsByOwner: tokenAccountsByOwnerList,
298
- result: data.params.result, // raw tx
298
+ result: data.params.result,
299
+ txDetails,
299
300
  })
300
301
 
301
302
  if (Object.keys(logItemsByAsset).length === 0) return // cannot parse tx
@@ -321,7 +322,7 @@ export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
321
322
  // Update token balances from this tx's postTokenBalances so the first receive shows the
322
323
  // correct balance even when accountNotification was missed (we subscribe after the tx is
323
324
  // processed, so the balance-change notification can already have been sent).
324
- const postTokenBalances = rawTransaction?.meta?.postTokenBalances ?? []
325
+ const postTokenBalances = txDetails?.meta?.postTokenBalances ?? []
325
326
  const tokenBalancesFromTx = {}
326
327
  for (const b of postTokenBalances) {
327
328
  if (b.owner !== address) continue
package/src/ws-api.js CHANGED
@@ -1,4 +1,12 @@
1
- import { PublicKey, Token, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, U64 } from '@exodus/solana-lib'
1
+ import {
2
+ isSystemProgram,
3
+ isTokenProgram,
4
+ PublicKey,
5
+ Token,
6
+ TOKEN_2022_PROGRAM_ID,
7
+ TOKEN_PROGRAM_ID,
8
+ U64,
9
+ } from '@exodus/solana-lib'
2
10
  import lodash from 'lodash'
3
11
 
4
12
  import { Connection } from './connection.js'
@@ -11,6 +19,42 @@ import { isSolAddressPoisoningTx } from './txs-utils.js'
11
19
  // Helius Enhanced WebSocket (https://www.helius.dev/docs/enhanced-websockets)
12
20
  const HELIUS_WS_URL = 'wss://solana-helius-wss.a.exodus.io/ws'
13
21
 
22
+ /**
23
+ * Normalize `transactionSubscribe` / JSON-RPC `params.result` into `getTransaction`-shaped txDetails:
24
+ * `{ slot?, transaction: { message, signatures }, meta }`.
25
+ *
26
+ * - **Triton Whirligig:** `result.value.transaction` is often `{ meta, transaction: { message, signatures }, ... }`.
27
+ * - **Helius / std RPC:** `meta` may be a sibling of `transaction` (same layout as `getTransaction`).
28
+ *
29
+ * Use this for any WS path that needs meta, inner instructions, or token balances — not only
30
+ * `parseTransaction`.
31
+ *
32
+ * @returns { { slot?: number, meta: object, transaction: { message: object, signatures: string[] } } | undefined }
33
+ */
34
+ export function normalizeTransactionNotificationResult(result) {
35
+ const rawFromValue = result?.value?.transaction
36
+ const rawFromRoot = result?.transaction
37
+ const candidate = rawFromValue ?? rawFromRoot
38
+
39
+ const isBareTransactionBody =
40
+ candidate != null &&
41
+ result?.meta != null &&
42
+ candidate.meta == null &&
43
+ candidate.message != null &&
44
+ Array.isArray(candidate.signatures)
45
+
46
+ const txDetails = isBareTransactionBody
47
+ ? { meta: result.meta, transaction: candidate }
48
+ : candidate
49
+
50
+ const slot = result?.slot ?? result?.value?.slot
51
+ if (txDetails == null || slot == null || txDetails.slot != null) {
52
+ return txDetails
53
+ }
54
+
55
+ return { ...txDetails, slot }
56
+ }
57
+
14
58
  export class WsApi {
15
59
  constructor({ rpcUrl, wsUrl, assets }) {
16
60
  this.setWsEndpoint(wsUrl)
@@ -313,10 +357,7 @@ export class WsApi {
313
357
  if (!value) return null
314
358
  const { pubkey, account } = value
315
359
  if (!account || !pubkey) return null
316
- const isTokenProgram =
317
- account.owner === TOKEN_PROGRAM_ID.toBase58() ||
318
- account.owner === TOKEN_2022_PROGRAM_ID.toBase58()
319
- if (!isTokenProgram) return null
360
+ if (!isTokenProgram(account.owner)) return null
320
361
 
321
362
  let owner
322
363
  let mintAddress
@@ -352,18 +393,14 @@ export class WsApi {
352
393
  */
353
394
  parseAccountNotification({ address, walletAccount, tokenAccountsByOwner, result }) {
354
395
  const value = result?.value ?? result // support both { context, value } and flat result
355
- const isSolAccount = value.owner === '11111111111111111111111111111111' // System Program
356
- if (isSolAccount) {
396
+ if (isSystemProgram(value.owner)) {
357
397
  // SOL balance changed
358
398
  const amount = value.lamports
359
399
  return { solAddress: address, amount }
360
400
  }
361
401
 
362
- const isSplTokenAccount = value.owner === TOKEN_PROGRAM_ID.toBase58()
363
- const isSpl2022TokenAccount = value.owner === TOKEN_2022_PROGRAM_ID.toBase58()
364
-
365
402
  // SPL Token balance changed (both spl-token and spl-2022 have the first 165 bytes the same)
366
- if (isSplTokenAccount || isSpl2022TokenAccount) {
403
+ if (isTokenProgram(value.owner)) {
367
404
  // Handle jsonParsed encoding (data is an object with parsed info)
368
405
  if (value.data?.parsed?.info) {
369
406
  const parsed = value.data.parsed.info
@@ -392,10 +429,12 @@ export class WsApi {
392
429
  * Derive token account entries from a transaction's meta (postTokenBalances / preTokenBalances)
393
430
  * for the given owner address. Use this to augment tokenAccountsByOwner when the wallet
394
431
  * receives a new ATA in the same tx (transactionNotification often arrives before programNotification).
432
+ *
433
+ * @param txDetails - `getTransaction`-shaped object (output of `normalizeTransactionNotificationResult`)
395
434
  */
396
- getTokenAccountsFromTxMeta(transaction, ownerAddress) {
397
- const meta = transaction?.meta
398
- const accountKeys = transaction?.transaction?.message?.accountKeys ?? []
435
+ getTokenAccountsFromTxMeta(txDetails, ownerAddress) {
436
+ const meta = txDetails?.meta
437
+ const accountKeys = txDetails?.transaction?.message?.accountKeys ?? []
399
438
  const getPubkey = (key) =>
400
439
  key && typeof key === 'object' && 'pubkey' in key ? key.pubkey : key
401
440
  const balances = [...(meta?.postTokenBalances ?? []), ...(meta?.preTokenBalances ?? [])]
@@ -416,6 +455,11 @@ export class WsApi {
416
455
  return [...byAddress.values()]
417
456
  }
418
457
 
458
+ /**
459
+ * @param result - Raw `params.result` from WS (used when `txDetails` is omitted).
460
+ * @param txDetails - Optional pre-normalized output of `normalizeTransactionNotificationResult(result)`;
461
+ * pass this when the caller already computed it (e.g. `getTokenAccountsFromTxMeta`) to avoid duplicate work.
462
+ */
419
463
  parseTransactionNotification({
420
464
  address,
421
465
  walletAccount,
@@ -424,14 +468,11 @@ export class WsApi {
424
468
  tokens,
425
469
  tokenAccountsByOwner,
426
470
  result,
471
+ txDetails: txDetailsPreNormalized,
427
472
  }) {
428
- // Triton: result = { context, value: { transaction } }. Helius: result = { slot, signature, transaction, transactionIndex }.
429
- const rawTransaction = result?.value?.transaction ?? result?.transaction
430
- if (rawTransaction && result?.slot != null && rawTransaction.slot == null) {
431
- rawTransaction.slot = result.slot // Helius puts slot on result, parser expects it on tx
432
- }
473
+ const txDetails = txDetailsPreNormalized ?? normalizeTransactionNotificationResult(result)
433
474
 
434
- const parsedTx = parseTransaction(address, rawTransaction, tokenAccountsByOwner || [])
475
+ const parsedTx = parseTransaction(address, txDetails, tokenAccountsByOwner || [])
435
476
  const timestamp = Date.now() // the notification event has no blockTime
436
477
 
437
478
  if (!parsedTx.from && parsedTx.tokenTxs?.length === 0) return { logItemsByAsset: {} } // cannot parse it