@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 +16 -0
- package/package.json +3 -3
- package/src/api.js +4 -7
- package/src/clarity-api.js +2 -6
- package/src/index.js +1 -1
- package/src/rpc-api.js +5 -5
- package/src/tx-log/ws-monitor.js +6 -5
- package/src/ws-api.js +61 -20
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.
|
|
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.
|
|
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": "
|
|
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
|
|
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
|
package/src/clarity-api.js
CHANGED
|
@@ -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 {
|
|
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 [
|
|
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
|
|
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
|
|
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
|
package/src/tx-log/ws-monitor.js
CHANGED
|
@@ -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
|
|
255
|
-
|
|
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,
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
397
|
-
const meta =
|
|
398
|
-
const 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
|
-
|
|
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,
|
|
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
|