@exodus/solana-api 3.27.4 → 3.27.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,26 @@
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.27.6](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.27.5...@exodus/solana-api@3.27.6) (2026-01-21)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: improve SOL WS events logging (#7293)
13
+
14
+ * fix: Solana merge multi-transfer transactions to prevent data loss (#7297)
15
+
16
+
17
+
18
+ ## [3.27.5](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.27.4...@exodus/solana-api@3.27.5) (2026-01-19)
19
+
20
+ **Note:** Version bump only for package @exodus/solana-api
21
+
22
+
23
+
24
+
25
+
6
26
  ## [3.27.4](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.27.3...@exodus/solana-api@3.27.4) (2026-01-15)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-api",
3
- "version": "3.27.4",
3
+ "version": "3.27.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",
@@ -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": "c617d7eefb871a0ee8f174cccc56c42c20203b01",
52
+ "gitHead": "9bc961015aa41912a6cbcfde9ef98e090fe39b41",
53
53
  "bugs": {
54
54
  "url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
55
55
  },
package/src/connection.js CHANGED
@@ -5,7 +5,7 @@ import assert from 'minimalistic-assert'
5
5
  import ms from 'ms'
6
6
 
7
7
  const DEFAULT_RECONNECT_DELAY = ms('15s')
8
- const PING_INTERVAL = ms('30s')
8
+ const PING_INTERVAL = ms('25s')
9
9
 
10
10
  const debug = debugLogger('exodus:solana-api')
11
11
 
@@ -118,7 +118,7 @@ export class Connection {
118
118
  this.onMsg(json)
119
119
  } catch (e) {
120
120
  debug(e)
121
- debug('Cannot process msg:', evt.data)
121
+ debug('Cannot process msg:', evt)
122
122
  }
123
123
  }
124
124
 
@@ -135,11 +135,11 @@ export class Connection {
135
135
  }
136
136
 
137
137
  onError(evt) {
138
- debug('Error on WS:', evt.data)
138
+ debug('Error on WS:', evt)
139
139
  }
140
140
 
141
141
  onClose(evt) {
142
- debug('Closing WS')
142
+ debug('Closing WS', evt)
143
143
  clearInterval(this.pingTimeout)
144
144
  clearTimeout(this.reconnectTimeout)
145
145
  this.onConnectionClose(evt)
@@ -5,6 +5,7 @@ import assert from 'minimalistic-assert'
5
5
  import ms from 'ms'
6
6
 
7
7
  import { DEFAULT_POOL_ADDRESS } from '../account-state.js'
8
+ import { mergeByTxId } from './merge-transactions.js'
8
9
 
9
10
  const DEFAULT_REMOTE_CONFIG = {
10
11
  clarityUrl: [],
@@ -261,7 +262,7 @@ export class SolanaClarityMonitor extends BaseMonitor {
261
262
  mappedTransactions.push(item)
262
263
  }
263
264
 
264
- const logItemsByAsset = lodash.groupBy(mappedTransactions, (item) => item.coinName)
265
+ const logItemsByAsset = lodash.groupBy(mergeByTxId(mappedTransactions), 'coinName')
265
266
  return {
266
267
  logItemsByAsset,
267
268
  hasNewTxs: transactions.length > 0,
@@ -0,0 +1,40 @@
1
+ import lodash from 'lodash'
2
+
3
+ const { groupBy, uniq } = lodash
4
+
5
+ // Merge multiple transfers of the same token within one transaction (e.g., Jupiter swaps)
6
+ export function mergeByTxId(transactions) {
7
+ const grouped = groupBy(transactions, (tx) => `${tx.txId}:${tx.coinName}`)
8
+
9
+ return Object.values(grouped).map((group) => {
10
+ if (group.length === 1) return group[0]
11
+
12
+ const base = group[0]
13
+ const currency = base.currencies[base.coinName]
14
+ const totalAmount = group.reduce((sum, tx) => sum.add(tx.coinAmount), currency.ZERO)
15
+
16
+ if (base.coinAmount.isPositive) {
17
+ return {
18
+ ...base,
19
+ coinAmount: totalAmount,
20
+ from: uniq(group.flatMap((tx) => tx.from)),
21
+ data: {
22
+ ...base.data,
23
+ received: group.map((tx) => ({
24
+ address: tx.from[0],
25
+ amount: tx.coinAmount.toDefaultString({ unit: true }),
26
+ })),
27
+ },
28
+ }
29
+ }
30
+
31
+ return {
32
+ ...base,
33
+ coinAmount: totalAmount,
34
+ data: {
35
+ ...base.data,
36
+ sent: group.flatMap((tx) => tx.data?.sent || []),
37
+ },
38
+ }
39
+ })
40
+ }
@@ -4,6 +4,7 @@ import assert from 'minimalistic-assert'
4
4
  import ms from 'ms'
5
5
 
6
6
  import { DEFAULT_POOL_ADDRESS } from '../account-state.js'
7
+ import { mergeByTxId } from './merge-transactions.js'
7
8
 
8
9
  const DEFAULT_REMOTE_CONFIG = {
9
10
  rpcs: [],
@@ -244,7 +245,7 @@ export class SolanaMonitor extends BaseMonitor {
244
245
  mappedTransactions.push(item)
245
246
  }
246
247
 
247
- const logItemsByAsset = _.groupBy(mappedTransactions, (item) => item.coinName)
248
+ const logItemsByAsset = _.groupBy(mergeByTxId(mappedTransactions), 'coinName')
248
249
  return {
249
250
  logItemsByAsset,
250
251
  hasNewTxs: transactions.length > 0,
package/src/ws-api.js CHANGED
@@ -2,11 +2,14 @@ import { PublicKey, Token, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, U64 } from '
2
2
  import lodash from 'lodash'
3
3
 
4
4
  import { Connection } from './connection.js'
5
+ import { mergeByTxId } from './tx-log/merge-transactions.js'
5
6
  import { parseTransaction } from './tx-parser.js'
6
7
  import { isSolAddressPoisoningTx } from './txs-utils.js'
7
8
 
9
+ // Triton Whirligig WebSocket
10
+ const WS_ENDPOINT = 'wss://solana-triton.a.exodus.io/whirligig' // pointing to: wss://exodus-solanama-6db3.mainnet.rpcpool.com/<token>/whirligig
8
11
  // Helius Advanced WebSocket
9
- const WS_ENDPOINT = 'wss://solana-helius-wss.a.exodus.io/ws' // pointing to: wss://atlas-mainnet.helius-rpc.com/?api-key=<API_KEY>
12
+ // const WS_ENDPOINT = 'wss://solana-helius-wss.a.exodus.io/ws' // pointing to: wss://atlas-mainnet.helius-rpc.com/?api-key=<API_KEY>
10
13
 
11
14
  export class WsApi {
12
15
  constructor({ rpcUrl, wsUrl, assets }) {
@@ -88,7 +91,10 @@ export class WsApi {
88
91
  {
89
92
  vote: false,
90
93
  // failed: true,
91
- accountInclude: addresses,
94
+ accounts: {
95
+ include: addresses,
96
+ },
97
+ // accountInclude: addresses, // Helius
92
98
  },
93
99
  {
94
100
  commitment: 'confirmed',
@@ -147,7 +153,8 @@ export class WsApi {
147
153
  tokenAccountsByOwner,
148
154
  result,
149
155
  }) {
150
- const parsedTx = parseTransaction(address, result.transaction, tokenAccountsByOwner)
156
+ const rawTransaction = result?.value ? result.value.transaction : result.transaction // for Triton Whirligig OR Helius Advanced WS
157
+ const parsedTx = parseTransaction(address, rawTransaction, tokenAccountsByOwner)
151
158
  const timestamp = Date.now() // the notification event has no blockTime
152
159
 
153
160
  if (!parsedTx.from && parsedTx.tokenTxs?.length === 0) return { logItemsByAsset: {} } // cannot parse it
@@ -256,7 +263,7 @@ export class WsApi {
256
263
  mappedTransactions.push(item)
257
264
  }
258
265
 
259
- const logItemsByAsset = lodash.groupBy(mappedTransactions, (item) => item.coinName)
266
+ const logItemsByAsset = lodash.groupBy(mergeByTxId(mappedTransactions), 'coinName')
260
267
  return {
261
268
  logItemsByAsset,
262
269
  cursorState: transactions[0]?.id ? { cursor: transactions[0].id } : {},