@exodus/solana-api 3.29.5 → 3.29.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 +10 -0
- package/package.json +2 -2
- package/src/staking/index.js +2 -2
- package/src/ws-api.js +86 -66
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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.29.6](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.29.5...@exodus/solana-api@3.29.6) (2026-02-23)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* fix: remove staking provider param (#7440)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
6
16
|
## [3.29.5](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.29.4...@exodus/solana-api@3.29.5) (2026-02-14)
|
|
7
17
|
|
|
8
18
|
**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.29.
|
|
3
|
+
"version": "3.29.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": "
|
|
52
|
+
"gitHead": "6ac0fce9b0cc8dc2fe3cb5b940f077ec36cfcae3",
|
|
53
53
|
"bugs": {
|
|
54
54
|
"url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
|
|
55
55
|
},
|
package/src/staking/index.js
CHANGED
|
@@ -5,8 +5,8 @@ import { stakingProviderClientFactory } from './staking-provider-client.js'
|
|
|
5
5
|
|
|
6
6
|
export { getStakingInfo } from '../staking-utils.js'
|
|
7
7
|
|
|
8
|
-
export const stakingApiFactory = ({ assetName, assetClientInterface
|
|
9
|
-
const stakingProvider =
|
|
8
|
+
export const stakingApiFactory = ({ assetName, assetClientInterface }) => {
|
|
9
|
+
const stakingProvider = stakingProviderClientFactory()
|
|
10
10
|
|
|
11
11
|
async function sendStake({
|
|
12
12
|
address,
|
package/src/ws-api.js
CHANGED
|
@@ -7,9 +7,9 @@ import { parseTransaction } from './tx-parser.js'
|
|
|
7
7
|
import { isSolAddressPoisoningTx } from './txs-utils.js'
|
|
8
8
|
|
|
9
9
|
// Triton Whirligig WebSocket
|
|
10
|
-
const
|
|
11
|
-
// Helius
|
|
12
|
-
|
|
10
|
+
const TRITON_WS_ENDPOINT = 'wss://solana-triton.a.exodus.io/whirligig' // pointing to: wss://exodus-solanama-6db3.mainnet.rpcpool.com/<token>/whirligig
|
|
11
|
+
// Helius Enhanced WebSocket (https://www.helius.dev/docs/enhanced-websockets)
|
|
12
|
+
const HELIUS_WS_URL = 'wss://solana-helius-wss.a.exodus.io/ws'
|
|
13
13
|
|
|
14
14
|
export class WsApi {
|
|
15
15
|
constructor({ rpcUrl, wsUrl, assets }) {
|
|
@@ -23,6 +23,7 @@ export class WsApi {
|
|
|
23
23
|
#resetSubscriptionState() {
|
|
24
24
|
this.accountSubscriptions = Object.create(null)
|
|
25
25
|
this.transactionSubscriptions = Object.create(null)
|
|
26
|
+
this.programSubscriptions = Object.create(null)
|
|
26
27
|
// subscription id (from RPC response) -> { owner, type: 'account'|'transaction'|'program' }
|
|
27
28
|
this.subscriptionIdToMeta = Object.create(null)
|
|
28
29
|
// request id (our conn.seq) -> { owner, type }, until we receive the subscription id
|
|
@@ -30,7 +31,13 @@ export class WsApi {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
setWsEndpoint(wsUrl) {
|
|
33
|
-
this.wsUrl = wsUrl ||
|
|
34
|
+
this.wsUrl = wsUrl || TRITON_WS_ENDPOINT
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** True when using Helius Enhanced WebSocket (different transactionSubscribe params and notification shape). */
|
|
38
|
+
#isHelius() {
|
|
39
|
+
if (!this.wsUrl) return false
|
|
40
|
+
return this.wsUrl.includes('helius') || this.wsUrl === HELIUS_WS_URL
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
/**
|
|
@@ -174,36 +181,79 @@ export class WsApi {
|
|
|
174
181
|
|
|
175
182
|
const id = ++conn.seq
|
|
176
183
|
this.pendingSubscriptionRequests[id] = { owner, type: 'transaction' }
|
|
184
|
+
const options = {
|
|
185
|
+
commitment: 'confirmed',
|
|
186
|
+
encoding: 'jsonParsed',
|
|
187
|
+
transactionDetails: 'full',
|
|
188
|
+
showRewards: false,
|
|
189
|
+
maxSupportedTransactionVersion: 255,
|
|
190
|
+
}
|
|
191
|
+
const filter = this.#isHelius()
|
|
192
|
+
? { accountInclude: difference }
|
|
193
|
+
: { vote: false, accounts: { include: difference } }
|
|
177
194
|
conn.send({
|
|
178
195
|
jsonrpc: '2.0',
|
|
179
196
|
id,
|
|
180
197
|
method: 'transactionSubscribe',
|
|
198
|
+
params: [filter, options],
|
|
199
|
+
})
|
|
200
|
+
this.transactionSubscriptions[owner] = [...subscriptions, ...difference]
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async programSubscribe({ owner }) {
|
|
204
|
+
const conn = this.connection
|
|
205
|
+
if (!conn || !conn.isOpen) {
|
|
206
|
+
console.warn('SOL Connection is not open, cannot programSubscribe for', owner)
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (this.programSubscriptions[owner]) return // already subscribed (SPL + Token-2022)
|
|
211
|
+
|
|
212
|
+
const splTokenProgramId = TOKEN_PROGRAM_ID.toBase58()
|
|
213
|
+
const token2022ProgramId = TOKEN_2022_PROGRAM_ID.toBase58()
|
|
214
|
+
const tokenAccountDataSize = 165
|
|
215
|
+
|
|
216
|
+
// SPL Token: fixed 165-byte account size
|
|
217
|
+
const id1 = ++conn.seq
|
|
218
|
+
this.pendingSubscriptionRequests[id1] = { owner, type: 'program' }
|
|
219
|
+
conn.send({
|
|
220
|
+
jsonrpc: '2.0',
|
|
221
|
+
id: id1,
|
|
222
|
+
method: 'programSubscribe',
|
|
181
223
|
params: [
|
|
224
|
+
splTokenProgramId,
|
|
182
225
|
{
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
},
|
|
226
|
+
encoding: 'jsonParsed',
|
|
227
|
+
commitment: 'confirmed',
|
|
228
|
+
filters: [{ dataSize: tokenAccountDataSize }, { memcmp: { offset: 32, bytes: owner } }],
|
|
187
229
|
},
|
|
230
|
+
],
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// Token-2022: no dataSize filter (accounts can have extensions and be larger than 165 bytes)
|
|
234
|
+
const id2 = ++conn.seq
|
|
235
|
+
this.pendingSubscriptionRequests[id2] = { owner, type: 'program' }
|
|
236
|
+
conn.send({
|
|
237
|
+
jsonrpc: '2.0',
|
|
238
|
+
id: id2,
|
|
239
|
+
method: 'programSubscribe',
|
|
240
|
+
params: [
|
|
241
|
+
token2022ProgramId,
|
|
188
242
|
{
|
|
189
|
-
commitment: 'confirmed',
|
|
190
243
|
encoding: 'jsonParsed',
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
maxSupportedTransactionVersion: 255,
|
|
244
|
+
commitment: 'confirmed',
|
|
245
|
+
filters: [{ memcmp: { offset: 32, bytes: owner } }],
|
|
194
246
|
},
|
|
195
247
|
],
|
|
196
248
|
})
|
|
197
|
-
|
|
249
|
+
|
|
250
|
+
this.programSubscriptions[owner] = true
|
|
198
251
|
}
|
|
199
252
|
|
|
200
253
|
async sendSubscriptions({ address, tokensAddresses = [] }) {
|
|
201
|
-
const conn = this.connection
|
|
202
|
-
|
|
203
254
|
const addresses = [address, ...tokensAddresses]
|
|
204
255
|
|
|
205
256
|
// 1. subscribe to each addresses (SOL and Token addresses) balance events
|
|
206
|
-
// transform this forEach in a for of to await each subscription:
|
|
207
257
|
for (const addr of addresses) {
|
|
208
258
|
await this.accountSubscribe({ owner: address, account: addr })
|
|
209
259
|
}
|
|
@@ -212,48 +262,7 @@ export class WsApi {
|
|
|
212
262
|
await this.transactionSubscribe({ owner: address, accounts: addresses })
|
|
213
263
|
|
|
214
264
|
// 3. subscribe to SPL Token and Token-2022 program account changes to detect new token accounts for this wallet
|
|
215
|
-
|
|
216
|
-
const token2022ProgramId = TOKEN_2022_PROGRAM_ID.toBase58()
|
|
217
|
-
const tokenAccountDataSize = 165
|
|
218
|
-
|
|
219
|
-
if (conn?.isOpen) {
|
|
220
|
-
// SPL Token: fixed 165-byte account size
|
|
221
|
-
const id1 = ++conn.seq
|
|
222
|
-
this.pendingSubscriptionRequests[id1] = { owner: address, type: 'program' }
|
|
223
|
-
conn.send({
|
|
224
|
-
jsonrpc: '2.0',
|
|
225
|
-
id: id1,
|
|
226
|
-
method: 'programSubscribe',
|
|
227
|
-
params: [
|
|
228
|
-
splTokenProgramId,
|
|
229
|
-
{
|
|
230
|
-
encoding: 'jsonParsed',
|
|
231
|
-
commitment: 'confirmed',
|
|
232
|
-
filters: [
|
|
233
|
-
{ dataSize: tokenAccountDataSize },
|
|
234
|
-
{ memcmp: { offset: 32, bytes: address } },
|
|
235
|
-
],
|
|
236
|
-
},
|
|
237
|
-
],
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
// Token-2022: no dataSize filter (accounts can have extensions and be larger than 165 bytes)
|
|
241
|
-
const id2 = ++conn.seq
|
|
242
|
-
this.pendingSubscriptionRequests[id2] = { owner: address, type: 'program' }
|
|
243
|
-
conn.send({
|
|
244
|
-
jsonrpc: '2.0',
|
|
245
|
-
id: id2,
|
|
246
|
-
method: 'programSubscribe',
|
|
247
|
-
params: [
|
|
248
|
-
token2022ProgramId,
|
|
249
|
-
{
|
|
250
|
-
encoding: 'jsonParsed',
|
|
251
|
-
commitment: 'confirmed',
|
|
252
|
-
filters: [{ memcmp: { offset: 32, bytes: address } }],
|
|
253
|
-
},
|
|
254
|
-
],
|
|
255
|
-
})
|
|
256
|
-
}
|
|
265
|
+
await this.programSubscribe({ owner: address })
|
|
257
266
|
}
|
|
258
267
|
|
|
259
268
|
async unwatchAddress({ address }) {
|
|
@@ -283,6 +292,7 @@ export class WsApi {
|
|
|
283
292
|
delete this.watchedAddresses[address]
|
|
284
293
|
delete this.accountSubscriptions[address]
|
|
285
294
|
delete this.transactionSubscriptions[address]
|
|
295
|
+
delete this.programSubscriptions[address]
|
|
286
296
|
|
|
287
297
|
if (Object.keys(this.watchedAddresses).length === 0 && conn) {
|
|
288
298
|
await conn.stop()
|
|
@@ -333,22 +343,27 @@ export class WsApi {
|
|
|
333
343
|
}
|
|
334
344
|
}
|
|
335
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Parse accountNotification from Triton or Helius.
|
|
348
|
+
* Triton: params.result = { context, value }. Helius: same shape (result.value = account).
|
|
349
|
+
*/
|
|
336
350
|
parseAccountNotification({ address, walletAccount, tokenAccountsByOwner, result }) {
|
|
337
|
-
const
|
|
351
|
+
const value = result?.value ?? result // support both { context, value } and flat result
|
|
352
|
+
const isSolAccount = value.owner === '11111111111111111111111111111111' // System Program
|
|
338
353
|
if (isSolAccount) {
|
|
339
354
|
// SOL balance changed
|
|
340
|
-
const amount =
|
|
355
|
+
const amount = value.lamports
|
|
341
356
|
return { solAddress: address, amount }
|
|
342
357
|
}
|
|
343
358
|
|
|
344
|
-
const isSplTokenAccount =
|
|
345
|
-
const isSpl2022TokenAccount =
|
|
359
|
+
const isSplTokenAccount = value.owner === TOKEN_PROGRAM_ID.toBase58()
|
|
360
|
+
const isSpl2022TokenAccount = value.owner === TOKEN_2022_PROGRAM_ID.toBase58()
|
|
346
361
|
|
|
347
362
|
// SPL Token balance changed (both spl-token and spl-2022 have the first 165 bytes the same)
|
|
348
363
|
if (isSplTokenAccount || isSpl2022TokenAccount) {
|
|
349
364
|
// Handle jsonParsed encoding (data is an object with parsed info)
|
|
350
|
-
if (
|
|
351
|
-
const parsed =
|
|
365
|
+
if (value.data?.parsed?.info) {
|
|
366
|
+
const parsed = value.data.parsed.info
|
|
352
367
|
return {
|
|
353
368
|
solAddress: parsed.owner,
|
|
354
369
|
amount: parsed.tokenAmount.amount,
|
|
@@ -357,7 +372,7 @@ export class WsApi {
|
|
|
357
372
|
}
|
|
358
373
|
|
|
359
374
|
// Handle base64 encoding (data is an array: [base64_string, "base64"])
|
|
360
|
-
const decoded = Token.decode(Buffer.from(
|
|
375
|
+
const decoded = Token.decode(Buffer.from(value.data[0], 'base64'))
|
|
361
376
|
const tokenMintAddress = new PublicKey(decoded.mint).toBase58()
|
|
362
377
|
const solAddress = new PublicKey(decoded.owner).toBase58()
|
|
363
378
|
const amount = U64.fromBuffer(decoded.amount).toString()
|
|
@@ -407,7 +422,12 @@ export class WsApi {
|
|
|
407
422
|
tokenAccountsByOwner,
|
|
408
423
|
result,
|
|
409
424
|
}) {
|
|
410
|
-
|
|
425
|
+
// Triton: result = { context, value: { transaction } }. Helius: result = { slot, signature, transaction, transactionIndex }.
|
|
426
|
+
const rawTransaction = result?.value?.transaction ?? result?.transaction
|
|
427
|
+
if (rawTransaction && result?.slot != null && rawTransaction.slot == null) {
|
|
428
|
+
rawTransaction.slot = result.slot // Helius puts slot on result, parser expects it on tx
|
|
429
|
+
}
|
|
430
|
+
|
|
411
431
|
const parsedTx = parseTransaction(address, rawTransaction, tokenAccountsByOwner || [])
|
|
412
432
|
const timestamp = Date.now() // the notification event has no blockTime
|
|
413
433
|
|