@exodus/solana-api 3.30.4 → 3.30.5

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,14 @@
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.5](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.30.4...@exodus/solana-api@3.30.5) (2026-03-27)
7
+
8
+ **Note:** Version bump only for package @exodus/solana-api
9
+
10
+
11
+
12
+
13
+
6
14
  ## [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
15
 
8
16
  **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.5",
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.2",
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": "07a8157aec179f656835db50568127788d4b91b6",
53
53
  "bugs": {
54
54
  "url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
55
55
  },
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'
@@ -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,11 @@
1
- import { PublicKey, Token, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, U64 } from '@exodus/solana-lib'
1
+ import {
2
+ isTokenProgram,
3
+ PublicKey,
4
+ Token,
5
+ TOKEN_2022_PROGRAM_ID,
6
+ TOKEN_PROGRAM_ID,
7
+ U64,
8
+ } from '@exodus/solana-lib'
2
9
  import lodash from 'lodash'
3
10
 
4
11
  import { Connection } from './connection.js'
@@ -11,6 +18,42 @@ import { isSolAddressPoisoningTx } from './txs-utils.js'
11
18
  // Helius Enhanced WebSocket (https://www.helius.dev/docs/enhanced-websockets)
12
19
  const HELIUS_WS_URL = 'wss://solana-helius-wss.a.exodus.io/ws'
13
20
 
21
+ /**
22
+ * Normalize `transactionSubscribe` / JSON-RPC `params.result` into `getTransaction`-shaped txDetails:
23
+ * `{ slot?, transaction: { message, signatures }, meta }`.
24
+ *
25
+ * - **Triton Whirligig:** `result.value.transaction` is often `{ meta, transaction: { message, signatures }, ... }`.
26
+ * - **Helius / std RPC:** `meta` may be a sibling of `transaction` (same layout as `getTransaction`).
27
+ *
28
+ * Use this for any WS path that needs meta, inner instructions, or token balances — not only
29
+ * `parseTransaction`.
30
+ *
31
+ * @returns { { slot?: number, meta: object, transaction: { message: object, signatures: string[] } } | undefined }
32
+ */
33
+ export function normalizeTransactionNotificationResult(result) {
34
+ const rawFromValue = result?.value?.transaction
35
+ const rawFromRoot = result?.transaction
36
+ const candidate = rawFromValue ?? rawFromRoot
37
+
38
+ const isBareTransactionBody =
39
+ candidate != null &&
40
+ result?.meta != null &&
41
+ candidate.meta == null &&
42
+ candidate.message != null &&
43
+ Array.isArray(candidate.signatures)
44
+
45
+ const txDetails = isBareTransactionBody
46
+ ? { meta: result.meta, transaction: candidate }
47
+ : candidate
48
+
49
+ const slot = result?.slot ?? result?.value?.slot
50
+ if (txDetails == null || slot == null || txDetails.slot != null) {
51
+ return txDetails
52
+ }
53
+
54
+ return { ...txDetails, slot }
55
+ }
56
+
14
57
  export class WsApi {
15
58
  constructor({ rpcUrl, wsUrl, assets }) {
16
59
  this.setWsEndpoint(wsUrl)
@@ -313,10 +356,7 @@ export class WsApi {
313
356
  if (!value) return null
314
357
  const { pubkey, account } = value
315
358
  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
359
+ if (!isTokenProgram(account.owner)) return null
320
360
 
321
361
  let owner
322
362
  let mintAddress
@@ -359,11 +399,8 @@ export class WsApi {
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